REVERSING WITH IDA FROM SCRATCH (P26)

m4n0w4r
tradahacking
Published in
13 min readDec 8, 2019

Trong phần 26 này, chúng ta tiếp tục tìm hiểu thêm về struct. Hãy xem ví dụ bên dưới đây, đó là toàn bộ code của hàm main():

Trong ví dụ trên, tôi có một cấu trúc là MyStruct. Cấu trúc này được khai báo global (toàn cục) thay vì được khai báo local (cục bộ) bên trong thân hàm main(). Việc khai báo như vậy có thể cho phép khả năng xử lý tốt hơn giữa các hàm khi truyền vào biến có kiểu struct. Ta khai báo biến values có kiểu MyStruct. Đây là biến local và được khai báo trong hàm main(), như vậy biến này chỉ xuất hiện trong Stack của hàm main() mà thôi.

Cấu trúc MyStruct được định nghĩa bên ngoài hàm main() cùng với các hàm khác như trong hình dưới đây:

Nhìn vào code tại main(), bạn sẽ hiểu ý tưởng của chương trình là truyền một con trỏ tới cấu trúc values, từ đó có thể đọc hoặc thay đổi giá trị các trường thuộc cấu trúc này thông qua ba hàm:

Sau khi biên dịch và load file vào trong IDA, tuy nhiên ta sẽ không load kèm theo file .pdb được sinh ra trong quá trình compile, lúc đó IDA sẽ không có thông tin căn cứ để nhận biết rằng biến values có kiểu cấu trúc:

Tại hàm main(), lúc này ta chỉ thấy chương trình thực hiện lấy địa chỉ của biến var_20, sau đó truyền địa chỉ của biến này cho các hàm sub_401090, sub_401010, sub_401050. Như vậy, ta mới chỉ biết được các hàm này nhận chung một tham số truyền vào. Khi truyền địa chỉ của biến vào cho một hàm như trên thì như vậy tham số của hàm này phải có kiểu pointer (ví dụ: int *ip; ip = & var;). Do đó, ta đi tới các hàm này và tạm thời thay đổi prototype của chúng tương tự như sau:

Kết quả tạm thời có được như hình dưới:

Nhấp đúp chuột vào biến var_20 sẽ chuyển tới cửa sổ biểu diễn Stack của hàm main(), quan sát bên dưới biến này ta thấy có nhiều từ khóa db, nên tạm đoán đây có thể là một buffer. Thực hiện chuyển đổi nó sang kiểu array, độ dài của mảng sẽ do IDA tự động nhận biết và đưa ra gợi ý:

Kết quả, ta sẽ có được một mảng có kích thước là 16 phần tử. Như tôi đã nói, nếu chúng ta không biết được mã nguồn gốc của chương trình, thì tới đây ta chỉ có thể suy đoán đó một buffer và không hề có ý tưởng hay suy nghĩ rằng nó là biến có kiểu struct. Nhấn OK để chấp nhận những gì IDA đã gợi ý, đồng thời đổi luôn tên biến thành buf:

Quay trở lại hàm main(), ta thấy có các biến cục bộ khác được khởi gán giá trị ban đầu:

Tất cả các biến đều được khởi tạo bằng 0 và không được sử dụng lại ở đâu khác trong code của chương trình. Đối với một biến cục bộ điều này tạo ra điểm bất thường vì ít khi nào biến được tạo ra, được khởi tạo giá trị nhưng lại không được sử dụng. Bên cạnh đó ta thấy có biến var_4, như một thói quen ta đổi tên nó thành Canary:

Tiếp tục kiểm tra lại toàn bộ code của hàm main(), ngoài các biến đã phân tích ở trên thì không còn biến nào khác. Tuy nhiên, chúng ta không thể đổi tên ba biến cục bộ đã được khởi gán bằng 0, bởi các biến này không được sử dụng trong hàm. Do đó, ta không biết ý nghĩa cũng như mục đích của chúng nên rất khó để đặt các tên sao cho có nghĩa. Tất nhiên, nếu không biết thì có thể đặt là temp, temp1, temp2 v..v…

Tạm thời bỏ qua các biến này, ta đi vào phân tích hàm đầu tiên sub_401090:

Ta thấy, chương trình lấy địa chỉ của biến buf và truyền địa chỉ của biến này như là một tham số cho hàm sub_401090. Đi vào hàm này, ta thấy nó yêu cầu nhập vào một số bất kì: “\nPlease Enter Number of Choice: \n”. Đọc qua code thì tạm thời đoạn được hàm này nhận giá trị do người dùng nhập vào và lưu giá trị vào buffer, do đó đổi tên hàm này thành enter(), tham số arg_0 của hàm này như phân tích chính là một pointer, nó chứa địa chỉ của biến buf, nên ta đổi tên nó thành pBuf:

Quay trở lại hàm main() ta sẽ thấy được sự thay đổi nhỏ như sau:

Kết quả này chỉ là để khẳng định lại chính xác rằng thanh EAX chứa địa chỉ của biến buf (thông qua lệnh LEA). Quay lại với hàm enter():

