逆向彙編的基本知識梳理

第一部分:基礎知識

彙編語言是一切程序的起點和終點,畢竟所有的高級語言都是建立在彙編基礎之上的。在許多高級語言中我們都需要相對明確的語法,但是在彙編中,我們會使用一些單詞縮寫和數字來表達程序。

I. 單元、位和字節

  1. ·BIT(位) - 電腦數據量的最小單元,可以是0或者1。
  2. 例:00000001 = 1;00000010 = 2;00000011 = 3     
  3. ·BYTE(字節) - 一個字節包含8個位,所以一個字節最大值是255(0-255)。爲了方便閱讀,我們通常使用16進制來表示。
  4. ·WORD(字) - 一個字由兩個字節組成,共有16位。一個字的最大值是0FFFFh (或者是 65535d) (h代表16進制,d代表10進制)。
  5. ·DOUBLE WORD(雙字DWORD) - 一個雙字包含兩個字,共有32位。最大值爲0FFFFFFFF (或者是 4294967295d)。
  6. ·KILOBYTE(千字) - 千字節並不是1000個字節,而是1024 (32*32) 個字節。
  7. ·MEGABYTE - 兆字節同樣也不是一兆個字節,而是1024*1024=1,048,576 個字節

II. 寄存器

寄存器是計算機儲存數據的“特別地方”。你可以把寄存器看作一個小盒子,我們可以在裏面放很多東西:比如名字、數字、一段話……

如今Win+Intel CPU組成的計算機通常有9個32位寄存器 (w/o 標誌寄存器)。它們是:

  1. EAX: 累加器
  2. EBX: 基址寄存器
  3. ECX: 計數器
  4. EDX: 數據寄存器
  5. ESI: 源變址寄存器
  6. EDI: 目的變址寄存器
  7. EBP: 擴展基址指針寄存器
  8. ESP: 棧指針寄存器
  9. EIP: 指令指針寄存器

通常來說寄存器大小都是32位 (四個字節) 。它們可以儲存值爲從0-FFFFFFFF (無符號)的數據。起初大部分寄存器的名字都暗示了它們的功能,比如ECX=計數,但是現在你可以使用任意寄存器進行計數 (只有在一些自定義的部分,計數才必須用到ECX)。當我用到EAX、EBX、ECX、EDX、ESI和EDI這些寄存器時我纔會詳細解釋其功能,所以我們先講EBP、ESP、EIP。 

  1. EBP: EBP在棧中運用最廣,剛開始沒有什麼需要特別注意的 ;) 
  2. ESP: ESP指向棧區域的棧頂位置。棧是一個存放即將會被用到的數據的地方,你可以去搜索一下push/pop 指令瞭解更多棧知識。 
  3. EIP: EIP指向下一個將會被執行的指令。

還有一件值得注意的事:有一些寄存器是16位甚至8位的,它們是不能直接尋址的。

通用寄存器包括: 

8個32位:EAX ECX EDX EBX ESI EDI EBP ESP

8個16位:AX CX DX BX SI DI BP SP

8個8位: AH AL CH CL DH DL BH HL

16位段寄存器:CS DS ES GS SS

由CPU 控制改變的 32位EIP / 16位 IP

32位EFLAGS / 16位FLAGS

比如,EAX是這個32位寄存器的名字,EAX的低16位部分被稱作AX,AX又分爲高8位的AH和低8位的AL兩個獨立寄存器。 

注意:即使不怎麼重要,你至少也要知道以下的寄存器

這些寄存器可以幫助我們區分大小: 

i. 單字節(8位)寄存器: 顧名思義,這些寄存器都是一個字節 (8位) :

  1. AL and AH
  2. BL and BH
  3. CL and CH
  4. DL and DH

ii. 單字(16位)寄存器: 這些寄存器大小爲一個字 (=2 字節 = 16 位)。一個單字寄存器包含兩個單字節寄存器。我們通常根據它們的功能來區分它們。 

