Loki博客

大龄程序员逆向之路

逆向工程(一):汇编、逆向工程基础篇

A逆向工程(一):汇编、逆向工程基础篇

单元、位和字节

  • BIT(位) - 电脑数据量的最小单元,可以是0或者1。
    例:00000001 = 1;00000010 = 2;00000011 = 3
  • BYTE(字节) - 一个字节包含8个位,所以一个字节最大值是255(0-255)。为了方便阅读,我们通常使用16进制来表示。
  • WORD(字) - 一个字由两个字节组成,共有16位。一个字的最大值是0FFFFh (或者是 65535d) (h代表16进制,d代表10进制)。
  • DOUBLE WORD(双字DWORD) - 一个双字包含两个字,共有32位。最大值为0FFFFFFFF (或者是 4294967295d)。
  • KILOBYTE(千字) - 千字节并不是1000个字节,而是1024 (32*32) 个字节。
  • MEGABYTE - 兆字节同样也不是一兆个字节,而是1024*1024=1,048,576 个字节

一个字=2个字节=16位

寄存器

寄存器是计算机储存数据的“特别地方”。你可以把寄存器看作一个小盒子,我们可以在里面放很多东西:比如名字、数字、一段话……

如今Win+Intel CPU组成的计算机通常有9个32位寄存器 (w/o 标志寄存器)

  • EAX: 累加器
  • EBX: 基址寄存器
  • ECX: 计数器
  • EDX: 数据寄存器
  • ESI: 源变址寄存器
  • EDI: 目的变址寄存器
  • EBP: 扩展基址指针寄存器
  • ESP: 栈指针寄存器
  • EIP: 指令指针寄存器

通常来说寄存器大小都是32位 (四个字节) 。它们可以储存值为从0-FFFFFFFF (无符号)的数据。起初大部分寄存器的名字都暗示了它们的功能,比如ECX=计数,但是现在你可以使用任意寄存器进行计数 (只有在一些自定义的部分,计数才必须用到ECX)。当我用到EAX、EBX、ECX、EDX、ESI和EDI这些寄存器时我才会详细解释其功能,所以我们先讲EBP、ESP、EIP。

  • EBP: EBP在栈中运用最广,刚开始没有什么需要特别注意的 ;)
  • ESP: ESP指向栈区域的栈顶位置。栈是一个存放即将会被用到的数据的地方,你可以去搜索一下push/pop 指令了解更多栈知识。
  • EIP: EIP指向下一个将会被执行的指令。

有一些寄存器是16位甚至8位的,它们是不能直接寻址的

32位寄存器 16位寄存器 8位寄存器
EAX AX AH/AL
EBX BX BH/BL
ECX CX CH/CL
ESI SI -
EDI DI -
EBP BP -
ESP SP -
EIP IP -

** 通常一个寄存器可以看成 **

1
2
3
4
5
6
7
|----------------- EAX:32 位(=1 DWORD =4BYTES)-----------------|

|------ AX:16 位 (=1 WORD =2 BYES)-------|

|-AH:8 位 (=1 BYTE)-|-AL:8位(=1 BYTE)-|

AH为高8位独立寄存器;AL为低8位独立寄存器

通用寄存器

1
2
3
4
5
6
7
* AX (单字=16位) = AH + AL -> 其中‘+’号并不代表把它们代数相加。AH和AL寄存器是相互独立的,只不过都是AX寄存器的一部分,所以你改变AH或AL (或者都改变) ,AX寄存器也会被改变。
-> 'accumulator'(累加器):用于进行数学运算
* BX -> 'base'(基址寄存器):用来连接栈
* CX -> 'counter'(计数器):
* DX -> 'data'(数据寄存器):大多数情况下用来存放数据
* DI -> 'destination index'(目的变址寄存器): 例如将一个字符串拷贝到DI
* SI -> 'source index'(源变址寄存器): 例如将一个字符串从SI拷贝

索引寄存器(指针寄存器)

1
2
BP -> 'base pointer'(基址指针寄存器):表示栈区域的基地址
SP -> 'stack pointer'(栈指针寄存器):表示栈区域的栈顶地址

段寄存器

