Nuit Du Hack CTF 2013 : k1986 write-up

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

img1

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 !

img2

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:

img3

>>> 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:
img4
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

No Comments Yet

You can be the first to comment!

Speak Your Peace

  • Comment Policy:Could go here if there's a nagging need Login Instructions: Would go here if there's a desire.