1. 通用寄存器:

  1. AX-> 'accumulator'(累加器):用於進行數學運算。 (單字=16位) = AH + AL -> 其中‘+’號並不代表把它們代數相加。AH和AL寄存器是相互獨立的,只不過都是AX寄存器的一部分,所以你改變AH或AL (或者都改變) ,AX寄存器也會被改變。 
  2. BX -> 'base'(基址寄存器):用來連接棧(之後會說明)
  3. CX -> 'counter'(計數器):
  4. DX -> 'data'(數據寄存器):大多數情況下用來存放數據
  5. DI -> 'destination index'(目的變址寄存器): 例如將一個字符串拷貝到DI
  6. SI -> 'source index'(源變址寄存器): 例如將一個字符串從SI拷貝

2. 索引寄存器(指針寄存器): 

  1. BP -> 'base pointer'(基址指針寄存器):表示棧區域的基地址
  2. SP -> 'stack pointer'(棧指針寄存器):表示棧區域的棧頂地址

3. 段寄存器:

  1. CS -> 'code segment'(代碼段寄存器):用於存放應用程序代碼所在段的段基址(之後會說明)
  2. DS -> 'data segment'(數據段寄存器):用於存放數據段的段基址(以後會說明)
  3. ES -> 'extra segment'(附加段寄存器):用於存放程序使用的附加數據段的基地址
  4. SS -> 'stack segment'(棧段寄存器):用於存放棧段的段基址(以後會說明)

4. 指令指針寄存器:

IP -> 'instruction pointer'(指令指針寄存器):指向下一個指令 ;)

iii. 雙字(32位)寄存器:

2 字= 4 字節= 32 位, EAX、EBX、ECX、EDX、EDI…… 

如果16位寄存器前面加了‘E’,就代表它們是32位寄存器。例如,AX=16位,EAX=32位。 

III. 標誌寄存器

標誌寄存器代表某種狀態。在32位CPU中有32個不同的標誌寄存器,不過不用擔心,我們只關心其中的3個:ZF、OF、CF。在逆向工程中,你瞭解了標誌寄存器就能知道程序在這一步是否會跳轉,標誌寄存器就是一個標誌,只能是0或者1,它們決定了是否要執行某個指令。

Z-Flag(零標誌):

ZF是破解中用得最多的寄存器(通常情況下佔了90%),它可以設成0或者1。若上一個運算結果爲0,則其值爲1,否則其值爲0。(你可能會問爲什麼‘CMP’可以操作ZF寄存器,這是因爲該指令在做比較操作(等於、不等於),那什麼時候結果是0什麼時候是1呢?待會再說) 

The O-Flag(溢出標誌):

OF寄存器在逆向工程中大概佔了4%,當上一步操作改變了某寄存器的最高有效位時,OF寄存器會被設置成1。例如:EAX的值爲7FFFFFFFF,如果你此時再給EAX加1,OF寄存器就會被設置成1,因爲此時EAX寄存器的最高有效位改變了(你可以使用電腦自帶計算器將這個16進制轉化成2進制看看)。還有當上一步操作產生溢出時(即算術運算超出了有符號數的表示範圍),OF寄存器也會被設置成1。 

The C-Flag(進位標誌):

進位寄存器的使用大概佔了1%,如果產生了溢出,就會被設置成1。例,假如某寄存器值爲FFFFFFFF,再加上1就會產生溢出,你可以用電腦自帶的計算器嘗試。 

IV. 段偏移 

內存中的一個段儲存了指令(CS)、數據(DS)、堆棧(SS)或者其他段(ES)。每個段都有一個偏移量,在32位應用程序下,這些偏移量由 00000000 到 FFFFFFFF。段和偏移量的標準形式如下:

段:偏移量 = 把它們放在一起就是內存中一個具體的地址。

可以這樣看:

一個段是一本書的某一頁:偏移量是一頁的某一行

V. 棧

