Challenge

file applestore

checksec

run binary

Ghidra
main()

menu()

handler()


list()

add()


create()

insert()

delete()


cart()

checkout()

struct PRODUCT

Analysis
Chương trình mô phỏng 1 cửa hàng bán điện thoại iphone, cho ta các lựa chọn tương ứng với các hàm:
- list(): liệt kê các mặt hàng trong shop.
- add(): thêm 1 sản phẩm vào giỏ hàng.
- delete(): xóa 1 sản phẩm khỏi giỏ hàng.
- cart(): kiểm tra giỏ hàng hiện tại, in ra cho người dùng, trả về tổng số tiền.
- checkout(): kiểm tra nếu tổng tiền = 7174 $ thì sẽ tặng thưởng 1 iphone 8 với giá chỉ 1 $.
Phân tích pseudocode ta có 1 số nhận xét:
- Các sản phẩm trong giỏ hàng được lưu trên heap dưới dạng 1 double linked list, mỗi node có kiểu là struct PRODUCT. Trong đó: PRODUCT.fd là con trỏ trỏ đến node tiếp theo trong list, PRODUCT.bk là con trỏ trỏ đến node phía trước.
- Mỗi khi người dùng thêm sản phẩm mới, hàm create() được gọi để tạo 1 node mới và đưa vào hàm insert() để chèn node vào cuối danh sách.
- Hàm delete() thực hiện xóa bỏ 1 node bằng cách thay đổi con trỏ .fd và .bk của 2 node phía trước và phía sau nó. Hoàn toàn không có câu lệnh free() nào để giải phóng bộ nhớ cho node. Do đó không thể khai thác lỗi double free và UAF (use after free).
- Trong hàm checkout(), khi kiểm tra điều kiện total = 7174 thỏa mãn, chương trình tạo 1 node mới để add vào cuối danh sách. Tuy nhiên node này lại không nằm trong heap mà nằm trên stack, do khai báo: PRODUCT product (lẽ ra phải là PRODUCT *product). Vì stack dễ bị thay đổi khi chương trình nhảy đến các hàm khác nên dữ liệu trên node mới này có thể điều khiển được.
Exploit
1. bypass checkout
Trước hết ta phải bypass điều kiện của checkout() để nhận được phần thưởng là iphone 8 giá 1$. Tta cần add 20 cái iphone 6 plus (index 2) và 6 cái iphone 6 (index 1) để đạt được điều kiện total = 20*299 + 6*199 = 7174 $. Cấu trúc stack khi load stack frame của hàm checkout() và khi load stack frame của hàm delete() tương ứng như sau:

Ta nhận thấy vị trí của biến product trong hàm checkout() nằm sau biến choose() trong hàm delete() 2 bytes. Như vậy, sau khi bypass checkout() để tạo node product. Chương trình trở về hàm handler(). Lúc này nếu ta gọi hàm delete(), chương trình sẽ yêu cầu nhập lựa chọn và lưu vào biến choose, đây là lúc ta chèn payload để overwrite node product.
Vậy ta sẽ overwrite gì vào product? Đầu tiên ta cần leak libc address.
2. leak libc
Trong hàm delete(), sau khi đã delete node, có câu lệnh printf() để in P.name:

Do đó nếu ta ghi đè lên vị trí của P.name bằng địa chỉ của 1 hàm nào đó trong libc, câu lệnh trên sẽ in ra địa chỉ thực của hàm đó. payload như sau:

Có được atoi_address ta tính được libc_base_address, và từ đó tính ra system_address và binsh_address:
(libc_base_address) = (atoi_address) – (offset_of_atoi_in_libc)
(system_address) = (libc_base_address) + (offset_of_system_in_libc)
Bước thứ 2 là ta cần leak được địa chỉ của stack.
3. leak stack
Ta sử dụng payload tương tự bên trên để leak địa chỉ của environ.

Vì environ nằm trên stack nên ta có thể tính được địa chỉ ebp của hàm Delete() dựa vào khoảng cách giữa ebp và environ:


(ebp_address) = (environ_address) – 0x104
4. overwrite .GOT and call system(“/bin/sh”)
challenge này được set Partial RELRO, nên ta có thể overwrite vùng .GOT. Vùng .GOT – Global offset table chứa bảng tra cứu địa chỉ thực của các hàm trong libc khi được load cùng với binary. Ta sẽ ghi đè vị trí của hàm atoi() thành hàm system(). Như thế mỗi khi gọi hàm atoi(), chương trình sẽ tra bảng .GOT và lấy địa chỉ của hàm system() ra mà execute.
Trong hàm delete(), để xóa 1 node khỏi danh sách chương trình thực hiện đoạn lệnh

Vì ta control được product.fd và product.bk nên có thể lợi dụng lệnh BK->fd = FD để thay đổi giá trị của ebp thành atoi_address + 0x22 bằng payload.

Như thế, sau khi trở về hàm handler(), ta truyền payload “system_address” + “;/bin/sh\x00”

Khi chương trình gọi hàm my_read() sẽ overwrite system_address vào vị trí của atoi_address trên vùng .GOT. Ngay sau đó là lời gọi hàm atoi() thực chất sẽ gọi hàm system() và ta có được shell.

Run exploit


