Challenge

file calc

checksec

run binary

IDA
main()

calc()

init_pool()

get_expr()

parse_expr()


eval()

Analysis
Chương trình mô phỏng 1 máy tính, cho ta nhập vào biểu thức, tính ra kết quả và in ra màn hình. Tuy nhiên sau khi chạy thử binary với một số test khác nhau, ta có nhận xét:
- Khi biểu thức phức tạp, chương trình sẽ tính ra kết quả không đúng.
- Khi nhập vào biểu thức lỗi như: 8 – – 6, chương trình báo lỗi.
- Tuy nhiên khi nhập biểu thức dạng + 2, chương trình lại không báo lỗi mà cho ra kết quả là 0.

Chắc chắn có gì đó ta có thể khai thác được ở đây. Đó không phải tính năng, đó là lỗi :v.
Ta đi vào mổ xẻ binary bằng IDA, ở đây tôi đã đổi tên một số biến để thuận tiện cho quá trình phân tích.

Chương trình bắt đầu với hàm main(). Hàm main() đầu tiên sẽ set alarm(60) để ngắt chương trình khi chương trình kéo dài > 60 giây. Sau khi hiển thị thông báo chào mừng sẽ gọi hàm calc() để tương tác với người dùng.

Hàm calc() có 1 biến count để đếm số phần tử trong mảng NUMBERS[100]. Mảng NUMBERS[100] chứa các con số xuất hiện trong biểu thức mà người dùng nhập vào. Lưu ý là biến NUMBERS[] nằm tại địa chỉ [ebp-59Ch] ngay bên dưới biến count ở địa chỉ [ebp-5A0h]. Hàm calc() gọi hàm get_expr() để đọc input từ người dùng từng byte một, chỉ nhận những kí tự trong white-list [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -, +, /, *, %]. Chuỗi nhận được sẽ lưu vào biến s, trong đó s là chuỗi có độ dài tối đa 0x400 = 1024, đã được khởi tạo bởi hàm bzero() trước đó.
Sau đó, hàm calc() gọi hàm init_pool() để khởi tạo giá trị cho biến count.

Nhìn vào vòng lặp của hàm init_pool(), ta thấy rằng nó không chỉ đơn thuần khởi tạo biến count = 0 mà nó còn khởi tạo 100 giá trị kiểu int đằng sau count. Điều này đồng nghĩa với việc khởi tạo mảng NUMBERS[100] trong hàm calc() bên trên, vì biến NUMBERS[100] nằm ngay sau count trên stack. Có thể hiểu nôm na là NUMBERS[i] sẽ tương đương với count[i+1].
Sau khi khởi tạo các giá trị cần thiết, hàm calc() gọi hàm parse_expr() để thực hiện tính toán kết quả từ biểu thức. Nếu quá trình tính toán không có lỗi gì, chương trình sẽ in ra NUMBERS[count-1], đó chính là kết quả phép tính mà ta nhận được. Như vậy, phần tử cuối cùng của mảng NUMBERS[] sẽ chứa kết quả tính toán.
Giờ đến phần quan trọng nhất của chương trình, hàm parse_expr().

Tham số expr là chuỗi input đã được filter qua hàm get_expr(). Biến v5 chứa chuỗi expr sau khi đã cắt bỏ con số đầu tiên sau mỗi lượt xử lý. Câu lệnh if kiểm tra nếu kí tự expr[i] là phép toán [+, -, *, /, %] thì sẽ lưu con số ngay phía trước biểu thức vào biến s1 (v2 là kích thước của chuỗi số). Tiếp đó, kiểm tra nếu s1 == “0” thì báo lỗi “prevent division by zero”. Đó là lý do mà biểu thức sau bị báo lỗi:

Tiếp theo, chuyển chuỗi s1 thành số, lưu vào v9. Kiểm tra nếu v9 > 0 thì thêm v9 vào mảng NUMBERS[] và tăng biến đếm count lên 1. Hay nói cách khác: NUMBERS[count] = v9; count += 1;