棧是內存裏可以存放稍後會用到的東西的地方。可以把它看作一個箱子裏的一摞書,最後一本放進去的永遠是最先出來的。或者把棧看作一個放紙的盒子,盒子是棧,而每一張紙就代表了一個內存地址。總之記住這個規則:最後放的紙最先被拿出來。’push’命令就是向棧中壓入數據,‘pop’命令就是從棧中取出最後放入的數據並且把它存進具體的寄存器中。

VI. 指令 (字母表排序) 

請注意,所有的值通常是以16進制形式儲存的。

大部分指令有兩個操作符 (例如:add EAX, EBX),有些是一個操作符 (例如:not EAX),還有一些是三個操作符 (例如:IMUL EAX、EDX、64)。如果你使用 “DWORD PTR [XXX]”就表示使用了內存中偏移量爲[XXX]的的數據。注意:字節在內存中儲存方式是倒過來的(Win+Intel的電腦上大部分採用”小端法”, WORD PTR [XXX](雙字節)和 BYTE PTR [XXX](單字節)也都遵循這一規定)。 

大部分有兩個操作符的指令都是以下這些形式(以add指令舉例):

add eax,ebx                          ;; 寄存器, 寄存器
add eax,123                          ;; 寄存器, 數值
add eax,dword ptr [404000]           ;; 寄存器, Dword  指針 [數值]
add eax,dword ptr [eax]              ;; 寄存器, Dword  指針 [寄存器值]
add eax,dword ptr [eax+00404000]     ;; 寄存器, Dword  指針 [寄存器值+數值]
add dword ptr [404000],eax           ;; Dword 指針[數值], 寄存器
add dword ptr [404000],123           ;; Dword 指針[數值], 數值
add dword ptr [eax],eax              ;; Dword 指針[寄存器值], 寄存器
add dword ptr [eax],123              ;; Dword 指針[寄存器值], 數值
add dword ptr [eax+404000],eax       ;; Dword 指針[寄存器值+數值], 寄存器
add dword ptr [eax+404000],123       ;; Dword 指針[寄存器值+數值], 數值

ADD (加)

語法: ADD 被加數, 加數

加法指令將一個數值加在一個寄存器上或者一個內存地址上。

add eax,123 = eax=eax+123;

加法指令對ZF、OF、CF都會有影響。

AND (邏輯與)

語法: AND 目標數, 原數    

AND運算對兩個數進行邏輯與運算。

AND指令會清空OF,CF標記,設置ZF標記。

爲了更好地理解AND,這裏有兩個二進制數:

1001010110
0101001101

如果對它們進行AND運算,結果是0001000100

即同真爲真(1),否則爲假(0),你可以用計算器驗證。

CALL (調用)

語法:CALL something

CALL指令將當前的相對地址(IP)壓入棧中,並且調用CALL 後的子程序

CALL 可以這樣使用:

CALL 404000                ;; 最常見: CALL 地址
CALL EAX                   ;; CALL 寄存器 - 如果寄存器存的值爲404000,那就等同於第一種情況
CALL DWORD PTR [EAX]       ;; CALL [EAX]偏移量所指向的地址
CALL DWORD PTR [EAX+5]     ;; CALL [EAX+5]偏移量所指向的地址

CDQ

Syntax: CDQ

CDQ指令第一次出現時通常不好理解。它通常出現在除法前面,作用是將EDX的所有位變成EAX最高位的值,

比如當EAX>=80000000h時,其二進制最高位爲1,則EDX被32位全賦值爲1,即FFFFFFFF

若EAX<80000000,則其二進制最高位爲0,EDX爲00000000。

然後將EDX:EAX組成新數(64位):FFFFFFFF 80000000

CMP (比較)

語法: CMP 目標數, 原數

CMP指令比較兩個值並且標記CF、OF、ZF:

CMP     EAX, EBX              ;; 比較eax和ebx是否相等,如果相等就設置ZF爲1
CMP     EAX,[404000]          ;; 比較eax和偏移量爲[404000]的值是否相等
CMP     [404000],EAX          ;; 比較[404000]是否與eax相等

DEC (自減)

語法: DEC something

dec用來自減1,相當於c中的–

