# Static Code Analysis

## 1 Fundamentals of Code Analysis

### **1.1 Các thuật ngữ chính**

* Disassembling - là quá trình chuyển đổi các lệnh cấp nhị phân thành ngôn ngữ hợp ngữ cấp thấp (assembly), dễ dàng hơn cho con người diễn giải. Nó là các công cụ được thiết kế cho tác vụ này và vượt xa bản dịch cơ bản bằng cách phân biệt giữa mã và dữ liệu nhúng, thêm nhận xét, nhãn và các chú thích hữu ích cho kỹ thuật đảo ngược.
* Decompiling - tiến thêm một bước bằng cách cố gắng tạo lại mã nguồn cấp cao ban đầu từ tệp nhị phân. Nó cung cấp một bản gần đúng dễ đọc hơn của chương trình, thường bằng các ngôn ngữ như C và C++. Tuy nhiên, các trình dịch ngược có thể gặp khó khăn với phần mềm độc hại phức tạp hoặc bị xáo trộn nhiều, đó là lý do tại sao sự hiểu biết vững chắc về ngôn ngữ hợp ngữ vẫn rất quan trọng cho phân tích chuyên sâu.
* Debugging - cho phép các nhà phân tích thực thi mã từng bước, đặt breakpoint và kiểm tra memory, registers và variables trong thời gian thực. Chức năng này giúp theo dõi chặt chẽ hành vi của chương trình, xác định lỗi, lỗ hổng hoặc các hoạt động độc hại không rõ ràng chỉ thông qua phân tích tĩnh.

### 1.2 Vòng đời malware

<figure><img src="/files/QWem7EzYLVokg15geVlc" alt=""><figcaption></figcaption></figure>

Vòng đời của malware vạch ra các giai đoạn khác nhau mà phần mềm độc hại (hoặc bất kỳ phần mềm nào) trải qua, từ khi nó được tạo ra dưới dạng mã nguồn đến khi thực thi trên hệ thống đích.

* **Source Code  (mã nguồn)**: Vòng đời bắt đầu từ khi malware được viết dưới dạng mã nguồn bởi attacker. Mã nguồn này được viết bằng một ngôn ngữ lập trình như C, C++, Python hoặc assembly và bao gồm logic và hướng dẫn xác định hành vi dự kiến của phần mềm độc hại, chẳng hạn như phân phối payload, cơ chế lan truyền và kỹ thuật né tránh (evasion).
* **Compiler/Assembler**: Khi mã nguồn đã sẵn sàng, nó phải được chuyển đổi thành một định dạng mà máy tính có thể thực thi. Đối vưới các ngôn ngữ cấp cao hơn (như C hoặc C++), điều này được thực hiện thông qua trình biên dịch, trong khi mã ngôn ngữ assembly được xử lý bởi trình hợp ngữ. Đầu ra là machine code (mã máy), mà máy tính có thể hiểu được. Mã máy được lưu trữ ở định dạng Object code, là phiên bản nhị phân, cấp thấp của các hướng dẫn của malware.
* **Object File:** Ở giai đoạn này, phần mềm độc hịa tồn tại dưới dạng tệp đối tượng, là một tệp nhị phân trung gian. Tệp đối tượng chứa các hướng dẫn cấp máy nhưng chưa thể thực thi. Nó có thể bao gồm các tham chiếu đến các thư viện hoặc hàm bên ngoài cần thiết để malware hoạt động, nhưng nó vẫn cần liên kết để trở thành một chương trình hoàn chỉnh.
* **Linker:** Trình liên kết kết hợp một hoặc nhiều tệp đối tượng, giải quyết các tham chiếu bên ngoài và liên kết trong các thư viện cần thiết để tạo tệp thực thi. Đầu ra là một chương trình thực thi độc lập hoặc tệp nhị phân malware đã sẵn sàng để thực thi.
* **Executable File**: Đầu ra cuối cùng là tệp thực thi. Tệp này chứa tất cả mã, data, và hướng dẫn cần thiết để malware thực hiện các hành động độc hại của nó, chẳng hạn như lây nhiễm hệ thống, lan sang các hệ thống khác, đánh cắp thông tin hoặc thực hiện các hoạt động phá hoại.
* **Load into Memory**: Khi phần mềm độc hại chạy, nó có thể tải các thư viện động (ví dụ DLL) để truy cập các chức năng hệ thống như kết nối mạng hoặc xử lý tệp. Các thư viện này được tải trong quá trình thực thi, cho phép phần mềm độc hại vẫn nhỏ gọn và sử dụng tài nguyên hệ thống mà không cần nhúng tất cả chức năng vào tệp thực thi.

## 2 Tóm tắt x86 Assembly

Ngôn ngữ Assembly là một ngôn ngữ lập trình cấp thấp được sử dụng để giao tiếp với phần cứng của máy tính. Không giống như các ngôn ngữ cấp cao như Python hoặc Java, assembly gần với ngôn ngữ gốc của máy hơn nhiều (mã nhị phân). Mặc dù điều này làm cho nó hiệu quả và mạnh mẽ, nhưng nó cũng khó đọc và hiểu hơn do tính phức tạp của nó.

<figure><img src="/files/u5kONmPowzbYyWngqufz" alt=""><figcaption></figcaption></figure>

Phần mềm độc hại thường viết bằng các ngôn ngữ cấp cao như C hoặc C++, nhưng sau khi được biên dịch, nó sẽ được chuyển thành mã máy (một tập hợp các hướng dẫn nhị phân). Các hướng dẫn này được thực thi bởi CPU. Để phân tích và hiểu các hành động của phần mềm độc hại, các nhà phân tích thường sử dụng disassembler để chuyển đổi mã máy trở lại ngôn ngữ assembly, giúp hành vi của phần mềm độc hại dễ diễn giải hơn cho phân tích của con người.

<figure><img src="/files/ts70t22U6U3vrnhD9kht" alt=""><figcaption></figcaption></figure>

**Các lý do chính để sử dụng Assembly trong phân tích malware**

* **Hiểu các hoạt động cấp thấp**: Ngôn ngữ Assembly cung cấp cái nhìn sâu sắc về cách phần mềm độc hại tương tác với các tài nguyên hệ thống như bộ nhớ, tệp và mạng ở cấp máy. Nó cho phép các nhà phân tích xem các chuỗi lệnh chính xác, các lệnh gọi hàm và các thao tác bộ nhớ thường bị ẩn hoặc trừu tượng hóa trong các ngôn ngữ cấp cao. Chế độ xem cấp thấp này là điều cần thiết để hiểu cách phần mềm độc hại thao túng hệ thống.
* **Xác định ý định độc hại:** Trong phân tích phần mềm độc hại, mã nguồn hầu như không bao giờ có sẵn. Để khám phá mục đích của phần mềm độc hại, các nhà phân tích phải đảo ngược kỹ thuật mã nhị phân đã biên dịch thành một dạng có thể đọc được, thường là Assembly. Các công cụ như Ghidra hoặc Cutter được sử dụng để hợp ngữ ngược mã máy trở lại Assembly. Bằng cách phân tích các hướng dẫn Assembly, các nhà phân tích có thể hiểu luồng logic, xác định các hàm và xác định các tác vụ cụ thể mà phần mềm độc hại được lập trình để thực hiện.
* **Phát hiện các kỹ thuật làm rối và chống phân tích:** Phần mềm độc hại thường sử dụng các phương pháp làm rối và chống phân tích như đóng gói, mã hóa và đột biến mã để gây khó khăn hơn cho việc phân tích. Các kỹ thuật này hoạt động ở cấp mã máy, nơi kiến thức về Assembly trở nên quan trọng. Các nhà phân tích có kỹ năng về Assembly có thể phát hiện các thao tác luồng điều khiển, các bước nhảy và các mẫu làm rối, cho phép họ vượt qua các rào cản này và tiết lộ hành vi thực sự của phần mềm độc hại.

