Archive for December, 2019


Phần 27 này sẽ giải quyết bài tập (IDA_STRUCT.7z – do chính thầy Ricardo Narvaja biên soạn) mà tôi đã gửi kèm ở phần trước. File nén này gồm một file thực thi là ConsoleApplication4.exe và một file pdb đi kèm là ConsoleApplication4.pdb (mục đích để các bạn kiểm tra lại sau khi đã tự reverse code).

Thông thường, khi load chương trình vào IDA, IDA sẽ kiểm tra và phát hiện ra file mà chúng ta đang phân tích có link tới symbol file và hỏi xem có muốn load file này không? Ví dụ như hình dưới đây:

Tuy nhiên, trong các tính huống thực tế thì ta sẽ không có file pdb này. Do vậy, để đúng với thực tế tôi sẽ chọn No để không load kèm file. IDA sau khi phân tích xong sẽ dừng lại tại đây:

Rõ ràng là khi không có symbol file, IDA sẽ không phát hiện ra được vị trí chính xác của hàm main(). Nhưng vì đây là một chương trình dạng console, ta có thể tìm được hàm main() thông qua việc tìm kiếm các tham số argv hoặc argc hoặc là tìm kiếm theo các strings của chương trình.

Khi chạy chương trình, tương tự như ở phần trước, bạn sẽ nhận được thông báo sau “Please Enter Your Number of Choice:”. Dựa vào thông tin này, ta tìm được chuỗi tại tab Strings của IDA:

Nhấp đúp vào chuỗi này, IDA sẽ đưa ta đến đây:

Tại địa chỉ 0x402100, ta thấy chuỗi có một tham chiếu tới (XREF), bằng cách di chuyển chuột tới địa chỉ tham chiếu, hoặc nhấn ‘X’, ta sẽ có được các kết quả như sau:

Chuyển tới đoạn code có sử dụng tới chuỗi “Please Enter Your Number of Choice:”:

Quan sát trên hình, có thể thấy ta đang ở trong một hàm, bên dưới chuỗi là một lời gọi để in nó ra màn hình, nhưng vì chúng ta không load pdb file nên IDA không nhận biết được sub_401220() chính là hàm printf(). Nếu ta quan sát code tại sub này sẽ thấy nó lại gọi tới các hàm khác nữa:

Chuyển sang xem ở chế độ proximity:

Như trên hình, sub_401220() gọi tới 3 hàm là sub_401000(); sub_4010F0() và __acrt_iob_func(), nhưng cả hai sub_401000() và __acrt_iob_func() là các hàm thực hiện nhiệm vụ nào đó và trả về kết quả, các hàm này không gọi tới các hàm con nào khác. Chỉ riêng sub_4010F0() là gọi tới hai hàm khác và một trong số đó là __stdio_common_vfprintf(). Như vậy, thay vì phải đi phân tích lần lượt từng hàm thì với sự trợ giúp của chế độ proximity, ta có thể kết luận sub_401220() chính là hàm printf(). Vì vậy, tôi đổi tên cho sub_401220() thành printf():

Tiếp tục phân tích code, ta sẽ gặp lời gọi tới sub_401260(), mà nếu căn cứ theo ví dụ trước ta có thể đoán nó chính là hàm scanf():

Tiếp tục sử dụng chế độ proximity view, có được kết quả như hình:

Do vậy, ta đổi tên sub_401260() thành scanf():

Quan sát code của sub_00401080(), ta nhận thấy dấu hiệu có thể là một struct. Bời vì khi một địa chỉ được truyền vào như một tham số của hàm, sau đó nó được lấy ra và cộng thêm một giá trị offset để truy cập tới các trường tại những nơi mà nó được sử dụng thì khả năng cao đó là địa chỉ của một struct.

Tìm kiếm các tham chiếu gọi tới sub_00401080():

Kết quả có hai chỗ gọi tới:

Ta thấy rằng, tham số của hàm trong cả hai trường hợp là một địa chỉ nào đó, điều này đưa lại ý tưởng liên quan tới các struct. Tuy nhiên, nhận thấy đây là hai địa chỉ khác nhau, vậy khả năng chúng có thể là hai cấu trúc cùng kiểu. Ta sẽ bắt đầu tạo struct đầu tiên, lúc này không hề có thông tin gì về size của struct, không biết về các trường hay bất cứ thứ gì thuộc về struct. Ta sẽ thực hiện reverse từ từ, từng chút một.

Căn cứ vào code tại sub_00401080(), tạm thời có thông tin offset tối đa là 0x14. Do vậy, đầu tiên tôi sẽ tạo ra một struct có độ dài như vậy, nếu như trong quá trình phân tích nó lớn hơn thì ta sẽ mở rộng thêm sau. Chuyển tới tab Structures của IDA:

IDA hỗ trợ nhiều cách khác nhau để tạo một struct: có thể sử dụng tab Local Types (Shift+F1) để tạo struct dưới dạng mã C, hoặc là phân tích stack và sử dụng Create struct from selection để tạo struct, tương tự như phần trước đã làm. Ở phần này tôi sẽ tạo struct từ tab Structures, và để tạo struct ta nhấn phím Insert:

Bạn có thể đặt tên bất kỳ mà bạn muốn, ở đây tôi đặt là MyStruct:

Rõ ràng ta chưa định nghĩa trường nào trong struct nên nó được tạo ra với kích thước bằng 0x0. Do chưa có thông tin về các trường mà mới chỉ biết được offset lớn nhất là 0x14, nên đầu tiên tôi nhấn phím D để tạo một trường bất kỳ đã:

Như các bạn thấy trên hình, ta có được 1 trường thuộc struct có kích thước là 1 byte (db). Nếu tôi nhấn D một lần nữa tại trường này tôi sẽ thay đổi size của nó thành word (dw) và tiếp nữa sẽ thành dword (dd). Nhưng do không biết chính xác nên tạm thời cứ để nguyên và nhấn chuột phải vào struct, chọn Expand struct type… (Ctrl+E).

Như đã nói, ta đã biết có một trường nằm ở offset 0x14 và trường đó kích thước là 1 dword (dựa vào lệnh gán giá trị thanh ghi edx vào trường này):

Vì vậy, để tạo một trường có size là 1 dword, lại nằm ở offset 0x14, tôi sẽ thêm vào 0x17 bytes. Tổng cộng struct lúc này sẽ có 0x18 bytes (0x17 bytes thêm vào + 1 byte ban đầu):

OK! Với cách tạo như trên, tạm thời chúng ta có được struct là MyStruct, có kích thước là 0x18, nếu cần thiết ta có thể mở rộng tiếp.

Theo phân tích ở đầu bài viết, sub_00401080() sẽ được gọi hai lần. Lần thứ nhất được truyền tham số là một địa chỉ của một cấu trúc kiểu MyStruct, ta có thể đặt tên là struct1 và lần thứ hai cũng là một tham số là địa chỉ của cấu trúc thứ hai cũng cùng kiểu MyStruct, ta đặt tên là struct2. Mã nguồn gốc như hình dưới, để phân biệt giữa các biến có cùng kiểu MyStruct thì một biến được đặt tên là struct1 còn biến kia là struct2, các biến này được truyền địa chỉ của chúng như là tham số cho các hàm.

Quay lại lời gọi tới sub_00401080() đầu tiên tại địa chỉ 0x00401196, như đã phân tích cũng như nhìn vào mã nguồn gốc thì hàm này sẽ nhận vào địa chỉ của cấu trúc đầu tiên (struct1) và lưu vào arg_0 (do IDA không nhận biết được nên nó đặt tên cho tham số lưu địa chỉ này là như thế) và lần gọi thứ hai tại địa chỉ 0x004011BA thì sub_00401080() sẽ nhận địa chỉ của cấu trúc thứ hai (struct2). Do vậy, ở đây tôi đổi lại tên tham số arg_0 thành một tên chung cho cả hai struct, ví dụ _struct.