dec可以有以下使用方式:

dec eax                             ;; eax自減1
dec [eax]                           ;; 偏移量爲eax的值自減1
dec [401000]                        ;; 偏移量爲401000的值自減1
dec [eax+401000]                    ;; 偏移量爲eax+401000的值自減1

dec指令可以標記ZF、OF

DIV (除)

語法: DIV 除數

DIV指令用來將EAX除以除數(無符號除法),被除數通常是EAX,結果也儲存在EAX中,而被除數對除數取的模存在除數中。

例:

mov eax,64                      ;; EAX = 64h = 100
mov ecx,9                       ;; ECX = 9
div ecx                         ;; EAX除以ECX

在除法之後 EAX = 100/9 = 0B(十進制:11) 並且 ECX = 100 MOD 9 = 1

div指令可以標記CF、OF、ZF

IDIV (整除)

語法: IDIV 除數

IDIV執行方式同div一樣,不過IDIV是有符號的除法

idiv指令可以標記CF、OC、ZF

IMUL (整乘)

語法:IMUL 數值

IMUL 目標寄存器、數值、數值

IMUL 目標寄存器、數值

IMUL指令可以把讓EAX乘上一個數(INUL 數值)或者讓兩個數值相乘並把乘積放在目標寄存器中(IMUL 目標寄存器, 數值,數值)或者將目標寄存器乘上某數值(IMUL 目標寄存器, 數值)

如果乘積太大目標寄存器裝不下,那OF、CF都會被標記,ZF也會被標記

INC (自加)

語法: INC something

INC同DEC相反,它是將值加1

INC指令可以標記ZF、OF

INT

語法: int 目標數

INT 的目標數必須是產生一個整數(例如:int 21h),類似於call調用函數,INT指令是調用程序對硬件控制,不同的值對應着不同的功能。

具體參照硬件說明書。

JUMPS

這些都是最重要的跳轉指令和觸發條件(重要用*標記,最重要用**標記):