**Tại sao chúng ta không chỉ dịch ngược tệp nhị phân?**&#x20;

* **Mất thông tin cấp cao**: Khi mã nguồn được biên dịch thành mã máy, các chi tiết quan trọng như tên biến và cấu trúc hàm sẽ bị loại bỏ. Các trình dịch ngược gặp khó khăn trong việc khôi phục chúng, khiến mã kết quả kém dễ đọc hơn và thiếu ngữ cảnh quan trọng.
* **Obfuscation Techniques (Kỹ thuật làm rối)**: Các tác giả phần mềm độc hại sử dụng các kỹ thuật như mã hóa, đóng gói và thay thế hướng dẫn để ẩn mã của họ. Những biến dạng này có thể gây nhầm lẫn cho các trình dịch ngược, trong khi assembly cung cấp cái nhìn rõ ràng hơn về những gì thực sự đang diễn ra bên dưới.
* **Tối ưu hóa trình biên dịch**: Trong quá trình biên dịch, các tối ưu hóa được áp dụng để thay đổi cấu trúc của mã, gây khó khăn hơn cho các trình dịch ngược trong việc tạo lại một phiên bản cấp cao chính xác.
* **Kỹ thuật chống dịch ngược**: Nhiều mẫu phần mềm độc hại bao gồm các kỹ thuật được thiết kế đặc biệt để phá vỡ các trình dịch ngược, chẳng hạn như mã rác hoặc luồng điều khiển phức tạp. Các chiến thuật này dễ nhận biết và bỏ qua hơn khi phân tích assembly.
* **Kiểm soát chi tiết và độ chính xác**: Assembly cung cấp một cái nhìn chi tiết về hành vi của phần mềm độc hại ở cấp độ hướng dẫn, cho phép các nhà phân tích theo dõi luồng thực thi của nó và khám phá các hoạt động ẩn. Mức độ chính xác này là rất quan trọng để hiểu đầy đủ chức năng độc hại.

Assembly là sự xấp xỉ gần nhất với "nguồn sự thật" trong kỹ thuật đảo ngược, cung cấp những hiểu biết cần thiết về cách phần mềm độc hại tương tác với phần cứng và tài nguyên hệ thống. Hiểu biết về assembly là rất quan trọng để phân tích mã sâu, kỹ thuật đảo ngược và khám phá hành vi và ý định của phần mềm độc hại.

### 2.1 Kiểu cú pháp&#x20;

Cú pháp ngôn ngữ Assembly có thể khác nhau tùy thuộc vào nền tảng, trình hợp ngữ và kiến trúc CPU. Hai kiểu cú pháp phổ biến nhất cho assembly x86 là cú pháp “**Intel**” và “**AT\&T**”. Trong khóa học này, chúng ta sẽ chỉ tập trung vào cú pháp **Intel** để đơn giản hóa các cuộc thảo luận của chúng ta và tập trung vào các khái niệm cốt lõi của phân tích mã.

Điều quan trọng là ưu tiên nắm vững các nguyên tắc của chính phân tích mã. Mặc dù có sự khác biệt giữa các kiểu cú pháp và công cụ, nhưng chúng tương đối nhỏ và có thể được học khi bạn tiến bộ.

#### Intel Instruction Format

<figure><img src="/files/U1hzYamyFshDRokDjVhD" alt=""><figcaption></figcaption></figure>

Lưu ý: Ở đây, dấu ngoặc vuông (\[ ]) biểu thị rằng thành phần là tùy chọn. Mnemonic là thành phần bắt buộc duy nhất trong một lệnh assembly.

<figure><img src="/files/OkSAlUWJ4vzpF2F7uAUK" alt=""><figcaption></figcaption></figure>

**Label (Nhãn) (Thành phần tùy chọn)**&#x20;

* Nhãn được sử dụng để đánh dấu các điểm cụ thể trong mã, chẳng hạn như vòng lặp, mục tiêu nhảy hoặc điểm vào hàm.&#x20;
* Nhãn thường kết thúc bằng dấu hai chấm (:) và được sử dụng bởi các lệnh nhảy hoặc gọi để xác định nơi cần nhảy hoặc phân nhánh.&#x20;
* Nhãn được hiển thị khác nhau tùy thuộc vào trình hợp ngữ ngược đang được sử dụng. Ví dụ: trong Ghidra, nhãn có thể xuất hiện dưới dạng "`someLabel:`" hoặc ở định dạng thập lục phân, chẳng hạn như "`0x00401234:`". Trong Cutter, nhãn thường có tiền tố "`sym.`" và được hiển thị là "`sym.someLabel`".

**Mnemonic**

* Mnemonic là hướng dẫn cho CPU biết thao tác nào cần thực hiện, chẳng hạn như `MOV` (di chuyển dữ liệu), `ADD` (cộng) hoặc `JMP` (nhảy).

**Toán hạng (Thành phần tùy chọn)**&#x20;

* Toán hạng là dữ liệu hoặc địa chỉ mà mnemonic (hướng dẫn) hoạt động trên đó. Số lượng toán hạng phụ thuộc vào loại hướng dẫn (0, 1, 2 hoặc thậm chí 3 toán hạng).&#x20;
* Trong assembly x86, toán hạng thường là các thanh ghi (ví dụ: EAX), các giá trị tức thời (ví dụ: 0x1044) hoặc vị trí bộ nhớ (ví dụ: \[1234h]).

**Chú thích (Thành phần tùy chọn)**&#x20;

* Chú thích bắt đầu bằng dấu chấm phẩy (;) và mọi thứ sau dấu chấm phẩy trên cùng một dòng sẽ bị trình hợp ngữ bỏ qua.&#x20;
* Chú thích được sử dụng để giải thích mã, giúp mã dễ hiểu và bảo trì hơn.

**Operand Order (Thứ tự toán hạng)**

* Thứ tự toán hạng chỉ định nơi dữ liệu sẽ đi và dữ liệu nào sẽ được sử dụng. Cú pháp Intel sử dụng định dạng **"Destination, Source" (“Đích, Nguồn”)**. Điều này có nghĩa là toán hạng đầu tiên là đích và toán hạng thứ hai là nguồn.

Trong ví dụ trên, EAX là Đích và 5 là Nguồn. Hướng dẫn này sẽ di chuyển giá trị 5 vào thanh ghi EAX:

* MOV: Mnemonic (hướng dẫn) cho CPU biết di chuyển dữ liệu.&#x20;
* EAX: Thanh ghi đích - đây là nơi giá trị sẽ được lưu trữ.&#x20;
* 5: Giá trị nguồn - đây là giá trị sẽ được di chuyển vào EAX.

### 2.2 Memory Addressing Modes (Các chế độ định địa chỉ bộ nhớ)

Một con trỏ là một biến hoặc thanh ghi giữ địa chỉ bộ nhớ của một giá trị khác. Thay vì giữ chính giá trị, nó "trỏ" đến địa chỉ nơi giá trị đó được lưu trữ trong bộ nhớ.

Truy cập giá trị tại địa chỉ đó được gọi là tham chiếu ngược. Trong assembly x86, tham chiếu ngược được thực hiện bằng cách sử dụng dấu ngoặc vuông \[ ].

Hãy hiểu cách nó khác với toán hạng tức thời hoặc thanh ghi bằng một ví dụ:

