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


Mặt trời là cái bếp lớn, còn tia nắng là than hồng
Mỗi ngày mà ta thức dậy, ta chỉ mong được an lòng
Hoàng hôn là dải lụa, còn màn đêm là tấm chăn
Mỗi đêm ta ngồi ta viết ta chỉ mong không bị cấm ngăn
_Đen_

STRUCTURES

Trong phần tiếp theo này, chúng ta sẽ bắt đầu tìm hiểu cách mà IDA Pro hỗ trợ để reverse các chương trình có sử dụng struct. Phần cuối bài viết sẽ là những giải đáp ngắn gọn về các bài tập IDA3.exeIDA4.exe đã gửi kèm ở phần trước.

Vậy structure là gì?

Nôm na như sau, các bạn đã biết về khái niệm một mảng (array) trong lập trình C, mảng là một tập hoặc một nhóm các phần tử (dữ liệu) có kiểu dữ liệu đồng nhất. Các phần tử của mảng được lưu tại các vùng nhớ liên tiếp. Tương tự như vậy, cấu trúc (structure) trong C cũng là một kiểu dữ liệu do người dùng định nghĩa, nhưng khác mảng, structure cho phép kết hợp các dữ liệu có kiểu khác nhau. Chi tiết hơn các bạn đọc thêm trong các tài liệu về lập trình C/C++.

Về cơ bản khi phân tích trong IDA, chúng ta thấy rằng arrays như kiểu một “thùng chứa” dữ liệu, được dành riêng một không gian liên tục trong bộ nhớ để chứa các trường có cùng kiểu dữ liệu. Vì vậy, ta có thể có mảng các bytes, words, dwords. Ý chính cần nhấn mạnh ở đây là mảng chỉ chứa các phần tử có cùng kiểu dữ liệu mà thôi. Ví dụ minh họa như sau:

Trong hình trên là ví dụ về một array có kích thước bằng 34, và mỗi phần tử có kích thước 2 bytes, tức là mỗi phần tử chiếm một word. Do đó, tổng chiều dài của mảng này sẽ là 34 * 2 hay 68 (ở hệ thập phân). Như vậy, trong ví dụ minh họa về mảng này, mỗi phần tử có kích thước 1 word, chiếm 2 bytes, nếu tôi muốn một mảng có các phần tử chỉ chiếm 1 byte thì tôi sẽ phải tạo ra một mảng khác, vì ta không thể trộn lẫn dữ liệu với kích thước hoặc kiểu khác nhau.

Để giải quyết hạn chế của mảng, cấu trúc ra đời cho phép kết hợp các loại dữ liệu khác nhau có kích thước khác nhau bên trong nó. Tóm lại, ta có thể xem structure cũng là một loại “thùng chứa” có thể chứa các loại dữ liệu khác nhau. Mỗi phần tử được lưu trữ bên trong cấu trúc được gọi là thành viên/trường (member) của cấu trúc này và nó sẽ thuộc về một loại dữ liệu nhất định. Như vậy, kích thước của structure sẽ là tổng kích thước của tất cả các thành viên hoặc các trường thuộc structure đó.

Ví dụ tôi có khai báo một structure đơn giản như sau:

struct MyStruct
{
	char * p_size;
	char size;
	int cookie;
	int leidos;
	int cookie2;
	int maxsize;
	int(*foo2)(char *);
	void * buf2;
	char buf[50];
};

Trong ví dụ trên, bạn thấy định nghĩa cho một cấu trúc có tên là MyStruct. Bên trong struct này có nhiều trường (biến) với các kiểu dữ liệu khác nhau (kiểu int; kiểu char hay một buffer có kích thước 50 bytes).  Nếu sử dụng Visual Studio, bạn có thể thấy được kích thước của toàn bộ struct này là 0x54.

Nếu các bạn cộng nhẩm bằng tay thì sẽ nhận thấy rằng kết quả sẽ nhỏ hơn một chút so với những gì mà VS hiển thị sau khi biên dịch. Đó là bởi vì trong quá trình compile code, trình compiler có thể thêm vào các padding bytes để căn chỉnh sao cho phù hợp. Do vậy, thông thường thì kích thước thực tế sẽ lớn hơn. Tuy nhiên, điều quan trọng hơn cả việc tính toán kích thước đó là để nhận ra được đâu là struct là rất khó khi đã được phân rã ở mã asm. IDA chỉ có thể nhận diện từng trường và kiểu dữ liệu một cách tự động, đặc biệt nếu chúng ta không có file pdb đi kèm thì rất khó để nhận diện.

Ta thử làm một ví dụ đơn giản về struct, sau đó sử dụng IDA để nhận diện cũng như quản lý struct trong IDA. Code của ví dụ như hình dưới đây:

Như các bạn thấy, code của chương trình khá đơn giản, nó sẽ nhận tham số truyền vào thông qua màn hình console khi chạy, nếu ta không truyền tham số, chương trình sẽ hiển thị thông báo “Bye Bye“.

Trường hợp có truyền tham số cho chương trình, kết quả khi thực thi sẽ như sau:

Nếu bạn chỉ nhấp đúp vào chương trình hoặc không truyền tham số cho nó, code trong chương trình sẽ kiểm tra thấy argc (số tham số truyền vào) khác 2 và sẽ thoát luôn:

if (argc != 2) 
{
        printf("Bye Bye");
        exit(1);
}

Khi ta có truyền tham số cho chương trình thì sẽ vượt qua được đoạn kiểm tra trên, vì lúc đó argc sẽ là 2. Số tham số của chương trình sẽ bao gồm tên của file thực thi (trong trường hợp này là struct.exe) và chuỗi “aaaaaaaaaa” mà ta truyền vào, tổng cộng sẽ là 2.

Để định nghĩa một cấu trúc nào đó ta sử dụng từ khóa là struct, trong ví dụ này là struct MyStruct. Sau khi có cấu trúc rồi thì nó cũng tương tự như một kiểu bình thường (int, float, char, …) và ta chỉ việc khai báo biến nữa là xong. Cũng giống như cách chúng ta khai báo một biến có kiểu integer, ta sẽ đặt kiểu dữ liệu phía trước tên biến, ví dụ:

int cookie2;

Tương tự với trường hợp của biến kiểu cấu trúc:

MyStruct values;

Với cách khai báo như trên thì biến values sẽ thuộc kiểu MyStruct, và sẽ có cùng định nghĩa, cùng độ dài và cùng các trường. Và như vậy, ta có thể tạo ra nhiều biến khác nhau có kiểu MyStruct:

MyStruct data;

Để truy cập đến các thành viên được khai báo bên trong của cấu trúc ta sử dụng toán tử chấm (.). Ví dụ:

values.size
data.size
values.cookie2

Khi đã truy xuất được tới các thành viên của cấu trúc thì mỗi thành viên đó được xem như là một biến bình thường và ta gán giá trị hoặc nhập xuất giá trị cho chúng như cách thông thường mà chúng ta vẫn làm. Ví dụ:

values.cookie2 = 0;
values.size = 50;

Tiếp theo, chương trình sử dụng hàm strncpy để copy chuỗi lưu tại biến argv[1] vào vùng đệm values.buf có kích thước là 50 bytes, với values.size là kích thước tối đa (giá trị của values.size là 50, hay cụ thể hơn chính là số lượng kí tự tối đa được copy từ chuỗi nguồn (lưu tại argv[1]) vào chuỗi đích tại values.buf).

strncpy(values.buf, argv[1], values.size);

Như vậy, rõ ràng là sẽ không xảy ra overflow vì chỉ được sao chép tối đa 50 bytes vào một bộ đệm có kích thước 50 bytes. Do vậy, không thể xảy ra overflow.

Chương trình sử dụng hàm printf để in ra màn hình nội dung chúng ta đã nhập mà hiện tại đang được lưu trữ trong values.buf:

printf(values.buf);

Và cuối cùng gọi hàm getchar() nhằm mục đích để chương trình không tự động đóng mà chờ cho đến khi ta nhập vào một phím bất kỳ. Mục đích là để ta có thể thấy được chuỗi in ra trên màn hình console.

Toàn bộ hoạt động của chương trình đơn giản chỉ có vậy. Bây giờ, ta hãy load file đã biên dịch vào trong IDA để phân tích và tìm hiểu cách để làm việc với struct như thế nào:

Ta thấy rằng khi phân tích file mà có kèm theo .pdb thì mọi thứ quá tuyệt vời. IDA sẽ dễ dàng nhận diện được biến có kiểu MyStruct. Thậm chí, ngay cả tên các trường (thuộc struct) được truy cập trong code của chương trình cũng được nhận diện một hoàn hảo thông qua các câu lệnh asm như hình dưới đây:

Và cả ở đoạn code này:

Hơn nữa, ngay cả khi chuyển tới Tab Structures của IDA (Shift+F9), ta cũng thấy MyStruct được định nghĩa tại đây:

Để xem nội dung của MyStruct ta có thể nhấn đúp chuột hoặc nhấn phím tắt là CTRL-NUMPAD+. Kết quả có được gần giống như ta đã định nghĩa struct ở VS:

struct MyStruct
{
	char size;		// 1 byte
	int cookie2;	// 4 bytes
	char buf[50];	// 50 bytes
};

Các bạn thấy rằng tên trường và kích thước của từng trường trong struct tương ứng với những gì đã định nghĩa trong mã nguồn của chương trình. Biến size có kiểu char, kích thước là 1 byte sẽ tương ứng với từ khóa là db, biến cookie2 kiểu int, có kích thước là 4 bytes sẽ tương ứng với từ khóa là dd và biến buf là một mảng có 50 bytes. Điểm khác biệt ở chỗ là khi compile chương trình thì compiler sẽ tự động chèn thêm các padding bytes vào, nên các bạn sẽ thấy có các “db” nằm ở giữa các biến như trên hình. Do vậy, kích thước của struct sau khi compile xong sẽ khác với những gì mà ta tính toán thủ công bằng cách tính tổng cộng size của từng trường.

Bên cạnh đó trong IDA còn cung cấp cho chúng ta một tab khác có tên là Local Types. Tab này cho phép ta có thể chỉnh sửa và thêm các struct theo định dạng C++. Để mở tab này các bạn có thể thông qua menu là Open subviews hoặc nhấn phím tắt là Shift + F1. Tại đây IDA sẽ hiển thị rất nhiều kết quả, để tìm được struct của chúng ta thì các bạn nhấn CTRL + F và nhập vào tứ khóa cần tìm (ví dụ: My), kết quả có được như sau:

Sau đó, nhấn chuột phải tại MyStruct và chọn Edit, hoặc nhấn phím tắt là Ctrl + E:

Kết quả hiển thị đúng như những gì ta đã khai báo tại VS.

Nhưng “cuộc sống chả giống cuộc đời”… thực tế mọi thứ sẽ không giống như những gì bạn đã thấy ở trên. Dễ thế Đông Lào người ta mở lớp dạy reverse đầy, đâu cần phải có người viết dạo như tôi 😦. Đến chuyên gia tây lông người ta còn phải đăng status như thế này là đủ hiểu rồi…

Khi bạn phân tích một file binary, bạn chỉ có được duy nhất binary đó thôi, lấy đâu ra cái file .pdb đi kèm để nó hiển thị rõ ràng như thế trong IDA. Khi không có .pdb file thì IDA cũng điếc, vì IDA sẽ không thể phát hiện ra tên các biến cũng như các struct của chương trình. Tuy nhiên, IDA là một công cụ “interactive”, nó sẽ cung cấp cho chúng ta khả năng tương tác tuyệt vời nhất có thể để chỉnh sửa hoặc định nghĩa struct trong quá trình phân tích.

Giờ ta thử load lại file mà không load kèm theo file .pdb xem thế nào:

Các bạn thấy rằng lúc này IDA không còn phát hiện ra struct nữa và các trường của struct lúc này được IDA nhận diện như là các biến riêng lẻ, độc lập. Tuy nhiên, chẳng ai có thể phản đối được việc IDA đảo ngược code kiểu như thế này.

Vấn đề ở chỗ đây chỉ là một ví dụ nhỏ, có duy nhất một hàm main() cùng với một struct được định nghĩa rất đơn giản. Sau này, khi các bạn làm nhiều sẽ gặp các chương trình có nhiều hàm cùng các cấu trúc được định nghĩa rất phức tạp, chúng được truyền trong cùng một hàm hoặc từ hàm này sang hàm khác thông qua địa chỉ bắt đầu của struct, khi đó rất khó để biết đó là struct cũng như các trường tương ứng trong struct đó.

Đây là một điều không thể tránh đươc, do đó bắt buộc bạn phải biết về struct, và để biết thì không có cách nào khác ngoài việc luyện tập. Bây giờ, trong trường hợp của ví dụ này, việc ta reverse các trường như các biến riêng lẻ hoàn toàn được, nhưng tuy nhiên chúng ta sẽ thực hiện như thể chúng ta đã biết rằng đó là một struct. Ở các phần sau tôi sẽ nêu cách để phát hiện/ nhận biết khi nào nó là một struct và khi nào nó là biến cục bộ.

OK, tiến hành reverse dần dần, đầu tiên ta biết được biến var_4 chính là CANARY (random cookie value), do đó ta đổi tên nó:

Chương trình sẽ thực hiện so sánh tham số argc với 2, nếu bằng (tức là có truyền tham số) thì sẽ tiếp tục chạy, nếu không bằng in ra màn hình thông báo “Bye Bye” và gọi hàm exit() để thoát:

Phía trên đoạn so sánh, ta thấy chương trình gán giá trị 0x32 vào biến var_40 và sau đó sử dụng biến var_40 này như là biến đếm số lượng kí tự được phép copy, và truyền vào làm tham số cho hàm strncpy(). Do vậy, ta đổi tên biến này thành size tương ứng như trong mã nguồn của chương trình:

Để chuột tại biến này, IDA sẽ hiển thị thông tin nhận diện nó như là một biến kiểu byte (db):

Hoặc bằng cách nhấn chuột phải tại biến này, IDA sẽ cung cấp cách biểu diễn khác cho chúng ta để biết rằng nó là một biến kiểu byte:

Trong mã nguồn của chương trình, biến cookie2 ban đầu được khởi gán bằng 0 (values.cookie2 = 0) và không được sử dụng ở bất kì đâu khác trong chương trình. Trong mã asm, biến này sẽ tương ứng với biến var_3C. Đổi tên nó thành cookie2. Kể cả khi không biết nó dùng làm gì thì tôi khuyên các bạn cũng nên cung cấp cho nó một cái tên nào đó để gợi nhớ, ví dụ temp chẳng hạn, vì như ta thấy sau khi khởi gán, nó sẽ không được sử dụng nữa và như vậy không ảnh hưởng gì đến hoạt động của chương trình.

Tiếp theo, nhấp đúp chuột vào một biến bất kì để chuyển tới cửa sổ biểu diễn Stack của hàm main():

Tại đây ta thấy biến strDest, biến này được sử dụng làm tham số cho hàm strncpy() như là chuỗi đích. Quan sát thêm, các bạn thấy có không gian trống bên dưới với rất nhiều từ khóa db, điều này cho ta biết rằng khả năng nó có thể là một buffer, và khi ta nhấn ‘x’ để tìm kiếm các tham chiếu tới biến này:

Thông thường thì các buffer sẽ được truy xuất thông qua lệnh LEA, vì để điền nội dung vào buffer, ta sẽ phải truyền địa chỉ của buffer cho hàm. Như trong ví dụ này là hàm strncpy() và lệnh LEA có nhiệm vụ lấy ra địa chỉ của buffer. Do vậy, ta tiến hành chuyển đổi nó thành kiểu array:

Trong trường hợp này, kích thước của mảng sẽ là 52 thay vì 50 như ta khai báo trong mã nguồn. Tôi nghĩ là do khi biên dịch đã thêm padding bytes để đảm bảo chia hết cho 4. Kết quả có được như sau:

Sau khi thực hiện xong, ta đã có được thông tin layout hoàn chỉnh về Stack của hàm main(). Căn cứ vào thông tin này nhận thấy không thể overflow được, bởi vì ta biết rằng bộ đệm strDest có kích thước là 52 bytes và trong code của chương trình ta chỉ cho phép sao chép vào buffer này đúng 0x32 bytes thông qua hàm strncpy().

Trở lại mục tiêu ban đầu là xây dựng struct mặc dù ở ví dụ này không nhất thiết phải làm như thế, tuy nhiên chúng ta sẽ thực hiện điều này. Quay trở lại với biểu diễn của Stack của main(). Nếu như có một struct thì struct này sẽ không bao gồm biến CANARY, vì biến này do trình biên dịch tự động thêm vào trong quá trình compile code.

Như vậy, struct có thể sẽ bao gồm các trường như tôi đã khoanh ở trên hình. Đánh dấu các biến này, sau đó chọn meun Edit > Create struct from selection:

Kết quả, một struct với tên do IDA tự đặt là struct_0 được tạo ra và IDA sẽ tự động chuyển tới định nghĩa của struct này tại tab Structures:

Để cho giống như trong mã nguồn của chương trình, ta đổi tên của struct thành MyStruct:

Và trong phần biểu diễn của Stack, tại biến có kiểu MyStruct ta sẽ đổi tên nó thành values như trong mã nguồn của chương trình:

Sau khi tạo và thay đổi xong, quay lại màn hình Disassembly, chúng ta sẽ có được kết quả gần tương tự như khi ta load kèm file .pdb:

Như vậy, các bạn thấy rằng ít nhất trong hàm này, khi biến values được định nghĩa, các trường sẽ tự động thay đổi tên của chúng thành các values.xxxx. Rõ ràng, đây là một cách đơn giản nhất để tạo struct. Tuy nhiên, các bạn phải biết rằng đối với các struct phức tạp hơn, chúng ta sẽ cần reverse từng trường và tìm cách để có thể tạo một struct hoàn chỉnh nhất có thể.

Tạm thời như thế, trong phần tiếp theo tôi sẽ tiếp tục với các ví dụ phức tạp hơn về struct. Còn bây giờ là thời gian dành cho việc giải quyết bài tập IDA3.exeIDA4.exe.

Load IDA3.exe vào IDA, tương tự như phần trước, đầu tiên ta thấy biến var_5C được thêm vào bởi compiler, do dó đổi tên nó thành Temp. Biến này nằm trên biến Buffer:

Tại code của chương trình, ta thấy hai biến cookiecookie_2 được so sánh với các giá trị mặc định, như vậy ta phải tìm cách để ghi đè giá trị mong muốn lên hai biến này. Do chương trình sử dụng hàm gets(), nên ta hoàn toàn có thể làm được việc này:

Đầu tiên cần tính được số lượng byte cần thiết để ghi đè vào biến cookie. Do biến cookie nằm ngay dưới biến Buffer có kích thước là 68 bytes, nên cần phải truyền dữ liệu như sau:

68 * ‘A’ + ‘XXXX’

Với việc truyền như trên sẽ ghi đè được vào biến cookie, giá trị XXXX lúc này phải là 0x71727374h hay ‘trsq’:

Script để thực hiện tự động như sau:

from subprocess import *
import struct
p = Popen([r'IDA3.exe', 'f'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)

cookie=struct.pack("<L",0x71727374)
cookie2=struct.pack("<L",0x1020005)
flag=struct.pack("<L",0x90909090)
string="I can do it!!\n"

print " Attach to the debugger and press Enter\n"
raw_input()

user_input=string + (68 -(len(string)))*"A" + cookie + flag + cookie2
p.stdin.write(user_input)

testresult = p.communicate()[0]

print user_input
print(testresult)

Kết quả sau khi chạy script như hình dưới đây:

Điểm khác biệt các bạn sẽ thấy so với script viết cho IDA2.exe là: string + (68 -(len(string)))*”A”

Với IDA3.exe như vậy là xong! Tiếp theo là IDA4.exe. Ta load file này vào IDA. Sau khi IDA phân tích xong ta sẽ thấy trong code của file này còn có thêm một hàm _check():

OK, tiến hành reverse hàm main(). Đầu tiên ta thấy chương trình sử dụng hàm printf() để in ra màn hình địa chỉ của 3 biến là buf, cookiecookie2 (“buf: %08x cookie: %08x cookie2: %08x\n”). Do vậy, ta sẽ đổi tên các biến tương ứng:

Sau đó, Buffer sẽ được truyền vào cho hàmgets(), do vậy ta biết rằng có thể overflow được buffer này. Mà để có thể overflow thì ta cần phải biết được chiều dài của Buffer.

Chuyển qua cửa sổ biểu diễn Stack và chuyển đổi Buffer này thành dạng array, ta sẽ biết được độ dài của nó là 50 bytes:

Kết quả sau chuyển đổi:

Như trên hình, ta sẽ thấy bên dưới Buffer là 2 biến cookie và cookie2, mỗi biến có kích thước là 4 bytes. Tiếp theo, ta sẽ thấy rằng hai biến cookie này được truyền là các tham số cho hàm check(), cộng thêm một biến nữa là var_4 mà chúng ta chưa biết là gì. Ta cứ tạm thời đổi tên nó thành flag để cho dễ nhìn:

Với kết quả trên hình thì có thể thấy tham số thứ 3 của hàm check() sẽ là flag, tham số thứ hai là cookie2 và tham số đầu tiên là cookie. Đi vào hàm check(), ta thấy hàm này có ba tham số tương ứng và một biến cục bộ:

Theo như phân tích về các tham số của hàm check(), ta chọn hàm và nhấn ‘Y’ để thiết lập lại kiểu của hàm như sau:

Kết quả có được như sau:

Quay trở về hàm main sẽ thấy IDA tự động comment các tham số truyền vào khớp như sau:

Đi sâu vào phân tích hàm check() ta sẽ có được kết quả biến cookie phải bằng 0x71725553h (“qrUS”), biến cookie2 phải có giá trị khác 0x1020D0Ah. Sau khi so sánh hai biến cookie với các giá trị mặc định, hàm check() sẽ trả về giá trị của flag:

Giá trị của flag sau khi thoát khỏi hàm check() sẽ được lưu vào thanh ghi eax. Khi quay trở lại hàm main(), giá trị này sẽ được lưu lại vào biến var_8. Biến var_8 sau đó sẽ được đem đi so sánh với một giá trị mặc định là 35224158h, nếu bằng giá trị này thì chương trình sẽ in ra màn hình thông báo “You win man”:

Tuy nhiên, quay trở lại Stack layout của main, ta thấy biến var_8 này nằm trên biến flag của chúng ta. Do đó để ghi đè giá trị 0x35224158h vào biến flag thì ta phải ghi đè một giá trị bất kỳ vào biến var_8 này:

Dựa vào toàn bộ kết quả đã phân tích ở trên, ta tạo script như sau:

from subprocess import *
import struct
p = Popen([r'IDA4.exe', 'f'], stdout = PIPE, stdin = PIPE, stderr = STDOUT)

cookie = struct.pack("<L", 0x71725553)
cookie2 = struct.pack("<L", 0x41424344)
temp = struct.pack("<L", 0x90909090)
flag = struct.pack("<L", 0x35224158)

print " Attach to the debugger and press Enter\n"
raw_input()

user_input = 50 * "A" + cookie + cookie2 + temp + flag
p.stdin.write(user_input)

testresult = p.communicate()[0]

print user_input
print(testresult)

Kiểm tra script này, kết quả như hình dưới đây:

Hẹn gặp lại các bạn trong phần 26 để tiếp tục tìm hiểu thêm về struct.

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

m4n0w4r

Đời có qua có lại thì mới toại lòng nhau
Người ta cho mình quá nhiều, mình thì cho cái mẹ gì đâu
Thấy nợ nần nhiều, như là người mà đang đi vay lãi
Bí quyết thành công, gói gọn trong hai từ “may vãi”

_Đen_

Ủ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


Bạn từng lo lắng trong số những người bạn của mình có thể đã chiếm tài khoản Facebook của bạn? Rằng máy tính của bạn hiện đang bị giữ làm con tin bởi một loại mã độc tống tiền (ransomware)? Hoặc là các tin tặc (hackers) đang chiếm giữ tài khoản ngân hàng của bạn?

Bài hướng dẫn này được ví như là một cuốn cẩm nang dành cho những người sử dụng thông thường, nó sẽ giải thích một cách dễ hiểu nhất cho các bạn cách làm thế nào để bảo vệ mình khỏi các hacker. Các hướng dẫn này được xây dựng dưới sự giúp đỡ của 6 hacker chuyên nghiệp  (thông tin của họ được nêu ở phần đầu bài viết).

Tài liệu này không đảm bảo bạn sẽ an toàn một cách tuyệt đối. Điều đó không tồn tại với môi trường Internet. Tuy nhiên, bằng cách áp dụng các mẹo trong bài viết này, bạn có thể khiến cho các hacker và virus gặp khó khăn hơn rất nhiều khi có ý định chọn bạn làm mục tiêu tấn công.

Trước khi chúng ta bắt đầu: các bạn đừng cảm thấy lo sợ khi ngồi trước máy tính của mình. Cơ hội để các hacker nhắm mục tiêu vào bạn thực sự là rất nhỏ. Hầu hết các mối nguy hiểm đều xuất phát từ thực tế là nhiều người dùng thiếu kiến thức chung về internet và máy tính, nơi mà từ đó bạn có thể bị khai thác. Vì vậy, hãy kiện toàn chính mình bằng các thông tin quan trọng nhất được đề cập trong bài viết này.

Download toàn bộ hướng dẫn tại đây:

Link: https://mega.nz/#!7C5yWKgJ!1LdSSipfBr5jE15ty_E0AO2W_rNwYAi9yP4XUiC91Qk

Regards,

m4n0w4r


Phần 24 này tôi sẽ thực hành với file IDA2.exe đã gửi kèm ở phần trước. Cách làm với IDA2.exe cũng tương tự như cách đã thực hiện với file IDA1.exe, chỉ khác là trong file này có hai biến cần kiểm tra điều kiện, nếu thỏa mãn cả hai thì chúng ta sẽ tới được đoạn code hiển thị thông báo “you are a winnner man je\n”:

Như trên hình ta thấy biến var_14 được so sánh với giá trị 0x71727374h còn biến var_C được so sánh với giá trị 0x91929394, do đó ta phải tìm cách để có thể thay đổi giá trị các biến này. Trước tiên, tôi sẽ đổi tên chúng thành cookiecookie_2 để dễ dàng nhận biết trong code của chương trình:

Sau khi đổi tên xong, chọn lần lượt từng biến này và nhấn “X”:

Ta thấy rằng các biến này duy nhất được truy cập thông qua lệnh LEA trước khi thực hiện hàm printf(). Lệnh LEA sẽ lấy thông tin địa chỉ của các biến để in ra màn hình, bên dưới là câu lệnh CMP làm nhiệm vụ so sánh. Như vậy là giá trị của các biến này không hề bị thay đổi trong hàm main().

Do đó, theo suy luận tương tự như phần trước, ta phải thay đổi giá trị của các biến này để có thể tới được thông báo hiển thị “…winner…”. Mà như đã thấy ở trên hình thì các biến này đều được so sánh với một giá trị cụ thể do chương trình mặc định sẵn. Vì vậy, để có thể thay đổi các biến thì khả năng sẽ phải thực hiện overflow lên vùng buffer, để từ đó có thể ghi đè giá trị ta mong muốn lên các biến này.

Bên cạnh các biến cookie mà ta vừa phân tích ở trên, để ý một chút các bạn sẽ thấy trong file IDA2.exe này còn có thêm một biến khác là var_10. Ban đầu, biến này được khởi gán bằng 0:

Nhấn ‘X’ để xem biến này còn được sử dụng ở những chỗ nào khác:

Theo kết quả trên hình, ta thấy ngoài lệnh khởi gán nó bằng 0 thì biến này còn được truy cập ở hai vị trí khác nữa thông qua lệnh LEA. Chúng ta sẽ xem xét từng chỗ một, nhưng trước tiên ta thực hiện việc điều chỉnh lại các tham số được truyền cho các hàm APIs để cho dễ nhìn hơn. Áp dụng kỹ thuật đã làm với file IDA1.exe, kết quả có được như sau:

Sau khi thay đổi xong các bạn sẽ thấy code nhìn rõ ràng hơn nhiều so với ban đầu, qua đó ta cũng hiểu được cách mà chương trình truyền tham số cho hàm APIs. OK, giờ quay lại vấn đề lúc trước, ta kiểm tra các đoạn code khác có truy cập tới biến var_10:

Phân tích đoạn code trên hình, đầu tiên thông qua lệnh LEA thì eax sẽ có được địa chỉ của biến, sau đó thực hiện tăng nội dung hay tăng giá trị của nó lên 1 thông qua lệnh INC. Sau đó, lấy lại địa chỉ của biến một lần nữa và truyền nó như một tham số của hàm printf(). Như vậy, hàm printf() sẽ in ra địa chỉ của biến chứ không phải là giá trị của biến.

Dựa vào thông tin chương trình sẽ in ra màn hình dòng thông báo “flag %x”, tôi đổi tên biến var_10 thành flag. Tuy nhiên, sau quá trình phân tích thì thấy rằng biến này không có ảnh hưởng gì tới quá trình hoạt động của chương trình.

Tóm lại, chúng ta chỉ cần nghiên cứu biến Buffer vì các biến ở trên biến này được sinh ra trong quá trình compile chương trình, được sử dụng làm các biến tạm thời chứ không có tác động đến chương trình:

Nhấp đúp vào biến Buffer, ta sẽ chuyển tới màn hình cung cấp thông tin biểu diễn Stack của hàm main() như hình dưới đây:

Rõ ràng, biến Buffer này là một nơi được dành riêng trong bộ nhớ, được sử dụng để lưu thông tin mà người dùng nhập vào từ bàn phím thông qua giao diện console. Biến này được truyền vào là một tham số của hàm gets():

Để kiểm tra kích thước của buffer này ta nhấn chuột phải và chọn Array, IDA sẽ cung cấp thông tin mà nó phân tích được:

Như vậy Buffer có kích thước gồm 68 phần tử, mỗi phần tử là 1 byte. Ta nhấn OK để đồng ý chuyển đổi, kết quả có được như hình:

Với kết quả có được, ta biết rằng đầu tiên phải điền đầy buffer với 68 ký tự, và do hạn chế của hàm gets nên chúng ta có thể overflow buffer này và ghi đè thêm 4 bytes vào biến cookie nằm bên dưới của buffer. Tiếp theo là ghi đè lên 4 bytes của biến flag (một dword (dd)) và cuối cùng là 4 bytes của biến cookie_2:

Tổng kết lại: user_input= 68 *”A”+ cookie + flag + cookie2. Với thông tin như vậy, chúng ta có thể xây dựng một python script như sau:

from subprocess import *
import struct
p = Popen([r'IDA2.exe', 'f'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)

cookie=struct.pack("<L",0x71727374)
cookie2=struct.pack("<L",0x91929394)
flag=struct.pack("<L",0x90909090)

print " Attach to the debugger and press Enter\n"
raw_input()

user_input=68 *"A"+ cookie + flag + cookie2
p.stdin.write(user_input)

testresult = p.communicate()[0]

print user_input
print(testresult)

Kiểm tra thử script, tôi có được kết quả như sau:

Như vậy là thành công!

Image result for deal with expert funny

Trong phần 24 này tôi gửi kèm thêm 2 file IDA3.exeIDA4.exe (https://mega.nz/#!bOxGWISb!kYzE2SsWPyo0YQg5tL72qy14ntuMlic02ML5bg7-GD0) để các bạn thực hành như một bài tập về nhà.

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

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


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àmmain(), 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à 140 phần tử, mỗi phần tử có kích thước 1 byte:

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 “winner” 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_input
print(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.

Image result for that was easy funny gif office space

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


Ở phần 22 này chúng ta sẽ tìm hiểu một chút về các công cụ bổ trợ cho IDA trong việc việc so sánh sự khác nhau giữa hai binary.

Nếu các bạn là dân dev, hay dân văn phòng suốt ngày lọ mọ với một mớ code/ tài liệu thì chắc sẽ chẳng lạ gì với việc tự làm “bằng cơm” hoặc sử dụng công cụ chuyên dụng để so sánh sự khác nhau/ sai khác trong cùng một source code hoặc giữa cùng một tài liệu tại những thời điểm khác nhau.

Tôi lấy ví dụ về việc so sánh giữa hai file văn bản: bên trái là file gốc vs bên phải là file đã chỉnh sửa (nhưng chữ sửa trong file để font màu trắng). Nếu không dùng tool mà làm “bằng cơm” để kiểm tra sự thay đổi, tôi đố các bạn tìm được hell-yes-onion-head-emoticon

Với các file binary cũng vậy, công cụ differ cũng sẽ cho ta biết sự khác biệt/ thay đổi giữa hai phiên bản của cùng một chương trình, các công cụ này sẽ cố gắng thực hiện phân tích, so khớp các hàm trong chương trình và đưa ra kết qủa về những hàm có sự thay đổi và thay đổi ở đâu.

Ta thấy rõ ràng để thực hiện công việc này không phải là dễ dàng, đặc biệt là khi có những thay đổi lớn từ phiên bản này sang phiên bản khác, sự thay đổi này có thể đến từ việc cập nhật các bản vá bảo mật để giải quyết các lỗ hổng tồn tại trong chương trình, hoặc có thể là những cải tiến mới trong tính năng của chương trình, v.v… Với dân chuyên crack soft hoặc tìm hiểu cracking thì việc so sánh giữa file gốc và file đã patch có thể giúp cho họ tìm hiểu được cách patch của các cracker khác. Với dân chuyên nghiên cứu exploit thì việc làm này có thể giúp họ biết được một bản vá bảo mật có giải quyết được triệt để lỗi hay không? Hay là patch lỗi này lại sinh ra lỗi khác có thể khai thác được.

Tính tới thời điểm hiện tại, cá nhân tôi biết được có 3 công cụ differ sử dụng kết hợp với IDA. Trong bài viết này, tôi sẽ giới thiệu lần lượt các công cụ này và tùy cảm nhận của từng người mà lựa chọn cho mình công cụ phù hợp hoặc sử dụng kết hợp.

Note: Thời điểm tôi chỉnh sửa lại bài viết này thì Bindiff đã ra phiên bản 5 (https://www.zynamics.com/software.html); Diaphora cũng được Joxean Koret cập nhật liên tục để có thể làm việc với các phiên bản IDA 6.9 tới 7.3

1. BinDiff

Công cụ phải nói tới đầu tiên chính là BinDiff, mục tiêu của nó là một công cụ hỗ trợ so sánh các tập tin nhị phân nhằm giúp các chuyên gia nghiên cứu lỗ hổng có thể nhanh chóng tìm thấy sự khác biệt và tương đồng trong mã chương trình được phân rã ở dạng câu lệnh asm. Công cụ này được phát triển bởi Zynamics, công ty này sau đó được Google mua lại vào năm 2011. Phiên bản mà tôi sử dụng tại thời điểm của bài viết này là BinDiff v4.3. Thông tin chi tiết về BinDiff có thể tìm đọc tại đây: https://www.zynamics.com/software.html

Các bạn download về và tiến hành cài đặt. Việc cài đặt BinDiff dễ như các bạn cài Win dạo, khác mỗi là bạn không phải kiếm cr@ck thôi, chú ý là trên máy cần phải cài đặt Java Runtime Enviroment (JRE) trước nhé. BinDiff 4.3 hiện chỉ hỗ trợ cho IDA 6.x, chưa có plugin cho 7.x (Note: hiện bản BinDiff 5 đã hỗ trợ cho IDA 7.x).

Để minh họa cho việc sử dụng BinDiff, tôi sẽ sử dụng các file .idb có sẵn: một file có vuln và một file đã được fix. Mở IDA lên và load database của file có vuln (VULNERABLE_o_NO.idb) vào:

Sau khi IDA load xong database của file có vuln, truy cập menu Edit > Plugins và chọn BinDiff 4.3:

Cửa sổ BinDiff sẽ xuất hiện như hình dưới:

Tiếp theo chọn Diff Database… và tìm tới database của file đã fix vuln để so sánh:

BinDiff sẽ chạy và phân tích hai databse này:

Sau khi thực hiện xong, BinDiff sẽ hiển thị kết quả cho chúng ta. Có thể các cửa sổ của BinDiff tại IDA của các bạn sẽ không giống như tôi, các bạn có thể thực hiện drag & drop để lựa chọn một chế độ xem phù hợp nhất:

Tab mà chúng ta quan tâm là “Matched Functions”:

Tại đây, các ban sẽ thấy cột đầu tiên (similarity) cung cấp kết quả về sự giống nhau giữa các hàm, Theo kinh nghiệm của nhiều người đã dùng BinDiff thì nếu giá trị trả về bằng 1.00 thì có nghĩa là hai hàm đó hoàn toàn giống nhau, không có thay đổi gì, ngược lại nếu giá trị này < 1.00 thì có nghĩa là cùng một hàm nhưng đã có sự thay đổi, khác biệt. Để dễ dàng và thuận tiện thì chúng ta chỉ việc nhấp chuột vào đầu cột đó để sắp xếp chúng theo kết quả từ khác nhau đến giống nhau. Tương tự như hình:

Theo kết quả có được như trên hình, ta thấy rằng ở đây chỉ có một điểm tương đồng nhỏ hơn 1. Nhấn chuột phải tại đó và chọn View Flowgraphs hoặc nhấn phím tắt là Ctrl+E:

Lúc này, plugin sẽ sẽ gọi tới bindiff.jar trong thư mục cài đặt của BinDiff để hiển thị FlowGraphs của hai hàm có sự thay đổi ở trên:

Như trên hình, ta thấy các khối được highlight bằng màu xanh, đó là những khối có code giống nhau, còn khối được highlight bằng màu vàng là khối có sự thay đổi và những câu lệnh thay đổi cũng được BinDiff highlight bằng những màu khác nhau để ta có thể dễ dàng nhận ra:

Như trên hình, các bạn sẽ nhận thấy được sự thay đổi ở lệnh nhảy, và chúng ta cũng đã biết việc đổi từ lệnh nhảy JLE thành JBE là cách để chương trình tránh bị dính lỗi Buffer Overflow, do vậy nếu trong một chương trình mà ta có cả phiên bản bị lỗi và phiên bản đã được vá lỗi, sau khi so sánh và nhìn vào các hàm đã thay đổi thì chúng ta sẽ phải thực hiện reverse lại hàm đó để xem nó có thực sự là lỗ hổng của chương trình hay không.

Một trong những ưu điểm của BinDiff so với hai công cụ sắp được đề cập là ngoài việc hỗ trợ chế độ đồ họa tương tác rất tốt, khả năng parse trực tiếp các file .idb mà không cần phải tạo ra các file trung gian thì nó còn cung cấp khả năng tìm kiếm rất tiện lợi, qua đó ta có thể tìm kiếm địa chỉ và bất kỳ đoạn text nào. Ví dụ, tôi thực hiện tìm kiếm chuỗi “cmp”:

Bên cạnh đó, BinDiff hỗ trợ sao chép địa chỉ của khối có thay đổi, qua đó ta có thể đi tới được khối này trong IDA bằng việc nhấn G và dán địa chỉ đã copy vào:

BinDiff cũng cung cấp một cửa sổ nhỏ tương tự như cửa sổ Graph Overview của IDA để giúp dễ dàng di chuyển, quan sát các hàm cùng danh sách các khối đã được phân rã:

Như các bạn thấy BinDiff cung cấp cho chúng ta một giao diện cùng các tính năng hữu ích, với những file đơn giản như trong ví dụ này thì các bạn thấy có vẻ dễ dàng, tuy nhiên với các ứng dụng lớn và phức tạp hơn thì sẽ không ngon ăn như thế này đâu.

“Dễ thế này thì Đông Lào người ta đi săn bug hết!!”

Do BinDiff hiện tại chỉ hỗ trợ IDA 6.x, với phiên bản IDA 7.x thì phải sử dụng BinExport (https://github.com/google/binexport) để xuất ra file và import vào Bindiff để so sánh. Phiên bản BinExport mà tôi đang sử dụng được build bởi bạn Ngôn Nguyễn (aka @computerline). Cách thức thực hiện như sau, đầu tiên load db của file có lỗi (VULNERABLE_o_NO.idb) vào IDA 7.x. Sau khi load xong, vào Edit > Plugins chọn BinExport:

Giao diện của BinExport sẽ xuất hiện, chọn BinExport v2 Binary Export:

Sau đó lưu lại với tên bất kì với phần mở rộng là .BinExport (ví dụ: VULNERABLE_o_NO.BinExport)

Tiếp theo, dùng IDA load db của file đã fix lỗi (NO_VULNERABLE.idb) và cũng thực hiện tương tự như trên, lưu thành file có tên là NO_VULNERABLE.BinExport:

Sau khi có hai file được export bằng BinExport, chạy trực tiếp Bindiff và load hai file này vào để so sánh:

Sau khi nhấn Diff thì bên tab Workspace sẽ hiển thị tên hai file được so sánh và tab Overview sẽ cung cấp kết quả so sánh:

Nhấp đúp vào tên hai file được so sánh tại tab Workspace, BinDiff sẽ hiển thị kết quả chi tiết:

Tiếp theo, chọn Matched Functions, sắp xếp lại theo mức độ tương đồng (Similarity):

Cuối cùng, nhấn chuột phải tại hàm có sự khác biệt, chọn Open Flow Graph và phân tích kết quả trả về:

2. Turbodiff

Công cụ cho phép so sánh binary tiếp theo là TurboDiff. Đây là một plugin được code bởi tác giả Nicolas Economou (@NicoEconomou), là đồng nghiệp trước đây với thầy Ricardo Narvaja tại Core Security. Các bạn có thể download plugin này tại: https://www.coresecurity.com/corelabs-research/open-source-tools/turbodiff, tuy nhiên đây là phiên bản cũ. Phiên bản mà thầy Ricardo Narvaja sử dụng là phiên bản mới hơn. Việc cài đặt rất dễ, chỉ việc chép file turbodiff.plw vào thư mục plugins của IDA là xong.

Tương tự như đã làm với BinDiff, nhưng khác chút là ta load database NO_VULNERABLE.idb của file đã fix lỗi trước. Sau khi IDA load xong, vào menu Edit > Plugins và chọn Turbodiff:

Để so sánh được thì TurboDiff cần phải lấy thông tin từ file .idb:

Lựa chọn “take info from this idb” và nhấn OK, turbodiff sẽ phân tích và tạo ra 2 file có đuôi mở rộng là .dis và .ana:

Sau đó, mở database của file có lỗi và cũng làm tương tự như trên:

Khi phân tích xong, ta tiếp tục chọn turbodiff một lần nữa, nhưng lần sẽ thực hiện so sánh bằng cách chọn “compare with…” và nhấn OK:

Chọn file cần so sánh là db của file đã được fix lỗi:

Giữ nguyên cấu hình mặc định của turbodiff và nhấn OK:

Ta sẽ có được kết quả như sau:

Tại tab “Turbodiff results” ta nhấn CTRL + F và tìm kiếm từ khóa là “changed” hoặc “suspicious” để hiển thị những chỗ thay đổi:

Sau khi có được kết quả như hình, nhấp đúp vào đó, turbo diff sẽ sử dụng wingrap32 để hiển thị flow graph:

Như các bạn thấy turbodiff đã cung cấp thông tin về khối lệnh có sự thay đổi. Tương tự như Bindiff, turbodiff cũng sử dụng một mã màu để biểu diễn cho tỉ lệ thay đổi, màu xanh lá cây được sử dụng cho các khối có những thay đổi ít, màu vàng sẽ dùng cho các khối có thay đổi nhiều và màu đỏ sử dụng cho các khối được thêm vào. Rõ ràng, về mặt đồ họa và khả năng tương tác thì không thể so sánh được với Bindiff, nhưng bù lại là tốc độ của Turbodiff thực sự rất nhanh. Hiệu năng là điều rất đáng quan tâm nếu áp dụng với các file có kích thước lớn và nó không hiển thị quá màu mè như Bindiff. Một điểm khác nữa với Bindiff là Turbodiff phải parse database của IDA thành các file trung gian rồi mới so sánh.

3. Diaphora

Công cụ so sánh binary cuối cùng mà tôi giới thiệu với các bạn là diaphora, đây là một plugin do Joxean Koret (@matalaz) viết bằng Python. Joxean Koret là tác giả của cuốn sách “The Antivirus Hacker’s Handbook” và hình như có quen biết với anh Quỳnh khi làm việc ở Coseinc. Để sử dụng được diaphora các bạn có thể download tại đây: https://github.com/joxeankoret/diaphora. Tính đến thời điểm hiện tại diaphora không còn hỗ trợ IDA 6.8 nữa, mà chỉ làm việc với IDA Pro 6.9, 6.95 và 7.0.

Tương tự như đã làm với các công cụ trước, đầu tiên ta load db của file đã fix lỗi vào IDA trước. Do là dạng python script, nên để chạy được diaphora ta vào File > Script File…

Tìm tới thư mục chứa file diaphora.py và lựa chọn file này để chạy:

Màn hình Diaphora sẽ xuất hiện, ta có thể lựa chọn đường dẫn để lưu file SQLite db cho file hiện tại đang phân tích bởi IDA hoặc giữ nguyên đường dẫn mà Diaphora thiết lập:

Sau đó nhấn OK để Diaphora tiến hành công việc phân tích và lưu thành file sqlite:

Thực hiện tương tự với file có lỗi:

Sau khi thực hiện xong hai bước trên ta sẽ có được 2 file sqlite để phục vụ cho việc so sánh. Tiếp theo, ta mở lại diaphora.py (lúc này IDA đang mở db của file có lỗi):

Ở phần “SQLite database to diff against”, ta tìm đến file sqllite được tạo ra trước đó của file đã fix lỗi. Chọn file này và nhấn Open:

Cuối cùng nhấn OK để Diaphora tiến hành xử lý vá so sánh. Diaphora sẽ hỏi như hình dưới, chỉ việc nhấn Yes là xong:

Sau khi compare xong, diaphora sẽ hiển thị 2 tab mới tại IDA với tên là Best matchesPartial matches. Tại tab Best matches là các hàm giống nhau hoàn toàn và không thay đổi, nên ta không cần quan tâm tới tab này:

Tab Partial matches sẽ cung cấp cho chúng ta thông tin về các hàm có khả năng khác nhau:

Ta thấy diaphora đã tìm được hai hàm bị thay đổi như trên. Về cơ bản, chủ quan mà nói tôi thấy diaphora cho kết quả hên xui so với Bindiff và Turbodiff, nếu chạy với IDA7 thì không ra được Partial maches, chạy với IDA 6.8 thì mới ra được kết quả mong muốn. Ngoài ra, do được viết bằng python nên diaphora chạy khá lâu, theo tôi thì nó chạy lâu nhất, gặp phải db lớn là xác định ngồi đợi. Để xem được sự thay đổi ở một hàm nào đó, nhấn chuột phải tại hàm đó và chọn chế độ Diff assembly in a graph:

Kết quả có được như hình dưới đây:

Chế độ graph này của diaphora nhìn có vẻ tốt hơn Turbodiff một chút, nhưng cũng không cho ta khả năng tương tác như ở Bindiff, khối quan trọng đã thay đổi được highlight bằng màu đỏ, còn màu vàng là khối có những thay đổi nhỏ, ví dụ như ta đổi tên của một biến. Ngoài ra, diaphora còn cung cấp tùy chọn khác là Diff pseudo code, sử dụng Hexrays decompiler đi kèm với IDA để xây dựng lại mã nguồn của file thực thi. Kết quả có được khi lựa chọn tùy chọn này như hình dưới đây:

Phần 22 đến đây là kết thúc nhé các bạn!

Image result for diff funny

Trong phần này tôi cung cấp một file IDA1.exe (https://mega.nz/#!mHAU1AaS!Zm2g_pXoiCDWiKwSdRJsJ0tYNTRjc6JSkBJyr6h7j0g). Tôi muốn các bạn phân tích nó và xem liệu nó có lỗi hay không và nếu có thì bạn thực hiện khai thác buffer overflow làm sao để thay đổi luồng thực thi của chương trình để từ đó hiển thị thông báo You are a winner man.

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

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


Ở phần 20, tôi đã đưa ra một bài tập nhỏ để các bạn phân tích xem chương trình có khả năng bị vuln hay là không. Trong phần 21 này, tôi sẽ cùng với các bạn tìm hiểu, phân tích và đưa ra câu trả lời. Mã nguồn gốc của chương trình được thầy Ricardo Narvaja cung cấp lại dưới đây để các bạn có hình dung rõ ràng hơn về chương trình. Các file .idb và file thực thi để bạn có thể reverse bằng IDA và thực hiện debug nếu cần có thể download tại: https://mega.nz/#!uXQExSYD!6GT0LsA803BVGoXp82muCnk-GsCEGBEuT7BZwrhSBBA

Đây là toàn bộ mã nguồn của chương trình:

Giống như ở các phần trước, trước tiên ta sẽ tiến hành việc phân tích tĩnh chương trình bằng IDA. Sau khi load vào IDA, ta dừng lại tại hàm main(). Tại đây, giống như phân tích ở phần trước, ta đã biết được nhiệm vụ của biến var_4, do đó đổi tên nó thành CANARY như đã làm.

Nhấp đúp vào biến Buf, IDA sẽ chuyển tới cửa sổ biểu diễn Stack của hàm main(). Tiếp theo, chuột phải tại biến Buf và chọn Array:

IDA tự động nhận diện được kích thước của buffer là 16 bytes. Nhấn OK để chấp nhận, ta có được Stack layout của hàm main() như sau:

Theo lý thuyết đã tìm hiểu, nếu như chúng ta có thể ghi dữ liệu vào buffer này nhiều hơn 16 bytes thì chương trình sẽ có khả năng bị lỗi. Các bạn đừng bị nhầm lẫn giữa các lỗi phổ biến hoặc lỗi crash chương trình với các lỗ hổng, không phải cứ gây crash cho một chương trình thì khẳng định ngay đó là lỗ hổng. Ví dụ, tôi có các câu lệnh sau:

XOR ECX, ECX
DIV ECX 

Đây là một lỗi vì ta biết câu lệnh XOR sẽ xóa thanh ghi ECX hay cụ thể hơn sau lệnh XOR thì ECX sẽ bằng 0x0. Khi thực hiện câu lệnh DIV ECX, tức là thực hiện phép chia cho số 0, điều này sẽ gây ra một ngoại lệ (exception), nếu exception này không được xử lý thì sẽ gây crash chương trình. Như đã nói, có nhiều loại lỗ hổng khác nhau và ở bài viết này tôi chỉ tập trung vào một lỗi đơn giản là Buffer Overflow.

Khi xảy ra tràn tại một Buffer của chương trình tức là ta có thể ghi dữ liệu vượt ra ngoài không gian dành riêng cho Buffer đó, chương trình lúc đó sẽ được xem là Vulnerable bởi vì lỗi Buffer Overflow có thể xảy ra. Tiếp theo, ta sẽ phải kiểm tra xem bên cạnh việc chương trình có Vulnerable thì có thể Exploitable hay không? Vì nhiều lúc chương trình có thể có Vulnerable nhưng lại không thể Exploitable bởi chương trình đã có biện pháp phòng tránh hoặc các cơ chế mitigation của hệ thống như Stack Canary giúp ngăn chặn việc khai thác lổ hổng của chương trình. Tuy nhiên, đó sẽ một chủ đề khác ở các bài viết tiếp theo.

Vì vậy, ý tưởng ở đây là ta sẽ phân tích nếu buffer (cụ thể ở ví dụ này) với kích thước 16-bytes mà bị tràn, bên dưới nó sẽ là biến CANARY, nếu ta ghi đè được dữ liệu lên biến này khi đẩy dữ liệu vào Buf thì rõ ràng là sẽ có lỗi Buffer Overflow.

Tại IDA, bên dưới lệnh in ra màn hình chuỗi “Please Enter Your Number Of Choice” ta bắt gặp khối lệnh sau:

Toàn bộ đoạn code asm trên hình tương ứng với câu lệnh sau trong mã nguồn gốc:

while ((c = getchar()) != '\n' && c != EOF);

Lệnh này thường hay được sử dụng sau hàm scanf, sử dụng hàm getchar() đọc để đọc giá trị 0xA (là mã ASCII tương ứng với New line – line break (ngắt dòng)) từ standard input (do người dùng nhập vào). Tóm lại, ở đây hàm getchar() sẽ trả về mã phím do người dùng ấn, ‘\n’ sẽ tương ứng với việc người dùng nhấn phím Enter. Vòng lặp while sẽ kiểm tra phím người dùng nhập vào, nếu nhập vào Enter thì thoát vòng lặp, còn nhập vào phím khác là nó quay lại vòng lặp. Về bản chất khi lập trình thì người ta có thể ngầm hiểu việc nhấn phím “Enter” là sự kết hợp của hai mã ASCII là 0x13 (carriage return)0xA (line feed).

Bảng mã các kí tự ASCII (không thể in ra màn hình):
Ascii code 00 = NULL (null character)
Ascii code 01 = SOH (Header start)
Ascii code 02 = STX (Start of text)
Code ascii 03 = ETX (End of text, heart stick English poker cards)
Code ascii 04 = EOT (End of transmission, stick diamonds decks of poker)
Code ascii 05 = ENQ (Consult, stick clubs English poker cards)
Ascii code 06 = ACK (Recognition, stick cards poker cards)
Ascii code 07 = BEL (Ring)
Ascii code 08 = BS (Backspace)
Ascii code 09 = HT (horizontal Tab)
Ascii code 10 = LF (New line - line break)
Ascii code 11 = VT (Vertical Tab)
Ascii code 12 = FF (New page - page break)
Ascii code 13 = CR (ENTER - carriage return)

Để tiện cho việc kiểm tra chương trình, ở đây chúng ta sẽ sử dụng một đoạn python script như sau:

from subprocess import *
import time
p = Popen([r'VULNERABLE_o_NO.exe','f'],stdout=PIPE,stdin=PIPE, stderr=STDOUT)

print "Attach to the debugger and press Enter\n"
raw_input()

size="10\n"
p.stdin.write(size)

time.sleep(0.5)

user_input="AAAA\n"
p.stdin.write(user_input)

testresult = p.communicate()[0]
time.sleep(0.5)
print(testresult)
print size
print user_input

Script trên sử dụng module của python là subprocess để tương tác, quản lý process. Chi tiết về module này có thể xem tại đây: https://docs.python.org/2/library/subprocess.html. Đầu tiên script sử dụng Popen để khởi động process:

p = Popen([r'VULNERABLE_o_NO.exe','f'],stdout=PIPE,stdin=PIPE, stderr=STDOUT)

Kèm theo đó nó thực hiện chuyển hướng stdinstdout để từ đó ta có thể gửi các kí tự cho process này như thể là ta thực hiện nhập dữ liệu trên màn hình. Sau đó, ta sử dụng hàm raw_input() để sau khi process khởi động xong thì script sẽ dừng lại cho đến khi nhấn phím Enter. Việc này cho phép ta có thể dùng IDA để attach process, qua đó sẽ chờ thông tin nhập vào qua stdin.

Tiếp theo, script sẽ tự động thực hiện việc truyền dữ liệu cho process:

size="10\n"
p.stdin.write(size)

time.sleep(0.5)

user_input="AAAA\n"
p.stdin.write(user_input)

Trong mã nguồn gốc của chương trình ta thấy sẽ có hai lần nó yêu cầu người dùng nhập dữ liệu, vì vậy tại python script ta cũng thực hiện công việc tương tự. Đầu tiên, chương trình yêu cầu nhập vào một số tùy chọn và gán cho biến size, do đó trong script tôi truyền vào số 10. Tiếp theo, chương trình sử dụng gets_s() để nhận dữ liệu người dùng nhập vào với kích thước mà ta đã nhập trước đó, ở đây tôi truyền vào 4 chữ A, nhỏ hơn so với kích thước đã nhập.

Kiểm tra thử xem script có hoạt động đúng như chúng ta nghĩ hay không? Chạy thử script, nó sẽ dừng lại tại thông báo “Attach to the debugger and press Enter” để chờ ta nhấn Enter, như vậy ta sẽ có thể attach nó vào IDA để debug:

Hiện tại, ta vẫn đang mở file VULNERABLE_o_NO.exe để phân tích nó bằng Loader của IDA, do vậy để có thể attach được process thì chọn

sau đó vào menu Debugger và chọn Attach to process. Cửa sổ Choose process to attach to sẽ xuất hiện tương tự như sau:

Cuộn chuột xuống dưới để tìm tới process của chúng ta:

Chọn process cần attach và nhấn OK. Sau khi IDA attach xong sẽ dừng lại, lúc đó ta nhấn F9 để cho process tiếp tục thực thi. Sau đó, trước khi nhấn Enter tại màn hình thực thi python script, ta sẽ đặt một Breakpoint sau lệnh scanf_s, vì khi ta nhấn Enter thì script sẽ tự động truyền các input mà process yêu cầu. Đặt breakpoint tương tự như hình dưới:

Sau khi đặt bp xong, quay lại màn hình thực thi script và nhấn Enter. Lúc đó tại IDA sẽ break tại bp ta vừa đặt:

Với việc break tại breakpoint như trên, ta hi vọng biến Size lúc này sẽ chứa giá trị là 10 như ta đã thực hiện trong script. Để kiểm tra ta di chuyển chuột và trỏ vào biến Size, IDA sẽ cung cấp thông tin như sau:

Như vậy, ta thấy rằng biến Size đã nhận giá trị là 0xA (tương đương với 10 ở hệ thập phân) đúng như giá trị đã truyền vào thông qua script. Nên nhớ rằng khi làm việc với disassembler/debugger thì các số thập phân đều được chuyển đổi sang hệ hexa.

Nhấn F8 để trace tới hàm getchar(), các bạn sẽ thấy bên dưới có một lệnh so sánh thanh ghi eax (lưu kết quả trả về của hàm getchar()) với 0xA, mà thực tế là ta không hề truyền bất kỳ kí tự 0xA nào thông qua script cả.

Quay lại một chút các lệnh trong python script:

size="10\n"
p.stdin.write(size)

time.sleep(0.5)

user_input="AAAA\n"
p.stdin.write(user_input)

Nếu các bạn để ý thì với escape characternewline (\n) trong script sẽ tương ứng với ascii code là 10 (0xA) – Line Feed và CarriageReturn (\r)13 (0xD). Do đó, khi nhấn F8 để trace qua hàm getchar() và quan sát giá trị của thanh ghi eax, ta sẽ thấy kết quả như sau:

Như vậy ta thấy rằng thanh ghi eax sẽ lưu ascii code trả về từ hàm getchar()0xA. Do đó, chương trình ngầm hiểu rằng người dùng đã nhấn phím Enter nên sau khi so sánh sẽ thoát khỏi vòng lặp. Mục đích của đoạn code trên là khi đọc ra giá trị 0xA sẽ thực hiện loại bỏ nó ra khỏi stdin và xóa nó để phục vụ cho việc nhập dữ liệu tiếp theo từ bàn phím.

Sau khi thoát khỏi vòng lặp ta sẽ tới đoạn code so sánh kích thước ta truyền cho biến Size0xA với kích thước tối đa mà buffer có thể chấp nhận là 0x10. Và do lúc này giá trị của biến Size nhỏ hơn nên chương trình sẽ tiếp tục đi tới nhánh gọi hàm gets_s():

Tiếp tục trace bằng F8 qua lời gọi hàm gets_s():

Di chuyển chuột và lựa chọn biến Buf để kiểm tra xem sau khi thực hiện hàm gets_s() thì biến này đang lưu gì. Ta có kết quả như sau:

Ta thấy rằng biến Buf lúc này đang lưu các chữ cái ‘A’ mà đã truyền vào thông qua script. Như vậy, script đã hoạt động rất tốt, qua đó cho phép ta có thể kiểm tra và debug những gì sẽ xảy ra trong chương trình. Tạm thời dừng tại đây chút. Bây giờ nếu tôi cho thực thi lại script và làm lại công việc trên từ đầu, tuy nhiên khi tới lời gọi tới hàm getchar() thì tôi bỏ qua nó bằng cách thay đổi giá trị của thanh ghi EIP để trỏ tới lệnh khác và bỏ qua đoạn đọc ra 0xA để so sánh:

Khi ta thay đổi EIP như trên thì lúc đó chương trình sẽ không thực hiện lọc ascii code 0xA nữa. Vậy ta sẽ xem điều gì xảy ra nếu bỏ qua đoạn code đó?

Trace code bằng F8 qua lời gọi hàm gets_s(), khi quan sát nội dung của biến Buf thì ta thấy rằng nó không có thông tin gì cả, như kiểu là ta chẳng nhập gì cho chương trình (mặc dù rõ ràng script không hề thay đổi):

Điều này dẫn tới kết luận là kí tự 0xA vẫn còn trong stdin phải được lọc sau khi thực hiện nhận user input để nó không làm ảnh hưởng tới các đoạn code xử lý tiếp theo trong chương trình nếu muốn tiếp tục nhận user input, đó là lý do tại sao phải có dòng lệnh này:

while ((c = getchar()) != '\n' && c != EOF);

OK, quay trở lại nội dung chính, hiện ta đã có được script phục vụ việc test chương trình. Bây giờ ta sẽ chỉnh sửa script này một chút để có thể phân tích được crash xảy ra tại gets_s() khi chúng ta nhập vào kích thước tối đa, qua đó sẽ phân tích xem có điều gì khác lạ nữa không hay chỉ đơn giản là một crash bình thường. Tôi sửa lại script như sau:

size="16\n"
p.stdin.write(size)

time.sleep(0.5)

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

Ta sẽ xem điều gì sẽ xảy ra khi thay đổi lại script như trên. Thực hiện lại tương tự như trên, ta trace tới lệnh lệnh lea lấy địa chỉ của biến Buf:

Khi trace qua lệnh LEA, tôi ghi lại địa chỉ của biến Buf, trên máy của tôi lúc này là: 0x0115F9D4.

Tiếp theo, nhấp đúp chuột vào biến CANARY và nhấn phím D nhiều lần cho đến khi chuyển nó thành một dword (dd). Ta ghi lại địa chỉ của biến này, trên máy của tôi là 0x0115F9E4 và giá trị của CANARY lúc này trên máy tôi là 0x816077B5h:

Sau khi lưu lại các thông tin liên quan, nhấn F9 để thực thi:

Đây là một exception do API sinh ra, nhấn OK để chấp nhận.

Tại đây, ta nhấn G và nhập vào địa chỉ của biến BufferCANARY để kiểm tra xem có thay đổi gì với những biến này không. Kết quả ta thấy ta thấy rằng CANARY vẫn giữ nguyên giá trị ban đầu còn Buf được điền đấy các chữ cái ‘A’:

Như vậy, với việc nhập giá trị vào bằng với kích thước tối đa sẽ không gây ra tràn, một điều chắc chắn là vùng buffer lúc này đã được điền đầy đủ và không có kí tự null (zero) cuối cùng của chuỗi, vì thế có thể gây ra vấn đề nếu chương trình tiếp tục chạy. Tuy nhiên, trong trường hợp này khi có exception (ngoại lệ) thì exception đó đã không được kiểm soát, vậy nên chương trình bị close. Do đó, ở đây chỉ đơn giản là một crash (tôi cũng nghĩ rằng nó đặt một số không ở đầu của bộ đệm nhằm để hủy bỏ chuỗi).

Nếu chương trình có xử lý ngoại lệ và tiếp tục, nó sẽ loại bỏ dữ liệu khỏi bộ đệm vì nếu nó lấy dữ liệu đó và sử dụng như là một chuỗi, nhưng lại không có null byte để báo hiệu kết thúc chuỗi, thì có thể sẽ nối thêm vào dữ liệu tiếp theo trong ngăn xếp và gây ra vấn đề. Nhưng bằng cách thiết lập giá trị 0 ở đầu của bộ đệm thì sẽ làm mất hiệu lực của dữ liệu.

Như vậy, ta biết rằng sẽ không xảy ra tràn ở đây, ta quay trở lại với việc phân tích tĩnh. Hãy tập trung vào đoạn code này:

Như đã đọc ở các phần trước, ta biết rằng lệnh nhảy JL hoặc JLE sẽ xem xét vào số có dấu hoặc EAX có thể là số âm. Ví dụ, nếu EAX có giá trị là 0xFFFFFFFF (tức là -1) thì nó sẽ nhỏ hơn 0x10. Như vậy là nếu ta sửa lại script để truyền vào biến Size giá trị -1 thì có thể sẽ vượt qua được đoạn so sánh ở trên. Script được sửa lại như sau:

size="-1\n"
p.stdin.write(size)
time.sleep(0.5)

user_input="A" *0x2000 + "\n"
p.stdin.write(user_input)

Chạy lại script với các giá trị đã thay đổi. Ta dừng lại tại breakpoint đã đặt trong IDA:

Lúc này ta kiểm tra giá trị của biến Size xem thay đổi có đúng mong muốn không:

Ta thấy rằng nó chứa giá trị là 0xFFFFFFFF, nhấp đúp chuột vào biến này và nhấn D để nhóm lại cho đến khi trở thành một dword:

Tiếp theo, ta trace code cho tới đoạn thực hiện so sánh với giá trị maximum:

Rõ ràng nếu xem 0xFFFFFFFF-1, thì khi thực hiện câu lệnh so sánh và xem xét dấu, cờ SF lúc này sẽ được bật và như vậy ta sẽ vẫn đi tới đoạn code thực hiện hàm gets_s(). Thực tế thì hàm gets_s cũng giống như là memcpy và bất kỳ hàm API nào thực hiện việc sao chép hoặc cấp phát, kích thước truyền vào cho hàm sẽ luôn được hiểu như là số không dấu (unsigned), bởi vì đơn giản là không có kích thước âm, điều đó là không thể xảy ra, do vậy chương trình lúc này sẽ hiểu 0xFFFFFFFF là một số dương cực lớn được truyền cho hàm gets_s(), do vậy sẽ có tràn xảy ra.

Ta ghi lại địa chỉ của buffer, lần này trên máy tôi nó được cấp phát tại 0x004CFAF8.

Sau đó ta nhấn F9 để thực thi, chương trình sẽ dừng tại đây trong IDA:

Đi tới địa chỉ của buffer, kết quả có được như sau:

Tại đầu của buffer, ta nhấn phím ‘A’ nhằm chuyển toàn bộ kí tự đơn lẻ này thành chuỗi để dễ nhìn hơn:

Nhìn trông ngon lành hơn nhiều lolz. Tiếp theo ta chuyển nó thành dạng Array:

Ta thấy rằng với số lượng kí tự ghi vào buffer như trên đã chiếm hết toàn bộ Stack, đồng thời ghi đè lên cả biến CANARY cũng như các giá trị khác. Như trong hình, bên dưới Stack là một section khác có tên là debug024 (trên máy của tôi):

Bằng cách trên, ta đã xác minh được rằng chương trình này có vulnerable. Giờ làm cách nào để sửa được lỗi này? Rõ ràng là, thay vì sử dụng các lệnh nhảy như JL hoặc JLE (căn cứ vào dấu) thì ta cần sử dụng lệnh nhảy JB hoặc JBE, vì các lệnh này không quan tâm nếu ta truyền vào là -1, nó sẽ là 0xFFFFFFFF, nhưng khi tới lệnh so sánh nó sẽ được xem là số dương và như vậy sẽ lớn hơn 0x10. Khi lớn hơn giá trị maximum của buffer thì sẽ thoát chương trình.

Do vậy, ta sửa lại mã nguồn của chương trình như sau:

Ta thay đổi lại khai báo kiểu của biến size thành “unsigned”, qua đó chương trình từ có Vulnerable sẽ thành Not Vulnerable. Sửa xong code, biên dịch lại chương trình và lưu với tên mới là NO_VULNERABLE.exe. Sau đó load file đã sửa vào IDA, ta thấy lệnh nhảy bây giờ đã thay đổi như hình dưới:

Như các bạn thấy, chỉ cần thay đổi kiểu biến thành “unsigned”, sau khi biên dịch, lệnh nhảy sẽ được đổi sang kiểu lệnh nhảy không còn quan tâm đến dấu của số như ban đầu nữa. Tiếp theo, ta sửa lại script chỉ đơn giản bằng cách đổi tên của file thực thi, lúc này là NO_VULNERABLE.exe, các lệnh khác trong script giữ nguyên không thay đổi.

Thực hiện toàn bộ quá trình đã làm như trên bằng cách cho chạy script và attach process vào IDA, sau đó ta trace code tới câu lệnh so sánh để xem điều gì xảy ra. Ta thấy biến Size lúc này vẫn giữ giá trị là 0xFFFFFFFF do script truyền vào:

Nhấn F8 để trace qua lệnh so sánh và dừng tại lệnh nhảy:

Ta thấy lúc này mũi tên màu đỏ sẽ nhấp nháy báo hiệu chương trình sẽ rẽ nhánh sang khối lệnh gọi hàm exit() để thoát chương trình và như vậy sẽ tránh được tràn, đó là vì lệnh nhảy JBE xem xét rằng giá trị 0xFFFFFFFF (vì không quan tâm đến dấu) là một số dương rất lớn, do vậy sẽ lớn hơn giá trị 0x10.

Kết luận, câu trả lời cho bài tập này là đây là một chương trình có Vulnerable và nó đã được sửa lại bằng cách thay đổi kiểu của biến size thành “unsigned int size”, từ đó sau khi biên dịch lại lệnh nhảy JLE được thay bởi lệnh nhảy JBE.

Phần 21 kết thúc tại đây, hẹn gặp lại các bạn ở phần 22!

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


Vulnerabilities

Trong phần này, chúng ta sẽ tìm hiểu về các lỗ hổng bảo mật và cách phân tích một số lỗi dễ khai thác nhất.

Các ví dụ minh họa trong bài viết download tại đây: https://mega.nz/#!SOAzVCCK!rPFdjLSpDGFlNCho-xkkcWOj0Sy3GbEzShHwGtiqwCE

Lỗ hổng là gì?

Trong lĩnh vực bảo mật máy tính, từ “vulnerability” ám chỉ đến điểm yếu trong một hệ thống cho phép kẻ tấn công có thể xâm phạm tính bảo mật, tính toàn vẹn, tính sẵn sàng của hệ thống đó, từ đó có thể kiểm soát truy cập và tính nhất quán của hệ thống hoặc dữ liệu và các ứng dụng.

Vulnerabilities” là kết quả của lỗi hoặc tính toán sai trong thiết kế của một hệ thống. Mặc dù, xét theo nghĩa rộng hơn, chúng cũng có thể là kết quả của những hạn chế về mặt công nghệ, bởi vì, về nguyên tắc, không có hệ thống nào là an toàn tuyệt đối 100%. Do đó, sẽ có những lỗ hổng trên lý thuyết và các lỗ hổng thực tế có thể bị khai thác.

Quy tắc tương tự áp dụng cho các chương trình. Một chương trình có khả năng bị khai thác lổ hổng là chương trình có lỗi và tùy thuộc vào loại lỗi, chúng có thể bị khai thác để thực thi mã khai thác trong chương trình đó. Bên cạnh đó, các cơ chế kiểm soát của chương trình cũng có thể bị lỗi và cho phép người sử dụng thực hiện các hành động bất hợp pháp, gây crash cho chương trình hoặc thông qua đó có thể nâng quyền, v…v…

Tất nhiên, ở mức độ lỗi liên quan đến bộ nhớ thì đơn giản nhất là tràn bộ đệm. Lỗi này xảy ra khi một chương trình dành ra một vùng nhớ hay vùng đệm để phục vụ lưu trữ dữ liệu và vì lý do nào đó kích thước của dữ liệu được sao chép vào vùng nhớ này không được kiểm tra đúng đắn, dẫn đến lỗi tràn bộ đệm xảy ra, sao chép dữ liệu vượt quá kích thước dành riêng, dẫn tới có thể ghi đè lên các biến, các tham số và các con trỏ trong bộ nhớ. Một trong những lỗi tràn bộ đệm kinh điển nhất là stack overflow (tràn ngăn xếp), xảy ra khi có tràn bộ đệm trong ngăn xếp. Trong mã nguồn của một chương trình C, một buffer có thể được khai báo đơn giản như sau:

char buf[xxx];

Trong đó xxx là kích thước của bộ đệm. Ở ví dụ dưới đây, buffer được khai báo có kích thước là 0x300 bytes (vì là kiểu char), như vậy trong ngăn xếp sẽ dành ra cho buffer này 0x300 bytes để lưu dữ liệu.

Ngoài việc khai báo một buffer thì đoạn code trên không thực hiện thêm công việc nào khác, nhưng tôi vẫn thực hiện biên dịch và phân tích nó trong IDA. File này có tên là Compiled_1.exe (Compilado_1.exe). Sau khi load vào IDA, tại cửa sổ Names, thực hiện tìm kiếm các tham số argc hoặc argv mà hàm main() sử dụng. Bằng cách này ta sẽ tìm được hàm main() của chương trình, kết quả có được như sau:

Nhấp đúp vào một trong hai kết quả được đánh dấu trên hình, IDA sẽ đưa ta tới đây:

Tiếp tục nhấp đúp chuột vào thông tin tại DATA XREF được highlight như trên hình:

Sử dụng tính năng xrefs của IDA, chúng ta sẽ đến với khối lệnh gọi tới hàm main (call sub_401000) của chương trình:

Đi vào hàm sub_401000(), ta thấy trong code của hàm không dành ra bất kì không gian nào cho biến buf bởi vì rõ ràng ở code của chương trình không hề sử dụng tới buffer này, nó chỉ thực hiện thiết lập thanh ghi EAX = 0 tương đương với câu lệnh return 0 trong hàm main().

Để chương trình thực hiện việc dành ra không gian cho bộ đệm thì trong hàm main() chúng ta buộc phải sử dụng tới buffer đã khai báo này. Do đó, thực hiện sửa lại code của chương trình như sau:

Ở đây tôi sử dụng hàm gets_s() để nhận thông tin mà người dùng gõ tại màn hình console và lưu thông tin đó vào trong bộ đệm buf mà chúng ta đã khai báo trong chương trình. Dữ liệu lưu tại buf có thể được sử dụng sau này.

Như trên hình, ta thấy hàm gets_s() nhận hai tham số truyền vào: một buffer để lưu input từ người dùng và số kí tự tối đa mà người dùng có thể nhập nhằm tránh không bị tràn (overflow). Như vậy, trong ví dụ này rõ ràng là không có overflow xảy ra, vì kích thước được sao chép không được lớn hơn 0x300 bytes của buffer đã tạo.

Căn cứ vào đoạn code trên thì bạn có thể thấy rằng sẽ không có khả năng để xảy ra overflow, vì dữ liệu được phép copy vào biến buf chỉ nhỏ hơn hoặc bằng 0x300 bytes mà thôi. Hãy phân tích thử trong IDA xem thế nào, file này có tên là Compiled_2.exe (Compilado_2.exe). Nếu được load kèm theo pdb file thì kết quả tại IDA sẽ tương tự như sau:

Với việc load kèm file pdb như vậy thì IDA sẽ nhận diện được các tham số và các biến tốt hơn nhiều. Chúng ta hãy phân tích một chút trong IDA, nhấp đúp vào một biến hoặc một tham số bất kỳ ta sẽ chuyển tới cửa sổ cung cấp thông tin về Stack của hàm main():

Tại đây, ta thấy ba tham số chính của hàm main()envp, argvargc, tuy nhiên trong thân của hàm main ta không sử dụng tới bất kì tham số nào. Các tham số này được truyền vào hàm main thông qua các câu lệnh push, trước khi chương trình có lời gọi thực sự tới hàm main().

Như vậy, với các lệnh push này sẽ truyền ba tham số vào Stack trước khi thực hiện lệnh CALL. Ta biết rằng, trước khi thực hiện hàm thì chương trình cũng sẽ lưu địa chỉ trở về (return address) vào Stack, mục đích của việc làm như vậy là để chương trình biết nơi mà nó sẽ trở về sau khi thoát khỏi hàm. Trong ví dụ này, địa chỉ trở về sẽ luôn có giá trị cố định là 0x401200 (nếu như ta không sử dụng cơ chế bảo vệ ASLR).

Như vậy, chương trình sẽ quay trở về địa chỉ này sau khi thực hiện xong hàm main(). Vì vậy, phải lưu lại giá trị 0x401200 (r) vào stack và nó nằm ở trên 3 tham số của hàm main():

Sau khi thực hiện truyền tham số cho main() và lưu lại địa chỉ trở về, chương trình bắt đầu thực hiện hàm main(). Việc đầu tiên mà tại mỗi hàm luôn thực hiện chính là lệnh PUSH EBP.

Lệnh này sẽ lưu vào stack giá trị của thanh ghi EBP, giá trị này đang được sử dụng bởi hàm đã gọi hàm main(). Do đó, giá trị này sẽ nằm ngay phía trên địa chỉ trở về là 0x401200. Chúng ta không biết được giá trị này bởi vì nó sẽ thay đổi khi thực thi chương trình. Ta chỉ cần nhớ rằng câu lệnh này sẽ lưu lại giá trị EBP của hàm gọi (caller) lên stack và nó sẽ nằm ngay trên địa chỉ trở về (r), hay ngắn ngọn hơn là lưu lại old frame vào Stack.

Câu lệnh tiếp theo được thực hiện là:

Lệnh này làm nhiệm vụ thiết lập lại thanh ghi EBP trở thành Base (cơ sở) của hàm main(), bằng cách gán nó bằng với ESP. Lệnh mov ở đây chỉ thay đổi giá trị của EBP, không tác động gì tới ngăn xếp. Sau khi thiết lập xong EBP, hàm main thực hiện lệnh sub esp, 0x304, lệnh này có nhiệm vụ di chuyển thanh ghi ESP để dành ra không gian cho các biến cục bộ và bộ đệm trong ngăn xếp. Các biến cục bộ và các buffer này sẽ nằm phía trên giá trị EBP vừa được lưu.

Như trên hình ta thấy không gian dành cho các biến cục bộ và bộ đệm sẽ nằm ngay phía trên s (Stored EBP). Tại hàm main(), ta sẽ thấy biến đầu tiên hầu như luôn luôn xuất hiện nếu chương trình áp dụng cơ chế bảo vệ Stack (stack canary), trong trường hợp này nó được đặt tên là var_4:

Như quan sát trong đoạn code trên, ta thấy nó đọc giá trị của __security cookie và lưu vào thanh ghieax, đây là một giá trị ngẫu nhiên, được sinh ra khác nhau mỗi khi chương trình được thực thi. Sau đó, nó được đem xor với EBP và lưu lại vào biến var_4 như chúng ta đã thấy. Do đó, ta đổi tên nó thành CANARY.

Tiếp theo, ta quan sát và thấy ở trên biến CANARY này là biến Buf. Kiểm tra thông tin của Buf tại cửa số Stack của hàm main().

Một kinh nghiệm nhỏ khi phân tích các biến của một hàm đó là, nếu ta thấy bên dưới của một biến là không gian trống liên tục (gồm toàn các db ? tương tự như trên hình) tại cửa sổ Stack của hàm, thì rất có thể đó là một buffer. Do đó, tôi nhấn phải chuột tại Buf và chọn Array:

Ta thấy IDA tự động tính toán được kích thước của mảng này là 768 (ở hệ thập phân), và mỗi phần tử của mảng có kích thước là 1 byte. Nếu ta chuyển đổi giá trị 768 sang hệ hexadecimal thì sẽ chính là 0x300. Ta nhấn OK để chấp nhận thông tin mà IDA đã cung cấp. Như vậy, ta đã có được biến Buf như trong mã nguồn gốc của chương trình, được định nghĩa là một bộ đệm có kích thước 0x300 bytes ở hệ hexa hay 768 ở hệ thập phân.

Tiếp theo, bên dưới ta sẽ thấy lời gọi tới hàm gets_s(), hàm này nhận hai tham số truyền vào như đã phân tích ở trên. Trong đó, tham số thứ hai là kích thước tối đa, ở đây là 0x300 và tham số thứ nhất là địa chỉ của buffer, địa chỉ này có được thông qua lệnh lea:

Với thông tin như vậy ta đã xác minh được rằng kích thước của Buf0x300 bytes và thông qua hàmgets_s() thì dữ liệu sao chép vào buffer này tối đa là 0x300. Như vậy, rõ ràng là nếu chúng ta có thể làm tràn bộ đệm này bằng cách ghi vào đó nhiều hơn 0x300 bytes, thì dữ liệu ghi vào sẽ ghi đè lên biến CANARY, giá trị của EBP (đã lưu bằng lệnh push ebp) và địa chỉ trở về r (return address) nằm ngay bên dưới buffer:

Tuy nhiên, ở ví dụ này thì ta sẽ không thể thực hiện overflow được vì code của chương trình đã kiểm soát tốt vùng buffer. Ta xem xét một ví dụ khác như sau:

Trong ví dụ tiếp theo này, chương trình yêu cầu người dùng nhập vào một số tùy ý và số đó sẽ tương ứng với kích thước tối đa của dữ liệu được phép sao chép vào buffer. Nếu như kích thước này được kiểm tra kĩ lưỡng thì chương trình sẽ tránh được lỗi tràn bộ đệm. Nếu ta thay lại kích thước của biến buf trên thành 0x10 byte hay tương ứng với 16 ở hệ thập phân và yêu cầu người dùng nhập vào kích thước của buffer, kích thước này sau đó sẽ được sử dụng trong hàm gets_s.

Tuy nhiên, trong code của chương trình lại không hề kiểm tra giá trị kích thước tối đa mà buf có thể chấp nhận, tức là kiểm tra giá trị nhập vào có lớn hơn kích thước khai báo của buf hay không. Nếu ta biên dịch chương trình và thực thi để kiểm tra (Compiled_3.exe (Compilado_3.exe)):

Có lỗi đã xảy ra và như vậy khả năng chương trình có thể có vuln. Thử load chương trình vào trong IDA (ngay cả khi chúng ta không có mã nguồn của nó), ta sẽ thấy code như sau:

Giống như ví dụ trước, ta đã biết được đâu là biến CANARY, ở ngay phía trên của CANARYBuf. Chiều dài hay kích thước của buffer là 16 byte tương ứng với 0x10 ở hệ hexa:

Vì vậy, nếu ta có thể sao chép nhiều hơn 16 bytes vào bộ đệm thì sẽ xảy ra overflow và dữ liệu sẽ được ghi đè lên các giá trị CANARY, Stored ebpReturn Address:

Ta xem xét tiếp biến Size:

Sau khi in ra màn hình thông báo “Please Enter Your Number” bằng hàm printf(),chương trình sử dụng hàm scanf_s() để đọc dữ liệu mà người dùng nhập vào từ bàn phím và lưu giá trị nhập vào đó vào biến Size. Đây là một biến có kích thước dword và địa chỉ của biến được lấy ra bằng lệnh LEA vào thành ghi eax như các bạn đã thấy trên hình.

Thông tin về hàm scanf_s() trên MSDN như sau:

Như vậy, hàm scanf_s() chịu trách nhiệm đọc dữ liệu từ stdin (standard input stream) và ghi dữ liệu vào vị trí được cung cấp bởi tham số truyền vào. Mỗi tham số của hàm này phải là một con trỏ tới một biến có kiểu tương ứng với kiểu dữ liệu được chỉ ra trong format. Do đó, hàm scanf_s() sẽ trái ngược với hàm printf(), thay vì in ra màn hình một chuỗi theo định dạng thì nó nhận dữ liệu do người dùng nhập vào, tham số lưu dữ liệu có kiểu trùng với kiểu định nghĩa bởi format. Ví dụ, trong trường hợp này là “%d” thì có nghĩa là nhận một số thập phân mà người dùng nhập vào, cho nên biến Size phải có kiểu là int.

Bằng cách này, khi bạn gọi hàm gets_s() mà sử dụng kích thước do người dùng nhập vào trước đó, thì gets_s() sẽ sao chép số byte tương ứng và nếu như lớn hơn 0x10 thì sẽ xảy ra tràn bộ đệm.

Một giải pháp khả thi để bảo vệ là sẽ tiến hành kiểm tra độ dài của kích thước mà người dùng nhập vào trước khi thực hiện hàm gets_s(), như trong ví dụ minh họa dưới đây:

Phần 20 xin phép được dừng lại ở đây. Trong phần tiếp theo chúng ta sẽ cùng phân tích xem với giải pháp trên thì có thể giúp chương trình hết vuln không hay là vẫn bị dính. Target đã được biên dịch sẵn với tên là VULNERABLE_o_NO.exe để các bạn phân tích thử. Chúng ta sẽ thảo luận về nó ở phần sau.

Hẹn gặp các bạn ở phần 21.

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


Trải qua 18 phần tôi ghĩ các bạn đã có được những kiến thức nhất định, trong phần 19 này chúng ta sẽ thực hiện reverse Cruehead Crackme một cách đầy đủ nhất để hiểu cách hoạt động cũng như code một keygen nhỏ.

Load crackme vào IDA và bỏ chọn Manual Load bởi đây là file gốc không bị packed nên ta không cần phải sử dụng đến tùy chọn này.

Sau khi phân tích xong, ta thấy IDA dừng lại EP của crackme như trên hình. Do crackme này không phải là một ứng dụng dạng console như ở các phần trước, nên nhiệm vụ của ta không nhất thiết là phải đi tìm được hàm main. Ta biết rằng với các ứng dụng kiểu Window Applications thường có một message loop nhằm xử lý những gì người dùng tương tác với cửa sổ, nút bấm và thực hiện theo từng hành động của người dùng. Nó được lập trình để thực hiện các chức năng khác nhau.

Cách phổ biến nhất khi làm việc trong IDA vẫn là tìm kiếm các strings quan trọng tại cửa sổ Strings, nếu không có kết quả như mong muốn, ta sẽ tìm kiếm thông tin về các hàm APIs hoặc các hàm được viết bởi chính người lập trình. Trong trường hợp này, các strings của crackme có thể quan sát được rất dễ dàng, vì vậy chúng ta sẽ đi theo hướng tiếp cận này.

Tại màn hình Strings, có rất nhiều chuỗi nhưng ta sẽ dò theo chuỗi “No luck!” vì ta đoán chuỗi này sẽ đưa chúng ta đến vùng code đưa ra quyết định rẽ nhánh nào đó của Crackme. Trước tiên, nhấp đúp vào chuỗi này, ta sẽ chuyển tới màn hình Disassembly như hình dưới:

Tại đây, ta tìm kiếm các tham chiếu bằng cách nhấn phím tắt X hoặc Ctrl + X. Kết quả có hai tham chiếu đến chuỗi “No luck!”:

Hãy xem xét lệnh đầu tiên. Chọn lệnh này và nhấn OK, ta sẽ tới đây:

Tôi đổi màu cho khối lệnh này thành màu đỏ (hoặc màu nào khác tùy bạn) đồng thời cũng đổi tên luôn cho sub này vì nhìn vào các lệnh tại đây các bạn cũng đã hiểu được mục đích của nó rồi.

Tiếp theo ta xem sub này được gọi từ đâu khác trong code của Crackme, ta có được kết quả như sau:

Tại vùng code này, tôi cũng đổi màu cho block để dễ dàng nhận biết. Đồng thời với kết quả có được thì chắc chắn nhánh bên cạnh sẽ là nơi hiển thị thông báo “Good work!”. Ta đi tới các lệnh tại sub_0x40134d:

Rõ ràng là hàm này làm nhiệm vụ hiển thị thông báo “Good work!”, do vậy tôi thực hiện đổi màu của khối lệnh này cũng như tên của hàm như trên hình. Nhấn phím tắt ESC để quay ngược trở lại nơi gọi tới hàm này, ta có kết quả như sau:

Tạm thời phân tích sơ bộ về các đoạn code liên quan đến việc hiện thị chuỗi “No luck!” đầu tiên đã xong. Tiếp theo, ta chuyển tới đoạn code thứ hai có tham chiếu tới chuỗi “No luck!”. Tại đây tôi cũng chuyển màu cho khối lệnh này và đổi tên cho loc_ tương tự như hình:

Như vậy, ngoài thông báo lỗi ở trên, chúng ta còn có một thông báo lỗi tương tự khác ở đoạn code sau:

Tại đây, chúng ta thấy rằng sub_40137E() nhận một tham số truyền vào cho hàm này, đó là địa chỉ (offset) của một biến được IDA nhận diện là String. Nhấp đúp chuột vào biến này ta sẽ đi tới đây:

Với thông tin IDA cung cấp, thì đây là một buffer có kích thước 3698 bytes tại địa chỉ 0x40218E, thuộc section DATA của Crackme. Nhấn X tại biến này ta sẽ thấy có hai tham chiếu tới nó:

Trước tiên, đi tới lệnh push như tôi đã đánh dấu mũi tên trên hình. Tại đó có các lệnh như sau:

Ta thấy chuỗi này được truyền vào như là tham số cho hàm API là GetDlgItemTextA(), API này có nhiệm vụ nhận thông tin người dùng nhập vào từ hộp thoại. Tra cứu thêm thông tin về hàm này trên MSDN:

Như vậy là biến String sẽ được sử dụng làm buffer để lưu trữ dữ liệu mà người dùng nhập vào thông qua hàm GetDlgItemTextA(). Độ dài tối đa của chuỗi được copy vào buffer là 0xB (11).

Quan sát trên hình, bên dưới ta thấy có khối code tương tự như khối ta vừa phân tích và khối này cũng xử lý cùng một hWnd. Do đó, tôi suy đoán toàn bộ khối lệnh này được sử dụng để lấy thông tin về tên người dùng và mật khẩu khi nhập vào các textbox của Crackme.

Qua đoạn code trên, ta cũng thấy rằng cả hai textbox này đều có một giá trị định danh là nIDDlgItem tương ứng cho chúng là 0x3E80x3E9. Bằng việc sử dụng ứng dụng Greatis WinDowse (http://greatisprogramming.com/freedownload/wdsetup.exe), chương trình này sẽ cung cấp cho ta thông tin về các cửa sổ:

Với sự hỗ trợ của Greatis WinDowse, tôi đã xác định được ID của textbox đầu tiên (Name) là 0x3E8, do đó textbox thứ hai (Serial) sẽ là 0x3E9. Do đó, quay trở lại IDA để thực hiện đổi tên lại cho các buffer này, buffer đầu tiên tương ứng với chuỗi name người dùng nên tôi đặt là String_User và buffer thứ hai tương ứng với mật khẩu nên tôi đặt là String_Password. Cả hai buffer này chỉ chấp nhận chuỗi nhập vào có chiều dài tối đa là 0x0B như đã phân tích ở trên:

Tới lúc này ta đã biết được nơi sẽ lưu trữ thông tin mà người dùng nhập vào, chúng ta tiếp tục phân tích tiếp xem code của chương trình sẽ làm gì với các buffer này. Đầu tiên là String_User, code xử lý nó tại đây:

Trước khi đi vào phân tích sâu hơn ta sẽ đổi tên lại các hàm để code nhìn rõ ràng hơn. Hàm đầu tiên nhận tham số truyền vào là biến String_User, do đó hàm này có nhiệm vụ xử lý chuỗi Name, còn hàm thứ hai nhận tham số truyền vào là biến String_Password, do đó nhiệm vụ của hàm này chắc chắn sẽ liên quan tới mật khẩu mà người dùng nhập vào. Sau khi đổi tên hàm tôi có kết quả như hình:

Bây giờ, ta sẽ đi vào phân tích hàm đầu tiên là Process_User():

Tại hàm này, ta thấy nó có một tham số là arg_0, do tham số của hàm được truyền vào trước khi gọi hàm nên tham số này chắc chắn là chuỗi tên của người dùng. Tôi đặt lại tên cho tham số này và sử dụng lại cùng một tên để cho dễ nhớ. Tôi ghép thêm tiền tố offset để nhớ rằng đây là một biến con trỏ, trỏ tới chuỗi được lưu. Tiếp theo tôi đặt lại kiểu cho hàm này bằng cách nhấn phím tắt Y.

Khi quay trở lại vị trí thực hiện lời gọi hàm, chúng ta thấy IDA đã tự động bổ sung thêm comment trùng với tên của tham số mà ta đã đặt trong hàm.

Quay trở lại phân tích hàm Process_User(), hàm này sử dụng một vòng lặp thực hiện đọc từng byte của String_User. Vòng lặp này sẽ đi theo hướng mũi tên màu đỏ và lặp lại chừng nào byte đọc ra không phải là 0x0, nghĩa là khi chưa kết thúc chuỗi:

Sau mỗi lần xử lý của vòng lặp ta sẽ tấy nó thực hiện lệnh inc nhằm tăng giá trị của thanh ghi ESI để đọc kí tự tiếp theo trong String_User, sau đó thực đi theo hướng mũi tên màu xanh lam để thực hiện tiếp:

Kí tự được đọc ra đầu tiên sẽ được đem so sánh với giá trị 0x41. Để biết nó là gì, nhấn chuột phải tại giá trị 0x41 này và đổi thành ‘A’ là kí tự ASCII của giá trị đó:

Kết quả có được như sau:

Ta thấy, nếu kí tự đọc ra thấp hơn chữ cái ‘A’, crackme sẽ rẽ nhánh sang đoạn code hiển thị thông báo “No luck!”. Vì vậy, nếu quan sát nội dung của bảng mã ASCII thì ta hiểu rằng Crackme không chấp nhận có số trong chuỗi tên của người dùng (vì chúng phải lớn hơn hoặc bằng với A).

Vậy tóm lại, đầu tiên crackme sẽ kiểm tra tất cả các ký tự trong String_User phải lớn hơn hoặc bằng 0x41 (A).

Tiếp theo, crackme một lần nữa kiểm tra kí tự đọc ra nếu không thấp hơn (JNB) chữ cái ‘Z’ thì sẽ rẽ đến nhánh tới địa chỉ 0x401394, còn nếu kí tự đọc ra là thấp hơn thì sẽ tiếp tục với các kí tự tiếp thông qua khối màu xanh lam như tôi đã đổi trên hình.

Như vậy, crackme này chấp nhận các chữ cái hoa do người dùng nhập vào. Các chữ cái lớn hơn hoặc bằng ‘Z’ sẽ đi đến khối lệnh tại địa chỉ 0x401394. Ta phân tích hàm sub_4013D2() xem nhiệm vụ của nó là gì.

Tôi đổi tên hàm này thành Convert_to_Uppercase() dựa vào các câu lệnh mà hàm này thực hiện. Nếu kí tự nhập vào lớn hơn ‘Z’ thì sẽ được trừ đi 0x20 và được lưu lại vào cùng buffer.

Nghĩa là, nếu bạn nhập vào là chữ “a” thường, tương đương mã là 0x61, sau khi trừ 0x20 thì kết quả sẽ là 0x41, tương đương với chữ “A“. Hàm này sẽ thực hiện công việc tương tự cho tất cả các ký tự lớn hơn hoặc bằng “Z”. Do đó, nếu ta nhập vào là “Z”, sau khi trừ đi 0x20 ta có kết quả là 0x3a, đó chính là dấu “:” trong bảng mã ASCII:

Note: Thực ra cũng không hẳn là Convert_to_Uppercase(), nó phụ thuộc vào người dùng nhập vào. Ta cứ giả sử lý tưởng hóa người dùng nhập vào một chuỗi không vi phạm 🙂 .

OK, vậy là ta có thể xây dựng một Python script để tạo ra keygen như sau:

user = raw_input()
len_user = len(user)
if (len_user > 0xb):
    exit()

userName=""

for i in range(len_user):
    if (ord(user[i]) < 0x41):
        print "INVALID CHARACTER!"
        exit()
    if (ord(user[i]) >= 0x5A):
        userName+= chr(ord(user[i]) - 0x20)
    else:
        userName+= chr(ord(user[i]))

print "User Name:", userName


Chúng ta thấy rằng script trên thực hiện lại công việc tương tự như crackme đã làm, nó sẽ lấy các ký tự trong chuỗi người dùng nhập vào và so sánh nó với 0x41. Nếu nhỏ hơn sẽ thông báo đó là một kí tự không hợp lệ và thoát luôn. Còn nếu không nhỏ hơn 0x41, kí tự được so sánh tiếp với 0x5A. Nếu lớn hơn hoặc bằng 0x5A, kí tự đó được trừ đi 0x20 và được gán lại vào chuỗi userName.

Kiểm tra thử script này như hình dưới đây:

Và nếu tôi nhập chuỗi có chứa chữ “Z”, nó sẽ được thay thế bằng dấu “:” như chúng ta đã phân tích ở trên:

OK, như vậy là đoạn script trên đã thực hiện đúng công việc mà vòng lặp trong code của crackme đã làm. Quay trở lại IDA, ta tiếp tục phân tích tiếp xem sau khi thoát khỏi vòng lặp thì code của Crackme sẽ làm gì tiếp. Khi vòng lặp kiểm tra và biết kí tự của chuỗi nhập vào bằng 0 (báo hiệu kết thúc chuỗi), thì nó sẽ thoát khỏi Loop và rẽ nhánh theo hướng mũi tên màu xanh lá cây như hình:

Tại khối lệnh bắt đầu từ địa chỉ 0x40139C, ta thấy chương trình gọi lệnh pop esi. Nhấn vào thanh ghi esi này thì IDA sẽ highlight toàn bộ các vị trí trong code có sử dụng tới thanh ghi này:

Chúng ta thấy rằng trước khi khôi phục lại thanh ghi esi, đoạn đầu code của crackme đã sử dụng lệnh push để lưu giá trị ban đầu của esi vào ngăn xếp, mà ta thấy rằng giá trị ban đầu này của esi là trỏ tới đầu chuỗi mà người dùng nhập vào. Như vậy, với lệnh pop esi, giá trị của esi sẽ được khôi phục lại trước khi gọi hàm sub_0x4013c2(). Tóm lại, trước khi đi vào sub_0x4013c2() thì thanh ghi esi lúc này sẽ trỏ lại về chuỗi mà người dùng đã nhập vào, tuy nhiên lúc này chuỗi đó đã được chuyển đổi rồi.

Phân tích lệnh tại hàm sub_0x4013c2() như trên hình, ta thấy rằng code tại đây là một vòng lặp thực hiện lấy ra từng kí tự trong chuỗi và cộng dồn lại. Đó là lý do tại sao tôi đặt trên cho hàm này là Sum_Chars(). Bổ sung code này vào trong Python script ở trên:

Như vậy, script của ta đã hoàn thành công việc cộng tất cả các byte và in ra tổng. Để kiểm tra script có thực hiện đúng không, ta có thể đặt một breakpoint tại lệnh XOR bên dưới hàm cộng, cho chạy chương trình và nhập vào tên người dùng là “manowar” giống như lúc chạy script và mật khẩu tùy ý, ví dụ 989898.

Như trên hình, ta thấy thanh ghi EDI sẽ lưu kết quả của hàm cộng, giá trị của thanh ghi EDI lúc này đang là 0x215, hoàn toàn khớp với script ta đã viết ở trên. Sau đó, giá trị của EDI được đem đi XOR với giá trị mặc định 0x5678 (thường gọi là XOR_KEY). Do đó, tôi bổ sung thêm lệnh này vào script như sau:

Nếu vẫn đang dừng lại tại BP ở trên, ta nhấn F8 để trace qua lệnh XOR này, quan sát giá trị của thanh ghi EDI và so sánh với kết quả thực hiện bằng script hoàn toàn khớp nhau:

Sau đó, tại code của chương trình, kết quả sau lệnh XOR được gán cho thanh ghi EAX để làm kết quả trả về của hàm và nhảy tới địa chỉ 0x4013C1 để thoát khỏi hàm.

Sau khi ra khỏi hàm, giá trị của thanh ghi EAX sẽ được lưu lại thông qua lệnh push eax và sau đó được khôi phục lại bằng lệnh pop eax trước khi thực hiện so sánh. Điều này có nghĩa là trong câu lệnh cmp eax, ebx, toán hạng đầu tiên là thanh ghi EAX sẽ là kết quả trả về từ hàm Process_User().

Với chuỗi tên người dùng nhập vào đã xong, tiếp tục phân tích để xem điều gì sẽ xảy ra với mật khẩu mà người dùng nhập vào tại hàm Process_Password(). Đi vào phân tích code tại hàm:

Sau khi phân tích code, nhận thấy hàm này sẽ thực hiện đọc từng byte của chuỗi password, lưu vào thanh ghi BL và đem trừ đi giá trị là 0x30, kết quả vẫn lưu lại trong thanh ghi EBX. Tiếp theo, lấy giá trị của EDI (ban đầu được khởi tạo bằng 0) đem nhân với 0xA (được gán cho EAX) rồi cộng với EBX. Viết lại toàn bộ đoạn code này trong script như bên dưới. Trong script tôi để sẵn mật khẩu cố định là “989898” để xem kết quả sau khi tính toán là thế nào.

Chạy lại script, kết quả có được như sau:

Như vậy sau khi thực hiện script, với mật khẩu là 989898 thì kết quả tính toán là một giá trị ở dạng hex là 0xf1aca. Nếu thử chuyển đổi ngược lại thì tôi thấy giá trị hex này chính là của chuỗi 989898.

Do vậy, có thể tóm tắt lại đoạn code của hàm xử lý mật khẩu bước đầu sẽ thực hiện việc chuyển đổi chuỗi số mà ta nhập vào sang dạng hex, tương tự như việc ta gõ lệnh hex() ở thanh Python bar của IDA như trên hình. Do đó, trong script tôi có thể thay bằng câu lệnh đơn giản là hex(password). Khi chạy sẽ vẫn cho ra kết quả tương tự.

Tiếp theo, sau khi chuyển sang hạng hexa và lưu kết quả chuyển đổi tại thanh ghi EDI, trong code của crackme sẽ gọi lệnh xor edi với giá trị mặc định là 0x1234. Kết quả được bao nhiêu sẽ gán lại cho thanh ghi EBX thay vì sử dụng thanh ghi EAX (vì thanh ghi EAX đang được sử dụng để lưu kết quả khác). Sau khi thoát ra khỏi hàm, trước khi thực hiện so sánh chương trình sẽ khôi phục lại giá trị của EAX mà đã được tính toán bởi hàm Process_User().

Đến đây, ta có thể tổng kết lại toàn bộ quá trình xử lý của crackme như sau:

hex(password)^0x1234 = XOR_Result (Là kết quả tính toán mà hàm Process_User() đã trả về).

Do đó:

hex(password) = XOR_Result ^ 0x1234

Như vậy, bằng cách xor ngược lại như trên ta sẽ có được password tương ứng với chuỗi name nhập vào. Chỉnh sửa lại script trên như sau:

Chạy thử script sau khi đã sửa lại với chuỗi “manowar”:

Với kết quả có được, ta nhập vào Crackme để kiểm tra thử:

Tới đây ta đã có được một keygen hoàn chỉnh. Trong script này ta không cần thiết phải thực hiện chuyển đổi lại kết quả sang dạng thập phân bởi vì Python đã làm việc chuyển đổi này một cách tự động.

Dưới đây là toàn bộ source code của keygen:

sum = 0
user = raw_input()
len_user = len(user)
if (len_user > 0xb):
	exit()
userName=""
for i in range(len_user):
	if (ord(user[i]) < 0x41):
		print "INVALID CHARACTER!"
		exit()
	if (ord(user[i]) >= 0x5A):
		userName+= chr(ord(user[i]) - 0x20)
	else:
		userName+= chr(ord(user[i]))
print "User Name:", userName
for i in range(len(userName)):
	sum+= ord(userName[i])
print "Total:", hex(sum)
xor_result = sum ^ 0x5678
print "XOR_Result:", hex(xor_result)
#-----------------------------------
password = xor_result ^ 0x1234
print "Password:", password

Ta kiểm tra thử keygen với tên nhập vào có chứa “Z”:

Toàn bộ phần 19 đến đây là kết thúc, như vậy là chúng ta đã reversed toàn bộ quá trình hoạt động của Cruehead crackme và viết được một keygen hoàn chỉnh bằng Python. Hẹn gặp lại các bạn trong phần 20!

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


Tôi đọc được bài viết này https://isc.sans.edu/diary/25158 của ông Didier Stevens thấy hay nên viết lại.

Sử dụng công cụ oledump.py để kiểm tra xem tài liệu có chứa macro hay không:

Theo kết quả trên hình, tài liệu này có chứa VBA code. Xem thử nội dung của VBA code này thì thấy sẽ thực hiện lấy nội dung của object qmhxyrgkymtfz để gán cho các biến

Set DicqGKLkCZm = GetObject(qmhxyrgkymtfz.Controls(1).Text)
nLnTGkzGrXqrWp = qmhxyrgkymtfz.Controls(100 - 98).Value
nLnTGkzGrXqrWp = nLnTGkzGrXqrWp & "" & qmhxyrgkymtfz.Controls(0).Value

Cũng theo kết quả parse bằng oledump thì thấy có stream 15 là một “Stream O”, và theo nghiên cứu của ông Didier Stevens thì kiểu stream này thường được sử dụng để che dấu các payload. Sử dụng plugin_stream_o.py của chính tác giả để dump nội dung của Stream 15:

Theo kết quả trên hình thì có thể thấy đây là một powershell script đã được encode sang dạng base64. Thực hiện decode chuỗi base64 ở trên bằng base64dump.py. Kết quả như sau:

Chọn id đầu tiên và dump qua dạng Ascii:

Nhìn vào kết quả trên hình có thể thấy nội dung của Powershell là ở dạng Unicode. Sử dụng tùy chọn -t và chọn utf16:

Với kết quả có được, ta thấy powershell này sẽ thực hiện decode một chuỗi base64 và giải nén nó COMPREsSiON.deflATeSTREAm([syStem.IO.MEMOrYstreAm][sYSTeM.CONVerT]::FRoMBase64strinG . Tiếp tục thực hiện:

Khối data ở trên đã bị nén ” DeflateStream ” – là dạng Zlib. Sử dụng công cụ translate.py để giải nén:

Sau khi giải nén xong ta có được PowerShell cuối cùng, nhiệm vụ của nó là một downloader, kết nối tới C2 để tải các file khác:

hxxp://dx019xsl1pace[.]xyz/sywo/fgoow[.]php?l=styer7[.]gxl
hxxp://109[.]196[.]164[.]79/3[.]php

End.


Ở phần trước, tôi đã thực hiện unpack file thành công và file sau khi unpack có thể thực thi bình thường. Trong phần này, chúng ta sẽ tiến hành reverse nó để tìm hiểu cách thức hoạt động cũng như xem xét có thể viết keygen đơn giản bằng Python hay không.

Bạn nên nhớ rằng nếu chỉ để phân tích tĩnh chương trình thì ta không cần phải thực hiện đầy đủ các bước unpack như phần trước. Chúng ta chỉ cần tới được OEP và tạo một bản snapshot (Take Memory Snapshot), sau đó chép file idb sang chỗ khác và mở nó. Bằng cách này ta có thể tiếp tục quá trình phân tích tĩnh. Tuy nhiên, việc unpack file hoàn chỉnh sẽ hỗ trợ chúng ta rất nhiều, cho phép ta có thể debug được chương trình dễ dàng hơn.

OK, mở IDA và load file đã unpacked vào. Sau khi IDA phân tích xong, chuyển tới cửa sổ Strings để tìm kiếm các chuỗi:

Chúng ta đã biết được đầu tiên chương trình thực hiện in ra chuỗi yêu cầu người dùng nhập vào một tên bất kỳ:

Vì vậy, tại màn hình Strings ở trên, ta nhấp đúp vào chuỗi đó sẽ chuyển qua màn hình IDA Disassembly:

Tiếp theo nhấn “x” để tìm kiếm các đoạn code sử dụng tới chuỗi này. Kết quả có được:

Ta đi tới địa chỉ trên:

Từ đây, ta sẽ bắt đầu quá trình phân tích tĩnh.

Đầu tiên, các bạn sẽ thấy đoạn code prologue khi bắt đầu của một hàm, thanh ghi EBPbase frame của hàm trước đó sẽ được lưu vào ngăn xếp bằng câu lệnh PUSH EBP, sau đó hàm sử dụng lệnh MOV EBP, ESP để thiết lập cho thanh ghi EBP trở thành base frame cho hàm hiện tại đang phân tích (thanh ghi EBP kể từ đây sẽ được sử dụng để tham chiếu tới các biến cục bộ và tham số truyền vào cho hàm).

Câu lệnh tiếp theo SUB ESP, 94h làm nhiệm vụ dành riêng ra một khoảng không gian là 0x94 bytes cho các biến cục bộ và các buffers, kể từ giá trị base của thanh ghi EBP.

Bằng cách nhấp đúp vào bất kỳ biến hoặc tham số nào của hàm, IDA sẽ đưa ta tới cửa sổ biểu diễn thông tin về Stack của hàm đó:

Theo quan sát tại Stack của hàm, ta thấy rằng đây là một hàm không nhận bất kì tham số nào, bởi vì tham số thường sẽ được truyền vào thông qua các lệnh PUSH trước khi thực hiện lời gọi hàm, và các tham số này sẽ phải nằm bên dưới địa chỉ trở về (r). Trong trường hợp hàm mà ta đang phân tích, bên dưới r không có thông tin gì và đó là lý do tại sao ta biết được đây là hàm không nhận tham số truyền vào.

Đây cũng chính là hàm main() của chương trình và thường hàm main() sẽ có các tham số là: env, argv argc, vv. Nhưng vì các tham số này không được sử dụng bên trong hàm nên IDA sẽ không quan tâm đến các tham số này. Nhấn x tại đầu hàm để tìm xrefs:

Nhấn OK, ta sẽ tới đây:

Với kết quả như trên hình, tôi sẽ đổi tên hàm thành main(). Sau khi đổi tên như vậy, IDA sẽ tự động thêm ba tham số vào cho hàm như hình dưới đây:

Nếu chúng ta nhấn x tại bất kỳ một trong ba tham số này, ta sẽ nhận được thông báo như bên dưới. Đó là bởi vì trong hàm main() không sử dụng tới chúng:

Vì các tham số này không được sử dụng nên chúng ta sẽ không cần quan tâm đến chúng nữa. Ta quay trở lại với cửa sổ Stack của hàm để phân tích tiếp:

Lúc này tại cửa sổ Stack, ta thấy các tham số đã được bổ sung thêm và nằm bên dưới của địa chỉ trở về. Phía trên địa chỉ trở về là “s”, hàm ý rằng đó chính là giá trị EBP của hàm trước khi gọi hàm main() được lưu vào Stack. Như đã đề cập ở trên, giá trị EBP này được lưu lại thông qua lệnh PUSH EBP, bên trên “s” sẽ là không gian dành cho các biến cục bộ được khai báo trong main, thông thường các biến này có dấu hiệu nhận biết là var_4, var_x v..v… Biến var_4 này được sử dụng để bảo vệ ngăn xếp khỏi lỗi tràn bộ đệm (buffer overflow). Ta chọn biến này và nhấn x sẽ có kết quả như sau:

Ta thấy có hai chỗ sử dụng tới biến var_4. Một là tại chỗ bắt đầu của hàm khi hàm lấy giá trị của security cookie vào thanh ghi eax, rồi gán lại giá trị đó cho biến var_4:

Cookie security là một giá trị ngẫu nhiên, được đem XOR với thanh ghi EBP, kết quả được bao nhiêu sẽ lưu lại vào biến var_4 khi hàm bắt đầu. Ta phân tích đoạn code thứ hai sử dụng tới biến var_4:

Tại đoạn code trên, ta thấy nó thực hiện lấy ra giá trị đã lưu tại biến var_4, sau đó XOR lại với EBP để phục hồi giá trị ban đầu trong ECX và bên trong lệnh CALL bên dưới sẽ thực hiện kiểm tra giá trị này:

Nếu như mọi thứ đều hợp lệ, hàm sẽ return bình thường, nhưng nếu thanh ghi ECX không giữ giá trị ban đầu của _security_cookie, nó sẽ rẽ nhánh sang lệnh JMP để exit và không cho phép ta thực hiện lệnh RET của hàm.

Chúng ta sẽ thấy rằng việc thực hiện exit nó chỉ có thể xảy ra khi có Overflow làm ghi đè lên giá trị của biến var_4 bên trong hàm. Do đó, chúng ta có thể đổi tên biến var_4 thành CANARY hoặc SECURITY COOKIE và đổi tên hàm thành Check_Canary():

Sau khi đổi tên như trên, ta thấy code trông cải thiện hơn một chút:

Tiếp theo, chúng ta thấy 3 biến mà chưa rõ mục đích sử dụng, hai biến được khởi gán bằng 0, và một biến IDA nhận diện được tên là Size, được khởi gán bằng 8. Quan sát các tham chiếu đến biến var_7d, chúng ta thấy nó được sử dụng ở đây:

Biến này sẽ nhận giá trị được lưu tại thanh ghi AL (giá trị của AL được gán thông qua lệnh CALL ở bên trên) và sau đó lại gán lại vào thanh ghi EDX. Tiếp theo chương trình kiểm tra xem có bằng 0 hay không để quyết định rẽ nhánh code theo hướng “Good” hoặc “Bad”. Do đó, đây là một biến kiểu byte. Ta sẽ đổi tên biến này thành SUCCESS_FLAG. Tại cửa sổ Stack của hàm, ta thấy rằng IDA đã nhận diện được đây là biến có kích thước một byte.

Nhấn N để đổi lại tên biến. Sau đó, tôi đổi màu lại các khối code trong IDA như trên hình để dễ dàng hơn trong việc nhận diện và phân tích bằng IDA:

Rõ ràng là nếu tôi chỉ cần patch tại lệnh nhảy JZ thì tôi sẽ đạt được mục đích của mình, nhưng chúng ta sẽ không làm như thế. Chúng ta cần phân tích sâu hơn để đạt được mục tiêu đã đề ra. Tiếp tục phân tích biến tiếp theo là var_90:

Ta thấy rằng, ban đầu biến này được khởi gán bằng 0 ở đầu hàm, sau đó nó được sử dụng tại đoạn code như trên hình. Đoạn code này nằm trong một vòng lặp. Phân tích sâu hơn ta thấy nó sẽ đọc lần lượt từng byte một từ biến Buf tại 0x231109 vào EDX, sau đó cộng với biến var_90 (ban đầu là 0) và lưu lại vào EDX, cuối cùng lại lưu lại vào biến var_90. Như vậy, ta nhận thấy thanh ghi EDX luôn là tổng của tất cả các bytes, do vậy ta có thể đổi tên biến này thành SUMMARY:

Cùng với quá trình phân tích trên, ta cũng có thể nhận ra biến var_84 chính là bộ đếm của vòng lặp, sau mỗi lần lặp biến này sẽ được tăng thêm 1, và thoát khỏi vòng lặp nếu như giá trị của biến này lớn hơn hoặc bằng 4. Đoạn code thực hiện tăng biến đếm này như hình dưới và tôi sẽ đổi tên nó thành COUNTER:

Biến COUNTER này cũng được sử dụng tại 0x231109 như là một index để đọc các bytes từ biến Buf:

Tiếp theo chúng ta sẽ nghiên cứu biến Buf để xem nó sẽ chứa nội dung gì:

Tại đoạn code trên ta có thể thấy chương trình sử dụng hàm gets_s() để nhận thông tin mà người dùng nhập vào từ bàn phím. Qua đó, ta biết được biến Buf sẽ là nơi chứa chuỗi tên của người dùng nhập vào và chuỗi này có kích thước tối đa là 8 byte (Size được gán bằng 8 ở đầu hàm).

Trước đó, ta thấy một hàm tại địa chỉ 0x2310A0 bên dưới chuỗi yêu cầu nhập tên người dùng, ta có thể khẳng định luôn đây là hàm printf. Do đó, ta đổi tên cho hàm này:

Chuyển qua cửa số Stack, có thể thấy độ lớn của biến Buf này bằng cách nhấn chuột phải và chọn Array. Kết quả, biến Buf sẽ có kích thước là 120 bytes:

Hoàn toàn khớp với khai báo của nó trong mã nguồn của thầy Ricardo:

Sau khi chuyển đổi như trên, ta thấy thông tin về tham số và các biến tại cửa sổ Stack đã trở nên rõ ràng hơn rất nhiều. Tiếp theo, sau khi nhận thông tin do người dùng nhập vào và lưu vào trong biến Buf, chương trình sẽ sử dụng hàm strlen() để lấy ra chiều dài của chuỗi đã nhập vào:

Chiều dài chuỗi thu được qua hàm strlen() sẽ được lưu vào biến var_88, do vậy ta đổi tên biến này thành string_length:

Nếu biến này nhỏ hơn 4, tức là chiều dài của chuỗi nhập vào nhỏ hơn 4 thì chương trình sẽ gọi hàm exit() để thoát luôn. Tiếp theo là vòng lặp (mà ta đã phân tích ở trên) thực hiện cộng 4 byte đầu tiên của chuỗi mà chúng ta nhập vào, vì vậy ta sẽ nhóm các khối lệnh này lại để dễ nhìn hơn. Ta nhóm bằng cách chọn từng khối và nhấn Ctrl. Đặt tên cho khối đã nhóm. Để quay trở về trạng thái cũ chỉ việc nhấn chuột phải tại đó và chọn Ungroup.

Tiếp theo, ta thấy rằng chương trình lại sử dụng lại biến Buf để nhận mật khẩu mà người dùng nhập vào. Có thể sử dụng lại biến Buf này là vì chương trình đã tính toán xong.

Cũng tương tự như trên, chương trình sử dụng hàm strlen() để tính toán độ dài của mật khẩu và nếu nhỏ hơn 4 thì cũng sẽ exit() luôn:

Tôi đổi màu các khối như trên hình để dễ nhìn hơn. Như vậy, nếu chiều dài mật khẩu là 4 hoặc lớn hơn, chúng ta sẽ rẽ nhánh theo khối màu xanh lam như trên hình. Tại khối lệnh màu xanh lam này sẽ lấy mật khẩu và chuyển đổi nó sang dạng thập lục phân bằng hàm atoi(). Tại thanh Python của IDA hành động trên sẽ tương đương với hàm hex(). Ví dụ, tôi thực hiện như sau:

Sau khi chuyển đổi bằng hàm atoi() thì chương trình lưu kết quả chuyển đổi vào biến var_8C, do đó tôi đổi tên biến này như hình:

Sau đó, ta thấy rằng mật khẩu ở dạng Hexa này sẽ được đem XOR với một giá trị mặc định của chương trình là 0x1234. Kết quả sau khi thực hiện lệnh XOR sẽ được lưu lại vào cùng một biến. Tiếp tục quá trình phân tích, ta thấy biến lưu tổng 4 byte đầu tiên của chuỗi tên người dùng và giá trị hex đã tính toán trước đó được truyền cho một hàm bên dưới tại 0x2311A4. Kết quả trả về của hàm này sẽ lưu vào thanh ghi AL để từ đó đưa ra quyết định nhảy tới “good” hay “bad”. Do đó, ta sẽ đổi tên hàm thành CHECK_EXIT():

Như vậy, có thể thấy hàm CHECK_EXIT() sẽ nhận hai tham số truyền vào thông qua hai lệnh PUSH. Vì vậy, chúng ta sẽ đổi tên cả hai tham số trong hàm này tương ứng với các biến ta đã phân tích như sau:

Sau khi thay tên cho các tham số để dễ hiểu hơn, ta nhấn chuột phải tại hàm để đặt lại kiểu cho hàm như sau:

Làm như vậy hàm sẽ được khai báo lại như hình:

Quay trở lại nơi gọi hàm sẽ thấy IDA tự động thêm các chú thích cho các tham số tương ứng:

Có thể thấy IDA đã thực hiện công việc rất tuyệt vời, giờ đi vào phân tích kĩ hơn nhiệm vụ của hàm CHECK_EXIT():

Quan sát code của hàm, trước khi làm phép so sánh thì hàm này thực hiện lệnh SHL EAX, 1, tương đương với việc đem giá trị tại EAX nhân 2. Vì vậy, nếu hai giá trị tại lệnh so sánh là bằng nhau, ta sẽ đi đến khối màu xanh lam, nơi code của hàm thiết lập cho thanh ghi AL bằng 1 (mà thanh ghi AL này sẽ được gán cho biến SUCCESS_FLAG để quyết định ta sẽ nhận thông báo “good” hay là “bad”).

Như vậy, tóm tắt lại toàn bộ quá trình đã phân tích ở trên:

  • Chương trình sẽ lấy 4 byte đầu tiên từ tên của người dùng nhập vào và thực hiện phép cộng dồn.
  • Tiếp theo sẽ nhận Password mà người dùng nhập vào, chuyển đổi sang dạng Hexa và đem thực hiện XOR với giá trị mặc định 0x1234. Cuối cùng sẽ đem kết quả tính toán này nhân với 2.

Chúng ta sẽ xây dựng một công thức tính toán với giả định rằng ta có tên của người dùng nhập vào bởi vì keygen sẽ dựa vào thông tin này để tính toán. Với tên của người dùng bất kỳ thì keygen sẽ sinh ra một mật mật khẩu tương ứng. Ta có như sau:

  • X = PASSWORD đã được chuyển đổi sang HEXA
  • (X ^ 0x1234) * 2 = SUMMARY
  • X ^ 0x1234 = (SUMMARY / 2)
  • X = (SUMMARY/2) ^ 0x1234

Ta đã biết cách hoạt động của lệnh XOR ở các phần trước rồi, tôi xin nhắc lại như sau: A ^ B = C –> A = B ^ C;

Do đó để tìm X thì công thức sẽ là: X = (SUMMARY/2) ^ 0x1234

Tôi viết thử một script bằng python như bên dưới, trong script này tôi cố định tên của người dùng là “manowar”, có chiều dài nhỏ hơn 8 bytes, vậy tổng của chuỗi này sẽ như sau:

Nhớ lại code của chương trình, ta không cộng toàn bộ chuỗi mà chỉ tính tổng có bốn kí tự đầu mà thôi. Do đó, tôi giới hạn lại như sau:

Với đoạn code trên, ta có thể tạo một keygen cho bất kỳ tên người dùng nào, việc lấy thông tin người dùng nhập vào sẽ thông qua hàm raw_input():

Kết quả cho chuỗi “manowar” vẫn giống như trên nhưng thay vào đó ta có thể tính toán cho bất kỳ tên người dùng nào. Ví dụ:

Chúng ta có biểu thức tính toán mật khẩu như sau:

X = (SUMATORIA/2) ^ 0x1234

Do vậy, ta viết lại biểu thức này bằng python như bên dưới đây:

Ta thử nhập tên và password tìm được ở trên, kết quả như hình minh họa dưới đây:

Chúng ta đã có keygen hoàn chỉnh, ở đây ta không cần phải thực hiện chuyển đổi mật khẩu từ dạng hexa sang thập phân bởi vì Python mặc định khi xuất ra màn hình là ở dạng thập phân rồi.

Kiểm tra với một chuỗi name dài hơn như trên hình, ta thấy rằng nó chỉ cộng 4 ký tự đầu tiên của tên người dùng, do đó kết quả tính toán ra password sẽ là như nhau. Bài tập này sẽ bị crash khi ta nhập vào chuỗi name có 8 kí tự bởi vì 8 kí này sẽ bao gồm cả kí tự kết thúc chuỗi. Chuỗi mà có 7 kí tự thì sẽ chạy bình thường. Chỉ có một vấn đề khi nếu tổng 4 kí tự cho ra kết quả là một số lẻ:

Sẽ không có giải pháp vì mật khẩu cuối cùng sẽ được nhân với 2 và được kết quả sẽ không bao giờ là số lẻ. Do vậy, ta cần phải bổ sung thêm đoạn kiểm tra sau:

Trong đoạn code trên, chúng ta sẽ kiểm tra phần dư khi thực hiện phép chia cho 2. Nếu kết quả bằng 0 thì là hợp lệ, còn khác 0 thì sẽ hiển thị thông báo để thử một chuỗi tên khác:

Tôi nghĩ rằng keygen hoạt động như vậy là ổn rồi, do đó tôi xin được kết thúc bài viết này tại đây.

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

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


Chúng ta lại gặp nhau ở phần 17 và trong phần này tôi và các bạn sẽ giải quyết bài tập đã đưa ra ở phần 16 (PACKED_PRACTICA_1.exe).

Bài tập này rất đơn giản và bạn phải thực hiện unpack như các bước tôi đã trình bày ở các phần trước. Sau đó reverse nó để tìm hiểu quá trình hoạt động cũng như viết một keygen bằng ngôn ngữ nào cũng được. Ở phần 17 này, ta thay đổi một chút, tôi sẽ thực hiện unpack từ xa thông qua một máy ảo Windows 10 chạy trên nền VMware Workstation. Máy chính dùng để debug từ xa trong trường hợp này cũng là Windows 10 (máy mà tôi đã cài đặt và sử dụng IDA), tuy nhiên bạn có thể tùy chọn sử dụng các hệ điều hành khác.

Tóm lại, để tránh bị nhầm lẫn, tôi sẽ gọi máy mà tôi đã cài đặt IDA là máy chính hay “Main” và máy Windows 10 mà tôi chạy trên VMware, nơi sẽ dùng để chạy packed file là “Target“.

Bên cạnh packed file đã được copy vào máy Target, tôi cũng phải chép file win32_remote.exe từ thư mục mà tôi đã cài đặt IDA trên máy Main ({IDA_path}\dbgsrv) vào máy Target. Do máy ảo Windows 10 của tôi là 64 bits, mà packed file lại là 32 bits, do vậy tôi phải chạy máy chủ 32-bit là vì thế.

Chúng ta cũng vẫn phải có file PACKED_PRACTICA_1.exe trên máy Main để thực hiện phân tích local. Load file này vào IDA để phân tích và nhớ chọn Manual Load để nạp tất cả các sections liên quan. Sau khi IDA loader phân tích xong, tại màn hình Disassembly của IDA sẽ hiển thị Entry Point của chương trình. Lúc này, chúng ta sẽ không chạy Debugger.

Theo lưu ý của Ricardo thì một điều khá quan trọng là không đổi tên của file idb, đó là vì nếu tệp thực thi là PACKED_PRACTICA_1.exe trong Main, khi load vào IDA để phân tích nó sẽ lưu một database với phần mở rộng là idb trong cùng thư mục, file này trùng tên và chỉ khác phần mở rộng là PACKED_PRACTICA_1.idb và không có thêm tên nào khác, vì nếu thay đổi sẽ có vấn đề trong việc nhận biết tiến trình từ xa thuộc về cùng một file thực thi đã được phân tích locally.

Cần đảm bảo máy MainTarget kết nối được với nhau. Tiến hành chạy file win32_remote.exe trên máy Target. Kết quả như hình dưới đây:

Như kết quả trên hình, IDA remote debug server đã chạy và đang lắng nghe tại một địa chỉ IP và cổng tương ứng. Trong trường hợp của tôi, địa chỉ IP là 192.168.100.5 và cổng là 23946. Bạn lấy kết quả có được trên máy bạn, sau đó cấu hình thay đổi debugger như sau:

Trong phần thiết lập cấu hình, ở Hostname nhập vào địa chi IP của IDA remote debug server và port tương ứng. Vì chúng ta cần phải thực thi packed file để thực hiện unpack nó, do ta không thể attach file nên sẽ phải sửa lại đường dẫn thư mục trỏ tới đúng vị trí của file thực thi trên máy Target.

Trong trường hợp của tôi, packed file trên máy Target được để tại Desktop, vậy nên đường dẫn của tôi sẽ là C:\Users\admin\Desktop\PACKED_PRACTICA_1.exe

Với thông tin trên, tôi sửa lại các đường dẫn như sau:

Cấu hình xong nhấn OK để xác nhận các thiết lập, sau đó nhấn Start Process, chương trình sẽ dừng lại tại Entry Point của packed file trong chế độ Debugger.

Chúng ta có thể nhận thấy có một điểm khác lạ ở đây, đó là file thực thi có các địa chỉ ngẫu nhiên sau mỗi lần thực thi. Đó là một điểm rất quan trọng, vì ở các phần trước chúng ta cho thực thi file, dump file và sửa IAT của cùng một tiến trình mà không hề tắt nó, địa chỉ của file cũng hề thay đổi sau mỗi lần thực hiện debug.

Mở cửa sổ Segments và quan sát section đầu tiên (UPX0) bên dưới HEADER. Tại máy của tôi thì section này bắt đầu từ 0x961000 và kết thúc tại 0x968000:

Sử dụng tính năng Search text của IDA để tìm lệnh POPAD hoặc POPA, chúng ta sẽ tìm thấy nơi lệnh này được thực hiện trước khi nhảy tới OEP.

Theo kết quả trên hình, ta biết được OEP của chương trình là 0x96146e. Tuy nhiên, tôi cũng có thể tìm thấy địa chỉ này bằng cách đặt một Breakpoint on execution lên toàn bộ section đầu tiên.

Chuyển tới địa chỉ 0x961000, nơi bắt đầu của section đầu tiên. Tại đây, ta quan sát thấy các thông tin về section, rõ ràng là các địa chỉ không khớp bởi vì nó được sinh ngẫu nhiên. Như ta thấy, địa chỉ Imagebase không phải là 0x400000, nhưng giá trị Virtual Size vẫn là 0x7000 (vì section này bắt đầu ở 0x961000 và kết thúc ở 0x968000).

Nhấn F2 tại địa chỉ bắt đầu của section này (trên máy tôi là 0x961000) và cấu hình thiết lập cho breakpoint cần đặt như sau:

Kiểm tra và xóa tất cả các breakpoint khác (nếu có) và chỉ để lại breakpoint vừa đặt ở trên, sau đó nhấn F9 để chạy. Chương trình break và thông tin tại màn hình Disassembly khớp với địa chỉ chúng ta đã thấy đó là OEP. Xóa Breakpoint đã đặt đi để nó không con bôi đỏ toàn bộ section nữa:

Sau đó, cho IDA thực hiện Reanalyze Program. Kết quả có được như sau:

Quay lại cửa sổ Segments, thu thập thông tin liên quan (địa chỉ bắt đầu & kết thúc của file) và chỉnh sửa lại Python script để phục vụ cho việc dump file

Trên máy tôi, giá trị Imagebase của file lúc này là 0x960000 và địa chỉ kết thúc của file là 0x96b000. Tôi chỉnh lại Python script như sau:

Thay đổi xong, quay lại IDA và cho thực thi script này thông qua File > Script file:

Script thực hiện sẽ dump ra cho chúng ta file có tên là dumped.bin (file này nằm trên máy Main do ta chạy script tại đó). Load file này vào PEditor và thực hiện Dump Fixer như đã làm ở các phần trước.

Fix xong, đổi lại tên của file thành dumped.exe:

Chép file dumped.exe đã fix ở bước trên vào máy Target. Tiếp theo sẽ là rebuild IAT. Tại máy Target ta mở Scylla và lựa chọn process của packed file.

Sửa lại OEP thành 0x96146e vì đó là địa chỉ mà chúng ta đã tìm thấy. Sau đó nhấn IAT Autosearch & Get Imports:

Sau khi thực hiện xong, ta thấy rằng có hai Invalid API. Lấy địa chỉ Invalid này cộng với thông tin ImageBase của file:

Python>hex(0x960000+0x20d4)
0x9620d4

Tại cửa sổ Hex View ta tới địa chỉ này như trên hình, thông tin có được trông có vẻ nó không phải là một phần của IAT. Để xác minh ta chuyển qua cửa sổ IDA Disassembly và tìm tại địa chỉ này có xrefs nào tới không. Kết quả có được như sau:

Hãy xem nó đi đâu:

Chúng tôi thấy rằng nó kết thúc bằng một câu lệnh RET. Nhự vậy, ta thấy đó không phải là một hàm API, vì vậy ta có thể nhấn chuột phải và chọn Cut Thunk để loại bỏ giá trị Invalid này ra khỏi IAT. Tương tự, ta kiểm tra với địa chỉ:

Python>hex(0x960000+0x20dc)
0x9620dc

Địa chỉ này sẽ gọi tới hàm API:

Kết quả này trùng với thông tin hàm mà Scylla đã tìm ra được:

Do đó, ta có thể Cut Thunk này và chuẩn bị bước Fix Dump:

Mọi thứ trông có vẻ OK rồi, giờ ta tiến hành bước Fix Dump cho file dumped.exe. Scylla sẽ lưu thành một file mới có tên là dumped_SCY.exe. Tuy nhiên, ta chạy thử thì file sau khi đã rebuild IAT này sẽ không chạy được:

Điều này xảy ra là bởi vì trong dumped file, các địa chỉ được tạo ra trong lúc runtime và không thuộc về IAT, không được phân bổ lại do chúng luôn luôn thay đổi mỗi khi bắt đầu. Do vậy, để file có thể run được, ta phải loại bỏ cơ chế bảo vệ địa chỉ ngẫu nhiên này.

Load dumped_SCY.exe vào một IDA mới cùng với tùy chọn Manual Load, sau đó ta đi tới HEADER của file:

Tại đây bạn sẽ nhìn thấy một trường có tên là Dll Characteristics. Trên máy bạn, trường này sẽ có một giá trị khác 0:

Để sửa nó thành giá trị 0, chọn Edit > Patch Program > Change Word và sửa thành 0 như hình dưới đây:

Nhấn OK để chấp nhận chỉnh sửa:

Và sau đó chọn Apply patches to input file để save lại.

Hoặc có một số cách khác ít thô bạo hơn cách trên:

  • Sử dụng các trình PE Editor: Các bạn có thể sử dụng DIE/CFF Explorer/ PE-bear để bỏ thuộc tính Dynamic Base:

Thử chạy lại file đã fix xem thế nào, kết quả là chạy được bình thường rồi 🙂 :

Như vậy là chúng ta đã thực hiện unpack thành công, trong phần tiếp theo tôi và các bạn sẽ reverse file đã unpack này để hiểu cách thức hoạt động của nó cũng như viết keygen. Hẹn gặp lại các bạn trong phần 18!!

Image result for it's over the ring 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


Trước khi tiếp tục tìm hiểu thêm về các chủ đề khác, chúng ta sẽ thực hành thêm một vài bài tập unpack với các packer khác. Trong phần này target sẽ là UnPackMe_ASPack 2.2 (https://mega.nz/#!7XJ33I6D!DvNo6dNeCeyTDoXpSM9zeZWIi1kpALs26oCNd2tCUbY)

Load file bị packed vào IDA:

Ta dừng lại tại Entry Point của file, tại đó bắt đầu bằng một lệnh PUSHAD. Lệnh PUSHAD này thực hiện lưu toàn bộ giá trị hiện thời của các thanh ghi vào Stack theo thứ tự như sau:

Trực quan hơn các bạn có thể xem ví dụ minh họa dưới đây:

Ngược lại với PUSHAD, ta có lệnh POPAD, là lệnh lấy các giá trị từ stack và lưu lại vào các thanh ghi theo thứ tự như mô tả bên dưới (giá trị của thanh ghi ESP trên Stack sẽ được bỏ qua. Thay vào đó nó sẽ được tăng lên sau khi mỗi thanh được pop ra).

Đối với các trình packer đơn giản, hầu hết chúng đều bắt đầu với lệnh PUSHAD để lưu trạng thái ban đầu của các thanh ghi khi bắt đầu và sử dụng POPAD để khôi phục lại các giá trị đã lưu, trước khi nhảy tới OEP để thực hiện code của chương trình đã “rã code” hoàn toàn trong bộ nhớ.

Nhờ vào dấu hiệu này, chúng ta có thể dễ dàng tìm thấy OEP bằng cách sử dụng phương pháp PUSHAD-POPAD. Tuy nhiên, với các trình packer tiên tiến hơn về sau, các nhà phát triển đã nhận ra được điểm yếu này và tránh sử dụng những câu lệnh trên.

Vậy phương pháp này là như thế nào? Chúng ta hãy cùng xem xét với file đã load.

Trước tiên, chúng ta cần lựa chọn trình debugger và chạy nó. Cách nhanh nhất là tại Debugger > Select Debugger, chọn Local Win32 Debugger (hoặc bạn nào dùng IDA 7+ thì là Local Windows Debugger). Nhưng bây giờ, để thực hành chúng ta sẽ thực hiện thông qua Python. Ta có thể gõ từng câu lệnh một tại thanh Python của IDA hoặc sử dụng plugin mà chúng ta đã cài đặt là IpyIDA sẽ tiện lợi hơn. Tôi sẽ sử dụng plugin này để minh họa:

Trước tiên, tôi import idc, sau đó khi gõ idc.Load và nhấn TAB, plugin sẽ cung cấp cho tôi các hàm có liên quan. Ở đây tôi chọn idc.LoadDebugger. Trong trường hợp của chúng ta, ta phải chọn win320 cho local debugger (1 là dành cho remote debugger). Sau khi gõ lệnh, ta có kết quả trả về là True như sau:

Như trên hình, ta thấy rằng nó đã được chọn, nếu lặp lại lệnh này một lần nữa ta sẽ nhận được FALSE vì đã hoạt động rồi.

Phương pháp PUSHAD dựa trên việc thực hiện lệnh PUSHAD và trong lệnh tiếp theo, ta tìm các thanh ghi đã được lưu vào Stack và sau đó đặt một breakpoint để dừng trình gỡ lỗi khi nó cố gắng phục hồi giá trị các thanh ghi bằng lệnh POPAD, ngay trước khi nhảy tới OEP sau khi giải nén xong mã gốc.

Vì vậy, nhấn F2 để đặt một breakpoint tại lệnh bên dưới lệnh PUSHAD, ta sẽ dừng lại tại lệnh này sau khi cho thực thi chương trình. (Ở đây lệnh PUSHA tương tự như PUSHAD).

Nếu bạn muốn thực hiện thao tác trên bằng Python thì có thể gõ lệnh sau:

Bằng lệnh trên, ta đã đặt một breakpoint từ Python. Tham số đầu tiên là địa chỉ ta muốn đặt bp, tham số thứ hai là kích thước của breakpoint và tham số thứ ba là kiểu bp. Trong trường hợp này, ta muốn dừng thực thi của chương trình thông qua software bp, do đó truyền vào là BPT_SOFT hoặc 0.

Chúng ta đã lựa chọn được Debugger ở bước trước, và cũng đã đặt breakpoint. Bây giờ, ta khởi động trình debugger để buộc nó dừng lại tại breakpoint đã đặt. Rất đơn giản bằng cách nhấn F9 hoặc từ Python, ta gõ lệnh sau:

Với lệnh này, trình debugger mà ta lựa chọn sẽ khởi chạy, và nếu tất cả mọi thứ đều ngon lành, ví dụ trong trường hợp này, nó sẽ dừng lại tại Breakpoint mà chúng ta đã đặt tại địa chỉ 0x46B002:

Bây giờ, quan sát giá trị các thanh ghi đã được lưu vào cửa sổ Stack, ta sẽ đặt một bp ở dòng đầu tiên để dừng lại ở đó, vì đó là nơi các giá trị của các thanh ghi được lưu bởi PUSHAD sẽ được khôi phục lại bằng POPAD.

Như vậy, chúng ta phải đặt BP tại 0x19FF64 (trong trường hợp trên máy của tôi). Chuyển con trỏ để lựa chọn cửa sổ Disassembly và sau đó nhấn vào mũi tên nhỏ bên cạnh thanh ghi ESP, ta sẽ tới đây:

Bằng cách nhấn mũi tên bên cạnh thanh ghi như thế IDA sẽ đi tới địa chỉ này tại màn hình Disassembly. Từ đó, chúng ta có thể đặt Breakpoint bằng cách nhấn F2, nhưng chúng ta sẽ phải cấu hình lại bp là vì trong trường hợp này cần phải sử dụng On Read and Write chứ không phải On execution, bởi ta muốn dừng lại khi nó phục hồi hoặc đọc giá trị chứ không phải là thực thi mã tại đó.

Khi nhấn F2, cửa sổ Breakpoint settings sẽ hiện ra cho phép ta cấu hình như sau:

Để kiểm tra breakpoint đã đặt có chính xác không, ta xem tại Debugger > Breakpoints > Breakpoint List:

Tại cửa sổ Breakponts ở trên, ta có thể nhấn chuột phải và chọn Edit để thay đổi cấu hình breakpoint mà chúng ta muốn.

Vậy ta có thể đặt bp tương tự như đã làm thông qua Python được không? Hoàn toàn được nhé:

Với câu lệnh trên thì tham số đầu tiên là địa chỉ cần đặt bp, tham số thứ hai là kích thước của bp và tham số 3 kiểu breakpoint cần đặt. Trong trường hợp này Read/Write Access như mô tả trong bảng trên. Do đó, nếu tôi gõ lệnh, một breakpoint tương tự sẽ được thiết lập như khi ta thực hiện bằng tay.

Bây giờ vô hiệu hóa các BP đã đặt trước đó trong danh sách các Breakpoints bằng cách nhấp chuột phải và chọn Disable hoặc từ Python gõ lệnh:

Với tham số thứ hai bằng 1, tức là ta kích hoạt nó, còn bằng 0 thì tức là ta tắt nó. Kết quả tại màn hình danh sách các bp như sau (màu xanh tức là bp đã bị disable):

Tiếp theo nhấn F9 để tiếp tục hoặc gõ lệnh sau:

Nhấn OK ta sẽ dừng lại tại đây:

Ta thấy chương trình dừng lại ngay sau lệnh POPAD khi nó khôi phục các thanh ghi và cũng thấy rằng từ Stub này nó sẽ nhảy tới địa chỉ OEP tại 0x4271b0 thông qua cặp lệnh PUSH & RET (tương tự như lệnh JMP). Do vậy, ta trace code để thực hiện các lệnh này cho đến khi tới được OEP như hình dưới đây:

Tại OEP, cho IDA phân tích lại chương trình giống như đã làm trong bài viết trước. Kết quả có được như sau:

Lúc này thì toàn bộ code của chương trình đã được unpack hoàn toàn trên bộ nhớ, công việc tiếp theo là dump chương trình. Ta phải tìm địa chỉ ImageBase và địa chỉ cuối cùng trong section cuối cùng của file thực thi. Trong cửa sổ Segments, tôi thấy ImageBase0x400000 và địa chỉ kết thúc là 0x46e000:

Thay vì sử dụng idc script đã đề cập ở phần 15, phần này tôi sẽ sử dụng một script tương tự như vậy nhưng viết bằng Python. Nội dung của script đơn giản như sau:

Đoạn code python sử dụng để dump file như trên hình, tôi lưu nó với tên là ipython_dump.py. Sau đó, thực thi script này thông qua File-> Script File. Sau khi thực hiện xong ta sẽ thấy có tập tin dumped.bin được tạo ra. Tiếp theo ta sẽ sử dụng trình PE Editor để fix lại file dump này (tại cửa sổ Section Table Viewer, nhấn chuột phải và chọn Dump Fixer):

Kết quả file sẽ lấy lại được icon như hình dưới đây:

Phần dump file là coi như xong. Tiếp là công việc rebuild lại IAT. Mở Scylla 0.98 và chọn process của file mà hiện ta đang dừng lại tại OEP:

Thay thế bằng OEP là 0x4271B0, nhấn IAT Autosearch (nhấn Yes để chấp nhận sử dụng ) và tiếp theo là Get Imports. Nếu nhấn Show Invalid, ta sẽ thấy có một loạt các APIs lỗi. Thử xem liệu Scylla có thể khắc phục các hàm invalid này một cách tự động hay không?

Chúng ta thấy rằng Scylla không thể sửa được, vì vậy ta sẽ thực hiện bằng tay.

Như trên hình, chúng ta thấy hàm API đầu tiên tại 0x460818, đây là hàm hợp lệ, còn trên đó là các giá trị không hợp lệ. Bắt đầu với địa chỉ không hợp lệ đầu tiên tại 0x4600ec.

Bạn thấy rằng nội dung không trỏ đến bất kỳ địa chỉ hợp lệ nào và nếu bạn nhấn CTRL + X sẽ không tìm thấy tham chiếu nào:

Trong khi ở các hàm API chuẩn sẽ có các tham chiếu sử dụng tới hàm API đó. Ví dụ:

Do vậy, các địa chỉ này không phải thuộc IAT, ta sẽ xóa chúng.

Nếu như tôi nhấn clear và nhấn lại IAT Autosearch một lần nữa, nhưng tuy nhiên lúc này tôi không đồng ý chọn Advanced mode, ta thấy Scylla sẽ tìm được đầy đủ các thông tin về IAT. Chỉ có duy nhất một vị trí invalid cần phải xác minh lại:

Căn cứ vào vị trí invalid ta tìm được hàm API bị thiếu là:

Tiến hành fix lại hàm này trong Scylla:

Sau khi fix xong, bước cuối cùng lựa chọn Fix Dump để repair lại dump file:

Sau khi sửa xong, ta có thể thực thi file một cách bình thường:

Phần 16 này xin được kết thúc tại đây. Tôi gửi kèm một bài tập nhỏ để các bạn thực hành, nhiệm vụ của các bạn là unpack file PACKED_PRACTICA_1.exe (https://mega.nz/#!jGQHDQpA!tRHdeG_z96GwEqY3ZDh0j0XEE0Ja01lIYbEEhufIVT8). Hẹn gặp các bạn ở phần 17!

Image result for unpackme funny

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


Vài dòng lan man:

Mới đây Sẻ đệ (yeuchimse) đóng Blog, làm tôi cũng nghĩ hay thôi, dăm ba cái Blog, view cũng lèo tèo, viết lách vừa tốn thời gian chỉnh sửa, chụp choẹt…. chắc cũng đóng nốt cho nhanh...

Anh em trong Gờ-rúp “kín” thi thoảng lại: “Già rồi, viết lách cái gì. Định kiếm fame đến bao giờ!!“. Thôi thì …..

………..

…………†

Dành cho những bạn nào chưa nghe bản guitar solo này, hãy xem cách Steve Vai “nựng” đàn để tìm cho mình những cảm hứng riêng

Trong phần trước, tôi đã cho các bạn thấy một vài phương pháp để có thể phát hiện và tới được OEP trong một file bị packed. Phần này, chúng ta sẽ tiếp tục với hai bước còn thiếu mà tôi đã đề cập, đó là: dump filerebuild IAT.

Dump file

Các bạn có thể thực hành lại để tới OEP và Reanalyze chương trình, sau khi thực hiện xong ta có mọi thứ sẵn sàng cho việc Dump file. Hành động dump file có thể hiểu một cách “chân phương” là sau khi tới được OEP, toàn bộ code gốc của chương trình cùng các hàm APIs đã được “bung” đầy đủ trên memory, lúc này ta thực hiện thao tác dump để lưu thành một file.

Ở đây, tôi sẽ sử dụng một IDC Script để thực hiện công việc này (không phải là Python script). Nội dung của script như sau:

static main()
{
	auto fp, ea;
	fp = fopen("dumped.bin", "wb");
	for (ea = 0x400000; ea < 0x40b200; ea++)
		fputc(Byte(ea), fp);
}

Trong script trên, chúng ta thực hiện dump từ địa chỉ ImageBase0x400000 tới địa chỉ lớn nhất mà ta thấy trong tab IDA Segments. Đó chính là section cuối cùng của file thực thi, trong trường hợp này là section OVERLAY, tại địa chỉ 0x40b200.

Copy&Paste script trên vào Notepad và lưu lại với tên là dumper.idc. Quay lại IDA và cho thực thi script này thông qua File > Script file  (IDA hỗ trợ cả Python và IDC scripts). Kết quả tôi có được tập tin dumped.bin như hình dưới:

Tôi tạo một bản sao của file đã dump ra và đổi tên nó thành một tập tin có đuôi mở rộng .exe:

Các bạn để ý thì thấy rằng, file sau khi đổi tên không hề có icon mặt cười như ở file gốc. Để fix được thiếu sót này các bạn có thể sử dụng chương trình PE Editor v1.7 (download tại đây: https://mega.nz/#!uGpHkKTD!bkxqBmj2Ib0FC6QDln7Cf1DnXSqRazinTuXNbAdFCjE). Chạy chương trình, load file exe và sau đó chọn sections:

Tại màn hình chứa các thông tin về Sections:

Nhấp chuột phải vào một section bất kì và chọn Dumpfixer, tương tự như hình dưới đây:

Bước dumpfixer này thực chất là thiết lập cho Raw_size = Virtual_sizeRaw_offset = Virtual_offset. Kết quả có được như sau:

OK, như vậy là đã lấy lại được icon của file. Tạm thời ở thời điểm này, file đã dump ra được tốt rồi, ít nhất là icon đã hiển thị đúng, nhưng tuy nhiên file này vẫn chưa thể thực thi được bình thường bởi vì cần phải sửa cả IAT nữa.

Rebuild IAT

IAT là gì?

IAT viết đầy đủ là Import Address Table, là một bảng nằm trong file thực thi và được sử dụng khi chương trình hoạt động. Bảng này lưu tất cả địa chỉ của các hàm được import, từ đó chương trình có thể sử dụng để chạy trên bất kỳ máy nào (chính xác là trên bất kỳ môi trường OS nào).

Nếu IAT là đầy đủ và chính xác thì khi ta đưa tập tin thực thi cho một người khác, IAT sẽ được điền đầy đủ các giá trị tương ứng (địa chỉ các hàm APIs) trên máy đó, bất kể hệ điều hành hoạt động trên máy đó là gì, tính tương thích sẽ được duy trì và chương trình sẽ thực thi được một cách bình thường.

Nghĩa là, IAT sẽ luôn nằm ở một vị trí nhất định trong mỗi tệp thực thi, và sẽ có các vị trí cố định cho mỗi hàm để điền vào. Các bạn nhớ lại phần trước tôi có so sánh (chưa giải thích kĩ) sự khác biệt giữa hình ảnh từ tệp tin bị packed khi ta tới được OEP (trước khi thực hiện việc dump) với file gốc ban đầu:

Cả hai đều hiển thị địa chỉ 0x403238 và dường như chúng có cùng nội dung. Bây giờ chúng ta mở lại file gốc:

Quan sát ở bên dưới thấy có địa chỉ file offset (trên ổ đĩa) là 0x1038. Tôi dùng trình Hex Editor là HxD để load file gốc và tìm tới địa chỉ offset này:

Tại HxD, ta thấy nội dung tại offset 0x10380x355e. Nếu tôi cộng giá trị 0x355e vào địa chỉ ImageBase0x400000, tôi có kết quả là 0x40355e. Vậy có thông tin gì lưu tại địa chỉ mà ta vừa tính toán được? Để làm rõ hơn, tôi cho IDA load lại file gốc cùng với tùy chọn Manual Load, mục đích để IDA nạp tất cả các sections của tập tin thực thi:

Sau khi chấp nhận cho tải tất cả các sections, đợi khi IDA phân tích toàn bộ file xong, ta đi đến địa chỉ 0x40355e. Ở bên phải lúc này ta thấy được tên của hàm API là:GetModuleHandleA

Tương tự đối với các ví trí khác mà tôi highlight làm ví dụ như hình dưới:

Như vậy, từng giá trị trên sẽ được cộng với địa chỉ ImageBase để từ đó tìm ra tên của các hàm tương ứng. Và cũng từ các tên hàm đó sẽ lấy được các địa chỉ tương ứng của từng hàm trong máy lúc runtime. Ví dụ, như trong trường hợp này nó sẽ tìm địa chỉ của hàm GetModuleHandleA() trên máy của chúng ta và sửa lại giá trị 5e 35 bằng địa chỉ thật của hàm API (ví dụ: 757116D0  kernel32.GetModuleHandleA)

Đó chính là lý do tại sao một tệp tin thực thi có thể chạy trên bất kỳ máy nào, bởi vì nó sẽ luôn luôn tìm tên của API tương ứng trong mỗi mục của IAT và tìm ra địa chỉ hợp lệ của hàm tương ứng với từng máy khác nhau khi nó thực thi. Và đó cũng là lý do tại sao trên bất kỳ máy nào nếu tôi thực hiện một lời gọi hàm như sau:

CALL [0x403238]

Chương trình sẽ luôn luôn hoạt động bởi vì 0x403238 là mục IAT của hàm GetModuleHandleA(), nội dung thay đổi chính là địa chỉ hàm mà hệ điều hành sẽ lưu lại bằng cách chỉnh sửa lại giá trị ban đầu 5e 35 (trỏ đến chuỗi tên của hàm sau khi cộng thêm ImageBase).

Giữ nguyên hai màn hình IDA đang mở (một cho file bị packed và đang dừng lại ở OEP; còn 1 cho file gốc), ta mở thêm một IDA thứ ba và load file đã dumped mà tôi vừa đổi tên là:dumped.exe.

Chuyển tới địa chỉ của IAT là 0x403238:

Quan sát bên dưới, ta thấy địa chỉ file offset lúc này là 0x3238, địa chỉ này không khớp với file gốc bởi sau khi thực hiện dumpfixer đã thay đổi kích thước trên đĩa (Raw Size) bằng với kích thước ảo (Virtual size), dẫn tới địa chỉ offset cũng thay đổi, cho nên địa chỉ của hàm API GetModuleHandleA() cũng vì thế mà thay đổi theo.

Sử dụng HxD mở file dump để kiểm tra, tìm tới offset 0x3238. Kết quả ta thấy như sau:

Chúng ta thấy rằng giá trị tại đó là địa chỉ của một hàm API, chứ không phải là một offset để cộng với ImageBase nhằm tìm tên của hàm API nữa. Do vậy, ta có thể hiểu rằng khi chương trình chạy, hệ thống sẽ lấy được địa chỉ chính xác của hàm API và lưu địa chỉ này ở đó, vậy nên khi thực hiện dump chương trình thì địa chỉ của hàm GetModuleHandleA() cũng sẽ được lưu kèm theo file đã dump. Để kiểm tra ta có thể tới địa chỉ của hàm này trong IDA như hình dưới:

Vậy vấn đề ở đây là gì?

Khi ta thực thi file dump, hệ thống sẽ tìm kiếm IAT và lấy ra giá trị này, sau đó cộng với địa chỉ ImageBase để từ đó tìm ra tên của hàm, cuối cùng sẽ tìm địa chỉ của hàm … Nhưng vì khi ta thực hiện dump file đã vô tình đã phá vỡ nguyên tắc này, do giá trị được lưu cuối cùng chính là địa chỉ thật của hàm API, và bởi vậy chương trình sẽ bị crash khi khởi chạy vì nó không thể điền vào IAT chính xác.

Để giải quyết được vấn đề này ta cần phải tìm một cách để sửa lại IAT và khôi phục lại tất cả các offset – trỏ vào các chuỗi chứa tên của các hàm API. Công việc này nếu làm bằng tay sẽ rất oải, do số lượng hàm API rất nhiều, vậy nên chúng ta sẽ sử dụng một công cụ hỗ trợ có tên là Scylla:

https://forum.tuts4you.com/files/file/576-scylla-imports-reconstruction/

Chạy ứng dụng này. Tại chỗ Attach to an active process, tôi chọn process của file bị packed:

Nhớ rằng bạn vẫn phải giữ nguyên màn hình của IDA đang dừng lại tại OEP.