Malware Analysis

Một trong những điều kiện cần khi bắt đầu nghiên cứu, phân tích mã độc là phải có kiến thức về ngôn ngữ assembly cũng như hiểu rõ cơ chế của thanh ghi, ngăn xếp. Sau đây tôi sẽ giới thiệu đến các bạn về một số kiến thức cơ bản cần lưu ý:

1. Thanh ghi (32 bit)

Khái niệm: Thanh ghi là một phần quan trọng của bộ vi xử lý (CPU), nó được sử dụng để lưu trữ và xử lý các giá trị trung gian trong quá trình thực hiện các lệnh của chương trình.

Các thanh ghi:

  • Thanh ghi đa năng
  • Thanh ghi điều khiển
  • Thanh ghi đoạn

Thanh ghi đa năng:

Các thanh ghi đa năng có thể được chia làm 3 nhóm:

  • Nhóm thanh ghi dữ liệu
  • Nhóm thanh ghi con trỏ
  • Nhóm thanh ghi chỉ số

Thanh ghi dữ liệu:

  • EAX: thanh ghi tích lũy. Thường được dùng trong nhập xuất và các lệnh tính toán số học.
  • EBX: thanh ghi cơ sở. Thường được dùng để đánh dấu địa chỉ, lưu địa chỉ bắt đầu của 1 mảng.
  • ECX: thanh ghi đếm. Thường được dùng trong vòng lặp, đếm số lần lặp.
  • EDX: thanh ghi dữ liệu. Thường được sử dụng trong nhập xuất dữ liệu như EAX.

Mỗi thanh ghi sẽ được chia thành các đoạn nhỏ hơn như trên: AH (8 bit) + AL (8 bit) = AX (16 bit). Trong đó AH chứa phần bit cao và AL chứa phần bit thấp của thanh ghi AX. AX chứa phần bit thấp của thanh ghi 32 bit EAX.

Thanh ghi con trỏ:

Có 3 thanh ghi con trỏ là EIP, ESP, EBP. Ba thanh ghi này cũng được chia nhỏ ra ba thanh ghi 16 bit tương ứng là IP (Instruction Pointer), SP (Stack Pointer), BP (Base Pointer).

  • EIP và IP: trỏ tới địa chỉ chứa lệnh tiếp theo sẽ được thực thi.
  • ESP và SP: trỏ tới đỉnh hiện thời của stack.
  • EBP và BP: thường dùng để tham chiếu đến các biến tham số sử dụng trong chương trình con.

Thanh ghi chỉ số:

Các thanh ghi chỉ số (Index Register) thường được dùng để đánh chỉ số cho các địa chỉ của mảng, xâu. Đôi khi sẽ được sử dụng trong các phép tính toán học. ESI và EDI cũng được chia thành các thanh ghi 16 bit SI và DI.

  • ESI và SI (Source Index) được sử dụng làm địa chỉ nguồn cho các phép toán với xâu.
  • ESI và SI (Source Index) được sử dụng làm địa chỉ nguồn cho các phép toán với xâu.

Thanh ghi điều khiển:

Khái niệm: Thanh ghi con trỏ lệnh 32 bit và thanh ghi cờ 32 bit (Flags Register) kết hợp với nhau gọi là thanh ghi điều khiển.