Khi nhấn F5 để decompile hàm này, ta sẽ thấy rằng nó vẫn chưa đúng như ý muốn:

Dựa theo kết quả decompile thì thấy tham số _struct được nhận biết và định nghĩa đơn giản là biến kiểu int. Nó không giống như đã thấy ở mã nguồn gốc khi tham số này là địa chỉ của một cấu trúc. Để điều chỉnh lại, ta nhấn chuột phải tại tham số này và chọn như hình:

Cửa sổ Select a structure xuất hiện, lựa chọn struct vừa tạo là MyStruct:

Kết quả prototype của hàm sẽ được chuyển thành:

Tại main() lúc này biến var_24 tự động được đổi tên thành struct, và rõ ràng struct này chính là struct1. Quan sát struct tại cửa sổ biểu diễn Stack của main(), để thông báo cho IDA biết struct có kiểu là MyStruct, ta nhấn ALT + Q (Edit > Struct var…) tại struct.

Làm như vậy sẽ gán struct là kiểu MyStruct, kết quả có được như hình dưới:

Ta đổi tên struct thành struct1. Ở lần gọi sub_00401080() thứ hai, địa chỉ của var_44 được truyền cho hàm, biến này cũng sẽ là kiểu MyStruct, làm tương tự như trên để chỉ định nó là biến kiểu MyStruct đồng thời đổi tên luôn thành struct2:

Tới thời điểm này ta đã có hai cấu trúc là struct1 và struct2 có kiểu MyStruct. Trở lại code của hàm:

Ta thấy đầu tiên code của hàm lấy ra địa chỉ base của struct và cộng với offset 0x10 để truy cập trường tại vị trí 0x10. Trường này có kích thước là 1 dword, và nó sẽ nhận giá trị của hàm scanf(). Do đó, đi đến MyStruct và tại offset 0x10 chúng ta nhấn D cho đến khi nó có kiểu dword (dd). Kết quả sẽ có được như sau:

Dựa trên thông báo yêu cầu nhập vào một số, tôi đổi tên cho trường này thành number. Đoạn code tiếp theo truy cập tới trường 0x14, được sử dụng trong vòng lặp để loại bỏ 0A. Tôi đổi tên trường này thành c.

Tại offset 0x14 của MyStruct, nhấn D cho đến khi chuyển thành một DWORD, ta đổi tên trường vừa tạo thành c.

Quay trở lại code và lựa chọn struct offset, kết quả ta sẽ được như sau:

Sau khi thực hiện xong ta đổi tên hàm sub_401080() thành enter().

Tiếp tục phân tích tiếp sub_401010() (lời gọi tới hàm này tại địa chỉ 0x4011A2). Tại đây, cũng đổi tên arg_0 thành struct:

Hàm này cũng được gọi hai lần và cũng được truyền vào địa chỉ của cả hai cấu trúc. Do đó, áp dụng cách tương tự như trên, nhấn F5 để decompile hàm này:

Sau đó, chọn biến struct và nhấn chuột phải chọn Convert to struct *:

Sau khi convert xong, đối với các trường đã biết, ta sẽ lựa chọn offset cho nó:

Ở đây, ta lại thấy lại đoạn code so sánh giống như ở phần 26. Giá trị số mà ta nhập vào được lưu tại trường number, và bởi vì so sánh ở đây là số có dấu nên trường number này có thể chứa giá trị 0xffffffff (-1), nhỏ hơn 0x10 và do đó sẽ vượt qua được đoạn kiểm tra. Hàm gets_s lúc này sẽ sử dụng giá trị của trường number như là tham số thứ hai đại diện cho kích thước của buffer. Như vậy, tham số thứ nhất phải là buffer và buffer này nằm tại đầu của struct. Ta chuyển tới định nghĩa của MyStruct, tại offset 0x0, nhấn phím D một lần để tạo một trường single byte.

