As of glibc 2.34, the CSU has been hardened to remove the useful gadgets. This patch is the offendor, and it essentially removes __libc_csu_init
(as well as a couple other functions) entirely.
Unfortunately, changing this breaks the ABI (application binary interface), meaning that any binaries compiled in this way can not run on pre-2.34 glibc versions - which can make things quite annoying for CTF challenges if you have an outdated glibc version. Older compilations, however, can work on the newer versions.
Obviously, you can do a ret2plt followed by a ret2libc, but that's really not the point of this. Try calling win()
, and to do that you have to populate the register rdx
. Try what we've talked about, and then have a look at the answer if you get stuck.
We can work out the addresses of the massive chains using r2, and chuck this all into pwntools.
Note I'm not popping RBX, despite the call
. This is because RBX ends up being 0
anyway, and you want to mess with the least number of registers you need to to ensure the best success.
Now we need to find a memory location that has the address of win()
written into it so that we can point r15
at it. I'm going to opt to call gets()
again instead, and then input the address. The location we input to is a fixed location of our choice, which is reliable. Now we just need to find a location.
To do this, I'll run r2 on the binary then dcu main
to contiune until main. Now let's check permissions:
The third location is RW, so let's check it out.
The address 0x404028
appears unused, so I'll write win()
there.
To do this, I'll just use the ROP class.
Now we have the address written there, let's just get the massive ropchain and plonk it all in
Don't forget to pass a parameter to the gets()
:
And we have successfully controlled RDX - without any RDX gadgets!
As you probably noticed, we don't need to pop off r12 or r13, so we can move POP_CHAIN
a couple of intructions along:
Controlling registers when gadgets are lacking
ret2csu is a technique for populating registers when there is a lack of gadgets. More information can be found in the original paper, but a summary is as follows:
When an application is dynamically compiled (compiled with libc linked to it), there is a selection of functions it contains to allow the linking. These functions contain within them a selection of gadgets that we can use to populate registers we lack gadgets for, most importantly __libc_csu_init
, which contains the following two gadgets:
The second might not look like a gadget, but if you look it calls r15 + rbx*8
. The first gadget chain allows us to control both r15
and rbx
in that series of huge pop
operations, meaning whe can control where the second gadget calls afterwards.
Note it's call qword [r15 + rbx*8]
, not call qword r15 + rbx*8
. This means it'll calculate r15 + rbx*8
then go to that memory address, read it, and call that value. This mean we have to find a memory address that contains where we want to jump.
These gadget chains allow us, despite an apparent lack of gadgets, to populate the RDX and RSI registers (which are important for parameters) via the second gadget, then jump wherever we wish by simply controlling r15
and rbx
to workable values.
This means we can potentially pull off syscalls for execve
, or populate parameters for functions such as write()
.
You may wonder why we would do something like this if we're linked to libc - why not just read the GOT? Well, some functions - such as write()
- require three parameters (and at least 2), so we would require ret2csu to populate them if there was a lack of gadgets.