1
2
3
4
CS -> 'code segment'(代码段寄存器):用于存放应用程序代码所在段的段基址(之后会说明)
DS -> 'data segment'(数据段寄存器):用于存放数据段的段基址(以后会说明)
ES -> 'extra segment'(附加段寄存器):用于存放程序使用的附加数据段的基地址
SS -> 'stack segment'(栈段寄存器):用于存放栈段的段基址(以后会说明)

指令指针寄存器

1
IP -> 'instruction pointer'(指令指针寄存器):指向下一个指令

双字(32位)寄存器

就是在16位寄存器前面加上E 如:AX 16位 EAX 32位

标志寄存器

标志寄存器代表某种状态。在32位CPU中有32个不同的标志寄存器,但是我们只关心其中的3个:ZF、OF、CF。标志寄存器就是一个标志,只能是0或者1,它们决定了是否要执行某个指令。

  • Z-Flag(零标志):ZF是破解中用得最多的寄存器(通常情况下占了90%),它可以设成0或者1。若上一个运算结果为0,则其值为1,否则其值为0。

  • The O-Flag(溢出标志):表示有符号运算结果是否超出范围,超出范围则溢出,OF置位,溢出是一种错误的运算结果,计算机产生溢出会发生意想不到的结果。OF寄存器在逆向工程中大概占了4%,当上一步操作改变了某寄存器的最高有效位时,OF寄存器会被设置成1。例如:EAX的值为7FFFFFFFF,如果你此时再给EAX加1,OF寄存器就会被设置成1,因为此时EAX寄存器的最高有效位改变了。溢出也会变为1。

  • The C-Flag(进位标志):表示无符号运算结果是否超出范围,超出范围产生则进位,CF置位,进位寄存器的使用大概占了1%,如果产生了溢出,就会被设置成1。例,假如某寄存器值为FFFFFFFF,再加上1就会产生溢出,你可以用电脑自带的计算器尝试。

段偏移

内存中的一个段储存了指令(CS)、数据(DS)、堆栈(SS)或者其他段(ES)。每个段都有一个偏移量,在32位应用程序下,这些偏移量由 00000000 到 FFFFFFFF。段和偏移量的标准形式如下:

1
段:偏移量 = 把它们放在一起就是内存中一个具体的地址。

一个段是一本书的某一页:偏移量是一页的某一行

栈遵循后进先出的原则,’push’命令就是向栈中压入数据,‘pop’命令就是从栈中取出最后放入的数据并且把它存进具体的寄存器中。

指令

ADD(加)

语法: ADD 被加数, 加数

加法指令将一个数值加在一个寄存器上或者一个内存地址上。

add eax,123 = eax=eax+123;

加法指令对ZF、OF、CF都会有影响。

AND(逻辑与)

语法: AND 目标数, 原数

AND运算对两个数进行逻辑与运算。

AND指令会清空OF,CF标记,设置ZF标记。

如:
1001011110
0101001101 其结果为001001100

CALL (调用)

语法:CALL something

CALL指令将当前的相对地址(IP)压入栈中,并且调用CALL 后的子程序

CALL 可以这样使用:

1
2
3
4
CALL 404000                ;; 最常见: CALL 地址
CALL EAX ;; CALL 寄存器 - 如果寄存器存的值为404000,那就等同于第一种情况
CALL DWORD PTR [EAX] ;; CALL [EAX]偏移量所指向的地址
CALL DWORD PTR [EAX+5] ;; CALL [EAX+5]偏移量所指向的地址

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:

1
2
3
CMP     EAX, EBX              ;; 比较eax和ebx是否相等,如果相等就设置ZF为1
CMP EAX,[404000] ;; 比较eax和偏移量为[404000]的值是否相等
CMP [404000],EAX ;; 比较[404000]是否与eax相等

DEC (自减)

语法: DEC something

dec用来自减1,相当于c中的–

1
2
3
4
dec eax                             ;; eax自减1
dec [eax] ;; 偏移量为eax的值自减1
dec [401000] ;; 偏移量为401000的值自减1
dec [eax+401000] ;; 偏移量为eax+401000的值自减1

DIV (除)

语法: DIV 除数

DIV指令用来将EAX除以除数(无符号除法),被除数通常是EAX,结果也储存在EAX中,而被除数对除数取的模存在除数中。

1
2
3
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。你最好自己动手尝试下,这样可以帮助你理解得更好。

Buy me a coffee