Archive for March 7, 2019


Tiếp tục với các lệnh assembly cơ bản, phần này sẽ là các lệnh tính toán và logic.

Các lệnh tính toán

Lệnh ADD

Cú pháp của lệnh như sau:

ADD A, B ; A = A + B

Câu lệnh ADD thực hiện cộng giá trị của B với A, kết quả tính toán sẽ lưu vào A. Tức là A = A + B. A ở đây có thể là một thanh ghi hoặc là nội dung của một ô nhớ, B có thể là một thanh ghi, một hằng số hoặc nội dung của một ô nhớ. Tuy nhiên, trong câu lệnh ADD thì cả AB không thể đồng thời là nội dung của ô nhớ.

Quan sát một số ví dụ về lệnh ADD có được bằng việc tìm kiếm chuỗi ADD trong Veviewer.

Ta thấy có khá nhiều câu lệnh ADD với toán hạng đầu tiên là một thanh ghi và toán hạng thứ hai là một hằng số. Như chúng ta biết, nó sẽ cộng vào giá trị mà thanh ghi đang có tại thời điểm đó với giá trị hằng số, kết quả tính toán sẽ được lưu lại vào thanh ghi.

Trong ví dụ này, nếu thanh ghi ECX có giá trị là 0x10000, nó sẽ được cộng thêm 4, kết quả thu được là 0x10004 sẽ được lưu vào chính thanh ghi ECX.

Còn trong trường hợp trên, lệnh ADD sẽ thực hiện cộng giá trị 0xFFFFFFFF vào giá trị có được tại địa chỉ được trỏ bởi ECX+30, và nếu địa chỉ đó có quyền ghi, nó sẽ thực hiện cộng thêm và lưu lại kết quả ở đó.

Giả sử, nếu thanh ghi ECX đang có giá trị 0x10000 thì kết quả phép tính trong [] cho ta một địa chỉ là 0x10030. Giả sử nội dung tại địa chỉ này có giá trị 1, thì khi được cộng thêm 0xFFFFFFFF (bằng -1) sẽ có kết quả sẽ bằng 0 và được lưu lại vào địa chỉ 0x10030.

Còn ở crackme.exe, ta có thể gặp ví dụ của lệnh ADD sử dụng các toán hạng là các thanh ghi như sau:

Trong trường hợp này, giá trị của cả hai thanh ghi sẽ được cộng với nhau và lưu vào EDI. Tất nhiên ta cũng có thể sử dụng các thanh ghi 16-bit và 8-bit. Ví dụ:

ADD AL, 8

ADD AX, 8

ADD BX, AX

ADD byte ptr ds: [EAX], 7

Lệnh trên cộng vào byte nội dung mà EAX trỏ đến với giá trị hằng số là 7 và lưu lại tại cùng một vị trí.

Lệnh SUB

Cú pháp của lệnh như sau:

SUB A, B ; A = A – B

Lệnh SUB cũng tương tự như lệnh ADD, ngoại trừ thay vì thực hiện cộng thì nó thực hiện trừ số nguyên và lưu kết quả vào A. Các kết hợp của lệnh có thể như sau:

Lệnh INC và DEC

INC A; A++

DEC A; A–

Các lệnh trên thực hiện tăng hoặc giảm giá trị thanh ghi hoặc nội dung của một địa chỉ bộ nhớ đi 1. Trên thực tế đây có thể xem là một trường hợp đặc biệt của phép cộng và trừ.

Bên lề: Cả hai lệnh này thường hay được sử dụng trong các vòng lặp để tăng hoặc giảm biến đếm.

Lệnh IMUL

Đây là lệnh thực hiện phép tính nhân số có dấu và có hai dạng như sau:

IMUL A, B    ; A = A * B

IMUL A, B, C ; A = B * C

Bên lề: Tại sao lại là câu lệnh imul (signed multiply) mà không phải là câu lệnh mul (unsigned multiply)? Đó là bởi trình Visual Studio dường như có một sự ưa thích đối với lệnh imul. Kể cả khi bạn khai báo biến có kiểu unsigned trong chương trình, khi compile code và chuyển qua assembly thì sẽ thấy chương trình sử dụng câu lệnh imul.

Quay trở lại với cú pháp của lệnh:

  • Câu lệnh đầu tiên sẽ thực hiện nhân A với B, kết quả được bao nhiêu sẽ được lưu lại vào A.
  • Câu lệnh thứ hai thì B và C được nhân với nhau và kết quả được lưu vào A.

