Challenge

file dubblesort

checksec

run binary

ghidra
main()


prolog()

sort()


Analysis
Chương trình yêu cầu người dùng nhập tên, sau đó nhập vào một dãy số, tiến hành sắp xếp dãy số theo thứ tự tăng dần và in kết quả ra màn hình.
Exploitable 1: Leak stack
Dưới con mắt của 1 pwner, chỗ nào cho nhập xuất dữ liệu là chỗ đó có thể chứa lỗi. Với tinh thần như thế, ta thử test đoạn nhập tên với các input khác nhau. Điều dễ dàng nhận thấy là với input có chiều dài thay đổi, có lúc chương trình in ra một số kí tự lạ phía sau tên.

Chỗ này chắc chắn có lỗi gì đó. Sử dụng Ghidra decompiler để xem pseudocode xử lý đoạn này ra sau:

Hàm read() đọc tối đa 64 bytes dữ liệu từ stdin, lưu vào biến name. Sau đó in biến name qua chuỗi định dạng “%s” ở câu lệnh tiếp theo. Ở đây, biến name chưa được khởi tạo nên chuỗi nhận vào sẽ không được kết thúc bởi ký tực NULL ‘\x00’. Khi in biến name ra, hàm printf sẽ in ra đến khi nào gặp byte NULL, do đó sẽ xuất hiện một số kí tự lạ trên màn hình. Ta sẽ check lại nhận định trên bằng gdb



Tôi set breakpoint ngay phía trước lời gọi hàm printf_chk(), lúc này tại $esp+8 lưu giá trị của biến name để in ra màn hình. Ta thấy phía sau chuỗi “nosystemissafe\n” còn 5 byte khác NULL nữa là 0xf7, 0xf7fa9588, sau đó mới đến byte NULL. Đây chính là 5 byte được in ra màn hình trong ví dụ bên trên. Như vậy ở đây ta có thể leak được giá trị tại một số địa chỉ trên stack trong phạm vi biến name, dù chưa biết để làm gì. Ta cứ để đấy tí sẽ quay lại sau.
Exploitable 2: Buffer OverFlow
Tiếp tục xem xét code xử lý sắp xếp ta phát hiện thêm một lỗi chết người nữa:

Trong khi mảng NUMBERS[] chỉ được khai báo tối đa 8 phần tử, chương trình lại không kiểm tra giá trị n do người dùng nhập vào, ta có thể nhập nhiều hơn 8 phần tử vào mảng NUMBERS, đây là lúc mà Buffer OverFlow xảy ra. Để xác nhận lỗi trên, ta hãy thử ghi đè lên canary để xem có gặp lỗi ** stack smashing detected ** không. Cấu trúc stack của hàm main() như sau:

Ta thấy canary cách NUMBERS 24 offset, do đó muốn overwrite canary ta phải overwrite offset thứ 25. Check thử:

Quả thực ta đã ghi đè lên canary giá trị 2 nên gặp lỗi stack smashing. Vì return address cách NUMBERS[] từ 32 offset nên nếu ta muốn overwrite return address ta phải overwrite offset thứ 33 tính từ vị trí NUMBERS[], ngoài ra ta còn phải tìm cách để bypass canary nữa.
Exploit
Binary lần này được set full security, nhằm gây khó khăn cho attacker.

NX enabled nghĩa là ta sẽ không thể dùng kĩ thuật ret2shellcode. Vậy ta sẽ dùng kĩ thuật ret2libc để gọi hàm system() với tham số “/bin/sh”. Có mấy vấn đề cần giải quyết:
- Canary found -> cần bypass canary
- PIE enabled -> địa chỉ các lệnh và hàm của chương trình bị thay đổi ngẫu nhiên mỗi lần chạy.
Bypass canary
Để giải quyết vấn đề số 1, ta đơn giản chỉ cần nhập “+” vào vị trí của canary thì sẽ không làm thay đổi giá trị của canary.


