PWN2: TSGCTF beginners_pwn 2020 format string and ret2csu

Hello guys this is another write-up for the challenge named "Beginner's Pwn" (and it's not for beginners xD ), let's go, the binary and the full exploit in my github repo, the link in the bottom !

File:


we're dealing with :
- 64 bits binary
- not stripped binary (it's easy to reverse engineer the binary and the functions named will take only their names and not gibberish names )

checksec:



As you can see :

- Partial RELRO, we can think of overwrite the GOT (we definitely will)

- No PIE, ASLR don't apply for the binary so the binary takes fixed addresses every time we run the program and we can see that the base address is 0x400000, please note that when you see PIE disabled we can only expect the addresses of the binary and not libc or heap or any other page of memory.

Reverse engineering

Let's open up ghidra and feed it our binary so we can see what's happening inside
, open ghidra, create a new project name it "TSGCTF", click 'import file':


Go here and filter for main (because the binary is not stripped just type main ghidra will find it ):



Click once in the main in see the decompiler in the right , this is the main function :



It only setup the canary, read 0x18 bytes from stdin to buffer, and scanf(buffer), which makes scanf vulnerable to format string vulnerability, then it will check if we didn't overwrite the stack using __stack_chk_fail(), and if not it will return 0.

Now I want to decompile readn(), what is readn() ?? it's definitely not from Glibc, let's decompile it , this is how ghidra decompile it :

Huh ?? what's that ? that definitely not readn(), sometimes ghidra or IDA are not perfect and you should audit the code using a debugger to see what the function is actually doing.

Let's fire up gdb and see what readn() do :





type disas readn and press enter :



This is the code that reads from stdin using syscall
After this code is only a check if read() succeed or not but I will be using it later on so let's take a look:



We can see that the code after the syscall, store the return value of read() into [rbp-0x8] , and then it will add whatever the value of rax to [rbp - 0x10] then later it will check by cmp rax , 1 if they are not equal it will just jump to readn+130, it will then check again [rbp-0x10] if it's equal to zero it will call exit

Please make notes of this :

- The value of RAX don't change from the return value of syscall up into call exit it remains the same.

I think we now know about the binary sufficiently and I we can exploit it.


The plan:

1) - Using the format string vulnerability in scanf to write to the GOT, and in the same time writing to the stack (overwrite the canary rbp and then the return value, I will show you how) make note : no size limitation for scanf you can write 99999999999999999 bytes of data no worries xD

2)- Overwrite the stack_chk_fail() with the gadget return so when overwriting the canary this function will not trigger.

3)- Overwrite exit with the gadget add rsp, 0x8; ret  (I will tell you why)

4)- Now the goal is to call execve("/bin/sh", 0, 0) so we will be using 2 syscalls, one to set RAX the value we want because we don't have a gadget that do this, and second is the call to execve()


The exploit :

For the first step of the plan, how to I get scanf to write to the GOT :
I was playing with the binary and I noticed that when I set the buffer to %7$s, scanf(%7$s) will read our input into the next address for buffer (in the stack) and we can set that address easily.

Example :

if "%7$s" get's written to 0x400
The address that it will writing our input to is the address in 0x408


Don't worry if you don't see this, I even don't LOL I just found it by try and fail , let's see an example (I want to write something into 0xffffffffffffffff), this is how  it looks like :



I made a pause() before sendline just to show this in gdb :
I will run this part of code :


Fire up gdb and attach it to the pid 7087:

f
presss "Enter ":


type "c" or "continue" in gdb and press enter :



Go to the process it will look like this waiting for our input :


Type blabla and press enter and go to the gdb normally it will trigger a problem because we can't write to 0xffffffffffffffff invalide address:


And yes as we expected let's see the reason of the SIGSEGV :

it stops here :

Because we wanted to write r15 to r14, let's see the value of those registers :


0x62 is "b" the first letter in blabla.


So perfect until here we know that we have write-what-where vulnerability.
I will be using this to overwrite the GOT.

So how we can just overwrite the GOT and in the same time the stack just add %s to the buffer  like this :


if you write "blabla maher" to this first blabla will go to 0xfffffffffffffff and then maher will go to the stack, easy just make sure to add space in between that's a separator for scanf()

So those are all examples this is the actual part of exploit that do what we want for step 1 :


and when we open the GOT three functions appear in this order : 



So this part will just make __stack_chk_fail() and scanf() as return and exit as add rsp+0x8, ret


Now that we have the GOT take the addresses that we want, now we need to add space and then add our ROP chain like this :



b"M"*(8*3) just a padding to the return pointer.

Now what the payload contain ???

The payload will have our Csu rop chain, please if you don't know how ret2csu works google it !

ret2csu is an easy technique to set your registers with the values you want.

This is the function that set the registers edi, rsi, rdx :


Please note that I set the rbp to bss + 0x3b just to pass some checks that appear in the second half of readn().
And the payload contain this :



Do you see what this is actually doing ??
It just the first part of the payload just call read(0, bss, 0x100)


bss = 0x404060 I pick it randomly, but it stills in rw- bss.

why I want to call read() ??
Answer :

To add /bin/sh to the a known memory (0x404060) and provide some data that will go bypass the second part of readn() checks, go back if you forget it. And the total number of bytes written to read need to be 0x3b so the return value will be 0x3b (RAX = 0x3b) which is the syscall to execve.


The second part of the payload just call execve(0x404060, 0, 0) which will pop a shell.


This is the data I sent to read.
I will explain why this is the data I want to send to read:

Because we are using syscall that is in readn() we need to complete the chain after the first call to read() after setting  RAX to 0x3b.

when the first syscall is used , we need to complete the second stage which is the second half of readn(), and we did set the rbp to BSS-0x3b if you remember using ret2csu,
so the check is this :

Add [RBP - 0x10], RAX
...
 CMP [RBP - 0x10], 0

Now if you think about rbp - 0x10 in our case it's 0xffffffffffffffc5 which is -0x3b
and we added some padding just to achieve 0x3B, now RAX is 0x3B, 
[RBP - 0x10]=-0x3B
Add [RBP - 0x10], RAX = 0

Boom we passed the check and we will call exit()


Now why I changed exit to add rsp, 0x8; ret simply to chain our ROP and execute execve(), because call will push an address in the top of the stack if I just changed exit() to ret this will not be helpfull and we will return to 0x4011d8
But to return to the ROP chain that will set our registers and call execve() we need Add rsp, 0x8; ret.


This is the full exploit :





I'm looking forward your feedback.

The exploit and the binary:
https://github.com/MaherAzzouzi/PWNing/tree/master/TSGCTF

Comments