I’ve participed to NDH2013 this year and worked on a very interesting binary : k1986. It comes with two files :
aris@kali64:~/ndh2013$ ls -l k1986 license.db -rwxr-xr-x 1 aris aris 14984 jun 23 02:07 k1986 -rwx------ 1 aris aris 360 jun 22 22:54 license.db aris@kali64:~/ndh2013$ file k1986-orig license.db k1986-orig: ELF 64-bit LSB executable, x86-64, invalid version (SYSV), for GNU/Linux 2.6.32, dynamically linked (uses shared libs), corrupted section header size license.db: data
It’s starting well, corrupted ELF file. The content of license.db seems encrypted, so my first guess was that it was a DRM server of some kind. It becomes more fun when you try to check what it does:
aris@kali64:~/ndh2013$ objdump -t k1986-orig objdump: k1986-orig: File format not recognized aris@kali64:~/ndh2013$ gdb --quiet ./k1986-orig "/home/aris/ndh2013/k1986-orig": not in executable format: Format de fichier non reconnu (gdb) quit aris@kali64:~/ndh2013$ nm ./k1986-orig nm: ./k1986-orig: File format not recognized aris@kali64:~/ndh2013$ ldd ./k1986-orig n'est pas un exécutable dynamique
So all classic tools give up… but that’s ok, let’s gdb-attach the process..
aris@kali64:~/ndh2013$ gdb --quiet -p 23627 Attaching to process 23627 "/home/aris/ndh2013/k1986": not in executable format: Format de fichier non reconnu (gdb) info reg eax 0xfffffe00 -512 ecx 0xffffffff -1 edx 0x5c4c 23628 ebx 0x18a70700 413599488 esp 0xf9154780 0xf9154780 ebp 0xf91547e8 0xf91547e8 esi 0x0 0 edi 0x18a709d0 413600208 eip 0x18e02e75 0x18e02e75 eflags 0x246 [ PF ZF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 (gdb) x/i $eip => 0x18e02e75: Cannot access memory at address 0x18e02e75 (gdb) x/$ $rip A syntax error in expression, near `$rip'. (gdb)
That’s the real fun. No matter what I did to that binary, I never managed to make gdb attach to it in 64 bits mode. I tried to make the binary unreadable (chmod 100) or to patch the missing fields/sections but it did not good. I lost too much time on this one (btw if you managed to make gdb work on this, I’d appreciate a lot).
Time to launch IDA. We discover a dynamically loadable executable that’s linked to libpthread and libc. Since the section header table is broken, IDA was not able to match the PLT and GOT entries with that of the binary. However it was smart enough to see a list of exports. We see very classic things for such a binary (open, socket, bind, listen, accept, read, write, close, printf, …) and other more unusual (pthread_mutex_lock). At which point I freaked out it would be a very hard to understand multithreaded program, as I had no debugger yet.
When having no symbols, you have to recognize the PLT and GOT by yourself. As I couldn’t simply gdb into the process to make it do the symbol resolutions for me, I took another approach and used the RTLD capabilities of rewriting symbols:
$ cat intercept.c #include int bind(){ void **i; i = &i; printf("bind: %p\n",i[2]); exit(0); } $ gcc -fPIC -shared -o intercept.so intercept.c && LD_PRELOAD=./intercept.so ./k1986 bind: xxxxxx
This gave me the return addresses of many libc functions, and I was able to rename the PLT entries according to them. from that moment, reversing the binary was piece of cake, if we except the many anti-debugger patterns found into the code. I only removed the patterns at the places that made the local variables resolutions hard for IDA when it was important to me. Go ahead, 0x90 all that crap !
Now I wanted to understand how the binary worked and what really happened with the multithreading. Details apart, there’s only one lock and its only purpose is to avoid fork(). Now, I found a very interesting snippet: In function compare_input_3 (which was, as you’re going to see, something else), we can see that the data we give in goes in many loops. The most interesting thing is that we initialize a local buffer of 256 bytes with values 00, 01, 02, 03, … If you are familiar with cryptography, you’d recognize it’s the first step of an RC4 key setup. The key is being set in an other function and is statically set at 0x4023ae : “90 3F 8E 7F 8A”. Let’s give it a try:
>>> rc4.WikipediaARC4("903F8E7F8A".decode("hex")).crypt("\x00"*256).encode("hex") '8fd94d70a9ce04bb7ba97fdd632d238e52bcdc0bab8bd9f0f7055e6084e76347fec2ce9910c7aaccac65b2c8f8c36 ee0d9cdaaa3f657173152a6580b468f91e91120c1384ec4210c564c7732e6bf80bbd35ccc9cd8fc1d9e44a425a85fc bfa96f746c0582b135bce5ca5a40be48eecdee6310fe7f10a3d775abf26c0b7610ed841ecd3d6aa691f8689981e43c c6b95822e3f632747aef16f5c1b25c611db3fa7e9348f96e1d9665963b3e01eb7e367986f413e6fe12dead33718542 32caac84f2f4c37448f9aa300fee983cbf4b6adb7405f5bf5cf9bc60ef3c2f550b2ad5fbe7275a2fadac0c627d490e 7c54389f4479726b835cd6c4503bc1d8706337a05aa'
So everything you send is going to be XOR’ed with this value. Now, let’s see what’s happening with that data when it’s decrypted:
We can see that for every byte that is received, something like this happens:
input[0] + input[0] == 0x9c input[1] << 2 == 0x10 input[2] << 3 == 0x40 input[3] << 4 == 0xa0 value = atoi (&input[4]) input[6] << 3 == 0xd0 read_license (value, &input[7]);
I noticed the license key was encrypted, and also that read_license was doing operations with malloc/free. I though there was a cheap occasion to avoid using the debugger and instrumenting one of the functions by myself:
$ cat intercept.c #include int (*fct)(int, char *); char buffer[256]; char buffer2[256]; int socket(){ fct = (void *)0x401cf2; fct(1,"Hello"); write(1,buffer,256); write(1,buffer2,256); exit(0); } int free(char *ptr){ if(ptr == 0) return; printf("free %s\n",ptr); write(1,ptr,256); } char *malloc(int s){ printf("Malloc\n"); static int a = 0; if (a) return buffer2; ++a; return buffer; } aris@kali64:~/ndh2013$ LD_PRELOAD=./intercept.so ./k1986 Malloc Malloc $*O~I*IpH..xL}J+{,+O)-|N/MysFtN}/*+M}H.L}Eu/IyIxL-,.rDsDNwNtFuMx{I~HxH})L~NwO,}|E'C%{K|vGp JzBq Fut'Dt%yMzIqEq&'us+'+xN{H,$vN+"!CrK{H.'.O*|vFsIqr#wwCvsFO~)K)J/(MtuDN|LvE}H~xH+ I+&$wFwDvCz(JrA%,!+)$64:e17cc98f772d417a3ce261df512c2ab4 52:3f45067f05fb180b8f0014a23648d677 99:2385ba276005a5e2098c0acb9bdf8f07 17:083d5f3bcd7c0b39e473844f1326decf 46:840c653d087e8e1821b1903f0981ae2d 05:8efc22fcc45fc5901f1bbce521f29bc1 20:3856bd0cbb94460c113259b0b83d9049
I found an easy way to dump the content of the key file. The two ciphers code on the left must be the value parsed by atoi, and the one on the right must be the right value. Doing so tentatives shows that the server is going to tell you if your encrypted input matches the begin of the token from the list. The exploit follows. (also on pastebin with a better presentation).
#!/usr/bin/python # Exploit for k1984 # Aris Adamantiadis (les pas contents) # unfortunately coded a few hours after the CTF was over :( # aris@kali64:~/ndh2013$ python xp.py # found 05:8efc22fcc45fc5901f1bbce521f29bc1 # found 06:98adbaaef36e718f479db3b8dad331c9 # found 13:7da8b66f82aeba067e33859583c4153f # found 17:083d5f3bcd7c0b39e473844f1326decf # found 20:3856bd0cbb94460c113259b0b83d9049 # found 35:167f0dbb43c6430cd2d3b4e8f79dd769 # found 46:840c653d087e8e1821b1903f0981ae2d # found 52:3f45067f05fb180b8f0014a23648d677 # found 64:e17cc98f772d417a3ce261df512c2ab4 # found 99:2385ba276005a5e2098c0acb9bdf8f07 import socket crypted = "8f d9 4d 70 a9 ce 04 bb 7b a9 7f dd 63 2d 23 8e" + \ "52 bc dc 0b ab 8b d9 f0 f7 05 5e 60 84 e7 63 47" + \ "fe c2 ce 99 10 c7 aa cc ac 65 b2 c8 f8 c3 6e e0" + \ "d9 cd aa a3 f6 57 17 31 52 a6 58 0b 46 8f 91 e9" + \ "11 20 c1 38 4e c4 21 0c 56 4c 77 32 e6 bf 80 bb" + \ "d3 5c cc 9c d8 fc 1d 9e 44 a4 25 a8 5f cb fa 96" crypted = crypted.replace(" ","").decode("hex") def xor_strings(xs, ys): return "".join(chr(ord(x) ^ ord(y)) for x, y in zip(xs, ys)) offset = 65 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,0) s.connect(("127.0.0.1",2001)) def try_pass(offset, string): payload = chr(0x9C /2) + chr(0x10/4) + chr(0x40/8) + chr(0xa0 / 16) +\ chr(ord('0') + offset/10) + chr(ord('0') + offset % 10) + chr(0xd0 / 8) +\ string + "\x00" s.send(xor_strings(payload,crypted)) x = s.recv(256) #print "recv:" + x if(x.find("True")!= -1): return True else: return False for offset in xrange(100): string = "" for i in xrange(32): if (i>0 and len(string)==0): break; for c in xrange(16): x = try_pass(offset, string + "%x"%c) if x: string += "%x"%c #print string break if(len(string) > 0): print "found %.2d:"%offset + string
Notes on debugger: while the write-up lets think I did not use a debugger, I extensively used EDB. Its main pros are : “not GDB”. Its main weakness is : “not GDB”. It was unfortunately not very stable and I couldn’t save a session or breakpoints for later use. Also, it sometimes confused .text/.data and refused to set my breakpoint because it thought it was in .data.
Well, still better than opening core files in gdb.
What I did that worked well:
– Identify crypto
– Use LD_PRELOAD to extract symbol names and instrument the print_license() functions
– patching the anti-debugging patterns in IDA (even tough I could automatize it)
What did not:
– Try for hours to make GDB work
– Not doing a POC in python from the very beginning. I lost too much time doing xor operations in an other window and putting the results back in my shell command
– Trying to patch the binary to make it acceptable for GDB, objdump & friends. I still haven’t found why ld.so can read the file fine and not bfd tools or IDA.
– Discovering edb for the first time that night. I’m sure I underused the capabilities of edb