Các bit cờ phổ biến:

  • Cờ tràn (Overflow Flag – OF) bằng 1 thì kết quả của phép toán có dấu lớn hơn so với kích thước của địa chỉ đích.
  • Cờ hướng (Direction Flag – DF) xác định hướng trái hay phải cho việc di chuyển hoặc so sánh chuỗi dữ liệu. Khi giá trị DF bằng 0, chuỗi hoạt động lấy từ trái qua phải và ngược lại khi DF bằng 1.
  • Cờ ngắt (Interupt Flag – IF) xác định khi nào các ngắt ngoài như nhập dữ liệu từ bàn phím được xử lý. Khi IF bằng 1 thì tín hiệu ngắt sẽ được xử lý, ngược lại thì bỏ qua.
  • Cờ dừng (Trap Flag – TF – chẳng biết dịch thành từ gì cho sát nghĩa nhất) hỗ trợ thực thi chương trình theo Single-step mode. Trình Debug chúng ta thường sử dụng sẽ đặt giá trị cho TF, nhờ đó chúng ta có thể thực thi từng lệnh một.
  • Cờ dấu (Sign Flag – SF) cho biết kết quả của phép toán số học là âm hay dương. Giá trị của SF tùy thuộc vào giá trị của bit ngoài cùng bên trái (High order bit – Most significant bit). SF bằng 1 nếu kết quả của phép toán số học là một giá trị âm.
  • Cờ không (Zero Flag – ZF) thể hiện kết quả của phép toán số học hoặc phép so sánh. Cờ dấu có giá trị bằng 1 khi kết quả của phép toán bằng 0, hoặc phép so sánh cho kết quả bằng nhau. Ngược lại, khi kết quả khác không hoặc so sánh cho kết quả là khác nhau thì ZF bằng 0.
  • Cờ nhớ (Carry Flag – CF) chứa giá trị nhớ (nhớ 0, hoặc nhớ 1) của MSB sau khi thực hiện phép toán số học. Ngoài ra, khi thực hiện lệnh dịch bit (shift) hoặc quay (rotate) thì giá trị của bit bị đẩy ra cuối cùng sẽ được lưu tại CF.
  • Cờ nhớ phụ trợ (Auxiliary Carry Flag – AF) chứa giá trị nhớ khi chuyển từ bit có trọng số 3 lên bit có trọng số 4 (nhớ từ lower nibble sang high nibble) khi thực hiện phép toán số học.
  • Cờ chẵn lẻ (Parity Flag – PF) bằng 0 khi số lượng bit 1 trong trong kết quả của phép toán số học là một số chẵn, và bằng 1 khi số lượng bit 1 là một số lẻ. Trong một số trường hợp, PF còn được dùng để kiểm tra lỗi.

Trong các cờ trên thì DF, IF và TF là 3 cờ điều khiển. OF, SF, ZF, CF, AF và PF là 6 cờ trạng thái.

Bảng sau cho biết vị trí của các bit cờ trong thanh ghi cờ 16 bit:

Thanh ghi đoạn:

Một chương trình Assembly được chia thành các đoạn (Segment) chứa dữ liệu, code và stack:

  • Code segment: chứa các mã lệnh thực thi. Thanh ghi đoạn code CS chứa địa chỉ bắt đầu của Code segment.
  • Data segment: chứa các biến, hằng số, dữ liệu của chương trình. Thanh ghi đoạn dữ liệu DS chứa địa chỉ bắt đầu của Data segment.
  • Stack segment: chứa dữ liệu và địa chỉ trả về của các chương trình con. Các dữ liệu này được lưu trữ theo cấu trúc Stack. Thanh ghi đoạn stack SS chứa địa chỉ bắt đầu của Stack segment.

Ngoài CS, DS và SS ra còn có các thanh ghi đoạn ES (Extra Segment Register), FS và GS cung cấp các phân đoạn bổ sung cho việc lưu trữ dữ liệu.

2. Instructions

Các câu lệnh trong asm có cấu trúc như sau:

3. Logical Instructions

Toán hạng đầu tiên trong tất cả các trường hợp có thể là thanh ghi hoặc bộ nhớ. Toán hạng thứ hai có thể là thanh ghi / bộ nhớ hoặc giá trị ngay lập tức (constant). Tuy nhiên, thực thi logic từ bộ nhớ đến bộ nhớ là không thể.

4. Arithmetic Instructions

Các phép toán cơ bản: cộng(ADD), trừ(SUB), nhân (MUL/IMUL), chia(DIV/IDIV)
Ngoài ra:

  • INC: tăng toán hạng lên 1 đơn vị. Câu lệnh: INC destination
  • DEC: giảm toán hạng xuống một đơn vị. Câu lệnh: DEC destination
  • ADD/SUB: ADD/SUB destination, source
  • MUL/IMUL: MUL/IMUL multiplier
    • MUL: tương ứng với unsigned data
    • IMUL: tương ứng với signed data
  • DIV/IDIV: DIV/IDIV divisor
    • DIV: tương ứng với unsigned data
    • IDIV: tương ứng với signed data

5. Conditions

Đối với dữ liệu có dấu(signed)

Đối với dữ liệu không dấu (unsigned)