Trong đoạn code trên, một lần nữa ta lại thấy một điểm bất thường khác trong code của chương trình. Như ở trên, ta đã phán đoạn biến buf có chiều dài là 16 bytes hay 0x10, ở đây địa chỉ biến buf được gán vào eax và đem cộng với 0x10, sau đó giá trị tại eax sẽ được truyền làm tham số cho hàm scanf để lưu giá trị mà người dùng nhập vào từ bàn phím. Như vậy, ta thấy dường như giá trị mà người dùng nhập vào sẽ không được lưu vào buf mà lưu vào bên dưới của buf (buf + 0x10).

Điều này đã cho tôi suy nghĩ, nếu bạn truyền một con trỏ tới buffer, sau đó lại ghi dữ liệu vào, đó thường là dấu hiện điển hình của một struc t. Ta truyền địa chỉ ban đầu (base_address) cho một hàm và từ đó có thể truy cập bất kỳ trường nào, trong trường hợp này là một trường nào đó bên dưới trường đầu tiên của buf.

Kiểm tra tại Stack của main() sẽ thấy có biến var_10 là biến nằm ngay dưới buf, tương ứng với việc lấy buf làm địa chỉ base và cộng thêm 0x10:

Như vậy, ta hiểu rằng hàm enter() sẽ ghi giá trị vào biến var_10. Do đó, chỉ có thể xảy ra khả năng duy nhất là buf và var_10 đều là các trường của cùng một struct.

Nhiều người sẽ thắc mắc tại sao tôi phải quan sát và phân tích tại Stack của hàm main() thay vì kiểm tra tại hàm? Vấn đề ở đây là khi ta truyền địa chỉ Buffer, mà đó là địa chỉ bắt đầu và của cùng Buffer trong hàm main(), và nếu tôi cộng thêm 0x10 vào địa chỉ này, tôi sẽ truy cập tới biến var_10 của hàm main(). Do var_10 là một biến cục bộ của main() nên sẽ không có ý nghĩa bên trong hàm enter(), nhưng nó sẽ có ý nghĩa nếu biến thuộc về một trường của một cấu trúc.

OK, như vậy bạn đã hiểu rồi chứ … tại hàm enter() chúng ta thấy rằng:

Chương trình yêu cầu ta nhập vào một số và lưu nó vào một trường trong struct được gọi là var_10, thông qua hàm scanf_s. Do vậy, tôi sẽ đổi tên biến var_10 thành number, nhưng vì sau khi phân tích tôi biết rằng có một struct cùng với ba trường chắc chắn được truy cập bởi các hàm, tôi sẽ tạo ra struct bao gồm các trường này. Lựa chọn các biến trừ biến Canary:

Sau đó truy cập menu Edit > Create structure from selection, kết quả như sau:

Struct được tạo ra có tên là buf, tôi sẽ đổi tên nó thành values. Tiếp theo tôi đổi tên struct_0 thành MyStruct tại tab Structures:

Tiếp theo, tôi đổi tên biến var_10 thành number như đã phân tích:

Sau toàn bộ quá trình thay đổi như trên, ta xem lại code tại hàm main lúc này sẽ thấy rõ ràng hơn nhiều:

Lại tiếp tục với hàm enter(), tôi đổi tên thành lại pvalues hàm ý là trỏ tới values và nhấn ‘Y’ để đổi lại định nghĩa mới cho hàm:

Tiếp tục reverse các đoạn code tiếp theo trong hàm enter():

Trong đoạn code trên, ta thấy nó có sử dụng một trường của struct vì nó sử dụng lệnh mov để gán địa chỉ base của struct vào eax và cộng thêm 0x14 để lưu giá trị đang có tại thanh ghi ecx. Lúc này ta không cần phải đoán nữa, tại lệnh được khoanh như trên hình tôi nhấn phím ‘T’ (Structure offset):

Chọn trường tương ứng như trên hình, kết quả có được như sau:

Như vậy, bạn thấy rằng thông qua việc nhận biết đó là một trường thuộc struct, nhưng vì ở đây IDA không biết trường này thuộc về cấu trúc nào nên nó sẽ đưa ra rất nhiều gợi ý, vì vậy ta sẽ phải lựa chọn đúng nó là offset của struct mà mình biết. Đoạn code trên là vòng lặp được sử dụng để lọc kí tự 0xA sau lệnh scanf, kí tự nhận được sau hàm getchar() sẽ được lưu vào biến var_44 và trường thuộc MyStruct, ta đổi tên lại như sau:

Tiếp tục các bạn sẽ thấy cơ chế tương tự để truy cập các trường thuộc struct: Lấy địa chỉ base của struct, sau đó cộng với một offset để truy cập tới trường mong muốn:

Tiếp tục nhấn ‘T’ để lựa chọn đúng offset:

Ta đi đến những dòng code cuối cùng của hàm enter(), nó không thực hiện thêm công việc gì khác cũng như không trả về giá trị cho thanh ghi eax. Hàm này tóm lại chỉ thực hiện nhiệm vụ là nhận số mà ta nhập vào và lưu nó vào trường number trong MyStruct.

Ta phân tích hàm tiếp theo là sub_401010(). Tại hàm này ta cũng thực hiện đổi tên tham số arg_0 thành pvalues giống như đã làm với hàm enter() đồng thời thay đổi luôn cả prototype của hàm. Kết quả có được như hình dưới đây:

Trong đoạn code trên, ta thấy nó truy cập tới một trường trong struct và so sánh giá trị của trường đó với 0x10, nhấn T tại lệnh đó và chọn đúng offset:

Như vậy, hàm sẽ so sánh số ta nhập vào với 0x10. Qua đó, ta thấy rằng khi chúng ta quản lý chúng như một cấu trúc, các trường trong cấu trúc có thể nhận giá trị qua một hàm, được kiểm tra giá trị bằng một hàm khác và sau đó có thể được xử lý ở một hàm thứ ba nào đó. Rõ ràng, nếu chúng ta đi theo con trỏ tới cấu trúc, chúng ta luôn có thể xác định được trường nào trong struct mà không cần phải debug, còn nếu ta xem nó như một biến thì sẽ rất phức tạp để xác định rằng nó luôn có giá trị như nhau.

Tiếp tục reverse code của hàm sub_401010(). Tiếp tục lựa chọn struct cho offset:

Vậy là trường number được sử dụng như là kích thước của buffer khi gọi hàm gets_s, và trường number này có thể chứa giá trị 0xffffffff vì lệnh kiểm tra trước sẽ kiểm tra số có dấu (căn cứ lệnh nhảy jle), như vậy giá trị 0xffffffff sẽ là -1, nhỏ hơn 0x10 và do đó sẽ vượt qua được đoạn kiểm tra ở trước. Hàm gets_s lúc này được truyền địa chỉ của pvalues, mà trường đầu tiên sẽ là buf, do đó hàm gets_s sẽ ghi dữ liệu vào buffer này. Nếu với trường number được gán giá trị 0xffffffff thì sẽ có overflow tại buffer. Ta đổi tên hàm sub_401010() thành check():

Ta phân tích hàm cuối cùng tại sub_401050(). Đổi tên biến arg_0 thành pvalues và thay đổi lại prototype của hàm:

Trong code có truy xuất tới một trường của struct, lựa chọn offset cho nó và đổi lại tên của trường từ var_8 thành cookie:

Ở đây nếu trường cookie có giá trị bằng giá trị mặc định là 0x45934215 thì code của hàm sẽ rẽ tới nhánh để in ra màn hình thông báo “You are a winner man”. Do vậy, ta đổi sub_401050 tên thành decision.

Với các thông tin sau khi phân tích, chúng ta sẽ có ý tưởng để có thể tới được đoạn code in ra thông báo “You are a winner man”. Tại màn hình bố trí Stack của hàm main(), ta đã biết được các trường buffer và cookie đều thuộc struct là MyStruct. Chuyển tới tab Structures để tính toán kích thước của từng trường:

Với thông tin trên, ta sẽ phải ghi vào buf 16 kí tự (ví dụ 16 chữ ‘A’), sau đó ghi đè lên hai dwords là number và c, cuối cùng là cookie:

user_input = "A" * 16 + number + c + cookie

Ta có script như sau:

from subprocess import *import structp = Popen([r'Struct.exe', 'f'], stdout = PIPE, stdin = PIPE, stderr = STDOUT)print "Attach to the debugger and press Enter\n"raw_input()num = "-1\n"p.stdin.write(num)
number = struct.pack("<L", 0x1c)c = struct.pack("<L", 0x90909090)cookie = struct.pack("<L", 0x45934215)user_input = "A" * 16 + number + c + cookie + "\n"p.stdin.write(user_input)testresult = p.communicate()[0]print user_inputprint(testresult)

Trong script trên, khi chương trình yêu cầu nhập 1 số, ta truyền cho nó số -1, như vậy sẽ vượt qua đoạn kiểm tra khi nó so sánh một số có dấu (số âm) với 0x10.

Khi đó hàm gets_s sẽ nhận được user_input truyền vào ở trên, qua đó sẽ ghi đè lên 16-byte của buf với 16 chữ ‘A’, trường number sẽ nhận giá trị 0x1c, trường c có thể nhận bất kỳ giá trị bất kì (ví dụ: 0x90909090) và sau đó là cookie với giá trị 0x45934215.

Kết quả khi chạy script:

Vâng với kết quả này, chúng ta đã kết thúc toàn bộ phần 26 ở đây. Tôi gửi kèm một bài tập gọi là IDA_STRUCT.7z ( https://mega.nz/#!qf5RECST!ifBTTbawra5hGCrh_mmJ9lutmafg9a31vuRy7lUQ2xw) để các bạn phân tích xem liệu nó có vulnerable hay không và nếu có thì khai thác thế nào.

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

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

--

--