**Register to Register Instruction**

<figure><img src="/files/RxdNm37Z2RKPBGXb7C4v" alt=""><figcaption></figcaption></figure>

Trong ví dụ trên, hướng dẫn này sao chép giá trị được lưu trữ trong thanh ghi EBX trực tiếp vào thanh ghi EAX. Nó chỉ đơn giản là sao chép dữ liệu giữa hai thanh ghi mà không tương tác với bộ nhớ.

Sau khi thực thi các hướng dẫn này:

EBX = 10 EAX = 10

**(Indirect) Memory to Register Instruction**

<figure><img src="/files/PmDrebWBxdTFlPNpwl6w" alt=""><figcaption></figcaption></figure>

Trong ví dụ này, hướng dẫn cuối cùng tham chiếu ngược \[EBX], coi nó như một con trỏ đến một địa chỉ bộ nhớ. Nó di chuyển giá trị được lưu trữ tại vị trí bộ nhớ được trỏ đến bởi EBX vào EAX.

Hướng dẫn không di chuyển giá trị của chính EBX mà di chuyển giá trị được lưu trữ tại địa chỉ bộ nhớ chứa trong EBX.

Sau khi thực thi các hướng dẫn này:

* EBX chứa địa chỉ “0x1000”.&#x20;
* Bộ nhớ tại địa chỉ “0x1000” chứa giá trị 20.&#x20;
* EAX bây giờ chứa giá trị 20, được đọc từ bộ nhớ tại “0x1000”.

**(Direct) Memory to Register Instruction**

<figure><img src="/files/sUW8oj6ureLYSiCcsG4K" alt=""><figcaption></figcaption></figure>

Trong ví dụ này, hướng dẫn truy cập địa chỉ bộ nhớ trực tiếp "0x1000" và di chuyển giá trị được lưu trữ tại địa chỉ đó vào EAX. Điều này được gọi là định địa chỉ bộ nhớ trực tiếp vì địa chỉ được chỉ định trực tiếp trong hướng dẫn mà không sử dụng bất kỳ thanh ghi nào.

Sau khi thực thi hướng dẫn này:

* Nếu vị trí bộ nhớ "0x1000" chứa giá trị 10, thì EAX sẽ bằng 10.

**Effective (Calculated) Addressing Instruction**

Định địa chỉ hiệu quả (hoặc định địa chỉ được tính toán) được sử dụng để tính toán địa chỉ bộ nhớ động dựa trên một hoặc nhiều thanh ghi và các hằng số tùy chọn. Địa chỉ hiệu quả được tính bằng công thức sau:

<figure><img src="/files/vPreghUFoaA4qiCqIh3v" alt=""><figcaption></figcaption></figure>

* Base: Một thanh ghi giữ địa chỉ cơ sở, thường chứa địa chỉ bắt đầu.
* Index: Một thanh ghi giữ giá trị bù, thường được sử dụng để bước qua các phần tử trong một mảng. (ví dụ: ESI)
* Scale: Một hệ số nhân hằng (1, 2, 4 hoặc 8) được sử dụng để chia tỷ lệ chỉ mục. (ví dụ: 4 cho số nguyên 4 byte).
* Displacement: Một giá trị hằng số cố định được thêm vào địa chỉ. (ví dụ: 8)

Ví dụ:

<figure><img src="/files/JurTUrSLn5KEPDSz5MhJ" alt=""><figcaption></figcaption></figure>

Trong ví dụ này, địa chỉ hiệu quả được tính bằng cách sử dụng một thanh ghi cơ sở EBX, một thanh ghi chỉ mục ESI nhân với 4 và một giá trị dịch chuyển là 8.

<figure><img src="/files/aZmd2ZrOoUSWyIG83x32" alt=""><figcaption></figcaption></figure>

Hướng dẫn này sẽ truy cập bộ nhớ tại địa chỉ "0x1010" và di chuyển giá trị của nó vào EAX.

Bạn sẽ thường xuyên bắt gặp định địa chỉ bộ nhớ hiệu quả (được tính toán) khi phân tích phần mềm độc hại. Chế độ định địa chỉ này thường được sử dụng và rất quan trọng để hiểu khi thực hiện kỹ thuật đảo ngược.

### 2.3 Common CPU Instructions

#### Các lệnh di chuyển và thao tác dữ liệu

<figure><img src="/files/L5V4bEaiXWsxiCD4gwD5" alt=""><figcaption></figcaption></figure>

#### Các lệnh số học

<figure><img src="/files/AhNzXpovmOu762OZtpjr" alt=""><figcaption></figcaption></figure>

#### Các lệnh luồng điều khiển

<figure><img src="/files/pgEHznZK0wwo3kuPibmt" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/dPo795UeQbPjQ08k7Wyg" alt=""><figcaption></figcaption></figure>

## 3 Binary Analysis with Cutter

Trong bài học này, chúng ta sẽ bắt đầu khám phá phân tích nhị phân với phần giới thiệu Cutter, một công cụ disassembler mạnh mẽ. Cutter giúp chúng ta dịch một tệp nhị phân thành ngôn ngữ assembly, cho phép chúng ta kiểm tra cấu trúc và chức năng của chương trình một cách chặt chẽ hơn.

Tải xuống và cài đặt Cutter: truy cập trang tải xuống <https://cutter.re/> và tải xuống phiên bản phù hợp với hệ điều hành của máy phân tích. Sau khi tải xuống, chạy trình cài đặt và làm theo lời nhắc để hoàn thiện cài đặt.

### **3.1 Overview của Cutter interface**

<figure><img src="/files/2QBNsDkY9s7O9o48gX7r" alt=""><figcaption></figcaption></figure>

**Top Menu Bar:** Thanh menu trên cùng cung cấp quyền truy cập vào các cài đặt và tùy chọn cấu hình của Cutter.

**Toolbar:** Ngay bên dưới thanh menu, thanh công cụ bao gồm một trường tìm kiếm có nhãn "Type flag name or address here" (Nhập tên flag hoặc địa chỉ vào đây). Trường này giúp bạn điều hướng đến các hàm, địa chỉ bộ nhớ hoặc flag cụ thể trong tệp nhị phân. Thanh công cụ cũng bao gồm các điều khiển để điều chỉnh chế độ xem, bước qua mã và quản lý quy trình phân tích.

**Side Panel:** Bên Bảng điều khiển bên hiển thị danh sách các hàm được xác định trong tệp nhị phân. Trường "**Quick Filter**" (Bộ lọc nhanh) ở cuối bảng điều khiển cho phép bạn lọc các hàm dựa trên đầu vào văn bản, giúp bạn nhanh chóng xác định vị trí các hàm cụ thể.

**Window (View) Tabs:** Các tab ở dưới cùng cung cấp quyền truy cập vào các công cụ và chế độ xem thiết yếu để phân tích chi tiết:

* **Dashboard (Bảng điều khiển):** Cung cấp tổng quan về các thuộc tính và thuộc tính của tệp nhị phân.&#x20;
* **Strings (Chuỗi)**: Liệt kê tất cả các chuỗi được tìm thấy trong tệp nhị phân. Nó đặc biệt hữu ích để xoay vòng đến các khu vực thú vị của mã bằng cách xác định các chuỗi đáng ngờ có thể cho thấy ý định độc hại.&#x20;
* **Imports (Nhập):** Hiển thị các hàm hoặc API mà tệp nhị phân nhập, có thể tiết lộ các lệnh gọi hệ thống và các phụ thuộc.&#x20;
* **Search (Tìm kiếm)**: Cho phép tìm kiếm trong mã hoặc dữ liệu đã hợp ngữ ngược, giúp bạn dễ dàng xác định vị trí các hướng dẫn hoặc giá trị cụ thể.&#x20;
* **Disassembly (Hợp ngữ ngược):** Hiển thị mã đã hợp ngữ ngược của tệp nhị phân, cho phép phân tích các hướng dẫn assembly và luồng điều khiển của nó.&#x20;
* **Graph (Đồ thị)**: Cung cấp một chế độ xem đồ họa về luồng điều khiển của mã, trực quan hóa các vòng lặp, nhánh và lệnh gọi hàm.&#x20;
* **Hexdump:** Hiển thị chế độ xem thập lục phân thô của tệp nhị phân, cung cấp quyền truy cập dữ liệu cấp thấp để phân tích sâu hơn.&#x20;
* **Decompiler (Trình dịch ngược):** Cung cấp chế độ xem cấp cao hơn của mã, giống với các ngôn ngữ như C hoặc Python, để đơn giản hóa việc hiểu so với assembly thô.

Các tab cửa sổ bổ sung có thể được truy cập bằng cách điều hướng đến tùy chọn "**Windows**" trên thanh menu trên cùng, cho phép tùy chỉnh thêm bố cục của Cutter.

### 3.2 Dashboard View

<figure><img src="/files/Q1QYfYc2QOvV6oODZWrd" alt=""><figcaption></figcaption></figure>

Cửa sổ **Dashboard (Bảng điều khiển)** cung cấp một bản tóm tắt nhanh về thông tin thiết yếu của tệp nhị phân:

* **Thông tin tập tin**
  * Kiến trúc (x86) và Hệ thống con (Windows CUI): Cho biết kiến trúc hệ thống đích và liệu nó có phải là ứng dụng dựa trên bảng điều khiển hay GUI.&#x20;
  * Ngày biên dịch: Cho biết thời điểm tệp nhị phân được tạo, có thể tương quan với các chiến dịch phần mềm độc hại đã biết hoặc thời điểm các mối đe dọa mới nổi.
* **Hashes**
  * MD5, SHA1 và SHA256: Các mã định danh duy nhất cho tệp, cho phép các nhà phân tích so sánh và phân loại các mẫu phần mềm độc hại bằng cách sử dụng cơ sở dữ liệu thông tin về mối đe dọa.
* **Entropy**&#x20;
  * Đo lường tính ngẫu nhiên trong tệp nhị phân, trong đó entropy cao thường cho thấy các kỹ thuật đóng gói hoặc làm rối mã, cho thấy các nỗ lực trốn tránh phát hiện hoặc cản trở phân tích tĩnh.
* **Libraries**&#x20;
  * Liệt kê các DLL được nhập (ví dụ: kernel32.dll, urlmon.dll), cho biết các hàm hệ thống được sử dụng bởi tệp nhị phân, chẳng hạn như xử lý tệp, quản lý bộ nhớ và giao tiếp mạng, gợi ý về khả năng của phần mềm độc hại.
* **Stripped**&#x20;
  * Nếu một tệp nhị phân bị loại bỏ thông tin biểu tượng, nó sẽ thiếu tên hàm và siêu dữ liệu hỗ trợ phân tích. Sự vắng mặt này làm phức tạp các nỗ lực xác định các hàm và phân tích các khả năng, đòi hỏi các kỹ thuật nâng cao hơn để hiểu cấu trúc và mục đích của tệp nhị phân.
* **Analysis Info**
  * Số lượng Imports and Functions có thể cho thấy độ phức tạp và khả năng của phần mềm độc hại.
  * Các chuỗi thường khám phá dữ liệu nhúng như URL, địa chỉ IP hoặc lệnh, cung cấp những manh mối quan trọng về các kênh liên lạc và cơ chế điều khiển của phần mềm độc hại.

### 3.3 Functions Panel

<figure><img src="/files/WFdiUvdOjYTFFusjbT1U" alt=""><figcaption></figcaption></figure>

Bảng Functions (Hàm) phân loại các hàm theo tiền tố, giúp các nhà phân tích nhanh chóng xác định và diễn giải các vùng mã quan trọng.

* **dbg\_ Functions**: Các hàm này thường liên quan đến các thói quen gỡ lỗi hoặc khởi tạo. Trong hầu hết các mẫu phần mềm độc hại trong thế giới thực, các hàm dbg\_ khó có khả năng xuất hiện, vì những kẻ tấn công thường loại bỏ các biểu tượng gỡ lỗi để cản trở phân tích. Tuy nhiên, nếu có mặt, chúng có thể tiết lộ các hoạt động thiết lập hoặc các kỹ thuật chống gỡ lỗi.
* **sym. Functions**: Các hàm Symbolic đại diện cho các thư viện hoặc lệnh gọi hệ thống đã biết. Ví dụ: các hàm như "`sym.URLDownloadToFileA_20`" cho thấy hoạt động mạng, chẳng hạn như tải xuống tệp từ xa và có thể cung cấp thông tin chi tiết về sự tương tác của phần mềm độc hại với API hệ thống.
* **main Function**: Điểm vào chính của tệp thực thi, nơi chứa logic cốt lõi. Phân tích hàm này là điều cần thiết để hiểu hành vi và ý định chính của phần mềm độc hại.
* **Fcn. (unnamed) Functions**: Các hàm chung này thiếu tên có thể xác định được, có thể là do trình hợp ngữ ngược không thể xác định chúng hoặc do thiếu thông tin biểu tượng. Chúng vẫn có thể đóng vai trò quan trọng trong hoạt động của phần mềm độc hại và đảm bảo điều tra thêm.

### 3.4 Strings View

<figure><img src="/files/ksEugY3xIijRP0G0DNNC" alt=""><figcaption></figcaption></figure>

Tab Strings (Chuỗi) hiển thị danh sách các chuỗi được tìm thấy trong tệp nhị phân, tương tự như kết quả thu được trong quá trình phân tích tĩnh cơ bản. Bằng cách lọc chế độ xem này thành phần "**.rdata**", nơi lưu trữ dữ liệu chỉ đọc, bạn thường có thể khám phá ra các chuỗi chính như URL, đường dẫn tệp hoặc chuỗi lệnh được nhúng trong phần mềm độc hại.

<figure><img src="/files/2EBaPZB1NtQIUHHdnkCS" alt=""><figcaption></figcaption></figure>

Nhấp đúp vào một chuỗi sẽ đưa bạn đến địa chỉ bộ nhớ của nó trong chế độ xem hợp ngữ ngược, cho phép bạn quan sát nơi nó được lưu trữ trong tệp nhị phân. Trong ví dụ này, chuỗi (URL) nằm ở địa chỉ bộ nhớ ảo "0x00404000" trong phần ".rdata", thường được dành riêng cho dữ liệu chỉ đọc.

#### X-Refs (Cross-References)

X-Refs hiển thị nơi một phần tử cụ thể (ví dụ: hàm, chuỗi hoặc biến) được sử dụng trong tệp nhị phân.

<figure><img src="/files/yuoBM1SjfYhc7kIBeUzG" alt=""><figcaption></figcaption></figure>

Trong ví dụ trên, cửa sổ X-Refs hiển thị rằng chuỗi nằm ở địa chỉ "0x00404000" (một URL) được tham chiếu tại "0x00401536" trong hàm "\_downloadFile()". Điều này giúp hiểu cách chương trình tương tác với các tài nguyên.