Trong cả hai trường hợp A chỉ có thể là một thanh ghi, B chỉ có thể là một thanh ghi hoặc nội dung của một vị trí bộ nhớ và C chỉ có thể là một hằng số.

imul eax, [ecx] 

imul esi, edi, 25 

Một vài ví dụ khác của lệnh IMUL tìm thấy trong file Veviewer

Trong hình minh họa trên, ta thấy hầu như chỉ có các lệnh imul theo dạng 1 (imul A, B). Với trường hợp lệnh chỉ có một toán hạng (ví dụ: imul ecx), thì tùy theo độ dài của toán hạng mà sẽ lấy giá trị trong các thanh ghi AL, AX, hoặc EAX để nhân và kết quả của phép nhân sẽ được lưu vào AX, DX:AX, hoặc EDX:EAX.

Lệnh DIV/ IDIV

Cú pháp của lệnh như sau:

DIV/ IDIV A

Trong câu lệnh này, A được hiểu là số chia. Số bị chia và thương số không được chỉ định bởi vì chúng luôn giống nhau. Tức là có 3 dạng như sau:

  • Nếu A có kiểu byte, lấy giá trị của thanh ghi AX chia cho A, kết quả thương số lưu vào thanh ghi AL, phần dư lưu vào thanh ghi AH.
  • Nếu A có kiểu word, lấy giá trị của cặp thanh ghi DX:AX chia cho A, kết quả thương số lưu vào thanh ghi AX, phần dư lưu vào thanh ghi DX.
  • Nếu A có kiểu dword, lấy giá trị của cặp thanh ghi EDX:EAX chia cho A, kết quả thương số lưu vào thanh ghi EAX, phần dư lưu vào thanh ghi EDX.

Xem xét ví dụ:

Với lệnh trên, ví dụ nếu EAX = 5, EDX = 0 và ECX = 2, nó sẽ thực hiện phép chia số nguyên. Kết quả của phép chia 5 / 2 sẽ được 2 và dư 1. Khi đó kết quả là 2 được lưu vào thanh ghi EAX và số dư 1 sẽ được lưu vào thanh ghi EDX.

Bên lề: Thông thường khi thực hiện phép chia, do thanh ghi EDX được sử dụng để lưu phần dư nên nó sẽ được thiết lập về 0 trước khi thực hiện phép tính. Để xóa EDX về 0 có hai cách:

  • Sử dụng câu lệnh XOR (chi tiết bên dưới): XOR EDX, EDX
  • Sử dụng câu lệnh CDQ (như trên hình minh họa): Câu lệnh này thực hiện mở rộng bit dấu (bit 31) của thanh ghi EAX sang thanh ghi EDX. Nếu bit này có giá trị 0 thì EDX sẽ bằng 0.

Điều tương tự sẽ xảy ra nếu như A là nội dung của một ô nhớ (dword ptr ds:[402000]), EDX:EAX sẽ được chia cho giá trị đó và kết quả sẽ được lưu trong EAX và phần dư trong EDX.

Các lệnh Logic

Lệnh AND, OR và XOR

AND A, B ; A = A & B

OR A, B  ; A = A | B

XOR A, B ; A = A ^ B

Lệnh đầu tiên thực hiện phép AND giữa hai giá trị và lưu lại kết quả vào A, tương tự với các lệnh OR hoặc XOR. Mỗi phép tính đều sử dụng một bảng thật tương ứng của nó. A và B có thể là thanh ghi hoặc nội dung của địa chỉ bộ nhớ, tuy nhiên các thao tác giữa hai ô nhớ là không hợp lệ.

Lệnh hay được sử dụng nhiều nhất là XOR cùng một thanh ghi để dễ dàng xóa thanh ghi đó về 0. Ví dụ: XOR EAX,EAX. Dưới đây là bảng thật hay bảng chân lý (như ở các trường đại học hay dạy) tương ứng cho từng lệnh:

Trong bảng trên chúng ta thấy rằng nếu XOR một số với chính nó thì kết quả sẽ luôn bằng không. Các phép tính này được thực hiện ở chế độ nhị phân (binary):

  • Lệnh AND có thể sử dụng để che đi/ giữ lại các bit nhất định của toán hạng đích. Bit 0 của mặt nạ sẽ xóa bit tương ứng, còn bit 1 của mặt nạ sẽ giữ nguyên bit tương ứng của toán hạng đích.
  • Lênh OR có thể được sử dụng để thiết lập các bit xác định của toán hạng đích trong khi vẫn giữ nguyên các bit còn lại. Bit 1 cua mặt nạ sẽ thiết lập bit tương ứng còn bit 0 của mặt nạ sẽ giữ nguyên bit tương ứng của toán hạng đích.
  • Lệnh XOR dùng để đảo các bit xác định của toán hạng đích trong khi vẫn giữ nguyên các bit còn lại. Bit 1 của mặt nạ làm đảo bit tương ứng còn bit 0 giữ nguyên bit tương ứng của toán hạng đích.

