No Gadgets
ROPing using `leave ; ret` rather than `pop rdi ; ret`
Background
No Gadgets
was an easy rated pwn I created for this CTF. The premise was based around the fact that pop rdi ; ret
is no longer present in binaries compiled against glibc 2.34+
. While a lot of challenges will add this gadget back in manually, I wondered whether you could still solve an overflow challenge without this seemingly essential gadget.
The process of crafting this challenge is what lead me to discover some of the techniques that I've detailed in this section of my blog, including the gets
techniques.
I decided that using gets
would make the challenge trivial, so I purposefully decided to not use it, instead opting for using leave ; ret
to control rbp
, which allowed you to get arbitrary writes to the GOT
.
Anyways, enough yapping, onto the challenge itself.
Analysis
Analyzing the source code
The application is quite simple, it just prompts the user for input, and reads too much data onto the stack leading to a classic buffer overflow. It then checks the string length to check for a buffer overflow, and if an overflow is detected it exit
s so that main
doesn't return. Otherwise it returns normally.
Protections
Running checksec
yields:
So we're dealing with pretty standard protections, minus PIE
. The lack of canary and PIE
will make our overflow feasible, and no NX
means no shellcode (oh well, who needs shellcode anyways).
Intended Solution
Disclaimer
The following is my intended solution, however after reading some of the other solutions from other players, I quickly realised that mine was overcomplicated. While they (mostly) used the same techniques that I wanted to cover, they were able to use them in a simpler and more concise way. I will briefly go over a few of them at the end.
Exploitation
Bypass strlen
check
strlen
checkThe first problem we face is the buffer overflow check, however fortunately for us this is a flimsy check. The string length is the number of bytes before a terminating null byte, however fgets
doesn't stop us from writing null bytes of our own, so we could place a null byte at the beginning of the buffer to force strlen
to return a value less than 0x80
, hence bypassing the check.
Finding gadgets
Due to the program being compiled to run on glibc versions 2.34 and above, the __libc_csu_init
function is no longer present, and so most of the useful pop
gadgets are gone, like pop rdi ; ret
. This stops us using the classic ret2plt
attack, where we can call puts
on some address containing a libc address, to get a leak. Instead we need to leak libc using ROP in a different way, and for that we need other gadgets.
Most of the gadgets found by ROPgadget
aren't very useful, except for leave ; ret
. This is a gadget found at the end of some functions, like main
in this case, which aids in switching between stack frames. It does this by restoring the old rbp
of the calling function (saved base pointer), moving rsp
up and returning. leave
is effectively
rbp
here points to the saved base pointer, so moving it into rsp
allows it to then be popped into rbp
. Then the return address is stored directly afterwards, so then the ret
returns to the previous code.
Example (some function called by main):
After leave ; ret
:
If you want more info, you can go here.
Rough exploit plan
rbp
is used to keep track of where the variables stored in a stack frame are. You can see this in action when looking at the disassembly. The following is from the call to fgets
.
The address of the buffer is relative to rbp
(as would other variables, if there were others).
In our buffer overflow, we don't only overwrite the return address, but also the saved base pointer, which would then get loaded into rbp
, due to leave
. So with our overflow, we can control rbp
, and we also have the above gadget which writes arbitrary data to the buffer at [rbp-0x80]
, so combining these theoretically grants an arbitrary write.
But what to overwrite? Remember we don't have a leak, so we're limited to the binary's memory. Recall that the binary has Partial RELRO
, which means the GOT
is writable!
An appealing target is strlen@GOT
, because it's a function that takes one argument: our buffer! So we could either:
overwrite
strlen@GOT -> puts@PLT
and pointbuf
to aGOT
entryoverwrite
strlen@GOT -> printf@PLT
and pointbuf
to a format string
Once we have a libc leak, the ret2libc should be trivial, because libc has a pop rdi
gadget.
Getting arbitrary writes
So to get an arbitrary write onto strlen@GOT
, we'll need to set rbp
to &strlen@GOT + 0x80
(since the fgets
gadget uses [rbp-0x80]
), and rip
to the fgets
gadget. This is easily done in the first overflow, but then what? Well we'll run through the rest of the main function and hit leave ; ret
again. Running through what leave
does, we see that it copies rbp
to rsp
, then pops rbp
and rip
. Since rbp
is 0x80
bytes after our data input, we can use the overflow to control rbp
and rip
again! (and we can do this for any rbp
)
However we run into a problem when going with this approach.
If we wrote to the earliest possible address 0x404000
, then rbp = 0x404080
. But the entries for stdin
, stdout
, stderr
are between our write and rbp
, and since we don't have a libc leak, we can't overwrite these without corrupting them and causing a crash.
This is why we need to make another write to 0x404080
first, to fill in a fake saved rbp
and rip
.
A naive approach to implement the above could look as follows:
This will fail for another reason (which isn't due to my incompetence (hopefully!)), which is easier to see in gdb.
Here we get to the point where we're about to overwrite the GOT
with fgets, but see something interesting. Since we've done multiple leave
s, we've stack pivoted to the writable region. This on its own isn't a huge deal, but since it's so close to all the important addresses in the GOT
, there's a good chance that the stack usage for the call to fgets
will clobber them.
And clobber them it does!
So we need to be more careful about where rsp
is. Fortunately most functions used in main
don't use that much stack space (except for printf
, which we can skip), so as long as rsp
is further along in the writable region, its stack usage won't clobber anything. We can achieve this by using an extra leave ; ret
.
Recall that after leave ; ret
, rsp
points to directly after the saved rbp and rip (due to popping), and since we want rsp
at a higher address, we can place our own saved rbp
and rip
pairs at high addresses. Then for the initial leave ; ret
(at the end of main), we set rbp
to point to that pair, and rip
to leave ; ret
. That way we control rbp
and rip
, while also having rsp
be far enough to prevent clobbering!
Getting libc leak
Applying this gives us the script above, which successfully leaks libc base. We overwrite strlen@GOT
to puts@PLT
(not printf@PLT
because it uses a lot of stack space), and after we overwrite GOT
, strlen
gets called. The way it's set up is that buf -> puts@GOT
, which is now puts@PLT + 6
. But when strlen
gets called, puts
is called, which resolves puts@GOT
in time for buf
to now point to puts@LIBC
, giving us a leak!
And we don't have to worry about the return value being greater than 0x80
, because puts
returns the number of bytes outputted, which would be 7
(6
for the address, 1
for the newline).
Getting a shell
From here getting a shell is trivial. Since we have libc, we can easily find all the gadgets we need, so all we need is to write another rop chain which calls system("/bin/sh")
. In my rop chain I used system
, which also uses a lot of stack space, so I made rsp
point to a further up location again, but you could just use execv
and not face this problem.
Solver
Other solutions
While the above solution works, there's a lot of fiddling around with stack frames, rbp
and rsp
which makes it a bit clumsy at times.
Many other solutions were able to use the idea of overwriting the GOT in a more effective way. strlen@GOT
was still a prime target for overwrites, as I had intended, however the way it was used was interesting.
What caused me problems was that I would overwrite strlen@GOT
to point directly to a puts
function, which returns normally, then I would reach the end of main
, which forced me to use the return address to continue execution, which led to stack fiddling nonsense.
Neatest solution
However many solutions were able to keep rsp
on the stack the whole time. My favourite one was by laxa, which overwrote strlen@GOT
to the part in main
which called printf
, without setting arguments.
This allowed them to call printf
on the GOT
, but was also able to immediately write to the GOT
again, as the call to fgets
was immediately afterwards. Then with the leak, they could overwrite strlen@GOT
again to system
.
Fun with (fun)lockfile
The funniest one was one by Nauxuron, which used a quirk of printf
that placed a pointer to funlockfile
in rdi
after returning, so that they could then call puts
on that to get a libc leak.
Interestingly when I was designing this challenge, I was using gets
, and discovered some nifty unintended solutions which also relied on a certain value being into rdi
after a call (to gets
in this case), which led to the ret2gets page. While I fixed that, I did not anticipate this happening with printf
, because why on earth would this happen??
After looking at glibc source code (yes I was that curious), I found out why. Just like with ret2gets, it's actually to do with unlocking happening at the end of the function, specifically here (and here when buffered).
The important macros here are __libc_cleanup_region_start
and __libc_cleanup_region_end
, defined here.
printf
stores a _pthread_cleanup_buffer
to keep track of how to cleanup the FILE
when the function returns. It does this by storing a cleanup routine, and an argument, which would be the FILE
. This is where that happens (in buffered_vfprintf
because the challenge unbuffered stdout
).
Huh look at that, the __routine
is _IO_funlockfile
, and if we look at _pthread_cleanup_buffer.
The __routine
is stored at the start. And lastly, in __libc_cleanup_region_end
, there's this line.
So this cleanup buffer is passed to a function (that I'm personally surprised isn't inlined), and rdi
doesn't get changed at all, so it ends up pointing to the cleanup buffer, with funlockfile
being the value it ends up pointing to!
But what about the cleanup routine? Interestingly funlockfile
isn't actually executed! The 0
in _IO_cleanup_region_end (0);
represents the DOIT
parameter, and if it's 0, then the function isn't called.
From what I can see, this behaviour has changed in 2.37
, at least this only happens when the application is multithreaded?
Who needs leaks anyway?
This one was by nobodyisnobody
(on discord), and it was probably my favourite, simply for how creative it was, and also how it required no leaks at all! It managed to squeeze blood out of the stone that was the limited collection of gadgets available in the binary itself!
It did this with the following gadgets:
By using the above gadgets, they were able to set rbp
so that rbp+0x3d
pointed to a GOT
entry, then using add dword ptr [rbp - 0x3d]
, they could add values to the GOT
entries, instead of overwriting them. This way, you don't need a libc leak, and you can change these functions to point to useful gadgets. However, to be able to do that, we need to be able to control ebx
.
That's where the top gadget comes in. You'll notice how I've added the ...
which is because this is a fairly long gadget, but not a massively complex one. It's as follows:
Adds al
to bl
(the lowest 8 bits of the eax
and ebx
registers respectively), then goes through a nop sled to end up in deregister_tm_clones
. This sets eax
to 0x404040
(thus setting al
to 0x40
), then just returns.
It's important to note that rbx
and rax
are set to 0
when main
returns, so the first add
does nothing to ebx
, but sets al
to 0x40
. Then if we call this gadget again, we can add 0x40
to bl
. Then we could add this to dword ptr [rbp - 0x3d]
!
So now we're able to add 0x40
, 0x80
or 0xc0
to an arbitrary dword
, but how do we use this? After all it's a fairly weak primitive, since we have such limited control over ebx
. Wouldn't it be nice if we could get pop rbx
?
Well it just so happens, when you add 0x80
to setvbuf
, you get:
A pop rbx
gadget! Fortunately the previous r13
checks don't pass, so we're able to reach pop rbx
. Now with complete control over ebx
, we can completely change a GOT
entry to system
, in this case we do strlen
, as our buffer is passed to it.
I cleaned up and slightly modified the script to get the following:
ret2dlresolve
While I didn't see any solutions using this, I always expected that it was possible, but I left it out as I thought it was more complicated than my intended.
Last updated