Như đã đề cập trước đó, các chuỗi thường là một điểm khởi đầu hiệu quả để phân tích phần mềm độc hại vì chúng có thể cung cấp thông tin chi tiết ngay lập tức về hành vi của tệp nhị phân. Các chuỗi như API, URL, đường dẫn tệp, địa chỉ IP hoặc lệnh đóng vai trò là điểm vào, hướng dẫn các nhà phân tích đến các phần có liên quan của mã. Bằng cách kiểm tra X-Refs, các nhà phân tích có thể theo dõi việc sử dụng các chuỗi này, khám phá thêm chi tiết về chức năng, kênh liên lạc và quy trình nội bộ của phần mềm độc hại.

### 3.5 Disassembly View

Để bắt đầu phân tích luồng chương trình, hãy chọn hàm "main" từ bảng "Functions" (Hàm) và điều hướng đến tab "Disassembly" (Hợp ngữ ngược). Chế độ xem hợp ngữ ngược trình bày các lệnh assembly cấp thấp, cung cấp cái nhìn sâu sắc về đường dẫn thực thi của tệp nhị phân.

<figure><img src="/files/S1zCDJF3liUFtxLgoR9G" alt=""><figcaption></figcaption></figure>

Trong Cửa sổ Hợp ngữ ngược:

* **Comments (Chú thích):** Các chú thích được thêm vào cung cấp thêm ngữ cảnh về mã.&#x20;
* **Memory Address (Địa chỉ bộ nhớ):** Mỗi hướng dẫn được liên kết với một địa chỉ bộ nhớ (ví dụ: 0x0040157b), giúp theo dõi trình tự thực thi mã.&#x20;
* **Operation (mnemonic) (Thao tác (mnemonic)):** Thao tác assembly (ví dụ: push, mov, call) chỉ định hành động được thực hiện, chẳng hạn như thao tác dữ liệu hoặc lệnh gọi hàm.&#x20;
* **Source and Destination Operands (Toán hạng nguồn và đích):** Các hướng dẫn như MOV bao gồm một toán hạng nguồn (dữ liệu sẽ được di chuyển) và một toán hạng đích (nơi dữ liệu sẽ được đặt).&#x20;
* **Control Flow Arrows (Mũi tên luồng điều khiển):** Các mũi tên màu minh họa các đường dẫn luồng điều khiển, cho biết cách thực thi nhảy hoặc phân nhánh giữa các phần mã khác nhau. Các đường dẫn này giúp hình dung các vòng lặp, điều kiện và lệnh gọi hàm trong chương trình.

### 3.6 Graph View

Chế độ xem Graph cung cấp một biểu diễn trực quan về luồng điều khiển của một hàm, giúp bạn dễ dàng hiểu cấu trúc và logic của chương trình. Khi bạn chọn hàm "main" và mở tab "Graph" (Đồ thị), Cutter sẽ sắp xếp mã thành một bố cục giống như sơ đồ luồng, trong đó các khối cơ bản được kết nối bằng các mũi tên. Hình ảnh trực quan này đặc biệt hữu ích để theo dõi các đường dẫn thực thi trong các tệp nhị phân phức tạp.

<figure><img src="/files/DacWYE8xX0hG8r70bLCs" alt=""><figcaption></figcaption></figure>

* **Basic Blocks (Khối cơ bản):** Mỗi hộp hình chữ nhật trong đồ thị đại diện cho một khối cơ bản, là một chuỗi các hướng dẫn với một điểm vào và điểm ra duy nhất.
* **Control Flow Arrows (Mũi tên luồng điều khiển)**: Các mũi tên màu cho biết cách luồng điều khiển giữa các khối. Mũi tên màu xanh lá cây đại diện cho các nhánh thành công, trong khi mũi tên màu đỏ cho biết các điều kiện không thành công.

### 3.7 Hexdump View

Chế độ xem Hexdump hiển thị biểu diễn thập lục phân thô của tệp nhị phân, cùng với tương đương ASCII và địa chỉ bộ nhớ của nó:

<figure><img src="/files/f2aDp8fKcZYiv5L9SU84" alt=""><figcaption></figcaption></figure>

* **Packed or Encrypted Sections (Các phần được đóng gói hoặc mã hóa)**: Chế độ xem Hexdump có thể giúp phát hiện các khu vực được đóng gói hoặc mã hóa bằng cách tiết lộ các mẫu byte không đều hoặc các phần có entropy cao. Các mẫu như vậy thường chỉ ra các nỗ lực làm rối mã hoặc che giấu chức năng độc hại.
* **Manual Inspection and Byte Modification (Kiểm tra thủ công và sửa đổi byte):** Các nhà phân tích có thể kiểm tra các chuỗi byte cụ thể theo cách thủ công hoặc thậm chí sửa đổi một số byte nhất định để kiểm tra các thay đổi trong hành vi của chương trình, hữu ích để khám phá cách tệp nhị phân phản hồi với dữ liệu bị thay đổi.

### 3.8 Decompiler View

<figure><img src="/files/jw3LJjiBqLvRk3o5nMTn" alt=""><figcaption></figcaption></figure>

Decompiler (Trình dịch ngược) dịch assembly cấp thấp hoặc mã máy trở lại ngôn ngữ cấp cao bằng cách suy ra các kiểu dữ liệu, luồng điều khiển và logic.

Tuy nhiên, do bản chất của quá trình biên dịch và tối ưu hóa:

* **Original Source Code Information is Lost (Thông tin mã nguồn gốc bị mất):** Tên biến, chú thích và một số thông tin cấu trúc bị xóa trong quá trình biên dịch, gây khó khăn cho trình dịch ngược trong việc tạo lại mã nguồn chính xác.
* **Control Flow and Functionality Gaps (Khoảng trống về luồng điều khiển và chức năng):** Trình dịch ngược có thể hiểu sai hoặc đơn giản hóa các câu lệnh luồng điều khiển, đặc biệt là với các nhánh hoặc nhảy phức tạp (ví dụ: câu lệnh goto hoặc lệnh gọi hàm động), dẫn đến sự không chính xác.

Mặc dù đầu ra dịch ngược là vô giá để hiểu logic tổng thể của một tệp nhị phân, nhưng nó là một sự gần đúng hơn là một bản sao chính xác của mã gốc. Những hạn chế này có nghĩa là Decompiler View nên được sử dụng cùng với các phương pháp phân tích khác, vì nó có thể không nắm bắt đầy đủ hoặc thậm chí có thể thay đổi một số hành vi nhất định của chương trình.

## 4 Interpreting Code Constructs

Trong phần này, chúng ta sẽ xem xét cách các cấu trúc mã C phổ biến được dịch sang ngôn ngữ assembly. Vì một lượng đáng kể phần mềm độc hại được viết bằng các ngôn ngữ cấp cao, nên việc nhận ra các mẫu này trong assembly sẽ vô cùng quý giá để hiểu hành vi của phần mềm độc hại.

### 4.1 Global and Local Variables

Trong ngôn ngữ C, các biến toàn cục và cục bộ được khai báo tương tự, nhưng cách biểu diễn của chúng trong assembly khác nhau đáng kể do cách chúng được lưu trữ và truy cập trong bộ nhớ.

#### Local Variable

<figure><img src="/files/XvaXPv4dYYugirBIOYSJ" alt=""><figcaption></figcaption></figure>

Các biến cục bộ được xác định trong các hàm và chỉ có thể truy cập được trong hàm cụ thể đó. Trong assembly, các biến này được lưu trữ trên stack, với địa chỉ của chúng tương ứng với con trỏ cơ sở (thanh ghi EBP).

