calc — pwnable.tw
###phân tích rõ các hàm trong challenge
hàm main
đầu tiên chương trình thực hiện gọi hàm ssignal
và alarm
The function ssignal() defines the action to take when the software signal with number signum is raised using the function gsignal(), and returns the previous such action or SIG_DFL.
nom na là chương trình sẽ gọi signal-handler với 2 args được pass là 14 và timeout. trong man 7 signal
ta sẽ thấy 14 chính là SIGALRM
. SIGALRM
này dùng cho hàm kế tiếp đó chính là alarm
để ngăn chương trình nếu không có tác động gì thì sẽ tự ngắt kết nối (arg timeout) nếu quá 60 giây.
puts thì write vào stdout sau đó fflush
stdout.
— — — — — — — — — — — — — — — — —
bên trong hàm calc
vào trong hàm calc
- khởi tạo biến count (với địa chỉ là
ebp — 5A0h
) - khởi tạo chuỗi int string[100] (với địa chỉ là
ebp-59Ch
chú ý là string nằm sau biến count 4 bytes) - khởi tạo chuỗi s
- khởi tạo unsigned int v4
v4 = __readgsdword(0x14)
v4 đọc giá trị từ thanh ghiGS
với số lượng là 0x14
— — — — — — — — — — — — — — —
hàm bzero
dùng để null các giá trị trong chuỗi s
The bzero() function sets the first n bytes of the area starting at s to zero (bytes containing '\0').
— — — — — — — — — —
hàm get_expr
pass vào 2 args là chuỗi s và 1024
- khởi tạo biến
s
với kiểu int - khởi tạo ký tự
v4
- khởi tạo biến
v5 = 0
với kiểu int
###
giải thích các điều kiện trong while
- kiểm tra xem v5 có nhỏ hơn a2 (1024)
- đọc 1 byte từ địa chỉ v4 kiểm tra xem ret value của read có khác -1(trả về -1 khi bị interupt bởi 1 signal) hay là kí tự newline hay không
###
vào trong hàm if thì nó chỉ đơn giản check các kí tự v4 có phải là các phép toán thông thường và là số≤ 9 hay không rồi sau đó gán cho chuỗi s là v4
###
phép toán gán cuối cùng chỉ là null phần tử cuối của dãy
###
tóm lại hàm get_expr
sẽ đọc input từ user từng byte một, cho phép trong list [ +, -, *, \, %, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] lưu vào biến s. chuỗi s sẽ có length tối đa là 1024 được khởi tạo từ hàm bzero
trước đó.
— — — — — — — — — — — — — —
hàm init_pool
nếu ta để ý thì bên dưới biến count chính là chuỗi string, hàm này công việc chính là khởi tạo giá trị cho chuỗi string[100] hay rõ hơn là string[i] = count[i+1]
— — — — — — — — — — — — — —
bây giờ đến hàm quan trọng nhất parse_expr
- khởi tạo các biến kiểu int: v2, v4, v5, i, v7, v9 và unsigned int v11
- khởi tạo kiểu char *s1, s_func[100]
kiểm tra trong chuỗi s[i] có kí tự [ +, -, *, /, %] hay không, nếu có thì sẽ lưu con số sau biểu thức vào s1. Sau đó thực hiện hàm if, nếu s1 == 0 thì sẽ báo lỗi nên input sau được cho là lỗi:
tiếp đến chuyển chuỗi s1 thành số lưu vào v9. Kiểm tra xem nếu v9 > 0 thì thêm v9 vào mảng s (vì s nằm sau biến count) và tăng biến count lên 1.
dễ hiểu hơn là string[count] = v9; count += 1 (ở đây ta hiểu v4 là 1 biến đếm)
tiếp sau đó sẽ check tiếp nếu trong chuỗi s[i+1] có thêm 1 ký tự nào trong [ +, -, *, /, %], nếu có báo ‘expression error!’ và v5 sẽ cập nhật là cắt bỏ chuỗi số ra khỏi s[i+1] sau đó count +=1
tiếp đó ta có thể thấy thì đoạn này chính là đoạn các phép toán logic được kiểm tra nhân chia trước cộng trừ sau nhưng như ta test thì nó sai tùm lum.
ta thấy chuỗi s_func chứa các phép toán được ưu tiên, nếu cái nào được giải quyết rồi thì nó sẽ loại bỏ bằng cách cho lùi biến đếm v7
bên trong hàm eval
để rõ hơn thì ta lấy ví dụ phép toán hiện tại là ‘+’ thì ta có count[*count -1] += count[*count], thì nó chính là string[*count -2] += string[*count -1] (vì v2 nằm sau count 4 byte), sau đó giảm biến count đi 1, như vậy kết quả cuối cùng đó chính là string[*count -1]
kết thúc hàm parse_expr
thì nó sẽ in ra giá trị cuối cùng trong string[count -1]
bây giờ chúng ta đã hiểu được flow của chương trình như thế nào, nhưng có 1 điểm đáng chú ý trong quá trình chúng ta test binary
ta lấy ví dụ +13. khi ta thực thi hàm parse_expr
, đầu tiên là dấu ‘+’. ta xét các biến của bin
- i = 0
- v2 = 0
- s1 = NULL
- v9 = 0
- count = 1
lúc này do là dấu nên nó sẽ được thêm vào s_func
sau khi vòng lặp đi tới 1 và 3 thì lúc này
- i = 2
- v2 = 2
- s1 = 13
- v9 = 13
vì đã hết kí tự nên sẽ thực hiện hàm eval
sẽ thực hiện phép tính này
lúc này thì
- count = 1 + 13 -1 = 13
- string[] = {13}
phép tính trên thực chất là count = count + string[count -1] = 1 + 13 = 14. rồi sau đó chương trình trừ đi count nên còn 13.
kết thúc parse_expr
thì chương trình lúc này sẽ in giá trị của vị trí count[13] = string[12] = 0
để rõ ràng hơn ta hãy nhìn trong gdb sau khi input vào +13
như vậy ta có thể thấy được ta đã có thể leak value trên stack
quay lại hàm main ta có thể thấy giá trị v4 này
nó chính là giá trị của canary. nếu ta xét count làm gốc thì canary có offset là +357(lấy 0x5A0–0xC rồi chia cho 4)
vậy nếu ta +357+1 thì sao?
oopsie vậy điều này xảy ra như thế nào? bỏ trước khi thực hiện parse_expr
số 1 thì ta có count[357]=string[356] =canary.
với phép tính này thì ta hiểu chính là count[357]= count[357] + count[358] mà count[358] = 1 nên canary+=1 dẫn đến việc stack smashing.
###SUM UP
nói dong nói dài thế này, để mình tóm tắt lại các chức năng chính của chương trình.
- get_expr: khởi tạo giá trị cho string
- par_expr: ưu tiên các phép toán nhân chia trước cộng trừ sau
- eval: nơi magic happens
vậy ta có đủ các thứ bây giờ chỉ cần dùng kĩ thuật gì để vượt qua challenge này. các bạn đoán xem, với NX, canary thì ta bypass bằng cách nào? ROP!!! và mình đã có bài giải thích rõ về ROP nếu ai chưa biết có thể vào xem
vậy ta cần:
- eax = 0xb
- ebx: trỏ vào địa chỉ chứa ‘/bin/sh’
- ecx=edx=0
giờ tôi tìm được các gadget có thể dùng sau
0x0805c34b : pop eax ; ret
0x080701d0 : pop edx ; pop ecx ; pop ebx ; ret
0x08049a21 : int 0x80
tôi sẽ đẩy /bin/sh vào trong stack, tìm stack addr để trỏ ebx vào vì vậy ta cần phải leak được ebp để có thể tính
dùng gdb ta tìm được ret addr tại count[361]
dựa vào gdb ta có thể tìm được ebp của cả main và calc, cách nhau 0x1c bytes, nghĩa là count[361+7]=count[368]
welp vậy stack hiện trạng sẽ như thế này
tks to drx wu that’s i can understand on my own!!!