REVERSING WITH IDA FROM SCRATCH (P27)

Posted: December 29, 2019 in IDA Tutorials, REVERSING WITH IDA FROM SCRATCH (P27)
Tags: ,

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.