Thông báo lỗi Segmentation fault chứng tỏ ta đã bypass được canary và overwrite return address như ý muốn.
leak libc
Để thực hiện return to libc, ta cần phải leak được địa chỉ của các hàm trong thư viện libc_32.so.6 khi import vào binary lúc chạy chương trình, cụ thể là hàm system(). Làm sao để leak libc address. Ta sẽ sử dụng lỗi đầu tiên: lỗi leak stack xảy ra khi nhập tên. Cấu trúc stack bắt đầu từ vị trí biến name:

Do ta có thể leak giá trị của stack xung quanh vị trí phía sau biến name, ta sẽ tìm thử trong đống này có địa chỉ nào hữu ích không. Để biết được các địa chỉ này nằm trong vùng nào của bộ nhớ, ta dùng lệnh vmmap

Như vậy, các địa chỉ bắt đầu bằng 0x5655…. nằm ở vùng chứa các lệnh của chương trình. Địa chỉ của thư viện libc-2.28.so nằm từ 0xf7dcc000 -> 0xf7fa7000. Nhìn lại cấu trúc stack lúc này ta thấy có địa chỉ 0xf7fa6000 nằm cách chuỗi name 28 bytes là một địa chỉ nằm trong thư viện libc. offset của địa chỉ này là 0xf7fa6000 – 0xf7dcc000 = 0x1da000.

Nhìn vào dòng cuối cùng ta thấy địa chỉ tại offset 0x1da000 thực chất là địa chỉ của vùng .got.plt trong thư viện libc-2.28.so. Đây là thư viện dùng chung tại máy local. Tuy nhiên trên server dùng thư viện libc_32.so.6, do đó ta phải chuyển đổi sang offset tương ứng của libc_32.so.6

Trong libc_32.so.6, vùng .got.plt nằm ở offset 0x1b0000. Như vậy để tính địa chỉ bắt đầu của libc_32.so.6 khi thực thi trên server, ta chỉ cần lấy địa chỉ leak được – 0x1b0000.
addr_libc = addr_leak - 0x1b0000
Có được địa chỉ bắt đầu của libc, ta dễ dàng tìm được địa chỉ của hàm system() và chuỗi “/bin/sh” có sẵn trong libc.

Như vậy:
addr_system = addr_libc + 0x03a940
addr_binsh = addr_libc + 0x158e8b
OK, đến đây coi như ta đã có đủ chất liệu cần thiết để exploit. Giờ ta review lại các bước cần làm:
- Truyền payload ‘A’*24 vào biến name để leak địa chỉ và tìm ra addr_libc
- Bypass canary và dùng kĩ thuật ret2libc để return về hàm system() với tham số “/bin/sh” và get shell.
Tuy nhiên mọi chuyện không hoàn toàn đơn giản như vậy. Đời mà, không như là mơ :v Có 1 số vấn đề nho nhỏ. Với bước 1 – leak addr, địa chỉ leak được sẽ là 0xf7fa600a chứ không phải 0xf7fa6000 do có kí tự ‘\n’, ta cần trừ thêm 0xa nữa để tính ra addr_libc.
Với bước bước thứ 2, có 1 trở ngại nhỏ là dãy số mà ta truyền vào sẽ thực sự được sắp xếp thông qua hàm sort(). Do đó, nếu không muốn payload bị rối tung lên thì thứ tự các số truyền vào dãy cũng phải đảm bảo theo thứ tự tăng dần. May mắn thay, addr_binsh > addr_system và addr_system > canary trong đa số trường hợp. Do đó payload ta truyền vào sẽ gồm:
- ‘1’ * 24 để điền đầy mảng NUMBERS
- ‘+’ để bypass canary
- addr_system * 8. Trong đó số thứ 8 là vị trí của return address thực sự, còn 7 số đầu nhằm mục đích padding và không làm thay đổi thứ tự stack.
- addr_binsh * 2. Trong đó số thứ 2 là vị trí của tham số, còn số thứ 1 là return address của hàm system().

Đến đây coi như xong, có thể code được rồi 🙂 Quá mệt mỏi cho 1 challenge. Have fun!
Run exploit

