Với chủ đề Phát hiện cấu trúc Code C trong hợp ngữ, tôi sẽ trình bày một số cấu trúc hợp ngữ cơ bản nhất tương ứng với các cấu trúc điều khiển trên C/C++, một số loại cấu trúc dữ liệu, quy ước gọi hàm, làm việc với một số hàm mã hóa/giải mã mức cơ bản.
Bài viết này được xây dựng dựa trên các mã nguồn mức đơn giản nhất để bạn đọc nắm được bản chất chính của chương trình. Trong thực tế, các chương trình thường rất phức tạp, chứa nhiều mã rác hoặc bản thân trình biên dịch cũng tự sinh thêm các mã lệnh rác nên thực tế khi phân tích sẽ gặp nhiều khó khăn hơn. Chưa kể đến việc mã độc có các phương pháp chống lại quá trình phân tích dịch ngược.
Tuy nhiên, tôi đánh giá việc hiểu được giữa ngôn ngữ máy và ngôn ngữ bậc cao là rất quan trọng trong quá trình phân tích mã độc. Do đó, bài viết này giúp các bạn có một góc nhìn cơ bản để làm bàn đạp cho các phân tích sâu hơn sau này.
Bạn tò mò muốn tìm hiểu về chuyên đề Giải mã mật khẩu, XEM THÊM: Giải mã mật khẩu – Phần 1: Các Nguyên Lý và Kĩ Thuật
Biến toàn cục và biến cục bộ
Các biến toàn cục có thể được truy cập và sử dụng bởi bất kỳ hàm nào trong chương trình. Biến cục bộ chỉ có thể được truy cập và sử dụng bởi hàm mà nó được định nghĩa trong đó. Cả biến toàn cục và biến cục bộ đều được khai báo như nhau trong C, nhưng chúng lại hoàn toàn khác nhau trong hợp ngữ.
Dưới đây là hai ví dụ về code C cho cả biến toàn cục và biến cục bộ. Biến toàn cục được định nghĩa bên ngoài hàm và biến cục bộ được định nghĩa bên trong hàm.
Khác biệt giữa biến toàn cục và biến cục bộ trong hai đoạn code C này là rất nhỏ và trong trường hợp này, hai chương trình cho cùng một kết quả. Nhưng trong hợp ngữ, chúng khác nhau rất nhiều. Các biến toàn cục được tham chiếu bởi các địa chỉ nhớ trong khi các biến cục bộ tham chiếu bởi địa chỉ stack.
Ở đoạn Code 1, biến toàn cục x được biểu thị bởi cs:x, ô nhớ có địa chỉ cs:x.
Ở đoạn Code 2, biến cục bộ x được đặt trong stack và tham chiếu bởi rbp qua giá trị offset đã được khai báo ngay đầu hàm. Trong Code 2, địa chỉ nhớ [rbp+var_4] được dùng nhất quán xuyên suốt hàm để tham chiếu biến x. Điều này cho thấy [rbp+var_4] là biến cục bộ stack (stack-based local variable) và chỉ được tham chiếu bên trong hàm mà nó được định nghĩa.
Việc hiểu được biến cục bộ và toàn cục có ý nghĩa quan trọng trong quá trình phân tích mã độc. Các biến toàn cục có thể được sử dụng như các cờ hoặc lưu trữ thông tin phục vụ quá trình lây nhiễm và thu thập thông tin.
Dịch ngược các mảng
Mảng được dùng để định nghĩa một tập các đối tượng dữ liệu tương tự nhau, theo một thứ tự nhất định.
Code 8 minh họa một chương trình sử dụng hai mảng, cả hai đều được gán giá trị trong vòng lặp for. Mảng a được định nghĩa cục bộ, mảng b được định nghĩa một cách toàn cục. Các định nghĩa này sẽ khác trong hợp ngữ.
Trong hợp ngữ, các mảng được truy cập bằng địa chỉ cơ sở trỏ tới vị trí đầu tiên. Kích thước của mỗi phần tử không phải luôn rõ ràng nhưng có thể được xác định qua quan sát cách mà mảng được đánh chỉ mục.
Trong đoạn code trên, địa chỉ cơ sở của mảng b được tham chiếu chéo từ .data:0x403010 và địa chỉ cơ sở của mảng a là [rbp+0+var_20]. Vì cả hai đều là các mảng số nguyên, mỗi phần tử có kích thước 4 byte mặc dù các lệnh tại dòng 0x401597 và là khác nhau về cách truy cập tới mỗi mảng. Trong cả hai trường hợp, rax được dùng như chỉ số, nó được nhân với 4, là kích thước mỗi phần tử. Giá trị địa chỉ được cộng vào địa chỉ cơ sở của mảng để truy cập đúng phần tử thích hợp.
Xác định các Struct
Các struct (structure) trong C tương tự như các mảng, nhưng chúng bao gồm các phần tử với kiểu khác nhau. Đôi khi dùng struct sẽ đơn giản hơn là sử dụng nhiều biến độc lập, đặc biệt là nếu nhiều hàm cùng yêu cầu truy cập tới một nhóm các biến. (Các hàm WinAPI thường xuyên sử dụng các struct được tạo và duy trì bởi chương trình gọi đến – callee.)
Trong Code 9, ta định nghĩa struct tại dòng 5 cho một mảng số nguyên, một biến char và một biến kiểu double. Trong hàm main, ta cấp phát bộ nhớ cho struct và truyền nó tới hàm test. Struct gms khai báo tại dòng 10 được coi như một biến toàn cục.
Các struct (cũng như các mảng) được truy cập bằng một địa chỉ cơ sở dùng như một con trỏ khởi đầu. Rất khó để xác định các kiểu dữ liệu kề cận có cùng là một phần của struct hay không (hay là thành phần của một struct khác hoặc các biến đơn). Tùy từng bối cảnh, khả năng phát hiện ra struct có thể ảnh hưởng đáng kể đến khả năng phân tích đúng một mã độc.
Ta xem xét hàm main trong Code 9 khi nó được dịch ngược. Vì struct gms là một biến toàn cục, địa chỉ cơ sở của nó là địa chỉ nhớ .bss:0x407970. Giá trị của gms (bản chất là địa chỉ vùng nhớ trỏ bởi gsm) được truyền vào hàm test bằng lệnh mov rcx, rax tại dòng 0x4015ED.
Nhìn vào hàm test trong Code 9, [rbp+arg_0] là địa chỉ cơ sở của struct. Offset 0x14 lưu biến char trong struct vì 61h tương ứng với ký tự a trong ASCII (dòng 0x401580).
Có thể chỉ ra offset 0x18 là một biến double khi nhìn vào lệnh movsd. Ta cũng có thể chỉ ra các số nguyên được chuyển vào các offset 0, 4, 8, 0x10 và 0xC khi nhìn vào vòng lặp for và nơi các offset này được truy cập tại dòng 0x4015AB. Ta có thể suy luận nội dung của struct từ phân tích này.
Việc phát hiện ra Struct giúp quá trình phân tích mạch lạc và rõ ràng hơn, các code rác cũng dễ dàng được loại bỏ giúp việc phân tích đơn giản và thuận lợi hơn.
Phân tích danh sách liên kết
Danh sách liên kết là một cấu trúc dữ liệu gồm một chuỗi tuần tự các bản ghi dữ liệu, mỗi bản ghi chứa một trường lưu thông tin tham chiếu (liên kết) tới bản ghi tiếp theo trong chuỗi. Ưu điểm của danh sách liên kết so với mảng là thứ tự của các đối tượng được liên kết có thể khác với thứ tự mà dữ liệu của các đối tượng đó được lưu trong bộ nhớ hay trên ổ đĩa. Vì thế, danh sách liên kết cho phép chèn và xóa các node tại bất kì điểm nào trong danh sách.
Code 10 trình bày một ví dụ danh sách liên kết trong C và đường đi của nó. Danh sách liên kết này chứa một chuỗi các node được đặt tên là pnode và nó được quản lý với hai vòng lặp. Vòng lặp thứ nhất tại dòng 14 tạo 10 node và truyền dữ liệu vào chúng. Vòng lặp thứ hai tại dòng 23 duyệt tất cả các bản ghi và in nội dung của chúng ra màn hình.
Cách tốt nhất để hiểu đoạn code hợp ngữ trên là xác định hai khối cấu trúc code trong hàm main.
Đầu tiên ta xác dịnh vòng lặp for. var_14 tương ứng với biến i, là biến đếm của vòng lặp. var_10 chính là biến head và var_8 là biến curr. var_8 là con trỏ trỏ tới một struct gồm hai biến (tại dòng 0x4015A3 và dòng 0x4015AD).
Vòng lặp while (loc_4015CD và loc_4015ED) thực hiện lặp trên danh sách liên kết. Trong vòng lặp, var_8 được gán bản ghi liền kề tại dòng 0x4015E9.
Để phát hiện danh sách liên kết, đầu tiên ta phải phát hiện một vài đối tượng chứa con trỏ trỏ tới một đối tượng khác cùng kiểu. Tính đệ quy của đối tượng khiến nó được liên kết và đây chính là điều ta cần phát hiện ra trong hợp ngữ.
Trong ví dụ này, nhận thấy tại dòng 0x4015E9, var_8 được gán giá trị rax mà rax là kết quả từ rax+8 trước đó (dòng 0x4015E5), lại là giá trị được gán từ var_8 (dòng 0x4015E1). Như thế có nghĩa là struct var_8 cũng phải chứa một con trỏ 4 byte bên trong nó. Con trỏ này trỏ tới một struct khác cũng chứa một con trỏ 4 byte trỏ tới một struct khác nữa, và cứ thế, đệ quy để thực hiện duyệt toàn bộ danh sách.
Thực ra, bản chất vẫn được xây dựng trên nhóm lệnh so sánh, lệnh gán và lệnh nhảy có điều kiện. Việc phát hiện ra danh sách liên kết cũng tương tự như phát hiện ra Struct, đều hỗ trợ quá trình phân tích đơn giản, nhanh chóng và thuận lợi hơn.
Theo dõi tiếp Phần 2 của chuyên đề bằng cách đăng ký nhận tin TẠI ĐÂY.
Theo Chuyên gia an ninh mạng: Nguyễn Việt Anh