Kế tiếp, kiểm tra nếu kí tự kế tiếp phép toán [+, -, *, /, %] hiện tại lại là một kí tự phép toán nữa thì sẽ báo lỗi “expression error”. Sau đó, v5 được cập nhật bằng cách cắt bỏ chuỗi số vừa duyệt ra khỏi biểu thức expr.

Câu lệnh if tiếp theo mục đích là xử lý thứ tự ưu tiên phép toán nhân chia trước, cộng trừ sau, tuy nhiên qua việc test binary ta biết rằng mớ logic này bị fail.

Chuỗi s chứa các phép toán để xử lý ưu tiên. Khi phép toán nào được xử lý rồi thì bỏ nó ra khỏi chuỗi bằng cách giảm biến đếm j. Phần xử lý tính toán được thực hiện bởi hàm eval().

Lấy ví dụ phép toán hiện tại là ‘+’ thì hàm eval() sẽ tính count[*count-1] += count[*count]. Phép tính này thực chất tương đương với: NUMBERS[count-2] += NUMBERS[count-1]. Sau khi tính xong kết quả, giảm biến count đi 1 đơn vị. Như vậy kết quả phép tính đến thời điểm hiện tại (sau khi đã giảm biến đếm count) được lưu vào NUMBERS[count-1].
Kết thúc hàm parse_expr(), chương trình sẽ in ra kết quả cuối cùng được lưu trong NUMBERS[count-1].

Đến đây ta đã hiểu gần hết flow của chương trình. Chỉ còn một chỗ ta chưa làm rõ, đó là tại sao chương trình lại cho ra kết quả là 0 khi ta nhập biểu thức “+8”

Nếu bạn đọc còn tỉnh táo, ta sẽ phân tích lại flow chương trình khi input là “+10”. Trong hàm parse_expr(), khi duyệt đến kí tự đầu tiên ‘+’, lúc này i = 0, v2 = 0, s1 = NULL, v9 = 0 nên không có số nào được thêm vào mảng NUMBERS[]. Vì chưa có phép toán nào trong chuỗi s nên phép toán ‘+’ sẽ được add vào. Tiếp theo, chương trình duyệt đến kí tự ‘1’ và ‘0’, lúc này i = 2, v2 = 2, s1 = ’10’, v9 = 10. Không còn kí tự nào nữa nên lệnh eval() trong nhánh default được thực hiện:

Hàm eval() thực hiện phép tính:

Lúc này, count = 1, NUMBERS[] = [10]. Do đó phép tính trên thực chất tương đương với: count = count + NUMBERS[count-1] = 1 + 10 = 11. Sau đó có lệnh count– nên count = 10. Hàm parse_expr() kết thúc, chương trình in ra giá trị tại NUMBERS[count-1] = NUMBERS[9] = count[10] = 0 vì mảng NUMBERS đã được khởi tạo tất cả giá trị = 0 trước đó.
Đến đây ta phát hiện được 1 lỗi có thể khai thác của chương trình: Ta thực sự có thể leak được giá trị tại một vị trí mong muốn, chỉ cần thay đổi con số phía sau dấu ‘+’. Để bạn đọc dễ hình dung, tôi sẽ chạy chương trình bằng gdb, set breakpoint ngay phía sau hàm parse_expr() và show stack:



Nhìn vào stack hiện tại, biến count được lưu tại địa chỉ 0xffffcb68, mảng NUMBERS[] bắt đầu tự địa chỉ 0xffffcb6c. Giá trị được leak là count[10] = NUMBERS[9] tại địa chỉ 0xffffcb90. Ta hãy thử áp dụng lỗi này để leak một giá trị nào đó trên stack, chẳng hạn giá trị của canary trên stack của hàm calc(). Muốn thế ta cần biết được biến canary cách biến count bao nhiêu offset.

