Archive for the ‘IDA Pro section’ Category


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


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