usd hacking challenge 2016 writeup: token 5
The last token was hidden in a downloadable binary. It was announced on the challenge website as "reversing" and as "harder" than the other challenges. By many people's standards it probably was not really that hard, however, I never had any 1337 r3v3R1ng sk1llz to begin with, and my knowledge of assembly and using gdb is decidedly rusty, so finding this token certainly took me a while.
root@kali:~/usdhc# wget http://22.214.171.124/dl/reverseChallenge --2016-03-19 15:55:38-- http://126.96.36.199/dl/reverseChallenge Connecting to 188.8.131.52:80... connected. HTTP request sent, awaiting response... 200 OK Length: 34188 (33K) Saving to: ‘reverseChallenge’ reverseChallenge 100%[========================================================================================>] 33.39K --.-KB/s in 0.02s 2016-03-19 15:55:38 (1.33 MB/s) - ‘reverseChallenge’ saved [34188/34188] root@kali:~/usdhc# file reverseChallenge reverseChallenge: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, stripped
A stripped 64-bit ELF executable for Linux. The website said "for Kali-Linux 64 bit", and in fact, the binary did not work correctly on my main Linux Mint installation, which means it thought I was running it inside a debugger when I was not (yet) - more on that later. It wanted a password:
root@kali:~/usdhc# ./reverseChallenge Please supply a password! root@kali:~/usdhc# ./reverseChallenge blah Wrong Password
Strace just showed a bunch of
mprotect() calls, and running it in gdb was not so encouraging either:
root@kali:~/usdhc# gdb -q (gdb) file reverseChallenge Reading symbols from reverseChallenge...(no debugging symbols found)...done. (gdb) info file Symbols from "/root/usdhc/reverseChallenge". (gdb)
Poking around with strings showed, among loads of garbage: $Info: This file is packed with the UPX executable packer http://upx.sf.net $ and several occurrences of USD!:
root@kali:~/usdhc# strings reverseChallenge | grep USD USD! USD!u USD! USD!
Unpacking with upx did not work:
root@kali:~/usdhc# upx -d reverseChallenge Ultimate Packer for eXecutables Copyright (C) 1996 - 2013 UPX 3.91 Markus Oberhumer, Laszlo Molnar & John Reiser Sep 30th 2013 File size Ratio Format Name -------------------- ------ ----------- ----------- upx: reverseChallenge: NotPackedException: not packed by UPX Unpacked 0 files.
After some quick research on that UPX packer it was clear that UPX had been replaced with USD in the packed binary, so I changed those strings back to UPX with hexeditor:
root@kali:~/usdhc# hexeditor reverseChallenge root@kali:~/usdhc# strings reverseChallenge | grep USD root@kali:~/usdhc# strings reverseChallenge | grep UPX UPX! $Info: This file is packed with the UPX executable packer http://upx.sf.net $ $Id: UPX 3.91 Copyright (C) 1996-2013 the UPX Team. All Rights Reserved. $ UPX!u UPX! UPX! root@kali:~/usdhc# upx -d reverseChallenge Ultimate Packer for eXecutables Copyright (C) 1996 - 2013 UPX 3.91 Markus Oberhumer, Laszlo Molnar & John Reiser Sep 30th 2013 File size Ratio Format Name -------------------- ------ ----------- ----------- 38524 <- 34188 88.74% linux/ElfAMD reverseChallenge Unpacked 1 file.
Looking at the unpacked file with strings again, I saw ThisIsTheSuperSecretChallengePass, but even before trying this password for the sake of completeness it was obvious that I was being trolled just from the password choice alone. I would see that string again, though...
The unpacked binary could now be debugged on the asm level:
root@kali:~/usdhc# gdb -q (gdb) file reverseChallenge Reading symbols from reverseChallenge...(no debugging symbols found)...done. (gdb) info file Symbols from "/root/usdhc/reverseChallenge". Local exec file: `/root/usdhc/reverseChallenge', file type elf64-x86-64. Entry point: 0x4006c0 [..] (gdb) b *0x4006c0 Breakpoint 1 at 0x4006c0 (gdb) run blah Starting program: /root/usdhc/reverseChallenge blah Breakpoint 1, 0x00000000004006c0 in _start () (gdb) x/20i $pc => 0x4006c0 <_start>: xor ebp,ebp [..] 0x4006dd <_start+29>: mov rdi,0x400dbb 0x4006e4 <_start+36>: call 0x400630 <__libc_start_main@plt> 0x4006e9 <_start+41>: hlt (gdb) del gdb) b *0x400dbb Breakpoint 2 at 0x400dbb (gdb) c Continuing. Breakpoint 2, 0x0000000000400dbb in main () (gdb) x/50i $pc => 0x400dbb <main>: push rbp 0x400dbc <main+1>: mov rbp,rsp 0x400dbf <main+4>: sub rsp,0x10 0x400dc3 <main+8>: mov DWORD PTR [rbp-0x4],edi 0x400dc6 <main+11>: mov QWORD PTR [rbp-0x10],rsi 0x400dca <main+15>: mov eax,0x0 0x400dcf <main+20>: call 0x4009c3 <bHw> [..]
After setting a breakpoint at the beginning of
0x4006e4, I was finally seeing some code. The first function called from there is
(gdb) x/10i $pc => 0x4009c3 <bHw>: push rbp 0x4009c4 <bHw+1>: mov rbp,rsp 0x4009c7 <bHw+4>: sub rsp,0x20 0x4009cb <bHw+8>: call 0x4006a0 <fork@plt> 0x4009d0 <bHw+13>: mov DWORD PTR [rbp-0x4],eax 0x4009d3 <bHw+16>: cmp DWORD PTR [rbp-0x4],0xffffffff 0x4009d7 <bHw+20>: jne 0x4009e3 <bHw+32>
At first, I was puzzled by these
fork() calls. Why fork a new process?? Happens in three places:
root@kali:~/usdhc# objdump -d reverseChallenge | grep fork 4005d0: e8 db 00 00 00 callq 4006b0 <fork@plt+0x10> 00000000004006a0 <fork@plt>: 4009cb: e8 d0 fc ff ff callq 4006a0 <fork@plt> 400aaa: e8 f1 fb ff ff callq 4006a0 <fork@plt>
The program forks copies of itself, but only some of them actually continue the main control flow, while others die while the parent process waits for that to happen. The debugger must either follow the child process (if it represents the main code path) or stay with the parent process (if the child process is just killed off). Instead of analyzing exactly how the interaction between
waitpid() works, I figured out by trial and error that in the
<bHw> function, I needed to follow the child, whereas in
<gRb>, I had to stay with the parent. This is achieved in gdb with set follow-fork-mode child and set follow-fork-mode parent.
<gRB>, something similar was happening:
(gdb) set follow-fork-mode child (gdb) b *0x400a1a Breakpoint 6 at 0x400a1a (gdb) b *0x400bba Breakpoint 7 at 0x400bba (gdb) run blah Starting program: /root/usdhc/reverseChallenge blah [New process 1837] [Switching to process 1837] Breakpoint 6, 0x0000000000400a1a in gRb () (gdb) set follow-fork-mode parent (gdb) c Continuing. HiUere. I don't like debuggers. [Inferior 6 (process 1837) exited with code 01]
I set follow-fork-mode child, created breakpoints at the beginning and end of
<gRb>, set follow-fork-mode parent after entering
<gRb>, but the program quit with I don't like debuggers.
(gdb) x/30i $pc => 0x400acc <gRb+178>: call 0x400680 <getppid@plt> 0x400ad1 <gRb+183>: mov DWORD PTR [rbp-0xc],eax 0x400ad4 <gRb+186>: mov eax,DWORD PTR [rbp-0xc] 0x400ad7 <gRb+189>: mov ecx,0x0 0x400adc <gRb+194>: mov edx,0x0 0x400ae1 <gRb+199>: mov esi,eax 0x400ae3 <gRb+201>: mov edi,0x10 0x400ae8 <gRb+206>: mov eax,0x0 0x400aed <gRb+211>: call 0x400650 <ptrace@plt> 0x400af2 <gRb+216>: test rax,rax 0x400af5 <gRb+219>: jne 0x400b4b <gRb+305> 0x400af7 <gRb+221>: mov eax,DWORD PTR [rbp-0xc] 0x400afa <gRb+224>: mov edx,0x0 0x400aff <gRb+229>: mov esi,0x0 0x400b04 <gRb+234>: mov edi,eax 0x400b06 <gRb+236>: call 0x400660 <waitpid@plt> 0x400b0b <gRb+241>: mov edx,0x0 0x400b10 <gRb+246>: mov esi,0x0 0x400b15 <gRb+251>: mov edi,0x7 0x400b1a <gRb+256>: mov eax,0x0 0x400b1f <gRb+261>: call 0x400650 <ptrace@plt> 0x400b24 <gRb+266>: mov eax,DWORD PTR [rbp-0xc] 0x400b27 <gRb+269>: mov ecx,0x0 0x400b2c <gRb+274>: mov edx,0x0 0x400b31 <gRb+279>: mov esi,eax 0x400b33 <gRb+281>: mov edi,0x11 0x400b38 <gRb+286>: mov eax,0x0 0x400b3d <gRb+291>: call 0x400650 <ptrace@plt> 0x400b42 <gRb+296>: mov DWORD PTR [rbp-0x4],0x0 0x400b49 <gRb+303>: jmp 0x400b52 <gRb+312>
These calls to
<gRb> are apparently an old anti-debugging trick, as explained e.g. here. The forked process (which is not being followed by the debugger) realizes that
ptrace() has already been called and relays that information back to the parent process. The most straightforward way around this is to patch the program, i.e. to replace these calls with NOPs:
(gdb) set follow-fork-mode child (gdb) run blah Starting program: /root/usdhc/reverseChallenge blah [New process 1872] [Switching to process 1872] Breakpoint 6, 0x0000000000400a1a in gRb () (gdb) set *(0x400aed)=0x90909090 (gdb) set *(0x400aed+2)=0x90909090 (gdb) set *(0x400b1f)=0x90909090 (gdb) set *(0x400b1f+2)=0x90909090 (gdb) set *(0x400b3d)=0x90909090 (gdb) set *(0x400b3d+2)=0x90909090 (gdb) set follow-fork-mode parent (gdb) c Continuing. Breakpoint 7, 0x0000000000400bba in gRb ()
Et voilà, I got through
<gRb> undisturbed and was still debugging.
The main function
I left breakpoints at the beginning of
<gRb>, in case fork follow mode needed to be switched again.
<main>, there is a sequence of checks. The first one makes sure a password was supplied:
(gdb) x/6i $pc => 0x400dde <main+35>: cmp DWORD PTR [rbp-0x4],0x2 0x400de2 <main+39>: je 0x400df8 <main+61> 0x400de4 <main+41>: mov edi,0x407c88 0x400de9 <main+46>: call 0x400600 <puts@plt> 0x400dee <main+51>: mov edi,0x1 0x400df3 <main+56>: call 0x400690 <exit@plt> (gdb) j *0x400de4 Continuing at 0x400de4. Please supply a password! [Inferior 10 (process 2389) exited with code 01]
Then, the password is fetched from memory and checked for length:
(gdb) x/7i $pc => 0x400df8 <main+61>: mov rax,QWORD PTR [rbp-0x10] 0x400dfc <main+65>: add rax,0x8 0x400e00 <main+69>: mov rax,QWORD PTR [rax] 0x400e03 <main+72>: mov rdi,rax 0x400e06 <main+75>: call 0x400620 <strlen@plt> 0x400e0b <main+80>: cmp rax,0x22 0x400e0f <main+84>: je 0x400e1b <main+96>
A closer look:
(gdb) 0x0000000000400e06 in main () => 0x0000000000400e06 <main+75>: e8 15 f8 ff ff call 0x400620 <strlen@plt> (gdb) x/s $rdi 0x7fffffffe662: "blah"
Yep, that's my password. It is expected to have length
0x22, so from now on I would call the program with a password 34 characters long.
(gdb) x/2i $pc => 0x400e0b <main+80>: cmp rax,0x22 0x400e0f <main+84>: je 0x400e1b <main+96> (gdb) x/s $rdi 0x7fffffffe644: 'A' <repeats 34 times>
So, this check succeeds. The next few instructions fetch something from memory and then compare it to our password. Could this be the solution??
gdb) x/5i $pc-8 0x400e65 <main+170>: mov rsi,rax 0x400e68 <main+173>: mov edi,0x407ca8 => 0x400e6d <main+178>: call 0x400640 <strcmp@plt> 0x400e72 <main+183>: mov edx,eax 0x400e74 <main+185>: mov eax,DWORD PTR [rip+0x207402] # 0x60827c <a> 0x400e7a <main+191>: cmp edx,eax 0x400e7c <main+193>: jne 0x400e88 <main+205> (gdb) x/s $rsi 0x7fffffffe644: 'A' <repeats 34 times> (gdb) x/s $edi 0x407ca8: "ThisIsTheSuperSecretChallengePass"
Haha, right. Keep on trolling. The program continues if the password does not match this string.
Password munging part one
The next new function,
<doM>, consists mainly of a list of values - I noticed that there are 34 consecutive ones - and a loop. The important line is
0x400924 <doM+202>: xor al,BYTE PTR [rbp-0x5]
The supplied password is XOR'd character by character with the values at
rbp-0x30. The result is my password XOR'd with a bunch of arbitrary values:
Temporary breakpoint 25, 0x0000000000400934 in doM () => 0x0000000000400934 <doM+218>: 5d pop rbp (gdb) x/1s $rdx-33 0x7fffffffe644: "|\212\005E@AD_>*rb\241\311n3\351$,e\255\350n\255\256\207\354\064+*6\034%e"
This makes it a bit harder to extract the actual password in the next step.
Password munging part two
<tHw>, finally, is where it all happens. I found another list of values and a more complex series of transformations. Clearly, this was where the password was hidden. The exact mode of obfuscation did not matter any more now, though, because I had everything I needed. The password is revealed, character by character, on this line:
Breakpoint 27, 0x0000000000400d32 in tHw () => 0x0000000000400d32 <tHw+375>: 3a 45 f7 cmp al,BYTE PTR [rbp-0x9] (gdb) x/10i $pc => 0x400d32 <tHw+375>: cmp al,BYTE PTR [rbp-0x9] 0x400d35 <tHw+378>: jne 0x400d53 <tHw+408> 0x400d37 <tHw+380>: cvtss2sd xmm0,DWORD PTR [rbp-0x8] 0x400d3c <tHw+385>: movsd xmm1,QWORD PTR [rip+0x6f8c] # 0x407cd0 0x400d44 <tHw+393>: addsd xmm0,xmm1 0x400d48 <tHw+397>: cvtsd2ss xmm2,xmm0 0x400d4c <tHw+401>: movss DWORD PTR [rbp-0x8],xmm2 0x400d51 <tHw+406>: jmp 0x400d5d <tHw+418> 0x400d53 <tHw+408>: mov eax,0x0 0x400d58 <tHw+413>: call 0x40094e <wQx>
Looking at the values involved in this comparison:
(gdb) p /x $al $8 = 0x7c (gdb) x /x $rbp-0x9 0x7fffffffe257: 0x48
So, the XOR'd first character of the supplied password is expected to be
0x48, which means that XOR'ing
0x48 with the first value from the
<doM> list would give me the first character of the actual password. Actually, the
<doM> list is processed in reverse, so I needed to use
(gdb) x/5i 0x400869 0x400869 <doM+15>: mov BYTE PTR [rbp-0x30],0x3d 0x40086d <doM+19>: mov BYTE PTR [rbp-0x2f],0xcb 0x400871 <doM+23>: mov BYTE PTR [rbp-0x2e],0x44 0x400875 <doM+27>: mov BYTE PTR [rbp-0x2d],0x4 0x400879 <doM+31>: mov BYTE PTR [rbp-0x2c],0x1
0x48 ^ 0x3d is
0x75... which is the letter u. Hmm, I had an idea where this was going.
The kind of work robots do
I could have done this manually now for every loop iteration, but that seemed tedious, so I wrote a script instead:
import gdb passwd = "" xorlist =  # Replace ptrace() calls with NOPs def callback_patch_ptrace(): gdb.execute("set *(0x400aed)=0x90909090") gdb.execute("set *(0x400aed+2)=0x90909090") gdb.execute("set *(0x400b1f)=0x90909090") gdb.execute("set *(0x400b1f+2)=0x90909090") gdb.execute("set *(0x400b3d)=0x90909090") gdb.execute("set *(0x400b3d+2)=0x90909090") # Follow child process after fork() when in <bHw> def callback_follow_child(): gdb.execute("set follow-fork-mode child") # Follow parent process after fork() when in <gRb> def callback_follow_parent(): gdb.execute("set follow-fork-mode parent") # Snarf the XOR list in <doM> def callback_xorlist_append(): global xorlist xorlist.append(int(gdb.parse_and_eval("$eax"))) # Apply XOR again to compared chars in <tHw> def callback_passwd_append(): global xorlist, passwd passwd += chr(int(gdb.parse_and_eval("*(byte *)($rbp-0x9)")) ^ xorlist.pop(0)) print ("[+]", passwd) gdb.execute("set $al=*(byte *)($rbp-0x9)") # Breakpoint handler from w3pwnz <http://goo.gl/ljhyOk> class HitBreakpoint(gdb.Breakpoint): def __init__(self, loc, callback): super(HitBreakpoint, self).__init__( loc, gdb.BP_BREAKPOINT, internal=False ) self.callback = callback def stop(self): self.callback() return False HitBreakpoint("*0x400dbb", callback_patch_ptrace) HitBreakpoint("*0x4009c3", callback_follow_child) HitBreakpoint("*0x400a1a", callback_follow_parent) HitBreakpoint("*0x400924", callback_xorlist_append) HitBreakpoint("*0x400d32", callback_passwd_append)
It does all the stuff discussed so far. (I found the breakpoint handler here.)
Running it from scratch:
root@kali:~/usdhc# gdb -q (gdb) file reverseChallenge Reading symbols from reverseChallenge...(no debugging symbols found)...done. (gdb) source findpw.py Breakpoint 1 at 0x400dbb Breakpoint 2 at 0x4009c3 Breakpoint 3 at 0x400a1a Breakpoint 4 at 0x400924 Breakpoint 5 at 0x400d32 (gdb) run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Starting program: /root/usdhc/reverseChallenge AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [New process 2659] [New process 2661] [New process 2662] [+] u [+] us [+] usd [+] usd2 [+] usd20 [+] usd201 [+] usd2016 [+] usd2016- [+] usd2016-H [+] usd2016-Ha [+] usd2016-Hac [+] usd2016-Hack [+] usd2016-Hacki [+] usd2016-Hackin [+] usd2016-Hacking [+] usd2016-HackingC [+] usd2016-HackingCh [+] usd2016-HackingCha [+] usd2016-HackingChal [+] usd2016-HackingChall [+] usd2016-HackingChalle [+] usd2016-HackingChallen [+] usd2016-HackingChalleng [+] usd2016-HackingChallenge [+] usd2016-HackingChallenge- [+] usd2016-HackingChallenge-F [+] usd2016-HackingChallenge-Fo [+] usd2016-HackingChallenge-For [+] usd2016-HackingChallenge-Fore [+] usd2016-HackingChallenge-Foren [+] usd2016-HackingChallenge-Forens [+] usd2016-HackingChallenge-Forensi [+] usd2016-HackingChallenge-Forensik [+] usd2016-HackingChallenge-Forensik! Program received signal SIGSEGV, Segmentation fault. [Switching to process 2662] 0x0000000000400d92 in red_pill ()
I did not even bother with this
<red_pill> stuff any more. I think it is another trick to use a SIGSEGV handler to output the token in the end, which will not work in a debugger... or something.
root@kali:~/usdhc# ./reverseChallenge usd2016-HackingChallenge-Forensik! 611337