Nhưng mà trước khi jump thì chúng ta cần có 1 số điều kiện và chúng được được thực hiện qua CMP instructions
Có 2 loại condition: đó là có điều kiện và vô điều kiện(tức là bắt buộc nhảy)

  • Vô điều kiện thường sử dụng với jmp instruction.
  • Còn có điều kiện là những lệnh nhảy mà bạn đã thấy nó ở trên.

6. Stack

Ngăn xếp (stack) là một cấu trúc dữ liệu giống như mảng trong bộ nhớ, trong đó dữ liệu có thể được lưu trữ và xóa khỏi vị trí được gọi là đỉnh ngăn xếp. Dữ liệu cần lưu trữ sẽ được push vào ngăn xếp và dữ liệu cần lấy ra sẽ được pop khỏi ngăn xếp. Hoạt động theo nguyên lý cấu trúc LIFO (last in first out).
Câu lệnh:
– PUSH operand (đẩy dữ liệu vào ngăn xếp)
– POP address/register (lấy dữ liệu ra khỏi ngăn xếp)
Theo quy ước, ngăn xếp tăng dần về phía dưới địa chỉ bộ nhớ. Thêm một cái gì đó vào ngăn xếp có nghĩa là đỉnh của ngăn xếp hiện ở mức địa chỉ bộ nhớ thấp hơn.

  • PUSH: đẩy một giá trị vào stack (push word – 2 byte, doubleword – 4 byte,…)
    Ví dụ: push eax (DW)
  • POP: lấy giá trị ra khỏi stack
    Ví dụ: pop eax (DW)

6. Endianess

Khái niệm: Endianness là thứ tự byte lựa chọn cho tất cả các máy tính kỹ thuật số thực hiện trong một hệ thống máy tính cụ thể và mệnh lệnh kiến trúc và cấp thấp tiếp cận lập trình được sử dụng cho hệ thống đó. Đơn giản hơn endianess là việc sắp xếp dữ liệu trong bộ nhớ.
Để hiểu rõ thực hiện so sánh hệ thống Big Endian và Litle Edian:

  • Big Endian: Byte cao nhất sẽ được sắp đầu tiên tức là (nếu hàng ngang thì) xếp phía bên trái ngoài cùng.
  • Litle Edian: Byte thấp nhất sẽ đc sắp đầu tiên tức là (nếu hàng ngang thì) xếp phía bên trái ngoài cùng.

Ví dụ:
Số 123456 = 00000000 00000001 11100010 01000000
Số 123456 lần lượt được lưu trong hệ thống Big Endian và Little Endian dưới đây:


Trong Big Endian là: 00000000 00000001 11100010 01000000
Little Endian là : 01000000 11100010 00000001 00000000

7. Call convention

Khái niệm: Là một sơ đồ triển khai về cách các chương trình con hoặc hàm nhận tham số từ người gọi chúng và cách chúng trả về kết quả.

Calling conventions quyết định ba điều quan trọng:

  • Điều gì xảy ra với ngăn xếp trước và sau khi gọi hàm?
  • Điều gì xảy ra với thanh ghi trước và sau khi gọi hàm?
  • Tên hàm được trang trí như thế nào?
    • Có nhiều calling conventions (quy ước cuộc gọi): _stdcall (Win32 default), __cdecl (C/C++ default). Ngoài ra còn nhiều cái như: __fastcall, __clrcall, __thiscall và __vectorcall. Một số cái lỗi thời như __pascal, __fortran, __syscall.

Hai quy ước quan trọng nhất:

Ví dụ:

Thanh ghi sử dụng trong khung ngăn xếp: cả _cdecl và _stdcall cùng có bộ ba thanh ghi được tham gia vào khung gọi hàm:

  • ESP (Stack Pointer): thanh ghi này được điều khiển ngầm bởi một số lệnh (PUSH, POP, CALL, RET,…), nó luôn trỏ đến phần tử cuối cùng được sử dụng trong ngăn xếp:
    *–ESP = value; // push
    value = *ESP++; // pop
    Đỉnh ngăn xếp là vị trí bị chiếm dụng và nó nằm ở địa chỉ bộ nhớ thấp nhất.
  • EBP (Base Pointer): thnah ghi này được sử dụng để tham chiếu tất cả các tham số hàm và biến cục bộ trong khung ngăn xếp hiện tại.
  • EIP (Instruction Pointer): chứa địa chỉ của lệnh tiếp theo sẽ được thực thi và nó được lưu vào ngăn xếp như một phần của lệnh CALL. Tất cả lệnh jump nào cũng sửa đổi trực tiếp EIP.