Trong chương trình C ví dụ trên, chúng ta có hai biến cục bộ: "`attack_count`" và "`stealth_factor`". Khi chúng ta phân tích mã disassembled, các nhận xét (được đánh dấu màu hồng) cho biết nơi các biến này được lưu trữ trong stack.

Trình disassembler sử dụng các nhãn như "**`var_*`**" để biểu thị các biến cục bộ này:

* "`var_8h`" tương ứng với "`attack_count`", được lưu trữ tại "`stack - 0x8 ([ebp - 0x8])`"
* "`var_ch`" tương ứng với "`stealth_factor`", được lưu trữ tại "`stack - 0xC ([ebp - 0xC])`"

Các nhãn này cho phép các kỹ sư đảo ngược xác định và theo dõi các biến cục bộ mà không cần phải tính toán lại độ lệch stack nhiều lần. Thay vì liên tục tính toán độ lệch bộ nhớ như "`[ebp - 0xC]`", trình hợp ngữ ngược cung cấp một nhãn (như "`var_ch`") để biểu thị vị trí bộ nhớ, đơn giản hóa quá trình phân tích.

Các hướng dẫn sau gán giá trị cho các biến này trong bộ nhớ:

* `mov dword [var_8h], 5`&#x20;
* `mov dword [var_ch], 3`

Các hướng dẫn này được sử dụng để gán các giá trị "5" và "3" cho các biến cục bộ "`var_8h`" và "`var_ch`" tại độ lệch bộ nhớ tương ứng của chúng ("`[ebp-0x8]`" và "`[ebp-0xC]`").

#### Global Variable

<figure><img src="/files/T4Cpw6t5mB02zlHMtdNP" alt=""><figcaption></figcaption></figure>

Các biến toàn cục được khai báo bên ngoài các hàm và có thể được truy cập bởi bất kỳ hàm nào trong chương trình. Trong assembly, các biến này được lưu trữ tại các địa chỉ bộ nhớ cố định thay vì trên stack, giúp chúng có thể truy cập được trong toàn bộ chương trình.

* Hướng dẫn đầu tiên "`mov edx, dword [data.00403004]`" tải giá trị được lưu trữ tại địa chỉ bộ nhớ "`0x403004`" vào thanh ghi EDX. Địa chỉ này tương ứng với biến toàn cục đó.&#x20;
* Tương tự, "`mov eax, dword [data.00403008]`" tải giá trị từ địa chỉ bộ nhớ "`0x403008`" vào thanh ghi EAX, đại diện cho một biến toàn cục khác.

Không giống như các biến cục bộ, các biến toàn cục này được truy cập trực tiếp thông qua các địa chỉ bộ nhớ cố định của chúng ("`data.0x403004`" và "`data.0x403008`") mà không cần tham chiếu đến stack. Sự khác biệt này nhấn mạnh rằng các biến toàn cục có thể được sửa đổi hoặc đọc bởi bất kỳ hàm nào trong chương trình, vì chúng tồn tại bên ngoài phạm vi của các hàm riêng lẻ.

### 4.2 Arithmetic Operations

<figure><img src="/files/Q2kPL7F0HteqSUbJkTHe" alt=""><figcaption></figcaption></figure>

Trong ví dụ này, các biến "`a`" và "`b`" là các biến cục bộ được lưu trữ trên stack. Trình hợp ngữ ngược đã gắn nhãn cho các biến này là "`var_8h`" và "`var_ch`" tương ứng, trong đó "`var_8h`" tương ứng với "`a`" và "`var_ch`" tương ứng với "`b`".

**Khởi tạo biến (1 & 2)**&#x20;

* Giá trị "`10`" được gán cho "`var_8h`" (tương ứng với "`a`").&#x20;
* Giá trị "`20`" được gán cho "`var_ch`" (tương ứng với "`b`").&#x20;
* Biến "`c`" (được biểu diễn là "`var_10h`") sẽ được sử dụng để lưu trữ kết quả của các phép toán số học tiếp theo.

**Phép cộng (3)**&#x20;

* Để thực hiện phép cộng, giá trị của "a" ("var\_8h") được chuyển vào thanh ghi EDX, trong khi giá trị của "b" ("var\_ch") được tải vào thanh ghi EAX.&#x20;
* Hướng dẫn "`add`" được sử dụng để cộng nội dung của EDX (giá trị của "a") vào EAX (giá trị của "b"), lưu trữ kết quả trong EAX.&#x20;
* Kết quả của EAX (hiện là "a + b") sau đó được lưu trữ trong "`var_10h`", tương ứng với biến "`c`" trong chương trình C.

**Phép chia (6)**&#x20;

* Giá trị của "b" được tải vào EAX và hướng dẫn "`cdq`" được sử dụng để mở rộng dấu của EAX vào thanh ghi EDX. Điều này thiết lập cặp thanh ghi "`edx:eax`" để chia, trong đó EDX sẽ giữ 32 bit bậc cao và EAX sẽ giữ 32 bit bậc thấp của số bị chia.&#x20;
* Hướng dẫn "`idiv`" chia "`edx:eax`" cho giá trị của "a" (từ "`var_8h`"). Thương của phép chia được lưu trữ trong EAX và số dư được lưu trữ trong EDX.&#x20;
* Thương (kết quả của phép chia) sau đó được lưu vào "`var_10h`" (biến "c").

**Các phép toán khác (4, 5 & 7)**&#x20;

* Các phép toán trừ (c = a - b), nhân (c = a \* b) và Modulus (c = b % a) tuân theo một mẫu tương tự:
  * Giá trị của "a" và "b" được tải vào các thanh ghi EAX và EDX.&#x20;
  * Hướng dẫn số học tương ứng (sub, imul hoặc idiv) được thực thi.&#x20;
  * Kết quả sau đó được lưu trữ trong "`var_10h`" (tương ứng với "c").

### 4.3 If Statements

<figure><img src="/files/4TetdKDZ2L3QfNqkqmoO" alt=""><figcaption></figcaption></figure>

Ví dụ này minh họa cách một câu lệnh điều kiện if-else cơ bản từ C được dịch thành các hướng dẫn assembly.

**\[1] Khởi tạo biến**&#x20;

* Hướng dẫn "`mov dword [var_8h], 1`" đặt giá trị của biến "`virtual_machine`" thành "1".
* Ở đây, "`[var_8h]`" là biến stack cục bộ tương ứng với "`virtual_machine`".

**\[2] Bộ phận ra quyết định**

* Hướng dẫn CMP so sánh giá trị của "`virtual_machine`" (là "1") với "0". Điều này tương ứng với điều kiện "`if (virtual_machine == 0)`" trong mã C ở trên.
* Hướng dẫn so sánh được sử dụng để đặt các cờ điều kiện, sau đó được đánh giá bởi một hướng dẫn nhảy tiếp theo để xác định đường dẫn thực thi tiếp theo dựa trên kết quả so sánh.
* JNE là viết tắt của Jump if Not Equal (Nhảy nếu không bằng). Hướng dẫn này sẽ nhảy đến địa chỉ "`0x40155b`" nếu "`virtual_machine`" không bằng 0 (`ZF = 0`).
* Trong ví dụ này, vì "`virtual_machine`" là 1 (không phải 0), JNE sẽ nhảy đến địa chỉ "`0x40155b`", bỏ qua khối mã được liên kết với câu lệnh if.

**\[3] Nếu đúng**&#x20;