指令                條件                    條件
JA*          -    如果大於就跳轉(無符號)      - CF=0 and ZF=0
JAE          -    如果大於或等於就跳轉(無符號)- CF=0
JB*          -    如果小於就跳轉(無符號)   - CF=1
JBE          -    如果小於或等於就跳轉(無符號)- CF=1 or ZF=1
JC           -    如果CF被標記就了跳轉       - CF=1
JCXZ         -    如果CX等於0就跳轉      - CX=0
JE**         -    如果相等就跳轉        - ZF=1
JECXZ        -    如果ECX等於0就跳轉       - ECX=0
JG*          -    如果大於就跳轉(有符號)   - ZF=0 and SF=OF (SF = Sign Flag)
JGE*         -    如果大於或等於就跳轉(有符號) - SF=OF
JL*          -    如果小於就跳轉(有符號)    - SF != OF (!= is not)
JLE*         -    如果小於或等於就跳轉(有符號 - ZF=1 and OF != OF
JMP**        -    跳轉             - 強制跳轉
JNA          -    如果不大於就跳轉(無符號)   - CF=1 or ZF=1
JNAE         -    如果不大於等於就跳轉(無符號) - CF=1
JNB          -    如果不小於就跳轉(無符號)   - CF=0
JNBE         -    如果不小於等於就跳轉(無符號) - CF=0 and ZF=0
JNC          -    如果CF未被標記就跳轉     - CF=0
JNE**        -    如果不等於就跳轉       - ZF=0
JNG          -    如果不大於就跳轉(有符號)   - ZF=1 or SF!=OF
JNGE         -    如果不大於等於就跳轉(有符號) - SF!=OF
JNL          -    如果不小於就跳轉(有符號)   - SF=OF
JNLE         -    如果不小於等於就跳轉(有符號) - ZF=0 and SF=OF
JNO          -    如果OF未被標記就跳轉     - OF=0
JNP          -    如果PF未被標記就跳轉     - PF=0
JNS          -    如果SF未被標記就跳轉      - SF=0
JNZ          -    如果不等於0就跳轉      - ZF=0
JO           -    如果OF被標記就跳轉     - OF=1
JP           -    如果PF被標記就跳轉     - PF=1
JPE          -    如果是偶數就跳轉       - PF=1
JPO          -    如果是奇數就跳轉       - PF=0
JS           -    如果SF被標記就跳轉     - SF=1
JZ           -    如果等於0就跳轉      - ZF=1

LEA (有效地址傳送)

語法:LEA 目的數、源數

LEA可以看成和MOV差不多的指令LEA ,它本身的功能並沒有被太廣泛的使用,反而廣泛運用在快速乘法中:

lea eax,dword ptr [4*ecx+ebx]

將eax賦值爲 4*ecx+ebx

MOV (傳送)

語法: MOV 目的數,源數

這是一個很簡單的指令,MOV指令將源數賦值給目的數,並且源數值保持不變

這裏有一些MOV的變形:

MOVS/MOVSB/MOVSW/MOVSD EDI, ESI:這些變形能將ESI指向的內容傳送到EDI指向的內容中去

MOVSX:MOVSX指令將單字或者單字節擴展爲雙字或者雙字節傳送,原符號不變

MOVZX:MOVZX擴展單字節或單字爲雙字節或雙字並且用0填充剩餘部分(通俗來說就是將源數取出置於目的數,其他位用0填充)

MUL (乘法)

語法:MUL 數值

這個指令同IMUL一樣,不過MUL可以乘無符號數。

NOP (無操作)

語法:NOP

這個指令說明不做任何事

所以它在逆向中運用範圍最廣

OR (邏輯或)

語法:OR 目的數,源數

OR指令對兩個值進行邏輯或運算

這個指令會清空OF、CF標記,設置ZF標記

爲了更好的理解OR,思考下面二進制串:

1001010110
0101001101

如果對它們進行邏輯與運算,結果將是1101011111。

只有當兩邊同爲0時其結果爲0,否則就爲1。你可以用計算器嘗試計算。希望你能理解爲什麼,最好自己動手算一算

POP

語法:POP 目的地址

POP指令將棧頂第一個字傳送到目的地址。 每次POP後,ESP(棧指針寄存器)都會增加以指向新棧頂

PUSH

語法:PUSH 值

PUSH是POP的相反操作,它將一個值壓入棧並且減小棧頂指針值以指向新棧頂。

REP/REPE/REPZ/REPNE/REPNZ

語法: REP/REPE/REPZ/REPNE/REPNZ ins

重複上面的指令:直到CX=0。ins必須是一個操作符,比如CMPS、INS、LODS、MOVS、OUTS、SCAS 或 STOS

RET (返回)

語法:RET

RET digit

RET指令的功能是從一個代碼區域中退出到調用CALL的指令處。

RET digit在返回前會清理棧

SUB (減)

語法:SUB 目的數,源數

SUB與ADD相反,它將源數減去目的數,並將結果儲存在目的數中

SUB可以標記ZF、OF、CF

TEST

語法:TEST 操作符、操作符

這個指令99%都是用於”TEST EAX, EAX”,它執行與AND相同的功能,但是並不儲存數據。如果EAX=0就會標記ZF,如果EAX不是0,就會清空ZF

XOR

語法:XOR 目的數,源數

XOR指令對兩個數進行異或操作

這個指令清空OF、CF,但會標記ZF

爲了更好的理解,思考下面的二進制串:

1001010110
0101001101

如果異或它們,結果將是1100011011

如果兩個值相等,則結果爲0,否則爲1,你可以使用計算器驗算。

很多情況下我們會使用”XOR EAX, EAX”,這個操作是將EAX賦值爲0,因爲當一個值異或其自身,就過都是0。你最好自己動手嘗試下,這樣可以幫助你理解得更好。

VII. 邏輯操作符  

下面都是通常的邏輯操作符:

好了,彙編基礎已經梳理完畢!

發佈了56 篇原創文章 · 獲贊 157 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章