Cách tốt nhất để hiểu cách tổ chức ngăn xếp là xem từng bước khi gọi một hàm với quy ước _cdecl/_stdcall. Các bước này được trình biên dịch thực hiên tự động và mặc dù không phải tất cả chúng đều được sử dụng trong mọi trường hợp nhưng điều này cho thấy cơ chế tổng thể được sử dụng. (lấy _cdecl làm mẫu):

  • Đẩy tham số vào ngăn xếp, từ phải sang trái: Các tham số được đẩy lên ngăn xếp, từng tham số một, từ phải sang trái. Mã gọi phải theo dõi xem có bao nhiêu byte tham số đã được đẩy lên ngăn xếp để có thể dọn sạch sau này.
  • Gọi hàm: Bộ xử lý đẩy nội dung của %EIP (con trỏ lệnh) vào ngăn xếp và nó trỏ đến byte đầu tiên sau lệnh CALL.
  • Lưu và cập nhật %EBP: Bây giờ chúng ta đang ở trong hàm mới, chúng ta cần một khung ngăn xếp cục bộ mới được trỏ đến bởi %EBP, vì vậy việc này được thực hiện bằng cách lưu %EBP hiện tại (thuộc khung của hàm trước đó) và làm cho nó trỏ đến đầu của ngăn xếp.
    • push ebp
    • mov ebp, esp // ebp « esp
  • Lưu các thanh ghi được sử dụng tạm thời: Trước tiên nó phải lưu các giá trị cũ để tránh nó sử dụng dữ liệu được sử dụng bởi các chức năng gọi. Mỗi thanh ghi được sử dụng sẽ được đẩy lần lượt vào ngăn xếp và trình biên dịch phải ghi nhớ những gì nó đã làm để có thể giải nén nó sau này.
  • Phân bổ các biến cục bộ.
  • Thực hiện mục đích của hàm: Tại thời điểm này, khung ngăn xếp được thiết lập chính xác và điều này được thể hiện bằng sơ đồ sau. Tất cả các tham số và địa chỉ đều được bù đắp từ thanh ghi %EBP:
  • Giải phóng bộ nhớ cục bộ
  • Khôi phục con trỏ cơ sở EBP cũ
  • Trở về từ hàm: Đây là bước cuối cùng của hàm được gọi và lệnh RET lấy %EIP cũ từ ngăn xếp và nhảy đến vị trí đó.
  • Dọn dẹp: Trong quy ước __cdecl, caller phải dọn sạch các tham số được đẩy lên ngăn xếp.

Quy ước __stdcall chủ yếu được API Windows sử dụng và nhỏ gọn hơn __cdecl một chút. Sự khác biệt chính là bất kỳ hàm nhất định nào cũng có một bộ tham số được mã hóa cứng và điều này không thể thay đổi tùy theo lệnh gọi giống như trong C (không có “hàm biến thiên”).
Vì kích thước của khối tham số là cố định nên gánh nặng làm sạch các tham số này khỏi ngăn xếp có thể được chuyển sang hàm được gọi, thay vì được thực hiện bởi hàm gọi như trong __cdecl.
Tóm lại:

  • __cdecl là quy ước gọi mặc định cho các chương trình C và C++. Ưu điểm của cách gọi đối lưu này là nó cho phép sử dụng các hàm có số lượng đối số thay đổi. Nhược điểm là nó tạo ra các tệp thực thi lớn hơn.
  • _stdcall được sử dụng để gọi các hàm API Win32. Nó không cho phép các hàm có số lượng đối số thay đổi.
  • __fastcall cố gắng đặt các đối số vào thanh ghi, thay vì vào ngăn xếp, do đó thực hiện lệnh gọi hàm nhanh hơn.
  • Thiscall là quy ước gọi mặc định được sử dụng bởi các hàm thành viên C++ không sử dụng các đối số biến.

Trên đây là những kiến thức cơ bản cần thiết trong suốt quá trình phân tích mà tôi muốn chia sẻ, hi vọng chúng hữu ích với các bạn.

Cảm ơn và hẹn gặp lại.

(dt97_Malware_Analysis)

Chia sẻ