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.

Tại Scylla, thay đổi ​OEP​ thành giá trị chính xác là 0x​401000:

Tiếp theo nhấn​ ​IAT​ ​Autosearch, một bảng thông báo xuất hiện:

Điều này có nghĩa là, IAT bắt đầu tại địa chỉ 0x403184 và có kích thước là 0x108. Nhấn OK để chấp nhận, sau đó nhấn Get Imports, ta có kết quả như sau:

Các bạn thấy rằng Scylla đã lấy được tất cả thông tin trừ 1 giá trị khi ta nhấn nhấn Show Invalid. Mặt khác, chúng ta thấy rằng offset 0x3238 trong packed file tương ứng với GetModuleHandleA(), như vậy là Scylla đã làm việc rất tốt:

Tại packed file, đi tới địa chỉ API mà Scylla thông báo là không hợp lệ tại 0x403208 (kết quả có thể khác ở trên máy của các bạn), để xem tại đó có thông tin gì:

IDA cung cấp thông tin là unk_6E33AC50, nghĩa là nó chưa nhận biết được code tại đây. Đi tới địa chỉ này:

Ta tới thư viện apphelp.dll, nhấn C để chuyển đổi các bytes tại đây thành code, tạo hàm và cho Reanalyze lại chương trình:

Kết quả ta có được code thuộc thư viện apphelp.dll sẽ gọi tới hàm API GetDC(). Nhấp đúp chuột tại offset szGetdcWndPDcP, ta tới đây:

Như vậy, hàm API GetDC() thuộc thư viện user32.dll. Do đó, quay trở lại màn hình của Scylla, nhấp đúp chuột invalid API này và chọn như hình bên dưới để sửa lại:

Nhấn OK để sửa và như vậy ta sẽ có được các hàm API hợp lệ. Nếu ta bấm Show Suspect, Scylla sẽ cung cấp các APIs mà nó còn nghi ngờ, để xác minh ta lại quay lại IDA và đi đến các địa chỉ 0x4031BC, 0x40322C0x403278 (kết quả này có thể khác trên máy của các bạn):

Thông tin mà IDA cung cấp như sau:

Kết quả thu được là chính xác hoàn toàn rồi, cuối cùng là nhấn nút Fix Dump:

Scylla sẽ lưu file đã fix với tên mới là dumped_SCY.exe. Nhấp đúp vào để chạy thử ta thấy crackme chạy ok như file gốc:

Chúng ta thấy rằng khi mở file đã fix ở trên trong IDA, ta thấy nó bắt đầu từ OEP 0x401000 và tên của APIs đã được fix hoàn toàn và trông không khác gì với bản gốc:

Như vậy qua bài viết này, tôi đã hướng dẫn các bạn thực hiện xong toàn bộ quá trình unpack một file được pack bằng một packer dễ. Phần tiếp theo ta sẽ xem xét một vài ví dụ khó hơn.

Phần này xin được kết thúc tại đây. Hẹn … à mà thôi! Khi nào có hứng tôi đưa lên tiếp…..

Image result for brain dump 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:

Tôi đoán có nhiều bạn trông chờ và đặt nhiều kì vọng vào bộ tuts này như kiểu một tài liệu sẽ làm thay đổi cuộc đời :P. Nó sẽ biến bạn trở thành một “chuyên gia hàng đầu”, được nhiều người săn đón 😦

Đứng kì vọng để rồi thất vọng!!!

Không hiểu sao tôi rất dị ứng với từ chuyên gia, mỗi lần bị giới thiệu như vậy trong người tôi nó thất bứt rứt lắm…..

Nhiều bạn cũng có thắc mắc sao lâu tôi chưa ra bài mới. Điều này rất khó nói 😀:
– Nó phụ thuộc vào khung thời gian trống còn lại trong ngày.
– Hơn nữa cũng phụ thuộc nhiều vào người đọc. Đọc giả có tâm, đọc, góp ý/thắc mắc những chỗ tôi viết chưa ổn và hơn hết họ đọc tới đọc đến tận chỗ “bỉm sữa” :). Đọc giả có tâm hơn nữa, họ không cần đọc mà kéo roẹt xuống chỗ có dòng “bỉm sữa” :D. Còn phần đa thì chắc là đọc đến chỗ “ấy” thì dừng lại….

Tuyển tập các bài viết này sẽ thường xuyên thay đổi nội dung và sẽ bao gồm các chủ đề khác nhau liên quan đến reversing như: static reversing, debugging, unpacking và exploiting.

Trong phần này, chúng ta sẽ unpack file PACKED_CRACKME.exe (tải tại đây), được packed bằng phiên bản UPX mới nhất (tính đến thời điểm viết bài). Tuy nhiên, điều này không có nghĩa là tôi sẽ viết nhiều bài khác chỉ nói về chủ đề unpacking, chúng ta sẽ thay đổi và trộn lẫn các chủ đề khác nhau để không bị nhàm chán. Do đó, bên cạnh việc tìm hiểu Unpacking bằng IDA thì đồng thời ta sẽ vẫn nghiên cứu thêm các chủ đề khác.

File bị Packed?

Định nghĩa đơn giản về tập tin packed đó là một tập tin được ẩn mã thực thi gốc của chương trình và lưu lại bằng cách áp dụng các kỹ thuật nén hoặc mã hóa để tránh không bị reverse một cách dễ dàng. Bên cạnh đó nó cũng được chèn thêm một đoạn Stub hay một section, để khi thực thi chương trình, đoạn stub này sẽ nhận code đã được mã hóa, giải mã nó trong bộ nhớ, lưu code giải mã vào trong bất kỳ section nào hoặc là chính nó và cuối cùng nhảy tới vùng code này để thực thi (đó chính là code gốc ban đầu của file).

Có rất nhiều biến thể của các trình packers, và đa phần trong số chúng là các trình protectors, sử dụng các kĩ thuật như hủy bảng IAT hay import table, hủy thông tin về Header của file. Bổ sung thêm các cơ chế anti-debugger để tránh việc unpack và khôi phục lại tập tin ban đầu.

Image result for packers landscape

Thực hành

Ví dụ đơn giản nhất mà các chuyên gia hay sử dụng để minh họa về packer đó chính là UPX. Trình packer này không áp dụng các kĩ thuật anti-debug, hay các tiểu xảo nào khác được đề cập ở trên, tuy nhiên nó lại giúp ta khởi đầu với những kĩ năng đơn giản nhất trong quá trình thực hiện unpacking. Để tiện cho các bạn trong việc thực hành, file PACKED_CRACKME.EXE đã được đính kèm link tải về ở trên.

