REVERSING WITH IDA FROM SCRATCH (P23)

m4n0w4r
tradahacking
Published in
10 min readOct 17, 2019

--

Trong phần 23 này tôi sẽ giải quyết câu hỏi đặt ra đối với file IDA1.exe đã được gửi kèm trong phần 22. Đầu tiên, load file vào IDA để phân tích, ta sẽ dừng lại tại hàm main():

Tại đây, bạn cuộn chuột xuống dưới sẽ nhìn thấy chuỗi “you are a winnner man je\n”. Đó chính là nơi mà chúng ta cần phải tới được. Tôi đổi màu cho block này để tiện cho việc nhận diện một cách dễ dàng.

Quan sát bên trên một chút, ta thấy rằng để có thể tới được đoạn code hiển thị chuỗi mong muốn thì phải vượt qua được đoạn code thực hiện so sánh giữa biến var_C với một hằng số 0x51525354. Nhấn chuột phải tại giá trị 0x51525354, IDA sẽ cung cấp cho chúng ta một loạt các lựa chọn chuyển đổi, ở đây chúng ta sẽ lựa chọn chuyển thành các kí tự ‘QRST’:

Các bạn để ý một chút sẽ dễ dàng có thể nhận ra, file IDA1.exe được biên dịch không sử dụng cơ chế CANARY protection, vì nếu có áp dụng thì ở của đầu hàm nó sẽ phải đọc dữ liệu từ một địa chỉ thuộc section data vào thanh ghi eax và đem xor với thanh ghi ebp, kết quả được lưu vào biến nằm ngay trên giá trị của ebp đã được lưu vào Stack trước đó. Trước khi kết thúc hàm biến này sẽ được đọc ra để kiểm tra lại. Tóm lại file IDA1.exe không áp dụng Stack Canary, ta sẽ phân tích tĩnh bố trí các biến và tham số tại Stack của hàm main(), nhấp đúp vào bất kỳ biến nào ta sẽ có kết quả như sau:

Tại đây, ta thấy rằng biến duy nhất xuất hiện trong Stack, ngay bên trên giá trị của EBP được lưu, đó là biến var_C. Chọn biến này và nhấn “X”, sẽ thấy nó được sử dụng để so sánh với chuỗi “QRST”. Căn cứ trên kết quả so sánh sẽ đi tới đoạn code hiển thị thông báo “ winner “ hay là không:

Tóm lại, file này không áp dụng CANARY, vì như ta thấy rõ ràng trong trường hợp này thì đây là một lệnh kiểm tra thuộc mã nguồn của chương trình. CANARY không thể “trộn” lẫn nằm xen kẽ với các quyết định rẽ nhánh trong mã của chương trình, nó phải được bổ sung bởi chính trình biên dịch, và nằm ở bên ngoài code ban đầu.

Cho nên để tránh nhầm lẫn biến var_C với CANARY, ta có thể đổi tên nó thành var_Decision.

Như trên hình, biến này được sử dụng hai lần trong hàm main(). Vậy làm thế nào chúng ta có thể thay đổi giá trị của biến var_Decision để khiến cho chương trình rẽ nhánh đi tới đoạn code hiển thị thông báo “winner”?

Khi các bạn quan sát kĩ vào các dòng lệnh asm thì điều sẽ khiến bạn chú ý là có các biến được truy cập thông qua thanh ghi EBP và có những biến khác được truy cập thông qua thanh ghi ESP:

Toàn bộ các lệnh khởi tạo ban đầu được thêm vào bởi trình biên dịch nhằm thiết lập thanh ghi ESP nằm trên các biến cục bộ và buffer. Lệnh SUB ESP, 0xB8 làm nhiệm vụ dành ra không gian phục vụ cho các biến cục bộ và buffer được khai báo bên trong thân hàm main(). Để biết cụ thể thì các bạn có thể debug để hiểu rõ hơn. Bản thân IDA có một trợ giúp rất hữu ích là “ Stack Pointer”, hỗ trợ hiển thị sự thay đổi của thanh ghi ESP kể từ điểm bắt đầu của hàm ( giá trị tại đầu hàm được thiết lập bằng 0).

Kết quả tại code của chương trình sẽ xuất hiện thêm thông tin như hình dưới đây:

Quan sát trên hình các bạn sẽ thấy sự tác động của mỗi lệnh lên ngăn xếp bắt đầu từ đầu hàm, sau khi thực hiện PUSH EBP, thanh ghi ESP sẽ bị giảm đi 4, do vậy dòng lệnh thứ hai có thêm số 004 ở bên phải, hàm ý là stack pointer thay đổi. Tuy nhiên, ở câu lệnh thứ hai là MOV EBP, ESP, vì đây chỉ là một lệnh MOV nên lệnh này không làm thay đổi Stack, do vậy giá trị của ESP vẫn giữ nguyên, thanh ghi thay đổi là EBP.

Đó là lý do tại sao dòng lệnh thứ ba có cùng giá trị là 004 vì lúc này chưa thay đổi ESP. Như vậy, ta thấy rằng ở thời điểm này giá trị ESPEBP là giống nhau và thanh EBP từ đây có thể được sử dụng để truy xuất các biến cục bộ cũng như các tham số của hàm. Dòng lệnh thứ 3 thực hiện trừ ESP đi 0xB8, do vậy kết quả sẽ là giá trị 0xBC ( so với điểm bắt đầu) và vì EBP vẫn ở mức 004 nên chênh lệch giữa hai thanh ghi EBPESP sẽ là 0xB8.

Quan sát kĩ hơn thì thấy khoảng cách này không thay đổi nữa ngay cả khi thực hiện các lời gọi hàm call __allocacall ___main ( được tự động thêm vào trong quá trình biên dịch chương trình). Tóm lại, khoảng cách giữa EBPESP0xB8, và đây chính là khoảng không gian dành cho các biến cục bộ và buffer được khai báo trong thân của hàm main().

Tiếp tục đọc code, các bạn thấy có biến được IDA đặt tên là var_9c. Kiểm tra toàn bộ hàm main() thì thấy biến này không được sử dụng cho việc gì khác, vì vậy nó có thể là một biến tạm thời được tạo bởi trình biên dịch, ta sẽ đặt tên nó là Temp để dễ nhận biết:

Bên dưới biến Temp chúng ta có biến Buffer:

Nhấn chuột phải tại biến Buffer này và chuyển đổi sang kiểu array, ta sẽ được kết quả là một biến Buffer có kích thước là phần tử, mỗi phần tử có kích thước :

Nếu chúng ta có thể làm tràn biến Buffer bằng cách đẩy vào dữ liệu nhiều hơn 140 bytes, như vậy sẽ gây ra Buffer Overflow, lúc đó ta có thể ghi đè giá trị lên biến var_Decision và nếu có thể được sẽ ghi đè dữ liệu lên cả giá trị EBP đã được lưu và đè lên cả địa chỉ trở về ( r). Điều này tùy thuộc lượng dữ liệu mà ta có thể nhồi vào biến Buffer.

Tiếp tục quá trình reversing, điểm gây chú ý tiếp theo liên quan tới trình biên dịch, đó là là cách nó truyền các tham số cho hàm. Ở target này, thay vì sử dụng lệnh PUSH để lưu các tham số vào Stack ( như vẫn thường gặp), thì ở đây ta thấy chương trình lưu trực tiếp thông qua lệnh MOV. Các bạn quan sát lệnh printf, rõ ràng trong trường hợp này printf có một đối số duy nhất là địa chỉ của chuỗi “you are a winner man je\n” và không có tham số nào khác:

Ta thấy chương trình truy xuất địa chỉ của chuỗi trên vì có tiền tố offset ở phía trước và lưu vào biến có tên Format trong ngăn xếp. Nhưng cụ thể là ở đâu trong Stack? Lúc này chương trình không sử dụng thanh ghi EBP để truy cập đến tham số của hàm mà sử dụng chính thanh ghi ESP, tuy nhiên ký hiệu ( esp+0B8h+Format) hơi khó hiểu, nếu chúng ta nhấn chuột phải tại đây IDA sẽ cung cấp cho ta thông tin thay thế như sau:

Nếu ta chọn cách biểu diễn như IDA gợi ý:

Qua việc chuyển đổi trên, ta có thể hiểu bản chất của câu lệnh mov sẽ thực hiện việc lưu địa chỉ của chuỗi vào trong nội dung thanh ghi ESP ( đó chính là đỉnh của Stack), để sử dụng nó như một tham số của hàm. Do vậy, chương trình thay vì sử dụng lệnh PUSH thì nó sử dụng lệnh MOV để truyền tham số cho hàm, và theo logic thì lệnh PUSH sẽ thay làm đổi giá trị của ESP, trong khi lệnh MOV thì không, các bạn có thể thấy được thông qua giá trị 0xBC ngay trước hàm printf():