* Khối này thực thi "`if virtual_machine == 0`", nhưng nó bị bỏ qua trong ví dụ này.
* Nếu điều kiện là đúng, những điều sau sẽ xảy ra:
  * Hướng dẫn mov sẽ chuẩn bị chuỗi "`virtual_machine is 0.\nEncryption begins!`" để in bằng cách đẩy nó lên stack (\[esp]).&#x20;
  * Hướng dẫn "`Call`" sẽ gọi hàm printf để in chuỗi đã chuẩn bị.&#x20;
  * Hướng dẫn `JMP` sẽ bỏ qua khối "else" và nhảy đến cuối hàm tại địa chỉ "`0x401567`".

**\[4] Else**&#x20;

* Nếu JNE nhảy đến địa chỉ "`0x40155b`" (điều này xảy ra trong ví dụ này vì biến virtual\_machine là 1):
* Nó chuẩn bị chuỗi "`virtual_machine is 1.\nSelf Destruct!`" để in bằng cách đẩy nó lên stack. Gọi hàm printf để in thông báo "`virtual_machine is 1.\nSelf Destruct!`".

**Biểu diễn dạng đồ thị**: Chúng ta có thể xem cùng một mã ở dạng đồ thị, biểu thị các quyết định phân nhánh do chương trình đưa ra, giúp dễ dàng hình dung các đường dẫn có điều kiện hơn:

<figure><img src="/files/ebfaP8QVYJfi1r55dFEg" alt=""><figcaption></figcaption></figure>

Trong hình minh họa này, các mũi tên cho biết luồng thực thi dựa trên các điều kiện khác nhau.

* Mũi tên màu xanh lá cây (Nhánh đúng): Cho biết đường dẫn được thực hiện khi JNE đánh giá là đúng, có nghĩa là "virtual\_machine" không phải là zero.&#x20;
* Mũi tên màu đỏ (Nhánh sai): Đại diện cho đường dẫn cho điều kiện "`virtual_machine == 0`", dẫn đến khối "if".

### 4.4 Loops

<figure><img src="/files/im5A51ARVnWfhNUcUQQN" alt=""><figcaption></figcaption></figure>

Ví dụ này cho thấy cách vòng lặp "for" từ C được dịch thành mã assembly, sử dụng các hướng dẫn so sánh và nhảy để kiểm soát việc thực thi vòng lặp.

**Khởi tạo và kiểm tra điều kiện**&#x20;

* Giá trị ban đầu của bộ đếm vòng lặp ("`var_8h`") được đặt thành "1", khớp với câu lệnh khởi tạo "int i = 1" trong mã C.&#x20;
* Bộ đếm vòng lặp ("`var_8h`") được so sánh với "5" bằng cách sử dụng hướng dẫn CMP. Điều này tương ứng với kiểm tra điều kiện vòng lặp "for" ("`i <= 5`") trong C.&#x20;
* Nếu bộ đếm vượt quá "5", hướng dẫn JG (Jump if Greater - Nhảy nếu lớn hơn) sẽ phá vỡ vòng lặp bằng cách nhảy đến địa chỉ "`0x401568`", thoát khỏi vòng lặp.

**Thân vòng lặp**&#x20;

* Nếu điều kiện được đáp ứng (bộ đếm không lớn hơn 5), thân vòng lặp sẽ được thực thi. Giá trị hiện tại của bộ đếm được in bằng cách gọi `printf`, hiển thị giá trị của bộ đếm.

**Tăng bộ đếm và tiếp tục vòng lặp**&#x20;

* Sau khi in, bộ đếm được tăng thêm "1" bằng cách sử dụng hướng dẫn ADD.&#x20;
* Một hướng dẫn JMP vô điều kiện sau đó nhảy trở lại hướng dẫn so sánh, tạo thành một cấu trúc vòng lặp.&#x20;
* Vòng lặp này tiếp tục cho đến khi bộ đếm vượt quá "5", sau đó vòng lặp thoát đến địa chỉ "0x401568".

#### Graph View Representation

<figure><img src="/files/u0xujkgih4ieTg5cRNmM" alt=""><figcaption></figcaption></figure>

Trong Chế độ xem Đồ thị, cấu trúc của vòng lặp được biểu diễn trực quan:

* (A): Cho biết Khởi tạo Bộ đếm.&#x20;
* (B): Đại diện cho Khối so sánh và ra quyết định.&#x20;
* (C): Hiển thị Khối Thân vòng lặp nơi các hoạt động cốt lõi diễn ra.&#x20;
* (D): Đánh dấu Khối thoát, nơi vòng lặp kết thúc.

## 5 Sự phức tạp của hàm trong Assembly

Một hàm trong điện toán là một khối mã khép kín được thiết kế để thực hiện một tác vụ cụ thể. Khi sử dụng một hàm, có ba yếu tố chính liên quan:

* **Đối số**: Đây là dữ liệu được truyền vào hàm, thường thông qua các thanh ghi hoặc được đẩy lên stack bởi mã gọi. Các đối số cung cấp thông tin cần thiết để hàm hoạt động hiệu quả.&#x20;
* **Thân hàm**: Phần này chứa mã định nghĩa những gì hàm thực hiện. Nó bao gồm logic và các bước sẽ được thực thi để hoàn thành tác vụ mong muốn bằng cách sử dụng các đối số được cung cấp.
* **Giá trị trả về**: Sau khi hàm hoàn thành việc thực thi, nó thường đặt kết quả vào một thanh ghi (thường là EAX trong kiến trúc x86) để mã gọi có thể truy cập đầu ra của hàm.

Khi một hàm được gọi, CPU sử dụng một hướng dẫn nhảy để phân nhánh đến mã của hàm. Trước khi nhảy, nó lưu địa chỉ hiện tại (địa chỉ trả về) trên stack. Sau khi hàm thực thi, CPU truy xuất địa chỉ này để tiếp tục xử lý từ nơi nó đã dừng lại.

### 5.1 Example Function

<figure><img src="/files/NkPtkTL278Wracj7MCQA" alt=""><figcaption></figcaption></figure>

Chương trình này minh họa một hàm đơn giản in chi tiết của nhân viên (tên, tuổi và ID) bằng cách sử dụng hàm "`EmployeeDetails`". Hàm "`EmployeeDetails`" nhận một chuỗi (cho tên) và hai số nguyên (cho tuổi và ID), sau đó in chúng theo định dạng.

**Biểu diễn Assembly của hàm EmployeeDetails**

<figure><img src="/files/UFzmcW8yEBWZikMCDhCP" alt=""><figcaption></figcaption></figure>

Mã đã hợp ngữ ngược tương ứng cho hàm "EmployeeDetails" minh họa cách hoạt động này được triển khai ở cấp độ assembly. Hãy đi sâu vào các phần khác nhau của mã đã hợp ngữ ngược để hiểu sâu hơn về cách mỗi phần hoạt động.

**Prologue (Khúc dạo đầu)**

* Prologue bao gồm một tập hợp các hướng dẫn được thực thi khi bắt đầu một hàm. Mục đích chính của nó là thiết lập môi trường của hàm bằng cách tạo một khung stack để nó hoạt động.
* Khung stack là một phần của bộ nhớ stack chứa tất cả thông tin cần thiết để một hàm thực thi.

<figure><img src="/files/aBagiJ7OSHOFRtLVL1Kz" alt=""><figcaption></figcaption></figure>

* Lưu con trỏ cơ sở cũ (EBP): Giá trị hiện tại của con trỏ cơ sở (EBP) được đẩy lên stack. Hành động này bảo toàn khung stack trước đó, cho phép hàm gọi khôi phục nó sau này.&#x20;
* Thiết lập Con trỏ Cơ sở Mới: Con trỏ cơ sở được cập nhật thành con trỏ stack hiện tại (ESP), cho biết sự bắt đầu của khung stack của hàm mới. Bản cập nhật này đơn giản hóa việc tham chiếu các đối số hàm và các biến cục bộ trong suốt hàm.&#x20;
* Cấp phát không gian cho các biến cục bộ: Con trỏ stack được điều chỉnh (thường bằng cách trừ đi một giá trị được chỉ định) để dành không gian cho các biến cục bộ của hàm. Việc cấp phát này đảm bảo rằng hàm có bộ nhớ cần thiết để lưu trữ dữ liệu tạm thời trong quá trình thực thi.

