Mã nguồn của một chương trình được ghi vào đĩa và thực thi bởi CPU dưới dạng nhị phân. Nói cách khác, mã nguồn chính là một dãy liên tục các bit 1 và bit 0. Để làm dãy số này trở nên dễ hiểu hơn, chúng ta gom nhóm 8 bit thành 1 byte và biểu diễn bằng một con số thập lục phân (hexadecimal). Như vậy, các chỉ thị được thực thi bởi CPU chẳng qua là một dãy các số hex. Trong dãy các số hex đó thì gồm có opcode (mã hoạt động) và operand (toán hạng). Operand có thể là các thanh ghi hoặc địa chỉ vùng nhớ dùng để thực thi mã lệnh.

Opcodes

Chính là các con số tương ứng với các chỉ thị được thực thi bởi CPU. Khi disassemble một chương trình thì disassembler sẽ dịch các opcode thành các chỉ thị hợp ngữ mà con người có thể đọc được. Ví dụ, chỉ thị di chuyển giá trị 0x5f vào thanh ghi eax có dạng như sau:

mov eax, 0x5f

Khi nhìn vào disassebler, chúng ta sẽ thấy dạng hex của chỉ thị:

040000:    b8 5f 00 00 00    mov eax, 0x5f

Trong đó:

  • Giá trị 040000 là địa chỉ của chỉ thị.
  • Có hai toán hạng là:
    • b8 tương ứng với chỉ thị mov eax.
    • 5f 00 00 00 là giá trị 0x5f. Do đây là little endian nên 0x5f được ghi xuống bộ nhớ là 5f 00 00 00.

Tương tự đối với các opcode khác: mỗi opcode sẽ tương ứng với một chỉ thị hợp ngữ.

Types of Operands

Có ba loại toán hạng trong hợp ngữ:

  • Immediate operand: có thể xem như là các constant, chẳng hạn giá trị 0x5f ở trên.
  • Register: có thể được dùng như là các toán hạng, chẳng hạn thanh ghi eax ở trên.
  • Memory operand: được ký hiệu bằng cặp ngoặc vuông để tham chiếu đến địa chỉ vùng nhớ. Ví dụ, [memory_address] được xem như là một toán hạng và giá trị của nó sẽ là giá trị tại vùng nhớ memory_address.

General Instructions

Các chỉ thị sẽ cho biết CPU cần phải làm gì. Các chỉ thị thường sử dụng các toán hạng để tính toán kết quả rồi lưu vào các thanh ghi hoặc bộ nhớ.

The MOV Instruction

Chỉ thị mov cho phép di chuyển giá trị (bản chất là sao chép). Cú pháp:

mov destination, source

Một số ví dụ:

Gán giá trị 0x5f cho thanh ghi eax:

mov eax, 0x5f

Sao chép giá trị trong thanh ghi eax đến thanh ghi ebx:

mov ebx, eax

Sao chép giá trị ở vùng nhớ 0x5fc53e đến thanh ghi eax:

mov eax, [0x5fc53e]

Nếu thanh ghi nằm bên trong cặp ngoặc vuông:

mov eax, [ebx]

Thì có nghĩa là: giá trị lưu trong ebx là một địa chỉ vùng nhớ và giá trị tại địa chỉ đó sẽ được sao chép qua cho thanh ghi eax.

Chúng ta cũng có thể thực hiện các phép toán với giá trị vùng nhớ ở bên trong cặp ngoặc vuông:

mov eax, [ebp+4]

Một ví dụ khác, câu lệnh sau sẽ sao chép giá trị của thanh ghi ebx đến địa chỉ vùng nhớ được lưu ở trong thanh ghi eax:

mov [eax], ebx

Câu lệnh trên sao chép giá trị tại vùng nhớ có địa chỉ ebp+4 (4 có đơn vị là byte) qua cho thanh ghi eax.

Attention

Không thể di chuyển giá trị giữa hai vùng nhớ, chẳng hạn như:

mov [ebx],[eax]

The LEA Instruction

Chỉ thị lea là viết tắt của “load effective address” và có cú pháp như sau:

lea destination, source

Chỉ thị này sẽ sao chép địa chỉ vùng nhớ thay vì dữ liệu.

Ví dụ, trong câu lệnh bên dưới, địa chỉ vùng nhớ ebp+4 sẽ được sao chép đến eax thay vì giá trị tại địa chỉ này:

lea eax, [ebp+4]

Một ví dụ khác:

lea eax, [ebx+ecx]

Giả sử ebx = 0x00000045ecx = 0x00000006 thì giá trị của eax sẽ là 0x0000004B ở dạng địa chỉ.

The NOP Instruction

Chỉ thị nop là viết tắt của “no operation”. Chỉ thị này giúp trao đổi giá trị eax cho chính nó và khiến cho chương trình chạy đến chỉ thị tiếp theo mà không làm thay đổi bất cứ thứ gì. Nó thường được dùng để tiêu thụ chu kỳ CPU trong khi chờ một thao tác hoặc các tiến trình khác.

Cú pháp:

nop

Chỉ thị nop thường được dùng bởi malware nhằm chuyển hướng luồng thực thi của chương trình vào shellcode của nó. Vị trí chính xác của luồng thực thi bị chuyển hướng thường không xác định được nên người tạo mã độc thường sử dụng nhiều chỉ thị nop nhằm đảm bảo việc thực thi của shellcode không bắt đầu ở giữa.

Shift Instructions

Có hai loại chỉ thị dịch bit là dịch phải và dịch trái.

Cú pháp:

shr destination, count
shl destination, count

Với count là số bit cần dịch chuyển.

Các bit mới được thêm vào sẽ là bit 0. Ví dụ, nếu ta có giá trị 00000010 trong thanh ghi eax và ta dịch trái thì giá trị sẽ trở thành 00000100.

Trong trường hợp có một bit 1 đi ra khỏi bên ngoài phạm vi một byte thì carry flag sẽ có giá trị là 1. Ví dụ, nếu ta dịch phải giá trị 00000101 trong thanh ghi eax thì thanh ghi sẽ có giá trị là 0000010 và carry flag sẽ có giá trị là 1. Tương tự, nếu ta dịch trái giá trị 10000010 thì thanh ghi sẽ có giá trị là 00000100 và carry flag sẽ có giá trị là 1.

Các thao tác dịch bit dùng để thực hiện phép tính nhân và chia cho 2 hoặc lũy thừa 2 () với n là số lần dịch trái.

Rotate Instructions

Tương tự với phép dịch bit. Tuy nhiên, các bit đi ra khỏi phạm vi một byte thì sẽ quay trở lại ở đầu bên kia của byte thay vì được lưu trong carry flag.

Cú pháp:

ror destination, count
rol destination, count

Ví dụ, khi ta xoay giá trị 10101010 sang phải 1 bit thì nó sẽ trở thành 01010101.

Flags

Bảng các flag phổ biến có trong thanh ghi EFLAGS:

FlagAbbreviationExplanation
CarryCFSet when a carry-out or borrow is required from the most significant bit in an arithmetic operation. Also used for bit-wise shifting operations.
ParityPFSet if the least significant byte of the result contains an even number of 1 bits.
AuxiliaryAFSet if a carry-out or borrow is required from bit 3 to bit 4 in an arithmetic operation (BCD arithmetic).
ZeroZFSet if the result of the operation is zero.
SignSFSet if the result of the operation is negative (i.e., the most significant bit is 1).
OverflowOFSet if there’s a signed arithmetic overflow (e.g., adding two positive numbers and getting a negative result or vice versa).
DirectionDFDetermines the direction for string processing instructions. If DF=0, the string is processed forward; if DF=1, the string is processed backward.
Interrupt EnableIFIf set (1), it enables maskable hardware interrupts. If cleared (0), interrupts are disabled.

Các flag thường được sử dụng trong các bước nhảy có điều kiện (conditional jumps). Ví dụ, chúng ta thường nhảy đến địa chỉ nào đó nếu có một flag cụ thể được set.

Arithmetic and Logical Instructions