Load file vào IDA và chọn Manual load vì tùy chọn này sẽ giúp chúng ta có thể nạp tất các sections của file, đồng thời bỏ lựa chọn Create imports segment (Thầy Ricardo khuyến nghị nên bỏ chọn khi làm việc với các packed files). Kết quả sau khi load vào IDA như hình dưới:

Ta đang dừng tại địa chỉ bắt đầu hay còn gọi là Entry Point (EP) của PACKED_CRACKME.exe. Với file bị packed thì địa chỉ EP là 0x409be0, trong khi ở file gốc thì ta sẽ dừng tại địa chỉ EP là 0x401000, như hình minh họa bên dưới:

Ngoài ra, so sánh các sections hay segments của cả hai file, ta thấy rằng dưới header của file sau khi bị packed có thêm một section mới với tên là UPX0, có kích thước trong bộ nhớ lớn hơn so với file gốc ban đầu.

File gốc

File bị PACKED

Theo quan sát trên hình, section UPX0 của file bị packed kết thúc tại địa chỉ 0x409000, trong khi toàn bộ các sections ở file gốc đều nằm trong vùng nhớ bắt đầu từ 0x401000 đến 0x408200. Ở đây, ta đang đề cập đến bộ nhớ ảo, đó là khi một chương trình thực thi, nó có thể chiếm dung lượng 1k trên ổ đĩa (HDD) nhưng sẽ chiếm 20k hoặc hơn thế nữa trong bộ nhớ.

Điều này có thể thấy được khi phân tích trong IDA, ví dụ, tại địa chỉ bắt đầu 0x401000 của file gốc ta thấy các thông tin như sau:

Trong hình trên (Section size in file) chiếm 0x600 byte, trong khi trong bộ nhớ (Virtual size) chiếm 0x1000. Trở về file bị packed, nếu chúng ta chuyển tới địa chỉ 0x401000 – đó là nơi bắt đầu của section UPX0.

Chúng ta thấy rằng Section size in file chiếm 0x0 byte trên đĩa, nhưng trong bộ nhớ nó chiếm 0x8000. Điều này có nghĩa là, nó dành ra một không gian trống phục vụ cho việc khôi phục lại mã chương trình gốc ban đầu tại đây và sau đó nhảy tới đây để thực hiện lệnh. Như vậy, nó dành đủ không gian để thực hiện việc khôi phục code này.

Ta cũng thấy rằng địa chỉ 0x401000 có kèm theo tiền tố dword_ ở phía trước nghĩa là nội dung của nó là một DWORD. Dấu (?) hàm ý nó cần được dành riêng và không chứa bất kỳ giá trị nào, từ khóa dup có nghĩa là dword đó được nhân với 0xc00, như vậy kết quả là sẽ dành ra 0x3000 bytes:

Tiếp theo tại 0x404000, ta thấy có 1400h dup(?). Có nghĩa là cần dành ra vùng nhớ:

Do đó, tổng số cần có 0x8000 bytes sẽ được dành riêng trong bộ nhớ để đặt chương trình vào đó. Tại dword_401000 ta thấy có một tham chiếu trong mã thực thi, ta sẽ xem xét câu lệnh này làm gì sau.

Tiếp theo bên dưới, ta thấy file packed có thêm một section thứ hai là UPX1, section này có kích thước trên đĩa là 0xe00 và trong bộ nhớ 0x1000. Đây có khả năng là nơi chương trình lưu một số kĩ thuật mã hóa đơn giản nhằm để che dấu mã gốc.

Nếu kiểm tra thông tin tham chiếu tại địa chỉ bắt đầu của section là 0x409000, ta có kết quả:

Chúng ta thấy có một tham chiếu bên dưới (Down) trong code thực thi của chương trình. Chuyển tới vùng code đó:

Tại Stub trên, sau địa chỉ Entry Point, chương trình nạp địa chỉ 0x409000 vào thanh ghi ESI (Ta biết lấy địa chỉ là bởi có tiền tố offset ở phía trước). Chuyển qua chế độ Text mode bằng cách nhấn phím space bar, tại đó chúng ta thấy như sau:

Đoạn stub này nằm cùng section UPX1, bên dưới code của chương trình gốc đã packed. Nghĩa là tại section UPX1, trình packer đã thực hiện lưu các bytes đã encrypted của chương trình gốc tại đây và stub code bắt đầu từ 0x409be0.

Ta có thể dễ dàng nhận ra vùng Stub sẽ đọc các bytes từ 0x409000, sau đó áp dụng một số thao tác tính toán và lưu lại kết quả sau tính toán vào 0x401000. Ta thấy thanh ghi EDI = ESI-0x8000:

Nói cách khác, chương trình sẽ sử dụng vùng nhớ trỏ bởi ESI (như là source), từ đó đọc dữ liệu ra và áp dụng các tính toán nhất định, sau đó lưu vào vùng nhớ trỏ bởi EDI (như là dest) để khôi phục lại code ban đầu của chương trình.

Chúng ta đã biết tại 0x401000 có một tham chiếu trong mã thực thi, nếu chúng ta nhấp đúp vào tham chiếu đó:

Ta tới vùng code chứa một lệnh nhảy (jmp) tới 0x401000:

Jmp near là một lệnh nhảy trực tiếp đến địa chỉ, do vậy nó sẽ nhảy thẳng đến 0x401000. Rõ ràng, ở đây sau khi thực thi toàn bộ mã lệnh tại Stub và tái tạo lại mã gốc ban đầu của chương trình, chương trình sẽ nhảy tới OEP tại địa chỉ 0x401000 (Entry Point gốc), không giống như Entry Point của Stub0x00409BE0.

Ta sẽ gọi tắt là OEP hay Original Entry Point để hàm ý rằng đó chính Entry Point (EP) của chương trình gốc ban đầu. Điều này là hiển nhiên vì khi một chương trình bị packed, ta hoàn toàn không biết địa chỉ này ở đâu và chỉ khi ta có file gốc thì ta mới có biết được EP ban đầu là 0x401000 như hình minh họa dưới đây:

Tóm lại, khi chúng ta làm việc với một chương trình bị packed, ta sẽ không biết OEP của nó ở đâu bởi ta không có file gốc ban đầu, do đó chúng ta sẽ phải áp dụng các kĩ thuật để tìm ra OEP. Quay lại với phân tích ở trên, khi Stub hoàn thành tất cả các thủ thuật của nó và khôi phục lại mã gốc, nó sẽ nhảy tới OEP để từ đó thực thi chương trình.

Chính vì thế, ta có thể đặt một Breakpoint tại lệnh Jmp tới OEP để xem code của chương trình gốc có được khôi phục như ta đã suy đoán như trên không. Thử đặt một BP như hình dưới:

Sau đó chọn debugger là Local Win32 Debugger và nhấn Start debugger. Ngay lập tức, ta sẽ dừng tại BP vừa đặt ở trên:

Nhấn F8 để trace qua lệnh này:

IDA sẽ hiển thị một thông báo như trên, cứ nhấn Yes để thông báo cho IDA biết và nhận diện lại section UPX0 ban đầu như là CODE (ban đầu nó được định nghĩa là DATA).

Ta thấy rằng stub đã giải nén code và nhảy tới 401000 để thực thi. So sánh thì thấy code này rất giống với code tại địa chỉ 0x401000 ở file gốc, nhưng tuy nhiên ta lại không thể chuyển sang chế độ đồ họa bởi vì lúc này nó không được định nghĩa như là một function (chỉ là loc_401000).

Để chuyển được sang chế độ đồ họa, có một tùy chọn ẩn ở góc dưới bên trái của màn hình IDA, bằng cách nhấp chuột phải tại đó và chọn Reanalyze program:

Bằng cách này, địa chỉ loc_401000 đã thay đổi thành sub_401000 cho biết bây giờ nó đã được hiểu như là một hàm. Vì vậy, ta có thể chuyển sang chế độ đồ hoạ bằng cách nhấn phím tắt space bar:

Ta thấy, code giờ đây đã đẹp hơn 🙂

Tuy nhiên, quan sát kĩ một chút ta sẽ thấy có sự khác biệt, ở file gốc tại địa chỉ 0x401002 sẽ hiển thị lời gọi tới hàm API CALL GetModuleHandleA, trong khi tại file ta đang phân tích chỉ hiển thị lệnh CALL sub_401056. Đi vào lệnh Call này để xem code của nó là gì:

Ta lại thấy sự khác biệt với bản gốc. Nếu tại file gốc khi ta truy cập vào CALL GetModuleHandleA:

Ta thấy có một lệnh nhảy gián tiếp tới hàm API. Vậy còn tại file đang đang phân tích thì sao? Hàm API đã đi đâu? Thử follow theo lệnh nhảy tại file đang phân tích, ta có được thông tin như sau:

Như trên hình, nội dung tại 0x403028 là một offset (off_), đó là địa chỉ của API GetModuleHandleA và tại file gốc, cũng quan sát địa chỉ này tại section .idata thì ta thấy cũng chứa địa chỉ của cùng một hàm API.

Mặc dù, ta thấy chúng đều nhảy tới cùng một địa chỉ, tuy nhiên có một sự khác biệt rất quan trọng mà chúng ta sẽ tìm hiểu sau. Như vậy, có thể nói tới thời điểm này code gốc của chương trình đã được unpack hoàn toàn.

Có một cách khác nữa để tìm OEP, đó là tìm lệnh được thực thi đầu tiên ở section đầu tiên. Tuy nhiên, phương pháp này không phải lúc nào cũng có hiệu quả :). Để làm theo cách này, ta khởi động lại target trong IDA, cấu hình debugger để dừng lại tại Entrypoint khi thực thi. Sau đó, cho thực thi file packed, ta sẽ dừng lại ở entry point:

Tiếp theo, ta chuyển tới section đầu tiên bắt đầu tại địa chỉ 0x401000.

Tại đó, tôi nhấn F2 để đặt một breakpoint và cấu hình để dừng khi thực thi (Execute) – có nghĩa là chương trình sẽ break chỉ khi thực thi lệnh mà không dừng khi nó đọc hoặc ghi dữ liệu, đó là điều tôi kì vọng. Và vì tôi không biết chính xác lệnh nào sẽ được thực thi đầu tiên nên Breakpoint on execute mà tôi đặt sẽ bao gồm toàn bộ section (0x8000 bytes). Cụ thể như hình minh họa dưới đây:

Sau khi thiết lập breakpoint như trên xong, nhấn OK, lúc này toàn bộ section sẽ được đánh dấu bằng màu đỏ như hình:

Sau đó, nếu có breakpoint nào mà các bạn đã đặt trước đó thì hay tiến hành vô hiệu hóa bằng cách truy cập Debugger->Breakpoint->Breakpoint List.

Nhấn chuột phải tại breakpoint và chọn Disable breakpoint, tương tự như hình:

Sau đó cho thực thi chương trình. Khi chương trình break, ta thấy sẽ dừng lại tại lệnh đầu tiên ở section vừa được tạo, trong trường hợp này, 0x401000 chính là OEP mà tôi tìm được:

Sau khi đã đạt được mục đích, ta vô hiệu hóa breakpoint đã đặt. Sau đó tiến hành phân tích lại toàn bộ chương trình, kết quả có được tương tự như những gì đã làm ở trước:

OK, qua toàn bộ phân tích trên chúng ta đã có được OEP gốc của chương trình thông qua hai cách thực hiện khác nhau. Công việc tiếp theo mà ta cần làm là dump file và rebuild lại toàn bộ IAT để đảm bảo file sau khi unpacked thực thi được một cách bình thường như file gốc ban đầu. Tuy nhiên, công việc đó sẽ làm ở các phần tiếp theo, phần hôm nay là đủ rồi, các bạn có thể tạo một bản Snapshot để có thể quay lại vào phần tới.

Phần này xin được kết thúc tại đây. Hẹn gặp các bạn ở phần 15!

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 13 này, chúng ta sẽ thư giãn một chút trước khi tiếp tục với các bài thực hành khác để tìm hiểu sâu hơn về cách sử dụng IDA. Phần này tôi sẽ giới thiệu tới các bạn một plugin khá tiện lợi và thú vị, cho phép chúng ta có thể xử lý tốt hơn với Python.

