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 = 0x00000045 và ecx = 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, countshl 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 (2n) 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, countrol 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:
Flag
Abbreviation
Explanation
Carry
CF
Set when a carry-out or borrow is required from the most significant bit in an arithmetic operation. Also used for bit-wise shifting operations.
Parity
PF
Set if the least significant byte of the result contains an even number of 1 bits.
Auxiliary
AF
Set if a carry-out or borrow is required from bit 3 to bit 4 in an arithmetic operation (BCD arithmetic).
Zero
ZF
Set if the result of the operation is zero.
Sign
SF
Set if the result of the operation is negative (i.e., the most significant bit is 1).
Overflow
OF
Set if there’s a signed arithmetic overflow (e.g., adding two positive numbers and getting a negative result or vice versa).
Direction
DF
Determines the direction for string processing instructions. If DF=0, the string is processed forward; if DF=1, the string is processed backward.
Interrupt Enable
IF
If 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 eax và edx.
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 eaxdec 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:
Instruction
Explanation
jz
Jump if the ZF is set (ZF=1).
jnz
Jump if the ZF is not set (ZF=0).
je
Jump if equal.
jne
Jump if not equal.
jg
Jump if the destination is greater than the source operand. Performs signed comparison and is often used after a CMP instruction.
jl
Jump if the destination is lesser than the source operand. Performs signed comparison and is often used after a CMP instruction.
jge
Jump 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.
jle
Jump 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.
ja
Jump if above. Similar to jg, but performs an unsigned comparison.
jb
Jump if below. Similar to jl, but performs an unsigned comparison.
jae
Jump if above or equal to. Similar to the above instructions.
jbe
Jump if below or equal to. Similar to the above instructions.
Important
Trừ hai chỉ thị đầu (jz và jnz) 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.
Related
listfrom outgoing([[TryHackMe - x86 Assembly]])sort file.ctime asc