Nếu chọn count là offset 0 để tham chiếu thì cấu trúc stack như sau:

Vậy để leak canary thì pay load sẽ là “+357”



Giá trị canary được lưu tại ebp-0xc đúng bằng giá trị ta leak tại offset 357 = 594710272. Như vậy ta có thể leak được giá trị tùy ý trên stack. Hãy xem ta còn có thể làm gì hơn nữa với lỗi này. Giờ thay vì dùng payload “+357”, ta hãy dùng payload “+357+1”.

Sau khi truyền payload “+357+1” ta kiểm tra lại canary thì thấy canary đã được tăng lên 1 đơn vị. Vì canary thay đổi nên ta gặp lỗi *** stack smashing ***. Giải thích điều này như thế nào?
Sau khi duyệt đến số “357”, số 357 được lưu vào NUMBERS[], lúc này, NUMBERS[356] = canary, và count=357. Khi duyệt đến số “1”, số 1 được lưu vào NUMBERS[] lúc này NUMBERS[357] = 1, count=358. Tiếp đến chương trình thực hiện hàm eval() và thực hiện phép tính:

Phép tính trên tương đương với count[357] = count[357] + count[358]. Mà count[357] = NUMBERS[356] = canary, và count[358] = NUMBERS[357] = 1, do đó canary = canary + 1. Ta đã thay đổi được giá trị của canary.
So, what we have until now?
- Ta có thể leak được giá trị tại 1 vị trí tùy ý trên stack.
- Ta có thể thay đổi giá trị tại 1 vị trí tùy ý trên stack.
Sound great, right? Vậy ta sẽ khai thác như thế nào để chiếm được quyền shell.
Exploit
Binary này được bảo vệ bằng cách set NX enabled

NX = Non-eXectable, khi set NX enabled nghĩa là khi load chương trình vào bộ nhớ, sẽ không cho phép 1 vùng nhớ nào đó có cả 2 quyền writeable và executable. Vùng .text chứa code chỉ có thể execute mà không thể write, các vùng như stack, heap, data, bss chứa dữ liệu thì có thể write nhưng không thể execute. Do đó ta không thể khai thác theo hướng chèn shellcode vào stack và cho return address trỏ về shellcode được.
Vậy phải làm sao? Ta sẽ dùng kĩ thuật ROPing technique (Return oriented programing).
Tôi sẽ sử dụng lệnh ngắt “int 0x80” với eax=0xb để gọi hàm sys_execve(). Tôi sẽ cần set các giá trị eax, ebx, ecx, edx như sau:
- eax = 0xb = 11
- ebx = địa chỉ của chuỗi “/bin/sh”
- ecx = edx = 0
Tôi phải tìm được các ROP gadgets phù hợp để set các thanh ghi này. Để đơn giản tôi dùng công cụ ROPgadget


Tôi đã có được các ROP gadgets cần thiết:
0x0805c34b : pop eax ; ret
0x080701d0 : pop edx ; pop ecx ; pop ebx ; ret
0x08049a21 : int 0x80
Kế hoạch là ta sẽ thiết kế stack như sau:

Địa chỉ của offset 368 ta có thể tính được bằng cách leak địa chỉ của prev_ebp, tính xem prev_ebp nằm ở offset bao nhiêu và suy ra địa chỉ của offset 368. Tôi đặt breakpoint ngay trước lệnh ret của hàm calc(), lúc này esp sẽ chỉ đến return address tại offset 361.


prev_ebp nằm bên trên esp có giá trị là prev_ebp=0xffffd128, trong khi esp=0xffffd10c, 2 địa chỉ cách nhau 28 bytes, tức 7 offset. Như vậy prev_ebp nằm tại offset 361+7=368. May mắn thay, đó cũng chính là offset mà ta cần tìm, không cần phải tính toán lại nữa :v


code no khong ra flag a. co sai gi ko anh
ThíchThích