本系列文章将讲解逆向工程的各种知识,难度由浅入深。
汇编是逆向工程的基础,这篇文章讲解并不深入但是覆盖了你刚开始学习汇编需要了解的所有基础知识!汇编语言是一切程序的起点和终点,毕竟所有的高级语言都是建立在汇编基础之上的。在许多高级语言中我们都需要相对明确的语法,但是在汇编中,我们会使用一些单词缩写和数字来表达程序。
I. 单元、位和字节
·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 个字节
II. 寄存器
寄存器是计算机储存数据的“特别地方”。你可以把寄存器看作一个小盒子,我们可以在里面放很多东西:比如名字、数字、一段话……
如今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位的,它们是不能直接寻址的。
包括:
通常一个寄存器可以这样看:
由图可知,EAX是这个32位寄存器的名字,EAX的低16位部分被称作AX,AX又分为高8位的AH和低8位的AL两个独立寄存器。
注意:即使不怎么重要,你至少也要知道以下的寄存器
这些寄存器可以帮助我们区分大小:
i. 单字节(8位)寄存器: 顾名思义,这些寄存器都是一个字节 (8位) :
AL and AH
BL and BH
CL and CH
DL and DH
ii. 单字(16位)寄存器: 这些寄存器大小为一个字 (=2 字节 = 16 位)。一个单字寄存器包含两个单字节寄存器。我们通常根据它们的功能来区分它们。
1. 通用寄存器:
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拷贝
2. 索引寄存器(指针寄存器):
BP -> 'base pointer'(基址指针寄存器):表示栈区域的基地址
SP -> 'stack pointer'(栈指针寄存器):表示栈区域的栈顶地址
3. 段寄存器:
CS -> 'code segment'(代码段寄存器):用于存放应用程序代码所在段的段基址(之后会说明)
DS -> 'data segment'(数据段寄存器):用于存放数据段的段基址(以后会说明)
ES -> 'extra segment'(附加段寄存器):用于存放程序使用的附加数据段的基地址
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. 逻辑操作符
下面都是通常的逻辑操作符:
引入
目标:去掉烦人的消息框
工具:Ollydbg
1.载入程序,由图可以看出调用了两个MessageBox,当程序执行到这里的时候分别会有上述消息框弹出。
2.观察 cmp eax,0×0
这里判断eax是否等于0
je(如果相等就跳转 – ZF=1
由于eax等于40000
所以这一个跳转永远不会成立,所以一定会执行这个烦人的MessageBox。
那么我们就可以想办法使它跳转。
3.我们尝试改变ZF标记值(je跳转根据ZF标记判断)
如图,此时ZF标记为0,我们双击这个0,使其标记为1
再观察:
跳转实现了,我们成功跳过了消息窗口!!
不过这样每次要改变ZF标记很麻烦,我们可不可以让它直接跳转不进行判断呢?所以我们可以双击编辑je short 00401024为jmp short 00401024
成功跳过了MessageBox!!
我们想使用一种更加完美的方法来跳过这个消息框。。。。
假如我们把程序入口设置成00401024不就直接跳过了MessageBox了吗?在这之前,我需要解释一些PE的知识(请耐心地阅读,这才是本文所要讲的重点,而不是如何破解这个程序!!)
下面标有红色的代表重点,如果你时间紧迫,可以只看有下划线的文字
深入的必经之路:
PE(Portable Executable)文件简介
PE(Portable Executable)文件是Windows操作系统下使用的可执行文件格式。它是微软在UNIX平台的COFF(通用对象文件格式)基础上制作而成。最初设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在Windows系列操作系统下。
PE文件是指32位可执行文件,也称为PE32。64位的可执行文件称为PE+或PE32+,是PE(PE32)的一种扩展形式(请注意不是PE64)。
PE文件结构一般如上图所示。
当一个PE文件被执行时,PE装载器首先检查DOS header里的PE header的偏移量。如果找到,则直接跳转到PE header的位置。
当PE装载器跳转到PE header后,第二步要做的就是检查PE header是否有效。如果该PE header有效,就跳转到PE header的尾部。
紧跟PE header尾部的是节表。PE装载器执行完第二步后开始读取节表中的节段信息,并采用文件映射(在执行一个PE文件的时候,Windows并不在一开始就将整个文件读入内存,而是采用与内存映射的机制,也就是说,Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系)的方法将这些节段映射到内存,同时附上节表里指定节段的读写属性。
PE文件映射入内存后,PE装载器将继续处理PE文件中类似 import table (输入表)的逻辑部分
这四个步骤便是PE文件的执行顺序,具体细节读者可以参考相关文档。
(以上四个步骤摘自《黑客破解精通》)
下面用我们要破解程序进行简单说明:
首先用WinHex 打开破解程序。上图是程序的起始部分,也是PE文件的头部分。文件运行需要的所有信息就储存在这个PE头文件中。所以,学习PE文件格式就是学习PE头中的结构体。
事情根本没有这么简单:
上图描述了文件加载到内存的情形,包含了许多内容,我们逐一学习。
文件中使用偏移(offset),内存中使用VA(Virtual Address,虚拟地址)来表示位置。
VA指进程虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址)是指从某基准位置(ImageBase)开始的相对地址。VA与RVA满足下面的换算关系:
RVA+ImageBase=VA
PE头内部信息大多是RVA形式存在。原因在于(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他的PE文件(DLL)。此时必须通过重定向(Relocation)将其加载到其他空白的位置,若PE头信息使用的是VA,则无法正常访问。因此使用RVA来重定向信息,即使发生了重定向,只要相对于基准位置的相对位置没有变化,就能正常访问到指定信息,不会出现任何问题。
当PE文件被执行时,PE装载器会为进程分配4CG的虚拟地址空间,然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到虚拟地址空间中的0X400000的位置。
PE头:
DOS头
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部
USHORT e_magic; // DOS签名“MZ-->Mark Zbikowski(设计了DOS的工程师)”
USHORT e_cblp; // 文件最后页的字节数
USHORT e_cp; // 文件页数
USHORT e_crlc; // 重定义元素个数
USHORT e_cparhdr; // 头部尺寸,以段落为单位
USHORT e_minalloc; // 所需的最小附加段
USHORT e_maxalloc; // 所需的最大附加段
USHORT e_ss; // 初始的SS值(相对偏移量)
USHORT e_sp; // 初始的SP值
USHORT e_csum; // 校验和
USHORT e_ip; // 初始的IP值
USHORT e_cs; // 初始的CS值(相对偏移量)
USHORT e_lfarlc; // 重分配表文件地址
USHORT e_ovno; // 覆盖号
USHORT e_res[4]; // 保留字
USHORT e_oemid; // OEM标识符(相对e_oeminfo)
USHORT e_oeminfo; // OEM信息
USHORT e_res2[10]; // 保留字
LONG e_lfanew; // 指示NT头的偏移(根据不同文件拥有可变值)
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
其中比较重要的有e_magic和e_lfanew,由图可知
e_magic的值为4D5A,e_lfanew的值为000000C0(注意不是C0000000,详见我的上一篇文章)
WORD占2个字节,LONG占4个字节,刚好是30个WORD和1个LONG,从00000000到0000003F
DOS存根:
即使没有DOS存根,文件也能正常执行
NT头(PE最重要的头):
其定义如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature:类似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是'PE‘(00004550)。
IMAGE_FILE_HEADER:IMAGE_FILE_HEADER是PE文件头,定义如下:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
其中有4个重要的成员(若设置不正确,将会导致文件无法正常运行)
#1.Machine
每个CPU拥有唯一的Machine码,兼容32位Intel X86芯片的Machine码为14C(如图)。以下是定义在winnt.h文件中的Machine码:
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_AM33 0x01d3
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon
#define IMAGE_FILE_MACHINE_CEF 0x0CEF
#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE 0xC0EE
#2.NumberOfEsctions
PE文件把代码,数据,资源等依据属性分类到各节中储存。
NumberOfEsctions指文件中存在的节段(又称节区)数量,也就是节表中的项数。该值一定要大于0,且当定义的节段数与实际不符时,将发生运行错误。
#3.SizeOfOptionalHeader
IMAGE_NT_HEADERS结构最后一个成员IMAGE_OPTIONAL_HEADER32。
SizeOfOptionalHeader用来指出IMAGE_OPTIONAL_HEADER32结构体的长度。PE装载器需要查看SizeOfOptionalHeader的值,从而识别IMAGE_OPTIONAL_HEADER32结构体的大小。
PE32+格式文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,这两个结构体尺寸是不相同的,所以需要在SizeOfOptionalHeader中指明大小。
#4.Characteristics
该段用于标识文件的属性,文件是否是可运行的状态,是否为DLL文件等信息。
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
为方便理解,上述程序的NT头内容如下:
(成员功能概述)
NumberOfSections:该PE文件中有多少个节段,也就是节表中的项数。
TimeDateStamp:PE文件的创建时间,一般有连接器填写。
PointerToSymbolTable:COFF文件符号表在文件中的偏移。
NumberOfSymbols:符号表的数量。
SizeOfOptionalHeader:紧随其后的可选头的大小。
Characteristics:可执行文件的属性。
IMAGE_OPTIONAL_HEADER32:
其定义如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
我们需要关注下列成员,这些事运行程序必需的,设置错误将导致程序无法正常运行。
#1.Magic
为IMAGE_OPTIONAL_HEADER32时,magic码为10B,为IMAGE_OPTIONAL_HEADER64时,magic码为20B
#2.AddressOfEntryPoint
AddressOfEntryPoint持有EP的RVA值。该值指出程序最先执行的代码起始地址,相当重要。
#3.ImageBase
一般来说,使用开发工具(VB/VC++/Delphi)创建好EXE文件后,其ImageBase值为00400000,DLL文件的ImageBase值为10000000(当然也可以指定其他值)。
执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint
#4.SectionAlignment,FileAlignment
PE文件的Body部分被划分成若干节段,这些节段储存着不同类别的数据。FileAlignment指定了节段在磁盘文件中的最小单位,而SectionAlignment则指定了节区在内存中的最小单位(SectionAlignment必须大于或者等于FileAlignment)
#5.SizeOfImage
当PE文件加载到内存时,SizeOfImage指定了PE Image在虚拟内存中所占用的空间大小,一般文件大小与加载到内存中的大小是不同的(节段头中定义了各节装载的位置与占有内存的大小,后面会讲到)
#6.SizeOfHeader
SizeOfHeader用来指出整个PE头大小。该值必须是FileAlignment的整数倍。第一节段所在位置与SizeOfHeader距文件开始偏移的量相同。
#7.Subsystem
Subsystem值用来区分系统驱动文件(*.sys)与普通可执行文件(*.exe,*.dll)。
Subsystem成员可拥有值如下:
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn't require a subsystem. 系统驱动
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Image runs in the Windows GUI subsystem. 窗口应用程序
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Image runs in the Windows character subsystem. 控制台应用程序
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem.
#define IMAGE_SUBSYSTEM_EFI_APPLICATION 10 //
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11 //
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12 //
#define IMAGE_SUBSYSTEM_EFI_ROM 13
#define IMAGE_SUBSYSTEM_XBOX 14
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16
#8.DataDirectory
数据目录,定义如下:
· typedef struct _IMAGE_DATA_DIRECTORY {
· DWORD VirtualAddress;
· DWORD Size;
· } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
可以看出,有地址(VirtualAddress)有大小(Size),数组定义的一定是一个区域,数组每项都有被定义的值,不同项对应不同数据结构,比如导入表,导出表等,定义如下:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
各位重点关注标红的IMPORT和EXPORT,它们是PE头中的非常重要的部分,其它部分不怎么重要,大致了解下即可。
#9.NumberOfRvaAndSizes
NumberOfRvaAndSizes用来指定DataDirectory的数组个数,虽然结构体定义中明确指出了数组个数为16,但也有可能不是16,PE装载器需要通过这个值来识别。
各成员代表的值和偏移量就不一一写出了,累死咯。。。
(成员功能概述)
Magic:表示可选头的类型。
MajorLinkerVersion和MinorLinkerVersion:链接器的版本号。
SizeOfCode:代码段的长度,如果有多个代码段,则是代码段长度的总和。
SizeOfInitializedData:初始化的数据长度。
SizeOfUninitializedData:未初始化的数据长度。
AddressOfEntryPoint:程序入口的RVA,对于exe这个地址可以理解为WinMain的RVA。对于DLL,这个地址可以理解为DllMain的RVA,如果是驱动程序,可以理解为DriverEntry的RVA。当然,实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成,当然,这些不是本文的重点。
BaseOfCode:代码段起始地址的RVA。
BaseOfData:数据段起始地址的RVA。
ImageBase:映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。
SectionAlignment:节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。
FileAlignment:节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。
MajorOperatingSystemVersion、MinorOperatingSystemVersion:所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。
MajorImageVersion、MinorImageVersion:映象的版本号,这个是开发者自己指定的,由连接器填写。
MajorSubsystemVersion、MinorSubsystemVersion:所需子系统版本号。
Win32VersionValue:保留,必须为0。
SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。
SizeOfHeaders:所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。
CheckSum:映象文件的校验和。
Subsystem:运行该PE文件所需的子系统。
DllCharacteristics:DLL的文件属性,只对DLL文件有效,可以是下面定义中某些的组合:
SizeOfStackReserve:运行时为每个线程栈保留内存的大小。
SizeOfStackCommit:运行时每个线程栈初始占用内存大小。
SizeOfHeapReserve:运行时为进程堆保留内存大小。
SizeOfHeapCommit:运行时进程堆初始占用内存大小。
LoaderFlags:保留,必须为0。
NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数。
DataDirectory:数据目录,这是一个数组。
节段(区)头:
PE文件有不同的节段:code(代码),data(数据),resource(资源),这样设计避免了很多安全问题,比如向data写数据,由于某原因导致溢出,其下的code就会被覆盖,程序就会崩溃。
code/data/resource都有不同的权限,如下:
节段头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节段。
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
下表列出了需要了解的重要成员:
VirtualAddress与PointerToRawData不带有任何值,分别由(定义在IMAGE_OPTIONAL_HEADER32中的)SectionAlignment和FileAlignment确定。
上述程序有4个节段。
如何运用我们上面学习到的PE知识?
了解了PE知识,继续我们的破解工作!!
我们在数据窗口(dump)有点选择跳转,跳转到起始位置(400000):
点击OK
我们成功跳转到了起始位置。
点击右上方LEMTW的M查看内存情况。
双击该处进入,看到DOS HEADER已经载入了,直接向下翻,查找DOS头的e_lfanew成员:
查看到偏移量是000000C0,记住我们载入内存时,基准位置(ImageBase)是400000,相对虚拟地址(RVA)是:
RVA=ImageBase+VA
所以此时PE头位置是004000C0,我们向下翻到该处。
开始找最重要的的AddressOfEntryPoint
找到AddressOfEntryPoint=0×1000,我么需要让它跳转到401024:
那我们修改其值,双击004000E9(00 10 00 00 –>00001000)的值进行修改,修改为24 10 00 00 (00001024):
然后保存到可执行文件:
保存完成后,用OD载入刚保存的RegisterMe1.0,发现入口已经变成了我们修改的00401024,成果跳过了第一个烦人的消息框!
我们再向下执行:
执行到call Register.00401052时弹出了“我们需要注册的信息”
我们再向下执行:
再第二个MessageBox处又弹出了消息框,我们这次采用NOP(No operation)填充:
填充完成后,我们保存为可执行文件RegisterMe2.0。双击执行,再也没有可恶地消息框咯!
(三):实例破解
本系列文章将讲解逆向工程的各种知识,难度由浅入深。
补课:《逆向工程(一):汇编、逆向工程基础篇》、《逆向工程(二):从一个简单的实例来了解PE文件》
这次我们将破解一款真正的商业程序。
为了避免一些不必要的麻烦,选择的这款商业程序在2001年就停止维护了,所以我们可以放心大胆的破解学习!
(不要在这里灰心,即便是2001年的软件,仍有我们值得学习的地方!!)
这个软件也许我们在有些地方看过,很多教程都喜欢拿这个软件做靶子,但是我们还是要努力玩出一点花样,对不对,简简单单的破解当然学到的不会太多。
文中软件及原视频下载地址 密码: x3tv
无壳的实例
目标:破解软件功能限制
工具:OD
安装好这款软件打开如图:
这款软件有诸多功能限制,如:
当我们选择Add Group,提示没有注册只能创建3个Group:
选择Add,提示没有注册只能添加4个联系人:
我们打开About PixtopianBook,弹出未注册版本信息。
我们将其载入OD(建议下载一个原版OD#www.ollydbg.de#,我们尝试手动配置忽略异常)如图:
点击运行后,并没有出现程序界面,而是抛出了异常:
我们手动开启忽略异常:
打开后选择 exception:
勾选如上,再点击Add lase exception。
配置完成后点击OK。
配置完成后重新载入程序,点击运行,成功打开了程序。
正式开始!
1、Group
将软件载入OD,点击Add Group。
我们在其弹出消息框后暂停OD。
此时状态栏显示:暂停,我们此时需要返回到程序领空,快捷键Alt+F9。
然后点击消息框 确定 此时状态栏显示:返回到用户
我们成功返回到程序领空,向上翻。
看到了一个MessageBox,这就是我们刚才确定的那个消息框。
向下我们将
这个命令将带我们返回到调用CALL处。所以我们继续向下执行。
进行retn:
我们跳出了call,返回到了调用处、
我们看到:
1.这个Call就是我们刚才进入的Call,加入我们能跳过这个call,就能避免跳出消息框。
2.查看这个ascii码,正是消息框的标题和内容,在这里被压入栈,等待调用。
3.这里有一个cmp,比较eax与3大小(即Group是否等于3),如果小于就跳转到(JL)00408B34,如果已经有3个Group了,就执行call弹出未注册消息框。
你一定已经知道我们将要干什么了!
将JL(小于则跳转)改为jmp(强制跳转)。
这样我们就成功跳过了GROUP验证。
我们将其保存:
测试一下:
哈哈,已经可以自由添加Group了!!是不是很有成就感啊!学习逆向成就感很重要哦。
2、联系人
默认未注册用户只能添加四个联系人,我们采取同样方法:
1、弹出消息框后暂停
2、Alt+F9执行到用户代码
3、点击确定
跳转到这个位置,我们向上翻,看到同样的MessageBox,如下:
通过retn跳出call,回到调用处,向上翻:
1、这个call调用了MessageBox,弹出了消息框;
2、这句话同我们看到的一样,确定程序在这里将其压入栈;
3、比较eax与4的大小(联系人是否已经有4个),如果小于等于(JL)4个,那就跳过消息框,否则弹出未注册消息框。
我们同样修改jl为jmp,强制其跳转:
保存后测试一下:
我们又成功搞定了一个功能!这个阉割版已经成了注册版,不过还有一些美中不足的地方。比如:
那我们就开始让它从里到外都是注册版!
3、更改版本信息
输入查找关键字UNREGISTERED:
如果ASCII没有搜索到就用UNICODE
(这里ASCII也可以搜索到,不过不是版本信息,UNICODE下搜索到版本信息,点击确定):
记录下004D4830,在dump面板跳转:
跳转之后成功找到需要修改的内容:
我们选择要修改的内容,右键编辑:
编辑为自己想写的字符,如下:
4、DIY标题
同样是在内存中查找,在弹出的搜索框里输入关键字:(UNREGISTERED VER
(一般情况下是不知道要搜索的文字是ascii还是unicode,我们会都尝试搜索一次。
记下004E4BE6,在dump窗口跟随:
找到需要修改的字符然后编辑:
确定后如下图所示:
5、更改提示注册文字
同理,我们在内存中搜索:
首先尝试UNICODE,没有搜索到结果。
再尝试ASCII搜索:
成功搜到了字符串:
记录下位置48F974,并跳转到该处:
用同样的方法编辑ASCII码:
编辑完成后如下:
在所有工作做完之后,不要忘记保存:
保存后我们查看效果:
6、结尾
我们仍发现了一些美中不足的地方。
在进入程序时,首先是欢迎信息:
几秒后变为:
我们想让它更加简洁,一直显示welcome信息,该怎么操作呢?
那我们开始进行最后一步!!
通过上面的操作我们已经知道了注册文字在48F974处,在dump(数据窗口)窗口右键跳转到48F974
我们需要知道程序哪一段调用了这段字符串。
我们右键选择查找参考(Ctrl+R)(个人觉得做汉化翻译得有点问题,这个命令的英文是Find references,意思应该是查找引用,参考这个词不太合适):
找到一处引用,我们双击进入:
我们来到如图所示的代码位置:
发现上面有一处cmp和jnz(cmp与jnz组合代表:如果不相等,则跳转),如果ebp等于907,则将这串字符压入栈(可以猜测,907代表未注册),我们根据跳转向上翻:
向上翻到这里:
又有一处cmp和jnz,意思是如果ebp不等于906,就要跳转回0040C22F处。
(我们可以猜测,906代表注册过)
这个程序我们已经完全弄清楚了,我们直接在0040C235的jnz处进行强制跳转,让其不会将这串字符压入栈:
修改后,我们最后一次保存:
打开:
整款软件已经被我们完全破解了!!
虽然这是一款2001年的软件,但这是我们破解的第一款真正的软件!所以今晚好好庆祝一下吧,我们在下一篇教程里再见!!