**Epilogue (Khúc kết)**

* Epilogue bao gồm một tập hợp các hướng dẫn được thực thi ở cuối một hàm. Trách nhiệm chính của nó là dọn dẹp khung stack và trả quyền điều khiển cho người gọi.

<figure><img src="/files/bhiR1slyT9g3OAwnSyRA" alt=""><figcaption></figcaption></figure>

* **Deallocate Local Variables**: Con trỏ stack (ESP) được khôi phục về trạng thái ban đầu, giải phóng không gian được sử dụng bởi các biến cục bộ.
* **Restore Registers and Clean Up the Stack Frame**: Con trỏ cơ sở (EBP) được lấy ra khỏi stack, khôi phục khung stack trước đó.
* **Return to the Caller**: Địa chỉ trả về (được lưu trữ trên stack) được lấy ra và hướng dẫn RET được thực thi, chuyển quyền điều khiển trở lại hàm gọi.

Hướng dẫn LEAVE là viết tắt của hai hành động:

* `mov esp, ebp` (khôi phục con trỏ stack về giá trị ban đầu)&#x20;
* `pop ebp` (khôi phục con trỏ cơ sở về trạng thái trước đó)

RET là viết tắt của:

* `pop eip` (lấy địa chỉ trả về từ stack)

### 5.2 Call Conventions

Quy ước gọi hàm (Function call conventions) xác định cách các hàm nhận đối số, trả về giá trị và quản lý tài nguyên như stack và thanh ghi trong quá trình gọi hàm. Các quy ước này đảm bảo sự tương tác thích hợp giữa các hàm, ngay cả khi được biên dịch bởi các trình biên dịch khác nhau hoặc tuân thủ các tiêu chuẩn khác nhau. Các quy ước gọi phổ biến nhất trong assembly bao gồm `cdecl`, `stdcall` và `fastcall`.

**Argument Passing (Truyền Đối số)**&#x20;

* **cdecl và stdcall**: Trong cả hai quy ước này, các đối số được đẩy lên stack từ phải sang trái. Điều này có nghĩa là đối số cuối cùng được đẩy trước, cho phép hàm truy cập các đối số theo đúng thứ tự.&#x20;
* **fastcall**: Quy ước này tối ưu hóa việc truyền đối số bằng cách sử dụng các thanh ghi (thường là ECX và EDX) cho một vài tham số đầu tiên. Bất kỳ đối số còn lại nào được truyền trên stack, kết hợp lợi ích của cả hai phương pháp để gọi hiệu quả.

**Stack Cleanup (Dọn dẹp Stack)**

* **cdecl**: Người gọi chịu trách nhiệm dọn dẹp stack sau khi gọi hàm. Điều này cho phép linh hoạt hơn, đặc biệt với các hàm có đối số thay đổi.
* **stdcall và fastcall**: Người được gọi (hàm được gọi) xử lý việc dọn dẹp stack. Điều này đơn giản hóa mã của người gọi nhưng yêu cầu hàm tuân thủ nghiêm ngặt quy ước.

Trong cả ba quy ước, giá trị trả về được lưu trữ trong thanh ghi EAX. Cdecl thường được sử dụng trong các thư viện C và cho các hàm có đối số thay đổi, stdcall chủ yếu được sử dụng bởi Windows API và fastcall được sử dụng trong các tình huống nhạy cảm về hiệu suất để giảm thiểu chi phí gọi hàm.

**Right to Left Argument Passing (Truyền đối số từ phải sang trái)**&#x20;

Trong quy ước truyền đối số từ phải sang trái, các đối số hàm được đẩy lên stack theo thứ tự ngược lại. Điều này có nghĩa là đối số cuối cùng được đẩy trước, tiếp theo là đối số áp chót, v.v., cho đến khi đối số đầu tiên được đẩy cuối cùng.

Ví dụ: xét hàm "EmployeeDetails(char \*name, int age, int id)". Trong trường hợp này, đối số id sẽ được đẩy lên stack trước age hoặc name.

<figure><img src="/files/dh1pVlz43tTSTKKxFPbn" alt=""><figcaption></figcaption></figure>

Trong hình trên, bạn có thể thấy rằng đối số cuối cùng (1) được đẩy lên stack trước, tiếp theo là đối số áp chót (44) và cuối cùng là đối số đầu tiên (LetsDefend).

<figure><img src="/files/ME5mP26B34mfbRj1tUCe" alt=""><figcaption></figcaption></figure>

(Stack)

Thứ tự này đảm bảo rằng khi hàm truy cập các đối số của nó thông qua stack, đối số đầu tiên (cho "name") ở địa chỉ cao nhất, tiếp theo là "age" và sau đó là "id". Điều này hoàn toàn phù hợp với cách hàm mong đợi truy cập các đối số này dựa trên các offset stack của chúng.

### 5.3 MOV vs PUSH

Trong ngôn ngữ assembly, các đối số hàm thường được đặt trên stack trước khi gọi hàm. Có hai phương pháp chính để đạt được điều này: sử dụng hướng dẫn PUSH hoặc hướng dẫn MOV. Mỗi phương pháp có các đặc điểm và ý nghĩa riêng biệt đối với cách stack được quản lý trong quá trình gọi hàm.

#### MOV

Hướng dẫn MOV cung cấp khả năng kiểm soát trực tiếp nơi đặt dữ liệu, nhưng nó không tự động điều chỉnh con trỏ stack. Điều này có nghĩa là khi hướng dẫn MOV được sử dụng để đặt các đối số lên stack, con trỏ stack (ESP) phải được điều chỉnh thủ công trước bằng cách sử dụng các hướng dẫn SUB hoặc ADD để dành không gian cần thiết.

<figure><img src="/files/U9o50V6dPw8DMTFOuSsD" alt=""><figcaption></figcaption></figure>

* Điều chỉnh con trỏ stack thủ công: Hướng dẫn SUB thường được sử dụng để dành không gian cần thiết cho các đối số.&#x20;
* Lưu trữ giá trị: Các đối số được đặt tại địa chỉ bộ nhớ thích hợp so với con trỏ stack đã điều chỉnh.

#### PUSH

Không giống như MOV, hướng dẫn PUSH tự động giảm con trỏ stack (ESP), di chuyển nó xuống theo kích thước của toán hạng (thường là 4 byte cho kiến trúc 32 bit). Toán hạng (đối số) sau đó được lưu trữ tại vị trí mới được trỏ bởi con trỏ stack.

<figure><img src="/files/XudxJNb1lYF1RDqUZl9i" alt=""><figcaption></figcaption></figure>

Sự lựa chọn giữa MOV và PUSH để truyền đối số cuối cùng phụ thuộc vào trình biên dịch được sử dụng để biên dịch chương trình. Các trình biên dịch khác nhau có thể tạo ra các hướng dẫn assembly khác nhau dựa trên các cài đặt tối ưu hóa và quy ước gọi. Hiểu cả hai phương pháp là điều cần thiết đối với các nhà phân tích phần mềm độc hại, vì nó cho phép diễn giải chính xác mã assembly.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://viettaliii.gitbook.io/home/mal/static-code-analysis.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