Addition and Subtraction Instructions

Cú pháp của phép cộng:

add destination, value

Giá trị của value sẽ được cộng vào destination và kết quả sẽ được lưu ở trong destination.

Phép trừ tương tự:

sub destination, value

Giá trị của destination sẽ được trừ cho value và kết quả sẽ được lưu ở trong destination.

Trong hai phép toán trên thì value có thể là giá trị hằng số hoặc thanh ghi. Với phép trừ thì ZF sẽ được set nếu kết quả của phép trừ là 0 và CF sẽ được set nếu destination nhỏ hơn value.

Multiplication and Division Instructions

Phép nhân và chia sẽ sử dụng thanh ghi eaxedx.

Chỉ thị nhân có cú pháp như sau:

mul value

Chỉ thị trên sẽ nhân value cho giá trị trong thanh ghi eax và lưu kết quả ở trong edx:eax như là một giá trị 64-bit. Lý do cần dùng hai thanh ghi là vì kết quả của phép nhân hai giá trị 32-bit thường có kết quả lớn hơn 32-bit. Phần 32-bit thấp được lưu ở trong thanh ghi eax còn phần 32 bit cao được lưu ở trong thanh ghi edx.

Giá trị của value có thể là thanh ghi hoặc hằng số.

Với phép chia thì ta dùng chỉ thị như sau:

div value

Trong trường hợp phép chia thì ngược lại: nó sẽ chia giá trị nằm trong thanh ghi edx:eax cho value rồi lưu phần thương số ở trong eax và phần số dư ở trong edx.

Increment and Decrement Instructions

Các chỉ thị này giúp tăng giá trị của thanh ghi lên 1 giá trị hoặc giảm giá trị của thanh ghi xuống 1 giá trị. Cú pháp:

inc eax
dec eax

AND Instruction

Chỉ thị and giúp thực hiện phép toán AND trên các bit. Ví dụ:

and al, 0x7c

Trong ví dụ trên:

  • Giá trị 0x7c quy đổi thành 01111100 ở dạng nhị phân.
  • Giả sử địa chỉ al đang lưu giá trị 0xfc (tương ứng với 11111100).

Trong trường hợp này, kết quả của chỉ thị trên sẽ là 01111100. Kết quả này sẽ được lưu vào địa chỉ al.

OR Instruction

Thực hiện phép toán OR trên các bit. Ví dụ:

or al, 0x7c

Giả sử địa chỉ al lưu giá trị 0xfc thì kết quả của chỉ thị trên sẽ là 11111100.

NOT Instruction

Nó sẽ lật tất cả các bit. Ví dụ:

not al

Giả sử al lưu giá trị 11110000 thì kết quả sẽ là 00001111.

XOR Instruction

Thực hiện phép toán XOR trên các bit. Cú pháp:

xor al, 0x7c

Nếu al có giá trị là 0xfc (11111100) thì kết quả của ví dụ trên sẽ là 10000000. Đặc biệt, nếu giá trị của al cũng là 0x7c (01111100) thì kết quả sẽ là 0x00. Do đó, chỉ thị xor thường được dùng để clear giá trị của thanh ghi vì nó tối ưu hơn chỉ thị mov.

Conditionals

The TEST Instruction

Chỉ thị test thực hiện phép toán AND trên bit và set giá trị của zero flag và parity flag là 1 nếu kết quả là 0 (không lưu kết quả vào destination như chỉ thị and).

test destination, source

Chúng ta thường kiểm tra một toán hạng có là NULL hay không bằng cách sử dụng chỉ thị test cho chính nó. Lý do là vì việc dùng chỉ thị test là vì nó tiết kiệm byte hơn việc so sánh với số 0.

The CMP Instruction

Chỉ thị cmp so sánh hai toán hạng và set zero flag hoặc carry flag. Cú pháp:

cmp destination, source