Như đã nói, lệnh XOR dùng để xóa một thanh ghi về 0, bằng cách này sẽ thực hiện nhanh hơn lệnh MOV.

Để kiểm tra ta có thể viết một lệnh xor hai số giống nhau ở dạng binary trong khung Python. Kết quả trả về luôn là 0:

Tất nhiên, ta hoàn toàn có thể áp dụng với các số thập phân và thập lục phân. Ở trên tôi để ở dạng nhị phân để quan sát kết quả cụ thể ứng với từng bit. Còn trong ví dụ dưới đây tôi để ở dạng hexa:

Một ví dụ đơn giản của lệnh AND:

AND EAX, 0F

Biểu diễn ở dạng binary thì 0F sẽ là 1111:

Dựa vào bảng thật, chúng ta thấy rằng nếu cả hai bit là 1 thì kết quả sẽ không thay đổi, trong khi các cặp bit khác sẽ cho kết quả là 0. Bằng cách này tôi dễ dàng thiết lập lại tất cả các bit của một số là 0 và giữ nguyên 4 bit cuối cùng không thay đổi. Ví dụ:

Như đã biết phép AND được biểu diễn bằng dấu “&”. Với câu lệnh như trên, ta sẽ giữ lại được 4 bit cuối.

Lệnh OR được biểu diễn bằng dấu “|”, ví dụ như sau:

Lệnh NOT

NOT A

Lệnh NOT thực hiện đảo ngược tất cả các bit của A và lưu lại kết quả vào A. Trong Python không có lệnh NOT, nhưng nó rất đơn giản nếu bạn có một số nhị phân, ví dụ 0101 và bạn áp dụng lệnh NOT với số này:

Kết quả có được sau khi thực hiện đảo ngược từng bit một. Toàn bộ các bit 0 sẽ được thay bằng 1 và ngược lại.

Lệnh NEG

NEG A ; chuyển đổi A thành –A (reg = 0 – reg). Trên thực tế, lệnh neg là kết quả của một lệnh notadd 1.

Nó không giống như cú pháp ~ trong Python vì lệnh này chỉ là phép trừ đi 1.

Nói cách khác, để thực hiện lệnh NEG bằng Python, bạn cần cộng thêm 1 vào kết quả.

Các lệnh dịch bit SHL, SHR

SHL A, B; Dịch trái A đi B bit

SHR A, B; Dịch phải A đi B bit

A có thể là một thanh ghi hoặc một vị trí bộ nhớ và B là một hằng số hay một thanh ghi 8-bit. Các lệnh này thực hiên phép dịch bit sang trái (SHL) và sang phải (SHR), các bit bên phải/trái được thay thế bằng các số 0, chúng ta hãy xem ví dụ.

Nếu tôi có -1:

Khi thực hiện SHL 2, nó sẽ có kết quả:

Khi di chuyển các bit sang trái, mỗi lần dịch thì MSB sẽ được đưa qua cờ CF và 0 đưa vào LSB. Vì dịch đi 2, nên hai bit cuối cùng ở phía bên phải nhất sẽ được thay thế bằng 0.

Tương tự khi ta thực hiện lệnh SHR. Các bit sẽ di chuyển sang phải, sau mỗi lần dịch thì LSB sẽ được đưa qua cờ CF còn 0 đưa vào MSB.

Lưu ý: Việc dịch bit trái (phải) tương ứng với phép nhân (chia) cho lũy thừa 2.

  • shl eax, 0x2 à EAX << 2 or EAX = EAX * 4
  • shr eax, 0x2 à EAX >> 2 or EAX = EAX / 4

Phần 6 xin được dừng lại tại đây. Hẹn các bạn gặp lại ở phần 7!

Xin gửi lời cảm ơn chân thành tới thầy Ricardo Narvaja!

m4n0w4r

Ủng hộ tác giả

Nếu bạn cảm thấy những gì tôi chia sẻ trong bài viết là hữu ích, bạn có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy” qua địa chỉ:

Tên tài khoản: TRAN TRUNG KIEN
Số tài khoản: 0021001560963
Ngân hàng: Vietcombank