CSAW CTF Quals 2016 - deedeedee
Points: 150
Category: Reversing
Description
(I lost the description to this challenge)
deedeedee
This is a reversing challenge. Given a binary, our objective was to find the flag. I vaguely remember that the description of this challenge was something along the lines of being able to execute instructions at compile time.
We’re given a 64-bit ELF.
1
2
3
$ file deedeedee
deedeedee: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared
libs), for GNU/Linux 2.6.24, BuildID[sha1]=4fac9c863749015d039a3bf0a3a6c936f2f7eadd, not stripped
Running the binary in my CTF environment tells us that we have the encrypted flag, and that the flag was generated at compile time.
1
2
3
4
$ ./deedeedee
Your hexencoded, encrypted flag is: 676c60677a74326d716c6074325f6c6575347172316773616c6d686e665f68735e6773385e345e3377657379316e327d
I generated it at compile time. :)
Can you decrypt it for me?
The hexencoded flag decodes to gl`gzt2mql`t2_leu4qr1gsalmhnf_hs^gs8^4^3wesy1n2}
. Keep this in mind, we will need it later. You could use python to decode the hex encoded string or simply use any hex->text converter online.
Analysis
Running objdump -x
on the binary shows that there are many functions defined, and instincts tells me that at least one of these would contain the routine required to decrypt our flag.
Doing a simple grep for encrypt
finds the function we are searching for:
1
2
$ objdump -x deedeedee | grep encrypt
000000000044cde0 g F .text 000000000000158b _D9deedeedee7encryptFNaNfAyaZAya
Note that the names are all mangled, but we can still roughly make out the meaning of the functions.
The disassembly for the function shows some kind of chained function calls where the result of one call is used as argument for the other call.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
gdb-peda$ disas _D9deedeedee7encryptFNaNfAyaZAya
Dump of assembler code for function _D9deedeedee7encryptFNaNfAyaZAya:
0x000000000044cde0 <+0>: push rbp
0x000000000044cde1 <+1>: mov rbp,rsp
0x000000000044cde4 <+4>: sub rsp,0x10
0x000000000044cde8 <+8>: mov QWORD PTR [rbp-0x10],rdi
0x000000000044cdec <+12>: mov QWORD PTR [rbp-0x8],rsi
0x000000000044cdf0 <+16>: mov rdx,QWORD PTR [rbp-0x8]
0x000000000044cdf4 <+20>: mov rax,QWORD PTR [rbp-0x10]
0x000000000044cdf8 <+24>: mov rdi,rax
0x000000000044cdfb <+27>: mov rsi,rdx
0x000000000044cdfe <+30>: call 0x451470 <_D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya>
0x000000000044ce03 <+35>: mov rdi,rax
0x000000000044ce06 <+38>: mov rsi,rdx
0x000000000044ce09 <+41>: call 0x458280 <_D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya>
0x000000000044ce0e <+46>: mov rdi,rax
0x000000000044ce11 <+49>: mov rsi,rdx
0x000000000044ce14 <+52>: call 0x458358 <_D9deedeedee21__T3encVAyaa3_333333Z3encFNaNfAyaZAya>
[** Truncated **]
0x000000000044e348 <+5480>: mov rdi,rax
0x000000000044e34b <+5483>: mov rsi,rdx
0x000000000044e34e <+5486>: call 0x472428 <_D9deedeedee33__T3encVAyaa9_343937343937343937Z3encFNaNfAyaZAya>
0x000000000044e353 <+5491>: mov rdi,rax
0x000000000044e356 <+5494>: mov rsi,rdx
0x000000000044e359 <+5497>: call 0x472500 <_D9deedeedee33__T3encVAyaa9_343938343938343938Z3encFNaNfAyaZAya>
0x000000000044e35e <+5502>: mov rdi,rax
0x000000000044e361 <+5505>: mov rsi,rdx
0x000000000044e364 <+5508>: call 0x4725d8 <_D9deedeedee33__T3encVAyaa9_343939343939343939Z3encFNaNfAyaZAya>
0x000000000044e369 <+5513>: leave
0x000000000044e36a <+5514>: ret
End of assembler dump.
What we can notice from the disassembly is:
- Each function takes in two argument, registers $rdi and $rsi.
- The function names are slightly different, first one has
313131
and second one has323232
in it.
The next logical thing to do is to disassemble one of the function calls:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gdb-peda$ disas _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya
Dump of assembler code for function _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya:
0x0000000000451470 <+0>: push rbp
0x0000000000451471 <+1>: mov rbp,rsp
0x0000000000451474 <+4>: sub rsp,0xb0
0x000000000045147b <+11>: mov QWORD PTR [rbp-0xa8],rbx
0x0000000000451482 <+18>: mov QWORD PTR [rbp-0x10],rdi
0x0000000000451486 <+22>: mov QWORD PTR [rbp-0x8],rsi
[** Truncated **]
0x00000000004514aa <+58>: mov edx,0x49fa5c
[** Truncated **]
0x000000000045150c <+156>: xor esi,DWORD PTR [rdx]
0x000000000045150e <+158>: movzx ebx,BYTE PTR [rbp-0xa0]
0x0000000000451515 <+165>: xor esi,ebx
[** Truncated **]
0x0000000000451543 <+211>: leave
0x0000000000451544 <+212>: ret
End of assembler dump.
What I noticed is that the arguments provided is xor’ed with the string located at 0x49fa5c
, which is 0x31
. Remember the 0x313131 contained in the name of the function?
1
2
gdb-peda$ x/xw0x49fa5c
0x49fa5c <_TMP75>: 0x00313131
Now with these, I have some idea of how the encryption is done (probably a chain of xors with some constant byte values, 0x31 in this case). We can further verify this by doing dynamic analysis.
We first set a breakpoint at main, and run the binary in gdb. We then jump to 0x000000000044CDE0
, which is the start of the encrypt function.
1
2
3
4
gdb-peda$ b _D9deedeedee7encryptFNaNfAyaZAya
Breakpoint 2 at 0x44cde4
gdb-peda$ j _D9deedeedee7encryptFNaNfAyaZAya
Continuing at 0x44cde4.
Remember that the chain of function takes in two argument from register $rsi and $rdi, I assume that one of this register contains a pointer to the string we are encoding and the other pointer containing the len of the string.
With that assumption, we set the register to the correct values. We can do so by running the following gdb calls. We also set a breakpoint at the first xor instruction, so that we can verify the xor values.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gdb-peda$ p strcpy($rsi, "222")
gdb-peda$ set $rdi=3
gdb-peda$ b *0x000000000045150c
Breakpoint 3 at 0x45150c
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffe390 --> 0x3200000031 ('1')
RBX: 0x7fffffffe368 --> 0x3
RCX: 0x7fffffffe390 --> 0x3200000031 ('1')
RDX: 0x7fffffffe394 --> 0x300000032
RSI: 0x31 ('1')
RDI: 0x7fffffffe2d0 --> 0x3200000031 ('1')
[** Truncated **]
=> 0x45150c <_D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya+156>:
xor esi,DWORD PTR [rdx]
0x45150e <_D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya+158>:
movzx ebx,BYTE PTR [rbp-0xa0]
0x451515 <_D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya+165>: xor esi,ebx
[** Truncated **]
gdb-peda$
We see that the $ebx contains 0x3 which is the length we set at register $rdi as well. Thus, in this function, every byte in the input string is xor’ed with 0x31 as well as the length of the input string.
In python, the particular function would look something like the following
1
2
3
4
5
6
def enc313131(inputStr):
length = len(inputStr)
result = ""
for character in inputStr:
result += chr(ord(character) ^ 0x31 ^ length)
return result
Now that we have confirmed our assumptions, the flag can be easily decrypted by running the same function with the flag and the length as input.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gdb-peda$ b *0x000000000044E408
Breakpoint 1 at 0x44e408
gdb-peda$ r
[** truncated **]
=> 0x44e408 <_Dmain>: push rbp
[** truncated **]
gdb-peda$ p strcpy($rsi,"gl`gzt2mql`t2_leu4qr1gsalmhnf_hs^gs8^4^3wesy1n2}")
[** truncated **]
gdb-peda$ b *0x000000000044cde0
Breakpoint 2 at 0x44cde0
gdb-peda$ j *0x000000000044cde0
Continuing at 0x44cde0.
[** truncated **]
=> 0x44cde0 <_D9deedeedee7encryptFNaNfAyaZAya>: push rbp
[** truncated **]
gdb-peda$ set $rdi=48
gdb-peda$ b *0x000000000044e36a
Breakpoint 3 at 0x44e36a
gdb-peda$ c
Continuing.
[** truncated **]
RDX: 0x7ffff7ef1c80 ("flag{t3mplat3_met4pr0gramming_is_gr8_4_3very0n3}")
[** truncated **]
And we have our flag!
comments powered by Disqus