REVERSING WITH IDA FROM SCRATCH (P25)

Posted: November 18, 2019 in IDA Tutorials, REVERSING WITH IDA FROM SCRATCH (P25)
Tags: ,

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

Leave a Reply

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

WordPress.com Logo

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

Google photo

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

Twitter picture

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

Facebook photo

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

Connecting to %s

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