Plugin có tên là IpyIDA, được phát triển bởi Marc-Etienne M.Léveillé (https://twitter.com/marc_etienne_), một chuyên gia hiện làm việc tại hãng ESET. Việc cài đặt plugin này khá dễ dàng, chỉ bằng cách sao chép và dán dòng lệnh sau vào thanh Python của IDA:

import urllib2; exec urllib2.urlopen('https://github.com/eset/ipyida/raw/stable/install_from_ida.py').read()

Trong trường hợp có vấn đề hoặc gặp lỗi, bạn có thể truy cập trang chủ của plugin tại đây: https://github.com/eset/ipyida

Câu lệnh trên sẽ tiến hành cài đặt một cách hoàn toàn tự động trong vòng vài phút. Sau khi cài đặt xong, ta có thể sử dụng nó thông qua Edit > Plugins > IpyIDA, cửa sổ Ipython Console xuất hiện tương tự như hình minh họa bên dưới:

Ta thấy rõ ràng nó có nhiều tính năng mạnh hơn thanh Python của IDA. Để tra cứu các tính năng của plugin này, ta nhấn phím ?:

IPython -- An enhanced Interactive Python

=========================================

IPython offers a combination of convenient shell features, special commands and a history mechanism for both input (command history) and output (results caching, similar to Mathematica). It is intended to be a fully compatible replacement for the standard Python interpreter, while offering vastly improved functionality and flexibility.

At your system command line, type 'ipython -h' to see the command line options available. This document only describes interactive features.

MAIN FEATURES

-------------

* Access to the standard Python help. As of Python 2.1, a help system is available with access to object docstrings and the Python manuals. Simply type 'help' (no quotes) to access it.

* Magic commands: type %magic for information on the magic subsystem.

* System command aliases, via the %alias command or the configuration file(s).

* Dynamic object information:

Typing ?word or word? prints detailed information about an object. If certain strings in the object are too long (docstrings, code, etc.) they get snipped in the center for brevity.

Typing ??word or word?? gives access to the full information without snipping long strings. Long strings are sent to the screen through the less pager if longer than the screen, printed otherwise.

The ?/?? system gives access to the full source code for any object (if available), shows function prototypes and other useful information.

If you just want to see an object's docstring, type '%pdoc object' (without quotes, and without % if you have auto magic on).

* Completion in the local namespace, by typing TAB at the prompt.

At any time, hitting tab will complete any available python commands or variable names, and show you a list of the possible completions if there's no unambiguous one. It will also complete filenames in the current directory.

This feature requires the readline and rlcomplete modules, so it won't work if your Python lacks readline support (such as under Windows).

* Search previous command history in two ways (also requires readline):

- Start typing, and then use Ctrl-p (previous,up) and Ctrl-n (next,down) to search through only the history items that match what you've typed so far. If you use Ctrl-p/Ctrl-n at a blank prompt, they just behave like normal arrow keys.

- Hit Ctrl-r: opens a search prompt. Begin typing and the system searches your history for lines that match what you've typed so far, completing as much as it can.

- %hist: search history by index (this does *not* require readline).

* Persistent command history across sessions.

* Logging of input with the ability to save and restore a working session.

* System escape with !. Typing !ls will run 'ls' in the current directory.

* The reload command does a 'deep' reload of a module: changes made to the module since you imported will actually be available without having to exit.

* Verbose and colored exception traceback printouts. See the magic xmode and xcolor functions for details (just type %magic).

* Input caching system:

IPython offers numbered prompts (In/Out) with input and output caching. All input is saved and can be retrieved as variables (besides the usual arrow key recall).

The following GLOBAL variables always exist (so don't overwrite them!):

_i: stores previous input.

_ii: next previous.

_iii: next-next previous.

_ih : a list of all input _ih[n] is the input from line n.

Additionally, global variables named _i are dynamically created ( being the prompt counter), such that _i == _ih[]

For example, what you typed at prompt 14 is available as _i14 and _ih[14].

You can create macros which contain multiple input lines from this history, for later re-execution, with the %macro function.

The history function %hist allows you to see any part of your input history by printing a range of the _i variables. Note that inputs which contain magic functions (%) appear in the history with a prepended comment. This is because they aren't really valid Python code, so you can't exec them.

* Output caching system:

For output that is returned from actions, a system similar to the input cache exists but using _ instead of _i. Only actions that produce a result (NOT assignments, for example) are cached. If you are familiar with Mathematica, IPython's _ variables behave exactly like Mathematica's % variables.

The following GLOBAL variables always exist (so don't overwrite them!):

_ (one underscore): previous output.

__ (two underscores): next previous.

___ (three underscores): next-next previous.

Global variables named _ are dynamically created ( being the prompt counter), such that the result of output is always available as _.

Finally, a global dictionary named _oh exists with entries for all lines which generated output.

* Directory history:

Your history of visited directories is kept in the global list _dh, and the magic %cd command can be used to go to any entry in that list.

* Auto-parentheses and auto-quotes (adapted from Nathan Gray's LazyPython)

1. Auto-parentheses Callable objects (i.e. functions, methods, etc) can be invoked like this (notice the commas between the arguments)::

In [1]: callable_ob arg1, arg2, arg3

and the input will be translated to this::callable_ob(arg1, arg2, arg3)

This feature is off by default (in rare cases it can produce undesirable side-effects), but you can activate it at the command-line by starting IPython with `--autocall 1`, set it permanently in your configuration file, or turn on at runtime with `%autocall 1`.

You can force auto-parentheses by using '/' as the first character of a line. For example::

In [1]: /globals # becomes 'globals()'

Note that the '/' MUST be the first character on the line! This won't work::

In [2]: print /globals # syntax error

In most cases the automatic algorithm should work, so you should rarely need to explicitly invoke /. One notable exception is if you are trying to call a function with a list of tuples as arguments (the parenthesis will confuse IPython)::

In [1]: zip (1,2,3),(4,5,6) # won't work

but this will work::

In [2]: /zip (1,2,3),(4,5,6)

------> zip ((1,2,3),(4,5,6))

Out[2]= [(1, 4), (2, 5), (3, 6)]

IPython tells you that it has altered your command line by displaying the new command line preceded by -->. e.g.::

In [18]: callable list

-------> callable (list)

2. Auto-Quoting You can force auto-quoting of a function's arguments by using ',' as the first character of a line. For example::

In [1]: ,my_function /home/me # becomes my_function("/home/me")

If you use ';' instead, the whole argument is quoted as a single string (while ',' splits on whitespace)::

In [2]: ,my_function a b c # becomes my_function("a","b","c")

In [3]: ;my_function a b c # becomes my_function("a b c")

Note that the ',' MUST be the first character on the line! This won't work::

In [4]: x = ,my_function /home/me # syntax error

____________________________________________________________________

OK, như các bạn thấy IPyIDA có khá nhiều tính năng và cần phải có thời gian để tìm hiểu dần. Để xóa các thông tin đã hiển thị ở trên, nhấn phím ESC.

Một tính năng hay của plugin này là nó cũng cấp khả năng tự động hoàn tất lệnh bằng phím TAB giống như bạn gõ lệnh ở terminal trên Linux (tính năng này không có ở thanh Python mặc định của IDA). Ví dụ: nếu tôi gõ imp và nhấn TAB, nó sẽ tự động autocomplete thành import. Sau đó tôi nhấn phím cách và nhấn TAB một lần nữa:

Như trên hình, ta thấy được toàn bộ các modules có thể import, sau đó sử dụng các mũi tên điều hướng  để lựa chọn modules cần import và thoát ra bằng cách nhấn ESC.

Khi tôi nhập dấu “?” một lần, nó cung cấp cho ta thông tin nhanh về module đó:

Và nếu tôi nhập dấu “?” hai lần nó sẽ hiển thị code như hình dưới đây:

Để thoát ta chỉ việc nhấn phím ESC. Bằng việc sử dụng các mũi tên lên và xuống, ta có thể đi đến các lệnh trước đó đã sử dụng. Hoặc sử dụng %hist để hiện thị thông tin lịch sử về các lệnh:

%edit sẽ mở ứng dụng notepad của Windows. Còn %edit x-y sẽ mở một notepad chứa các lệnh đã gõ nằm trong khoảng đó, tương tự như hình sau:

%history -n thêm số dòng bên cạnh các câu lệnh đã sử dụng:

Rõ ràng IPython khá mạnh và nó có rất nhiều câu lệnh mà bạn có thể tìm thấy ở đây: http://ipython.org/ipython-doc/3/index.html

Chúng ta sẽ làm một vài ví dụ đơn giản với plugin mới này.

Câu lệnh trên thực hiện lấy địa chỉ hiện tại của con trỏ. Nếu tôi sử dụng lệnh %edit như dưới đây, trình notepad của Windows sẽ được gọi lên để lưu các lệnh thành python script:

Sau đó, ta cho thực thi thông qua menu File-Script file, script sẽ cho kết quả tương tự:

Ngoài ra, lệnh idc.GetDisasm (ea) sẽ cung cấp cho chúng ta lệnh ASM tại nơi con trỏ đang đứng:

Nếu thay đổi con trỏ sang câu lệnh khác, ta sẽ phải tìm lại giá trị ea một lần nữa. Với câu lệnh idc.GetOpnd, ta có thể lấy được thông tin về toán hạng đầu tiên hoặc thứ hai của câu lệnh. Ví dụ như sau:

Đoạn code dưới đây thực hiện in ra tên của hàm hiện tại:

Tên của tất cả các hàm được liệt kê thông qua đoạn code sau:

Các câu lệnh bên trong hàm:

Tìm tham chiếu đến hàm. Nếu ta đặt con trỏ tại đầu của một hàm, sau đó nhấn X để tìm các xrefs đến hàm này thì có được kết quả như sau:

Ta hoàn toàn có thể sử dụng câu lệnh để làm được việc tương tự như trên, ví dụ:

Có thể thấy plugin này mang lại cho chúng ta rất nhiều tiện lợi và IDApython có hàng ngàn câu lệnh phục vụ để thiết lập các breakpoints, log, thực thi debugger, v..v…

Phần 13 đến đây là kết thúc. Hẹn gặp lại các bạn ở phần 14!

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ánh tình trạng nhàm chán khi học một mớ lý thuyết, tôi sẽ cố gắng xen kẽ các bài tập để thực hành. Trong phần này tôi gửi kèm file TEST_REVERSER.exe mà thầy Ricardo đã code. Nó rất đơn giản! Tuy nhiên, thông qua ví dụ này sẽ giúp chúng ta nhìn thấy một số điều mới trong việc thực hiện static reversing cũng như áp dụng debugging.

Khi thực thi file bên ngoài IDA, các bạn sẽ thấy tương tự như sau:

Chương trình yêu cầu ta cung cấp tên của người dùng và một mật khẩu tương ứng. Nếu mật khẩu nhập vào không đúng sẽ hiển thị thông báo “Bad reverser” như trên hình.

Tiến hành mở file trong IDA để phân tích ở chế độ static. Do ta nạp file vào IDA không kèm theo symbol, nên sau khi IDA phân tích xong, code dòm khá tệ

Quan sát trên hình, các bạn thấy rằng IDA không nhận diện được hàm main() của chương trình. Tuy nhiên, IDA đang dừng lại tại Entry Point … như vậy là cũng tốt rồi. Thực tế, khi các bạn phân tích các ứng dụng khác cũng sẽ luôn luôn như thế và chúng ta phải tìm cách để giải quyết vấn đề nhỏ này.

Một trong những cách phổ biến mà các bạn đã thấy khi tìm đến phần code chính của chương trình là thực hiện tìm kiếm theo các chuỗi. Cách làm thì tôi đã giới thiệu thông qua các phần trước. Đối với các chương trình C/C++ kiểu này, có một cách để tìm ra hàm main() mà hầu như luôn cho kết quả chính xác như sau.

Chúng ta biết rằng hàm main() được truyền vào các tham số như argc, argv, v..v. Hay còn gọi là các console arguments: int main(int argc, char *argv[])

Trong ví dụ trước, các bạn có thể thấy thậm chí khi ta không sử dụng các tham số trong main() thì luôn luôn có các lệnh PUSH làm nhiệm vụ truyền các tham số này cho main(). Do vậy, các tham số này là mặc định, nên chúng ta có thể tìm kiếm trong tab NAMES để xem chúng có ở đó không:

Để không phải lặp lại quá nhiều, từ giờ trở đi, khi tôi đề cập thực hiện trên tab XXX, bạn đã biết rằng bạn phải mở tab này tại View-Open Subview-XXX.

Tại tab Names như trên hình, nhấn CTRL + F để thực hiện tìm kiếm theo điều kiện, ví dụ để tìm các tham số truyền cho hàm main() tôi nhập arg và sau khi có được kết quả, tôi nhấp đúp vào _p_argc. IDA sẽ đưa chúng ta đến đoạn code sau:

Sau đó nhấn X để tìm các references tới đoạn code trên. Ta sẽ tới đây:

Tại đoạn code trên hình, các bạn thấy cách ứng dụng gọi các hàm _p_argv và _p_argc và trả về kết quả, sau đó truyền các tham số đó cho hàm main() – trong trường hợp này là 0x401070.

Nếu xem xét vùng code này trong IDA khi được load kèm theo symbol:

Và reference:

Ở đây không phải là tôi chơi cheat lolz, tôi chỉ là thực hiện kiểm tra xem phương pháp tìm kiếm hàm main() như trên có chính xác không và như các bạn thấy nó hoạt động khá chuẩn. Bằng cách tìm kiếm các reference của các tham số được truyền qua giao diện console, ta có thể tìm được hàm main() của chương trình.

Khi đã biết được vị trí của hàm main(), thực hiện đổi lại tên hàm như sau:

Ngay lập tức, IDA tự động đổi tên các args sau khi chúng ta chỉ rõ đó chính là hàm main() của chương trình.

Bây giờ, code của chúng ta trông giống như phiên bản load kèm theo symbol:

Các bạn thấy trong trường hợp này các biến và các tham số nhiều hơn trong ví dụ trước. Nếu nhấn đúp vào bất kỳ biến hoặc tham số nào, chúng ta sẽ chuyển tới của sổ Stacknơi cung cấp thông tin về stack layout của hàm main().

Chúng ta quan sát từ phía dưới lên, về mặt logic đầu tiên sẽ là các tham số được truyền vào cho hàm. Các tham số này luôn luôn nằm dưới địa chỉ trở về (return address (r)), vì chúng được truyền vào bằng lệnh PUSH và được lưu trong ngăn xếp (Stack) trước khi gọi hàm bằng lệnh CALL. Tiếp theo đó, địa chỉ trở về (r) sẽ được lưu vào Stack.

Sau đó, chương trình sẽ lưu thanh ghi EBP (s), đó là giá trị EBP của hàm đã gọi hàm main(). Giá trị này được lưu trong ngăn xếp khi hàm được thực thi thông qua câu lệnh push ebp như trong hình minh họa bên dưới:

Tiếp theo, chương trình thực hiện copy thanh ghi ESP vào EBP. Bằng cách này sẽ đặt EBP vào trong hàm hay còn gọi là BASE, từ đó sử dụng thanh ghi này để truy cập các tham số (EBP + XXX) và các biến cục bộ (EBP – XXX) của hàm. Cuối cùng là lệnh SUB ESP, 0x94, lệnh này sẽ dịch chuyển thanh ghi ESP để tạo ra không gian trống dành cho các biến cục bộ và và các buffer, đó là lý do tại sao thanh ghi EBP phải – XXX để truy cập tới các biến này. Ở chương trình này có giá trị là 0x94, là do trình biên dịch (compiler) đã tự động tính toán cần dành bao nhiêu không gian là đủ cho các biến, tùy theo cách chúng ta lập trình.

Thanh ghi ESP có giá trị nằm trên không gian dành riêng cho các biến cục bộ (đỉnh của Stack) và thanh ghi EBP trỏ tới BASE, phân chia các biến ở trên và ở dưới là Return address và các Args.

Đây là lý do tại sao các hàm dựa vào thanh ghi EBP, một khi giá trị EBP của hàm mà tôi gọi được lưu bằng lệnh PUSH EBP, và sau đó copy ESP sang EBP thì ta thấy trong chế độ xem tĩnh của stack, nó hiển thị giá trị 000000000 như một ranh giới để phân tách giữa các biến cục bộ và các tham số của hàm.

Như vậy, các bạn đã hiểu tại sao var_4 có thông tin là -00000004, vì dùng thanh ghi EBP làm BASE nên địa chỉ tính toán cho biến sẽ là EBP-4. Bên dưới, argc sẽ tương ứng là EBP + 8 (quan sát cột bên trái):

Điều này có thể được xác minh tại màn hình disassembly của hàm main(), nơi var_4 được sử dụng. Khi nhấp chuột phải, chúng ta sẽ thấy như sau:

Quay trở lại với cửa sổ Stack của hàm main(). Khi nhìn thấy có một khoảng không gian trống, nơi không có các biến tiếp giáp, thì đó có thể bên trên là một buffer (sau này, các bạn sẽ thấy các trường hợp trong đó không gian trống lại là một structure). Bây giờ, cuộn chuột lên một chút:

Ở đó ta thấy Buf (hoặc var_7C) là biến đầu tiên ở trên vùng trống, nhấn phải chuột và chọn Array:

Các bạn sẽ thấy IDA tự động phát hiện kích thước của Array = 120, tức là nó bao gồm 120 phần tử có kích thước 1-byte:

Sau khi chuyển đổi xong ta thấy biểu diễn của Stack lúc này trông tốt hơn trước:

Thanh ghi EBP được dùng làm Base, và nhớ rằng khi EBPESP bằng nhau thông qua lệnh MOV EBP, ESP, thì thanh ghi ESP sẽ được trừ đi giá trị 0x94 để dành không gian cho các biến được khai báo trong hàm và ESP lúc đó sẽ hoạt động ở phía trên khu vực các biến, tức là đỉnh của Stack.

Ta thấy khu vực làm việc của thanh ghi ESP vẫn còn sau khi thực hiện SUB ESP, 0x94.

Ở cột bên trái là -00000094, vì vậy nó là ESP = EBP-094. Rõ ràng sau đó nó sẽ tiếp tục tăng lên khi hoạt động giữa các hàm với nhau. Nhưng khi nó hoạt động bên trong hàm main() này và cho đến khi thoát khỏi hàm thì ESP sẽ làm việc từ 0x94 trở lên, bởi vì nó không can thiệp tới phần dành riêng cho các biến.

Tại chương trình này, khi chúng ta phân tích thông tin tại cửa sổ Stack của hàm main(), ta đang xem xét các biến trong hàm vì các tham số của hàm (argc, argv, vv) là đã biết:

Các bạn sẽ nhận thấy rằng var_4 là biến lưu COOKIE_SECURITY. Nó nhận giá trị đã được XOR với thanh ghi EBP và lưu lại vào biến trên Stack, mục đích là để bảo vệ chương trình khỏi lỗi Overflows. Vì vậy chúng ta tiến hành đổi lại tên biến này:

Quan sát sub_0x4011b0 tại địa chỉ 0x4010A0 bên dưới, ta có thể đoán được đây là hàm API printf() vì có một string được truyền vào cho hàm, cũng như trong quá trình thực thi chương trình ta đã thấy chuỗi này được in ra tại màn hình console:

Và đi sâu vào trong hàm sub_0x401040, ta có được thông tin sau:

Như vậy, chúng ta sẽ đổi tên sub_0x4011b0 thành printf():

Tiếp tục phân tích tiếp chương trình:

Như trên hình, ta thấy rằng biến Size được khởi tạo giá trị là 8 và không bao giờ thay đổi trong chương trình. Quan sát cụ thể hơn tại màn hình xrefs, biến này chỉ được đọc ra có hai lần, vì vậy chúng ta sẽ đổi tên Size thành Size_CONST_8:

Tiếp theo, bên dưới ta thấy lời gọi tới hàm gets_s() (là cải tiến của hàm gets()). Hàm này giới hạn ký tự tối đa mà bạn có thể nhập vào. Trong trường hợp này tối đa là 8 kí tự, được truyền qua lệnh PUSH EAX và sau đó là lệnh LEA để lấy địa chỉ của biến Buf hay Buffer mà ta đã tìm hiểu ở trước.

Thông tin về hàm các bạn có thể xem tại đường link sau https://msdn.microsoft.com/en-us/library/5b5x9wc7.aspx?f=255&MSPPError=-2147217396

Như vậy, ta biết rằng biến Buf sẽ lưu thông tin về tên User chúng ta nhập vào từ bàn phím và tối đa chỉ được 8 kí tự:

Địa chỉ của Buf sau đó sẽ được đưa vào thanh ghi EDX. lệnh PUSH EDX sau đó truyền địa chỉ của Buf như là tham số cho hàm API strlen(). Hàm API này sẽ lấy độ dài của chuỗi trong Buf tương ứng với chuỗi người dùng vừa nhập. Độ dài có được sẽ lưu vào biến var_90, do đó, chúng ta đổi tên var_90 thành len_USER:

Mũi tên màu xanh trên hình cho thấy một bước nhảy lùi, vậy có thể đây là một vòng lặp (Loop). Ta thấy biến var_84 được khởi tạo trước khi sử dụng, và được dùng để so sành với độ dài chuỗi tại địa chỉ 0x4010ef, bên dưới là lệnh nhảy có điều kiện để xem xét việc thoát khỏi vòng lặp. Thông thường, bộ đếm của một vòng lặp sẽ được khởi tạo bằng 0 và sẽ chỉ thoát khỏi vòng lặp khi bộ đếm này lớn hơn hoặc bằng với độ dài của len_USER đã gõ. Như vậy, có thể khẳng định biếnvar_84 chính là bộ đếm của vòng lặp, ta đổi tên nó thành COUNTER.

Bộ đếm này được tăng lên ở cuối vòng Loop:

Tại khối lệnh trên, nó copy giá trị của COUNTER vào thanh ghi EAX, sau đó tăng EAX lên 1 và lưu lại vào biến một lần nữa. Việc làm này tương đương với một lệnh ở ngôn ngữ bậc cao là COUNTER++

Trong khối lệnh bên trên, ta thấy nó thực hiện chuyển byte đầu tiên EBP + EDX + BUF của buffer vào EAX, bởi vì EBP + BUF được được công thêm với biến COUNTER hiện đang bằng 0 (nhưng sẽ tăng lên theo chu kỳ của vòng lặp). Thanh ghi EAX sau khi nhận từng giá trị kí tự trong Buf sẽ được cộng với biến var_88. Qua đó, ta kết luận đoạn code trên thực hiện việc cộng toàn bộ giá trị các kí tự ta nhập vào và lưu vào biến var_88 (biến này ban đầu được khởi tạo bằng 0). Nên nhớ rằng biến var_88 cuối cùng chỉ lưu kết quả ở dạng hexa.

Ở đây chúng ta gặp một câu lệnh mới là: MOVSX

Lệnh MOVSXMOVZX, hai lệnh này đều lấy 1 byte và chuyển vào một thanh ghi. MOVZX sẽ điền 0 vào bytes cao. MOVSX sẽ xem xét bit dấu, nếu là số dương, nhỏ hơn hoặc bằng 0x77 thì nó sẽ điền 0; còn nếu là số âm, 0x80 hoặc lớn hơn, thì nó sẽ điền 0xFF.

Xem xét các ví dụ:

MOVZX EAX, [XXXX]

Nếu giá trị lưu tại XXXX là 0x40, thanh ghi EAX sẽ có giá trị là 0x00000040.

Cũng có thể sử dụng MOVZX EAX, CL. Nó cũng tương tự như trên, chuyển giá trị tại CL vào EAX và điền 0 vào các byte cao.

MOVSX EAX, CL

Lệnh này sẽ quan tâm tới bit dấu, nếu CL là 0x40, EAX sẽ là 0x00000040 và nếu là 0x85 thì là số âm, EAX sẽ là 0xFFFFFF85.

Do chúng ta nhập các kí tự và số tại màn hình console nên chúng sẽ là các positive hex values, nên sẽ không gặp vấn đề gì. Đoạn code sẽ thực hiện cộng dồn lần lượt từng kí tự một, do đó ta đổi tên biến var_88 thành SUMMARY:

Tóm lại, vòng lặp ở đây làm nhiệm vụ tính tổng các kí tự ta đã nhập vào. Thực hiện chuyển các khối lệnh của vòng lặp này về cùng màu để dễ nhận biết:

Hoặc để cho gọn, bạn có thể thực hiện nhóm các khối lệnh này lại bằng cách nhấn Ctrl và chọn lần lượt các khối cần nhóm. Sau đó, nhấn chuột phải trên một khối và chọn Group Nodes. Cuối cùng đặt tên cho node sau khi nhóm lại. Ví dụ:

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

Trong quá trình phân tích, nếu chúng ta cần kiểm tra lại thông tin, ta có thể lựa chọn để Ungroup Nodes.

Sau khi in ra màn hình chuỗi USER đã nhập vào, chương trình tiếp tục yêu cầu ta cung cấp một PASSWORD:

Sau đó, chương trình lại gọi hàm gets_s() một lần nữa bằng cách sử dụng lại cùng biến BufSize ở trên:

Ta hoàn toàn có thể sử dụng lại cùng Buffer để lưu thông tin Password, vì sau khi thực hiện tính toán xong thì ta không còn sử dụng tới chuỗi User nữa.

Sau khi có được Password nhập vào và lưu tại Buf, Password này sẽ được chuyển đổi sang dạng Hexadecimal như đã thấy ở ví dụ trước thông qua hàm atoi() và lưu vào biến var_94. Do vậy, tôi đổi tên biến var_94 thành PASSWORD_HEX:

Tiếp theo, gặp hàm sub_401010. Hàm này sẽ nhận hai tham số truyền vào, một là PASSWORD_HEX thông qua lệnh PUSH EDX và hai là SUMMARY (tổng các kí tự trong chuỗi User) thông qua lệnh PUSH EAX. Đi sâu vào trong hàm này để phân tích:

Khi vào trong hàm, các bạn thấy hàm có hai tham số, rõ ràng là tham số bên dưới sẽ là PASSWORD_HEX vì nó được truyền vào đầu tiên được thông qua lệnh PUSH và tham số còn lại sẽ là SUMMARY. Ta đổi tên lại các tham số cho phù hợp như sau:

Sau khi đổi tên các tham số, ta chọn sub_0x401010, nhấn chuột phải và chọn Set Type (hoặc nhấn phím tắt là Y).

Theo đó, IDA sẽ cố gắng khai báo lại hàm cùng với các tham số của hàm sao cho tường minh nhất, đồng thời ta cũng đổi luôn tên hàm thành Check() như hình:

Nếu quay ngược trở lại hàm main(), các bạn sẽ thấy IDA bổ sung thêm thông tin tại các lệnh PUSH như sau:

Vậy hàm Check chúng ta vừa đổi tên ở trên thực hiện công việc gì?

Để ý thấy rằng hàm có sử dụng lệnh so sánh CMP, và trước khi thực hiện so sánh nó copy PASSWORD vào thanh ghi EAX và thực hiện lệnh SHL EAX, 1

Lệnh SHL là lệnh dịch bit sang trái đi n bit. Trong trường hơp này của chúng ta là dịch 1 bit, tương đương với việc lấy giá trị của EAX nhân với 2 và lưu lại vào EAX.

Tổng hợp lại, toàn bộ hàm Check thực hiện việc lấy giá trị PASSWORD nhập vào, đem nhân với 2, được bao nhiêu đem so sánh với tổng các ký tự của USER.

Để chuyển đổi một kí tự sang hệ thập phân, trong IDA ta làm như sau:

Dựa vào đó, ta thực hiện việc tính tổng cho tất cả các ký tự của chuỗi old_man mà ta sẽ sử dụng nó như là USER nhập vào:

Tổng có được là 0x2da. Như đã tổng kết ở trên, password nhập vào sẽ được nhân 2 trước khi đem đi so sánh với giá trị tổng (ví dụ là: 0x2da vừa tính). Do đó, mật khẩu chính xác mà ta cần phải nhập vào là một giá trị sau khi nhân 2 phải bằng 0x2da. Ta có biểu thức như sau:

X*2=0x2da; X is password

Giải phương trình trên: X=0x2da/2, kết quả có được X là một số ở dạng thập phân (Số này sẽ được chuyển đổi thành Hexa khi đi qua hàm atoi trong hàm main())

Vì vậy, nếu tôi gõ tên của người dùng là old_man và password là 365, điều gì sẽ xảy ra?

Ta thấy, trong hàm Check sử dụng lệnh so sánh không bằng để đưa ra quyết định rẽ nhánh thực hiện:

Nếu không bằng nhau ta sẽ đi đến khối màu đỏ, thực hiện xóa thanh ghi AL về 0. Còn nếu bằng nhau sẽ đi tới khối màu xanh lá cây và thiết lập thanh ghi AL là 1. Quan sát xem với thiết lập kết quả trả về ở thanh ghi AL thì chương trình sẽ làm gì:

Như trên hình, giá trị của AL được lưu vào biến var_7D, sau đó giá trị biến này được gán cho thanh ghi ECX để kiểm tra. Ta đổi tên biến này thành SUCCESS_FLAG:

Nếu biến này bằng 0 thì thông báo “Bad reverser..” sẽ hiển thị. Ngược lại nếu biến này là 1, thông báo “Good Reverser” sẽ hiển thị.

Như vậy, toàn bộ phần 12 đến đây là kết thúc. Tôi muốn các bạn dành thời gian thực hành thử debug chương trình này và kiểm tra mọi thứ chúng ta đã reversed thông qua việc đặt các breakpoints, quan sát các giá trị trong từng trường hợp cho đến khi tới bước so sánh cuối cùng.

Image result for too long and crazy funny

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

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 không muốn nhồi nhét quá nhiều lý thuyết ngay từ đầu, vì vậy tôi đã lồng ghép và xen kẽ một số bài thực hành để các bạn không có cảm giác nhàm chán. Tuy nhiên, không vì thế mà chúng ta bỏ qua các kiến thức cơ bản, do đó trước khi tiếp tục các phần nâng cao hơn, phần này tôi sẽ cùng với các bạn xem xét một số cờ quan trọng trong ASM.

FLAGS

CARRY FLAG (CF)

Chúng ta đã tìm hiểu hoạt động của cờ CF (cờ nhớ) trong phần trước. Cờ này được kích hoạt trong quá trình tính toán của các số unsigned. Khi kết quả là số âm như ta đã gặp ở bài trước hoặc vượt quá mức biểu diễn tối đa trong trường hợp phép cộng. Hay nói cách khác, cờ CF được thiết lập là 1 khi có nhớ từ bit msb trong phép cộng hoặc có vay vào bit msb trong phép trừ.

Hãy quan sát các ví dụ sau thông qua trình debugger. Thực thi crackme.exe trong IDA thông qua debugger, ta dừng lại tại Entry Point của crackme (như đã cấu hình ở phần trước). Tại đây ta tiến hành patch lệnh như sau:

Khi patch như trên, chế độ graph của IDA sẽ bị lỗi, IDA tự động chuyển đổi về chế độ text. Để chuyển lại về chế độ graph, ta nhấn chuột phải tại địa chỉ entry point và chọn Create Function, sau đó nhấn phím space bar để chuyển lại về chế độ graph ban đầu. Tiếp theo, đặt lại giá trị cho thanh ghi EAX = 0xffffffff bằng cách nhấn chuột phải tại thanh ghi này và chọn Modify Value (E):