Chỉ thị này hoạt động tương tự như chỉ thị sub. Điểm khác biệt duy nhất là các toán hạng không bị thay đổi giá trị. Có ba trường hợp:

  • Zero flag được set là 1 nếu hai toán hạng bằng nhau (parity flag cũng bằng 1 do kết quả là 0).
  • Nếu source lớn hơn destination thì carry flag và sign flag sẽ được set.
  • Trong trường hợp destination lớn hơn source thì zero flag và carry flag đều có giá trị là 0.

Branching

Khi không có các câu lệnh rẽ nhánh thì instruction pointer sẽ thực thi chỉ thị này đến chỉ thị khác tùy theo thứ tự chúng được đặt vào bộ nhớ.

Các câu lệnh rẽ nhánh sẽ thay đổi giá trị của instruction pointer và làm thay đổi luồng thực thi của chương trình.

The JMP Instruction

Chỉ thị jmp khiến cho luồng điều khiển nhảy đến một vị trí cụ thể. Cú pháp:

jmp location

Về bản chất, câu lệnh trên sẽ chuyển giá trị của location vào instruction pointer.

Conditional Jumps

Trong hợp ngữ không có câu lệnh if mà chỉ có các conditional jump. Một vài các conditional jump phổ biến:

InstructionExplanation
jzJump if the ZF is set (ZF=1).
jnzJump if the ZF is not set (ZF=0).
jeJump if equal.
jneJump if not equal.
jgJump if the destination is greater than the source operand. Performs signed comparison and is often used after a CMP instruction.
jlJump if the destination is lesser than the source operand. Performs signed comparison and is often used after a CMP instruction.
jgeJump if greater than or equal to. Jumps if the destination operand is greater than or equal to the source operand. Similar to the above instructions.
jleJump if lesser than or equal to. Jumps if the destination operand is lesser than or equal to the source operand. Similar to the above instructions.
jaJump if above. Similar to jg, but performs an unsigned comparison.
jbJump if below. Similar to jl, but performs an unsigned comparison.
jaeJump if above or equal to. Similar to the above instructions.
jbeJump if below or equal to. Similar to the above instructions.

Important

Trừ hai chỉ thị đầu (jzjnz) thì các chỉ thị còn lại đều được sử dụng sau một chỉ thị cmp.

Stack and Function Calls

The PUSH Instruction

Chỉ thị push có cú pháp như sau:

push source

Câu lệnh trên sẽ đẩy source vào stack. Ví dụ, câu lệnh sau sẽ đẩy giá trị của thanh ghi eax vào stack:

push eax

Về bản chất, địa chỉ vùng nhớ của giá trị được đẩy vào stack sẽ được stack pointer trỏ đến.

Các chỉ thị sau sẽ đẩy các general purpose register vào stack:

  • pusha (push all words): đẩy tất cả các 16-bit general purpose register (AX, BX, CX, DX, SI, DI, SP, BP) vào stack.
  • pushad (push all double words): đẩy tất cả các 32-bit general purpose register (EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP) vào stack.

Info

Khi gặp các chỉ thị đẩy các general purpose register thì ta biết rằng có ai đó đang chèn các chỉ thị hợp ngữ một cách thủ công nhằm lưu lại giá trị của các thanh ghi.

The POP Instruction

Chỉ thị pop sẽ lấy ra giá trị ở đầu stack rồi lưu vào destination. Cú pháp:

pop destination

Sau đó, stack pointer sẽ được thay đổi để trỏ đến đầu stack.

Các chỉ thị sau sẽ lấy ra tất cả các general purpose register từ stack:

  • popa (pop all words): lấy ra tất cả các 16-bit general purpose register.
  • popad (pop all double words): lấy ra tất cả các 32-bit general purpose register.

Sau khi hai chỉ thị trên được thực thi thì thanh ghi SP hoặc ESP sẽ được điều chỉnh để trỏ đến vị trí đầu stack mới.

The CALL Instruction

Chỉ thị call dùng để gọi hàm trong hợp ngữ có cú pháp như sau:

call location

Các đối số sẽ được đặt vào stack hoặc các thanh ghi tùy vào quy ước gọi hàm.

list
from outgoing([[TryHackMe - x86 Assembly]])
sort file.ctime asc

Resources