Shellcode, but without the guesswork
The problem with shellcode exploits as they are is that the locations of it are questionable - wouldn't it be cool if we could control where we wrote it to?
Well, we can.
Instead of writing shellcode directly, we can instead use some ROP to take in input again - except this time, we specify the location as somewhere we control.
If you think about it, once the return pointer is popped off the stack ESP will points at whatever is after it in memory - after all, that's the entire basis of ROP. But what if we put shellcode there?
It's a crazy idea. But remember, ESP will point there. So what if we overwrite the return pointer with a jmp esp
gadget! Once it gets popped off, ESP will point at the shellcode and thanks to the jmp esp
it will be executed!
ret2reg extends the use of jmp esp
to the use of any register that happens to point somewhere you need it to.
Any function that returns a pointer to the string once it acts on it is a prime target. There are many that do this, including stuff like gets()
, strcpy()
and fgets()
. We''l keep it simple and use gets()
as an example.
First, let's make sure that some register does point to the buffer:
Now we'll set a breakpoint on the ret
in vuln()
, continue and enter text.
We've hit the breakpoint, let's check if RAX points to our register. We'll assume RAX first because that's the traditional register to use for the return value.
And indeed it does!
We now just need a jmp rax
gadget or equivalent. I'll use ROPgadget for this and look for either jmp rax
or call rax
:
There's a jmp rax
at 0x40109c
, so I'll use that. The padding up until RIP is 120
; I assume you can calculate this yourselves by now, so I won't bother showing it.
Awesome!
Super standard binary.
Let's get all the basic setup done.
Now we're going to do something interesting - we are going to call gets
again. Most importantly, we will tell gets
to write the data it receives to a section of the binary. We need somewhere both readable and writeable, so I choose the GOT. We pass a GOT entry to gets
, and when it receives the shellcode we send it will write the shellcode into the GOT. Now we know exactly where the shellcode is. To top it all off, we set the return address of our call to gets
to where we wrote the shellcode, perfectly executing what we just inputted.
I wonder what you could do with this.
No need to worry about ASLR! Neither the stack nor libc is used, save for the ROP.
The real problem would be if PIE was enabled, as then you couldn't call gets
as the location of the PLT would be unknown without a leak - same problem with writing to the GOT.
Thank to clubby789 and Faith from the HackTheBox Discord server, I found out that the GOT often has Executable permissions simply because that's the default permissions when there's no NX. If you have a more recent kernel, such as 5.9.0
, the default is changed and the GOT will not have X permissions.
As such, if your exploit is failing, run uname -r
to grab the kernel version and check if it's 5.9.0
; if it is, you'll have to find another RWX region to place your shellcode (if it exists!).
You can ignore most of it as it's mostly there to accomodate the existence of jmp rsp
- we don't actually want it called, so there's a negative if
statement.
The chance of jmp esp
gadgets existing in the binary are incredible low, but what you often do instead is find a sequence of bytes that code for jmp rsp and jump there - jmp rsp
is \xff\xe4
in shellcode, so if there's is any part of the executable section with bytes in this order, they can be used as if they are a jmp rsp
.
Try to do this yourself first, using the explanation on the previous page. Remember, RSP points at the thing after the return pointer once ret
has occured, so your shellcode goes after it.
You won't always have enough overflow - perhaps you'll only have 7 or 8 bytes. What you can do in this scenario is make the shellcode after the RIP equivalent to something like
Where 0x20
is the offset between the current value of RSP and the start of the buffer. In the buffer itself, we put the main shellcode. Let's try that!
The 10
is just a placeholder. Once we hit the pause()
, we attach with radare2 and set a breakpoint on the ret
, then continue. Once we hit it, we find the beginning of the A
string and work out the offset between that and the current value of RSP - it's 128
!
We successfully pivoted back to our shellcode - and because all our addresses are relative, it's completely reliable! ASLR beaten with pure shellcode.
This is harder with PIE as the location of jmp rsp
will change, so you might have to leak PIE base!
Using Registers to bypass ASLR
ret2reg simply involves jumping to register addresses rather than hardcoded addresses, much like . For example, you may find RAX always points at your buffer when the ret
is executed, so you could utilise a call rax
or jmp rax
to continue from there.
The reason RAX is the most common for this technique is that, by convention, the return value of a function is stored in RAX. For example, take the following basic code:
If we compile and disassemble the function, we get this:
As you can see, the value 0xdeadbeef
is being moved into EAX.