Sau đó nhấn chuột phải tại trường này và chọn Array. Thiết lập kích thước cho array này:

Và đổi tên trường thành Buffer:

Code được decompile lúc này sẽ rõ ràng hơn nhiều:

Tương tự như đã phân tích ở phần 26, hàm gets_s() chắc chắn sẽ bị overflow vì đơn giản ta có thể truyền vào một số âm 0xffffffff (-1) để bypass đoạn code so sánh và được sử dụng như là size của hàm. Nhưng khi được sử dụng tại hàm gets_s() thì size lúc này sẽ lại trở thành số dương lớn nhất. Ta sẽ đổi tên hàm sub_401010() thành check().

Đi tới hàm tiếp theo trong main() là sub_401060(). Thực hiện đổi tên của tham số, nhấn F5 để decompile và chuyển đổi tham số này thành kiểu struct:

Quay trở lại mã asm của hàm, sẽ thấy struct của chúng ta phải có thêm một trường nữa vì code của chương trình đang cố gắng so sánh [EAX + 0x18] với một giá trị mặc định. Do ban đầu tại cấu trúc MyStruct ta mới chỉ định nghĩa trường cuối cùng tại offset 0x14, để bổ sung thêm trường tại offset 0x18 thì ta phải mở rộng struct này ra.

Chuyển tới chỗ định nghĩa MyStruct, chuyển xuống cuối của struct này tại từ khóa “ends” và nhấn D cho đến khi một trường DD DWORD mới được tạo ra có tên là field_18:

Đổi tên trường này thành cookie:

Chuyển về code và lựa chọn offset của trường vừa tạo:

Đi xuống bên dưới, ta thấy code của hàm lại truy cập một trường khác nữa tại offset 0x1C, và trường này có kích thước là 1 byte:

Tiếp tục mở rộng MyStruct và tạo một trường có kích thước 1 byte tại offset 0x1C, đồng thời đổi tên luôn thành flag:

Quay lại code và lựa chọn offset, kết quả cuối cùng như sau:

Nhìn vào đoạn code trên, nếu giá trị tại trường cookie bằng 0x99989796 thì sẽ gán cho trường flag bằng 1. Do vậy, đổi tên sub_401060() thành decision():

Sau toàn bộ các thay đổi như trên, quay trở lại hàm main(), có vẻ các thay đổi này không có tác dụng tới toàn chương trình nên các biến tại hàm main() sẽ bị hỏng, tương tự như hình dưới đây:

Để có thể sửa lỗi này, chuyển lên đầu hàm main() và nhấn U (Undefine):

Nhấn Yes để đồng ý:

Sau đó, vẫn tại cùng vị chí đã chọn ở trên, nhấn C để IDA build lại code của hàm:

Cuối cùng, nhấn chuột phải và chọn Create Function. Kết quả có được sẽ như hình dưới đây, lỗi đã được fix hoàn toàn:

Quay lại cửa số biểu diễn Stack của hàm main(), nhận thấy rằng sau mỗi cấu trúc sẽ có thêm 3 bytes trống vì trường cuối cùng chỉ chiếm 1 byte, lúc này các biến không còn giữ tên mà ta thay đổi nữa mà đã bị đổi thành:

Đổi lại thành struct1 và struct2 như đã đặt ban đầu.

Trong hình trên, còn một hàm nữa vẫn chưa phân tích là sub_401040(). Hàm này nhận tham số truyền vào là địa chỉ của struct2. Đi vào hàm này sẽ thấy code của nó tương tự như hàm decision() đã phân tích ở trên, chỉ khác là lúc này [eax+0x18] sẽ được so sánh với một giá trị mặc định khác là 0x33343536h. Sử dụng phím tắt ‘T’ để lựa chọn các trường của struct, có được kết quả như sau:

Đổi tên sub_401040() thành decision2(). Tổng kết lại, trường cookie thuộc struct1 phải có giá trị 0x99989796, còn trường cookie của struct2 phải là 0x33343536, từ đó cả hai trường flag của mỗi cấu trúc sẽ được thiết lập bằng 1. Các trường flag này sau đó sẽ được đem so sánh với 1, nếu bằng sẽ hiển thị thông báo “Genious you are the man\n“, còn không sẽ hiển thị “Not not and not\n“:

Như vậy, để tới được thông báo “Genious…” thì bắt buộc cả hai trường flag phải được gán bằng 1.

Việc thực hiện overflow cho bài này sẽ có nhiều cách khác nhau, tùy giải pháp của từng người. Với tôi, tôi sẽ tìm cách ghi đè giá trị 1 vào trường flag, làm thế sẽ khiến flag luôn có giá trị 1 ngay cả khi các giá trị cookie không bằng với các giá trị mặc định. Để làm được như vậy, tôi sẽ tính toán xem cần phải vượt qua bao nhiêu bytes để ghi đè lên trường flag. Phân tích cấu trúc MyStruct:

Với thông tin có được trên hình, để ghi đè vào flag, tôi cần ghi dữ liệu vào 16 bytes của Buffer, cộng thêm 3 dwords nữa (tức là 12 bytes), tương ứng với các trường number, c và cookie. Tổng cộng sẽ là: 16 + 12 = 28 bytes. Như vậy user_input= 28* “A” + “\x01”. Script thực hiện sẽ như sau:

from subprocess import *
import struct
p = Popen([r'ConsoleApplication4.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)

user_input = "A" * 28 + "\x01" + "\n"
p.stdin.write(user_input)

num = "-1\n"
p.stdin.write(num)

user_input = "A" * 28 + "\x01" + "\n"
p.stdin.write(user_input)

testresult = p.communicate()[0]

print user_input
print(testresult)

Như vậy, với mỗi cấu trúc ta truyền vào số -1 cho hàm scanf() để vượt qua đoạn kiểm tra 0x10 và biến user_input để truyền cho hàm gets_s(). Kết quả khi thực hiện script:

Khi chúng ta thực hiện debug để kiểm tra, sẽ thấy giá trị -1(0FFFFFFFFh) được lưu tại trường number của struct1.

Với giá trị này sẽ vượt qua đoạn kiểm tra và đi tới hàm gets_s để nhận user_input.

Kiểm tra thông tin của trường Buffer thuộc struct1, có kết quả như sau:

Như vậy ta thấy hàm gets_s() đã nhận user_input mà chúng ta truyền vào. Tại đây, ta chuyển nó thành structure bằng cách nhấn ALT+Q và chọn MyStruct:

Kết quả có được như hình dưới, ta sẽ thấy trường flag lúc này chứa giá trị là 1, còn trường cookie đang có giá trị là 0x41414141:

Tiếp tục trace code ta tới đây:

Chương trình thực hiện so sánh cookie với 0x99989796, bằng nhau thì sẽ gán flag bằng 1. Nhưng vì ta đã có flag bằng 1 rồi nên không cần quan tâm đến kết quả của việc so sánh nữa. Cũng tương tự khi trace vào trong code của hàm desicion2().

Như vậy, các trường flag của cả hai cấu trúc lúc này đều đã được gán bằng 1. Các trường này sẽ được đem đi so sánh với 1:

Sau khi vượt qua hai đoạn so sánh, cuối cùng cũng tới được đoạn code hiển thị thông báo “Genious you are the man\n”.

Như vậy, tôi đã hoàn thành xong bài tập này. Phần 27 đến đây là kết thúc, hẹn gặp lại các bạn trong phần 28!

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


As part of my work at Vincss, our team recently analyzed malicious code embedded within document file that targeted to Viet Nam. You can see the write-up here.

Regards,

m4n0w4r


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 struct. 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 struct
p = 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_input
print(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!

Image result for are u understand gif

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