Cách truyền tham số này cũng được áp dụng đối với tất cả các hàm APIs khác mà IDA1.exe sử dụng. Áp dụng cách thức chuyển đổi tương tự như đã làm ở trên, ta sẽ có được các đoạn mã lệnh asm nhìn dễ hiểu hơn:

Tiếp tục quá trình phân tích, hàm printf() đầu tiên nhận ba tham số truyền vào cho nó. Vì sao lại biết được là có 3 tham số? Là bởi vì hàm in ra màn hình chuỗi sử dụng các giá trị để thay thế cho format string: “ buf:%08x cookie:%08x \ n”. Các giá trị “%08x” sẽ được thay thế bằng hai tham số được truyền trước đó tại [esp+8][esp+4]:

Cách truyền tham số cho hàm thường sẽ là từ phải qua trái. Do đó, tham số thứ 3 của hàm printf() là địa chỉ của biến var_Decision, có được thông qua lệnh LEA và lưu tại thanh ghi EAX. Sau đó lưu địa chỉ này vào vị trí [ESP+8] trên Stack. Tham số thứ hai là địa chỉ của biến Buffer, cũng thực hiện theo cách tương tự nhưng được lưu vào bị trí [ESP+4] trên Stack và tham số đầu tiên được lưu tại [ESP] là địa chỉ của chuỗi “buf: %08x cookie: %08x\n”.

Nếu chạy trực tiếp file này tại màn hình console, ta sẽ thấy chuỗi được in ra màn hình cùng với địa chỉ của cả hai biến:

Tiếp theo, chương trình sử dụng hàm gets() để đọc các kí tự từ stdin ( standard input) và lưu vào biến Buffer. Ta để ý ở đây số lượng kí tự nhập vào bằng bàn phím là tùy ý mà không có bất kỳ giới hạn nào, tức là ta có thể nhập vào nhiều hơn 140 ký tự mà không có vấn đề gì:

Đoạn code trên sẽ tương ứng với mã giả C như sau: gets(Buffer);

Vì vậy, giả sử nếu ta ghi vào Buffer này 140 chữ ‘A’ và kèm theo sau đó là chuỗi “TSRQ” ( theo kiểu little endian), chương trình sẽ nhảy tới đoạn code hiển thị thông báo “” vì khi đó biến var_Decision nằm ngay bên dưới Buffer, sẽ được ghi đè bởi giá trị “QRST”. Kiểm tra bằng tay trước khi chúng ta thực hiện tự động bằng script:

Thực hiện lệnh tại Python bar của IDA, sau đó copy chuỗi được sinh ra:

"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATSRQ"

Chạy chương trình và dán chuỗi này vào, kết quả có được như sau:

Kết quả đúng như mong đợi, ta tiến hành tạo script để chạy tự động, script này cũng giống như script đã tạo ở phần trước trước, chỉ khác ở này chỉ truyền một input cho chương trình thôi:

from subprocess import *p = Popen([r'IDA1.exe', 'f'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)print "Attach to the debugger and press Enter\n"raw_input()user_input="A" *140 + "TSRQ\n"p.stdin.write(user_input)testresult = p.communicate()[0]print user_inputprint(testresult)

Kiểm tra kết quả sau khi thực hiện script:

Phần 23 đến đây là kết thúc. Tôi gửi các bạn bài tập IDA2.exe ( https://mega.nz/#!jXpgGSKJ!jx8p41yH5Drqq78fKonMqVQhmQf2BqwbJh9aqHDwZ_o) để các bạn thực hành, nó tương tự như IDA1.exe.

Hẹn gặp lại các bạn ở phần 24!

Xin gửi lời cảm ơn chân thành tới thầy Ricardo Narvaja!

m4n0w4r

Ủng hộ tác giả

Nếu bạn cảm thấy những gì tôi chia sẻ trong bài viết là hữu ích, bạn có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy” qua địa chỉ:

Tên tài khoản: TRAN TRUNG KIEN
Số tài khoản: 0021001560963
Ngân hàng: Vietcombank

--

--