目录导航
恶意样本分析手册——理论篇(上)
在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。
第一章:大小端模式
1.1 大端模式
1.2 小端模式
第二章:数制
2.1 二进制
2.2 八进制
2.3 十六进制
2.4 进制转换
第三章:语言
3.1逻辑运算
与运算(AND)
或运算(OR)
非运算(NOT)
算术左移、逻辑左移
循环左移
带进位的循环左移
算术右移
逻辑右移
循环右移
带进位的循环右移
3.2 流程控制语句的识别
If语句
if…else…语句
if构成的多分支结构:
switch:
do循环:
while循环:
for循环:
3.3 栈
3.4 堆
3.5异常
错误类异常
陷阱类异常:
中止类异常:
3.6 中断/异常处理
3.7 中断
3.8 函数调用约定
cdecl
stdcall
fastcall
naked
pascal
thiscall
第四章:文件格式
4.1 ELF文件格式
目标文件格式
ELF Header部分
节区:
节区头部表格:
特殊节区:
符号表:
程序头部
4.2 MACH-O文件格式
Header
Load Commands
Segment&Section
4.3 PE文件格式
MS-DOS头部
PE文件头
区块表:
输入表
输出表:
第五章:Windows内核加载器
5.1 主引导记录MBR讲解:
5.2 SU模块
检测物理内存
开启A20地址线
重新定位GDT和IDT
保护模式
开启保护模式
加载Loader模块
第六章:Hook、RootKit
6.1 使用注册表来注入DLL
6.2 使用Widows挂钩来注入DLL
6.3 使用远程线程来注入DLL
6.4 动态库劫持
6.5 APC注入
6.6 使用CreateProcess注入代码
6.7 IDT Hook
6.8 SSDT Hook、SSSDT Hook
6.9 IAT Hook
6.10 EAT Hook
第七章:断点
7.1 软件断点
7.2 硬件断点
7.3 条件断点
7.4 内存断点
第八章:调试器的原理
8.1 加载调试程序
8.2 异常处理机制
8.3 INT3断点
8.4 内存断点
8.5 硬件断点
8.6 单步执行
第一章:大小端模式
在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于 大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以随时在程序中(在ARM Cortex 系列使用REV、REV16、REVSH指令)进行大小端的切换。
1.1 大端模式
所谓的大端模式(Big-endian),是指数据的高字节,保存在内存的低地址中,而数据的低字节,保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;
1.2 小端模式
所谓的小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
例如:对于0x11223344的存储如下
大端模式
11 | 22 | 33 | 44 |
低地址 —> 高地址
小端模式
11 | 22 | 33 | 44 |
高地址 —> 低地址
第二章:数制
进制也就是进位制,是人们规定的一种进位方法。 对于任何一种进制—X进制,就表示某一位置上的数运算时是逢X进一位。 十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一,以此类推,x进制就是逢x进位。
2.1 二进制
二进制数有两个特点:它由两个基本数字0,1组成,二进制数运算规律是逢二进一。
为区别于其它进制数,二进制数的书写通常在数的右下方注上基数2,或加后面加B表示。
例如:二进制数10110011可以写成(10110011)2,或写成10110011B
2.2 八进制
八进制的基R=8=2^3,有数码0、1、2、3、4、5、6、7,并且每个数码正好对应三位二进制数,所以八进制能很好地反映二进制。八进制用下标8或数据后面加O表示 例如:二进制数据 ( 11 101 010 . 010 110 100 )2 对应 八进制数据 ( 3 5 2 . 2 6 4 )8或352.264O.
2.3 十六进制
十六进制数有两个基本特点:它由十六个字符0~9以及A,B,C,D,E,F组成(它们分别表示十进制数10~15),十六进制数运算规律是逢十六进一,即基R=16=2^4,通常在表示时用尾部标志H或下标16以示区别。
例如:十六进制数4AC8可写成(4AC8)16,或写成4AC8H。
2.4 进制转换
说到进制转换,不得不提一下位权。对于形式化的进制表示,我们可以从0开始,对数字的各个数位进行编号,即个位起往左依次为编号0,1,2,……;对称的,从小数点后的数位则是-1,-2,……
进行进制转换时,我们不妨设源进制(转换前所用进制)的基为R1,目标进制(转换后所用进制)的基为R2,原数值的表示按数位为AnA(n-1)……A2A1A0.A-1A-2……,R1在R2中的表示为R,则有(AnA(n-1)……A2A1A0.A-1A-2……)R1=(AnR^n+A(n-1)R^(n-1)+……+A2R^2+A1R^1+A0R^0+A-1R^(-1)+A-2*R^(-2))R2
二进制数、十六进制数转换为十进制数的规律是相同的。把二进制数(或十六进制数)按位权形式展开多项式和的形式,求其最后的和,就是其对应的十进制数——简称“按权求和”.
例如:
把(1001.01)2 二进制计算。
解:(1001.01)2
=81+40+20+11+0(1/2)+1(1/4)
=8+0+0+1+0+0.25
=9.25
把(38A.11)16转换为十进制数
解:(38A.11)16
=3×16的2次方+8×16的1次方+10×16的0次方+1×16的-1次方+1×16的-2次方
=768+128+10+0.0625+0.0039
=906.0664
十进制数转换为二进制数,十六进制数(除2/16取余法)
整数转换.一个十进制整数转换为二进制整数通常采用除二取余法,即用2连续除十进制数,直到商为0,逆序排列余数即可得到――简称除二取余法.
例:将25转换为二进制数
解:25÷2=12 余数1
12÷2=6 余数0
6÷2=3 余数0
3÷2=1 余数1
1÷2=0 余数1
所以25=(11001)2
同理,把十进制数转换为十六进制数时,将基数2转换成16就可以了.
例:将25转换为十六进制数
解:25÷16=1 余数9
1÷16=0 余数1
所以25=(19)16
二进制数与十六进制数之间的转换
由于4位二进制数恰好有16个组合状态,即1位十六进制数与4位二进制数是一一对应的.所以,十六进制数与二进制数的转换是十分简单的.
(1)十六进制数转换成二进制数,只要将每一位十六进制数用对应的4位二进制数替代即可――简称位分四位.
例:将(4AF8B)16转换为二进制数.
解: 4 A F 8 B
0100 1010 1111 1000 1011
所以(4AF8B)16=(1001010111110001011)2
(2)二进制数转换为十六进制数,分别向左,向右每四位一组,依次写出每组4位二进制数所对应的十六进制数――简称四位合一位.
例:将二进制数(000111010110)2转换为十六进制数.
解: 0001 1101 0110
1 D 6
所以(111010110)2=(1D6)16
转换时注意最后一组不足4位时必须加0补齐4位
数制转换的一般化
1)R进制转换成十进制
任意R进制数据按权展开、相加即可得十进制数据。例如:N = 1101.0101B = 12^3+12^2+02^1+12^0+02^-1+12^-2+02^-3+12^-4 = 8+4+0+1+0+0.25+0+0.0625 = 13.3125
N = 5A.8H = 516^1+A16^0+8*16^-1 = 80+10+0.5 = 90.5
2)十进制转换R 进制
1.整数转换———除R 取余法 规则:(1)用R 去除给出的十进制数的整数部分,取其余数作为转换后的R 进制数据的整数部分最低位数字; (2)再用R去除所得的商,取其余数作为转换后的R 进制数据的高一位数字; (3)重复执行(2)操作,一直到商为0结束。例如:115 转换成 Binary数据和Hexadecimal数据 (图2-4) 所以 115 = 1110011 B = 73 H
2.小数转换—————乘R取整法规则:(1)用R去乘给出的十进制数的小数部分,取乘积的整数部分作为转换后R 进制小数点后第一位数字;(2)再用R 去乘上一步乘积的小数部分,然后取新乘积的整数部分作为转换后R 进制小数的低一位数字;(3)重复(2)操作,一直到乘积为0,或已得到要求精度数位为止。
第三章:语言
3.1逻辑运算
逻辑运算又称为布尔运算。通常用来测试真假值,最常见到的逻辑运算就是循环的处理,用来判断是否该离开循环或继续执行循环内的指令。
逻辑常量只有两个,0和1,用来表示两个对立的逻辑状态。在逻辑代数中,有与、或、非三种基本的逻辑运算。
与运算(AND)
“与”运算是一种二元运算,它定义了两个变量A和B的一种函数关系。用语句来描述它,这就是:当且仅当变量A和B都为1时,函数F为1。
下表是与运算的真值表:
AND | 1 | 0 |
1 | 1 | 0 |
0 | 0 | 0 |
或运算(OR)
“或”运算是另一种二元运算,它定义了变量A、B与函数F的另一种关系。用语句来描述它,这就是:只要变量A和B中任何一个为1,则函数F为1。
下表是或运算的真值表:
OR | 1 | 0 |
1 | 1 | 1 |
0 | 1 | 0 |
非运算(NOT)
逻辑“非”运算是一元运算,它定义了一个变量(记为A)的函数关系。用语句来描述之,这就是:当A=1时,则函数F=0;反之,当A=0时,则函数F=1
下表是非运算的真值表:
NOT | 1 | 0 |
0 | 1 |
算术左移、逻辑左移
左移用来将一个数的各位二进制位全部左移若干位。例如:
将a的二进制数左移2位,右补0。若a=15,即二进制数00001111,左移2位得00111100,即十进制数60,高位左移后溢出,舍弃。左移一位相当于该数乘以2,左移2位相当于该数乘以2^2=4。上面举的例子15<< 2=60,即乘了4。但此结论只适用于该数左移时被溢出舍弃的高位中不包含1的情况。
循环左移
循环左移类似于逻辑左移,不同的是,循环左移会将左边移出的位添补到左边。例如原数x3x2x1x0,循环左移一位后,变为x2x1x0 x3。
可见,所有的位顺序向左移1位,最低位由最高位循环移入。
带进位的循环左移
此方法和循环左移类似,只是多了一个符号位,举例来说:
原数:CX3X2X1X0,循环左移一位后变为X3X2X1X0C。
算术右移
算术右移用来将一个数的各位二进制位全部右移若干位,然后在左侧用原符号位补齐。在汇编语言中,如果最高位为1,则补1,否则补0。如将10000000算术右移7位,应该变成11111111。
逻辑右移
逻辑右移是将各位依次右移指定位数,然后在左侧补0.不考虑符号位。例如将10000000逻辑右移7位,变为00000001。
循环右移
循环右移类似于逻辑右移,不同的是,循环右移会将右边移出的位添补到左边。例如原数x3x2x1x0,循环右移一位后,变为x0x3x2x1。
可见,所有的位顺序向右移1位,最高位由最低位循环移入。
带进位的循环右移
此方法和循环右移类似,只是多了一个符号位,举例来说:
原数:CX3X2X1X0,循环右移一位后变为X0CX3X2X1。
3.2 流程控制语句的识别
流程控制语句的识别时进行逆向分析和还原高级代码的基础,详细的理解此基础可以更好的理解高级语言中流程控制的内部实现机制,对开发和调试大有益处。
If语句
If语句是分支结构的重要组成部分。If语句的功能是现对运算条件进行比较,然后根据比较结果选择对应的语句块来执行。If语句只能判断两种情况:0为假值,非0为真值。如果为真值,则进入语句块内执行语句;如果为假值,则跳过if语句块,继续执行程序的其他语句。要注意的是,if语句转换的条件跳转指令与if语句的判断结果是相反的。
If语句的一般流程如下:
//先执行各类影响标志位的指令
//其后是各种条件跳转指令
jxx xxxx
if…else…语句
if语句是一个单分支结构,if…else…组合后是一个双分支结构。两者完成的功能有所不同。从语法上看,if…else…只比if语句多出了一个else。else有两个功能,如果if判断成功,则跳过else分支语句块;如果if判断失败,则进入else分支语句块中。有了else语句的存在,程序在进行流程选择时,必会经过两个分支中的一个。
if…else…的大致流程如下:
先执行影响标志位的相关指令
jxx else_begin //该地址为else语句块的首地址
if_begin
……. //if语句块内的执行代码
if_end
jmp else_end //跳转到else语句块的结束地址
else_begin
…… //else语句块内的执行代码
else_end
如果遇到以上指令序列,先考察其中的两个跳转指令,当第一个条件跳转指令跳转到地址else_begin处之前有个jmp指令,则可将其视为由if…else…组合而成的双分支结构。根据这两个跳转指令可以得到if和else语句块的代码边界。通过cmp和jxx可还原出if的比较信息,jmp指令之后即为else块的开始。
if构成的多分支结构:
多分支结构类似于if…else…的组合方式,在if…else…的else之后再添加一个else if进行二次比较,这样就可以进行多次比较,再次选择程序流程,形成多分支流程。它的c++语法格式为:if…else if…else if…,可重复后缀为else if。当最后为else时,便到了多分支结构的末尾处。
一般流程如下:
jxx指出了下一个else if的起始点,而jmp指出了整个多分支结构的末尾地址以及当前if或者else if语句块的末尾。最后的else块的边界也很容易识别,如果发现多分支块内的某一段代码在执行前没有判定,即可定义为else块。
//可影响标志位的指令
jxx else_if_begin //跳到下一条else if语句块的首地址
if_begin
…… //if语句块内的执行代码
if_end
jmp end //跳转到多分枝结构的结尾地址
else_if_begin //else if语句块的起始地址
//可影响标志位的指令
jxx else_begin
……
else_if_end:
jmp end
else_begin:
……
end
……
当每个条件跳转指令的跳转地址之前都紧跟jmp指令,并且他们的跳转地址值都一样时,可视为一个多分支结构。
switch:
switch是比较常用的多分支结构,使用起来也非常方便,并且效率也高于if…else if多分枝结构。switch语句将所有的条件跳转都放置在了一起,并没有发现case语句块的踪影,通过条件跳转指令,跳转到相应的case语句块中。因此每个case的执行是由switch比较结果引导跳过来的。
一般流程如下:
mov reg,mem //取出switch中考察的变量
//影响标志位的指令
jxx xxxx
//影响标志位的指令
jxx xxxx
//影响标志位的指令
jxx xxxx
jmp end //跳到switch语句块的结尾地址出
…… //case语句块首地址
jmp end //跳到switch语句块的结尾地址出
…… //case语句块首地址
jmp end //跳到switch语句块的结尾地址出
…… //case语句块首地址
jmp end //跳到switch语句块的结尾地址出
end:
……
当分支数小于4的情况下,VC 6.0会采取模拟if else的方法
当分支数大于3,并且case的判定值存在明显线性关系组合时,它会制作一份case地址数组(case地址表),这个数组保存了每个case语句块的首地址,并且数组下标以0起始。如果每两个case值之间的差值小于等于6,并且case语句数大于等于4,编译器就会形成这种线性结构。
对于非线性的switch结构,会进行索引表优化,需要两张表:一张为case语句块地址表,另一张为case语句块索引表
地址表中的每一项保存了一个case语句块的首地址,有几个case语句就有几项。此情况适用于差值小于等于255的情况,大于255的话可以通过树方式优化。
do循环:
do循环的工作流程清晰,识别起来也相对简单。根据其特性,先执行语句块,再进行比较判断,当条件成立时,会继续执行语句块。
if语句的比较是相反的,并且跳转地址大于当前代码的地址,是一个向下跳转的过程;而do中的跳转地址小于当前代码的地址,是一个向上跳转的过程,所以条件跳转的逻辑与源码中的逻辑相同
do循环的一般流程:
do_begin
……. //循环语句块
;影响标记位的指令
jxx do_begin
while循环:
while循环和do循环正好相反,在执行循环语句块之前,必须要进行条件判断,根据比较结果再选择是否执行循环语句块。识别while循环,查看条件跳转地址,如果这个地址上面有一个jmp指令,并且此指令跳转到的地址小于当前代码地址,那么明显是一个向上跳转的地址。要完成语句循环,就需要修改程序流程,回到循环语句处,因此向上跳转就成了循环语句的明显特征。在条件跳转的地址附近会有jmp指令修改程序流程。
while循环用了两次跳转,因此比do循环效率低一些。
while循环的一般流程:
while_begin
;影响标记位的指令
jxx while_end
……
jmp while_begin
while_end:
for循环:
for循环是三种循环结构中最复杂的一种。for循环由赋初值,设置循环条件,设置循环步长这三条语句组成。由于for循环更符合人类的思维方式,在循环结构中被使用的频率也很高。
for循环的一般流程:
mov mem/reg ,xxx //赋初值
jmp for_cmp //跳到循环条件判定部分
for_step:
//修改循环变量step
mov reg,step
add reg,xxxx //修改循环变量的计算过程,在实际分析中,视算法不同而不同
mov step,eax
for_cmp: //循环条件判定部分
mov ecx,dword ptr step
//判定循环变量和循环终止条件stepend的关系,满足条件则退出for循环
cmp ecx,stepend
jxx for_end //条件成立则结束循环
…….
jmp for_step //向上跳转,修改流程回到步长计算部分
for_end:
在计数器变量被赋初值后,利用jmp跳过第一次步长计算,然后,可以通过三个跳转指令还原for循环的各个组成部分:第一个jmp指令之前的代码为初始化部分;从第一个jmp指令到循环条件比较处(也就是上面代码中for_cmp标号的位置)之间的代码为步长计算部分;在条件跳转指令jxx之后寻找一个jmp指令,这jmp指令必须是向上跳转的,且其目标是到步长计算的位置,在jxx和这个jmp(也就是上面代码的省略号所在的位置)之间的代码是循环语句块。
3.3 栈
从数据结构角度看,栈是一种用来存储数据的容器。放入数据的操作称为压入(push),从栈中取出数据的操作被称为弹出(pop)。存取数据的一条基本规则是后进先出。
X86架构有对栈的内建支持。用于这种支持的寄存器包括esp和ebp。其中esp是栈指针,包含了指向栈顶的内存地址。当数据被压入或弹出栈时,这个寄存器的值相应的改变。Ebp是栈基址寄存器,在一个函数中保持不变,因此程序把它当成定位器,用来确定局部变量和参数的位置。
与栈有关的指令包括push,pop,call,leave,enter和ret。在内存中,栈被分配成自顶向下的,最高的地址最先被使用。当一个值被压入栈时,使用低一点的地址。
栈只能用于短期存储。他经常用于保存局部变量、参数和返回地址。主要用途是管理函数调用之间的数据交换。不同的编译器对这种管理方法的具体实现有所不同,但大部分常见约定都使用ebp的地址来引用局部变量与参数。
栈的布局:
在经典的操作系统中,栈总是向下(低地址)增长的。
栈保存一个函数调用所需要的维护信息,常被称为堆栈帧或者是活动记录,堆栈帧一般包括:
(1)函数的返回地址和参数;
(2)临时变量:包括函数的非静态局部变量以及编译器生成的其他局部变量;
(3)保存的上下文:包括在函数调用前后保持不变的寄存器。
3.4 堆
堆是组织内存的另一种重要方法,是程序运行期动态申请内存空间的主要途径。与栈空间是由编译器产生的代码自动分配和释放不同,堆上的空间需要程序员自己写代码来申请(HeapAlloc)和释放(HeapFree),而且分配和释放操作应该严格匹配,忘记释放或多次释放都是不正确的。
3.5异常
异常通常是CPU在执行指令是因为检测到预先定义的某个(或多个)条件而产生的同步事件,异常的来源有3种,第一种是程序错误,即当CPU在执行程序指令时遇到操作数有错误(执行除法指令时遇到除数是0)或检测到指令规范中定义的非法情况(用户模式下执行特权指令等)。第二种来源是某些特殊指令,这些指令的预期行为就是产生相应的异常,比如INT3指令,该指令的目的就是产生一个断点异常,让CPU中断进调试器。第三种来源是奔腾CPU引入的机器检查异常,即当CPU执行指令期间检测到CPU内部或外部的硬件错误。
异常分为3类,错误,陷阱和中止
错误类异常
导致错误类异常的情况通常可以被纠正,而且一旦纠正后,程序可以无损失的恢复执行。此类异常的一个最常见的例子就是内存页错误。页错误异常的发生是因为它是虚拟内存的基础。因为物理内存的空间有限,所以操作系统会把某些在那时不用的内存以页为单位交换到外部存储器上。当有程序访问到这些不在物理内存种的页所对应的内存地址时,CPU便会产生一个页错误异常(缺页错误、缺页异常),并转去执行该异常的处理程序,后者会调用内存管理器的函数把对应的内存页交换回物理内存,然后再让CPU返回到导致该异常的那条指令处恢复执行。当第二次执行刚才导致异常的指令时,对应的内存页已经在物理内存中(错误情况被纠正),因此就不会再产生页错误异常了。
当CPU报告错误类异常时,CPU将其状态恢复成导致该异常的指令被执行之前的状态。而且在CPU转去执行异常处理程序前,在栈中保存的CS和EIP指针是指向导致异常的这条指令的(而不是下一条指令)。因此,当异常处理程序返回继续执行时,CPU接下来执行的第一条指令仍然是刚才导致异常的那条指令。所以,如果导致异常的情况还没有被消除,那么CPU会再次产生异常。
陷阱类异常:
当CPU报告陷阱类异常时,导致该异常的指令已经执行完毕,压入栈的CS和EIP值是导致该异常的指令执行后紧接着要执行的下一条指令。值得说明的是,下一条指令并不一定是与导致异常的指令相邻的下一条。如果导致异常的指令是跳转指令或函数调用指令,那么下一条指令可能是内存地址不相邻的另一条指令。
导致陷阱类异常的情况通常也是可以无损失的恢复执行的。比如INT 3指令导致的断点异常就属于陷阱类异常,该异常会使CPU中断到调试器,从调试器返回后,被调试程序可以继续执行。
中止类异常:
中止类异常主要用来报告严重的错误,比如硬件错误和系统表中包含非法值或不一致的状态等。这类异常不允许恢复继续执行。首先,当这类异常发生时,CPU并不总能保证报告的异常的指令地址是精确地。另外,出于安全性的考虑,这类异常可能是由于导致该异常的程序执行非法操作导致的,因此就应该强迫其中止退出。
3.6 中断/异常处理
中断和异常从产生的根源来看有着本质的区别,但是系统(CPU和操作系统)是用统一的方式来响应和管理他们的。中断和异常处理的核心数据结构是中断描述符表(IDT)。当中断和异常发生时,CPU通过查找IDT表来定位处理例程的地址,然后转去执行该处理例程。这个查找的过程是在CPU内部执行的。通常,系统软件(操作系统和BIOS固件)在系统初始化阶段就准备好中断处理例程和IDT表,然后把IDT表的位置通过IDTR寄存器告诉CPU。
实模式下IVT(中断向量表)位于物理地址0开始的1KB内存区中,每个IVT表项的长度是4个字节,共有256个表项,与x86CPU的256个中断向量一一对应。实模式下,每个IVT表项的资格字节分为两部分,高两个字节为中断例程的段地址,低两个字节为中断例程的偏移地址。因为是在实模式下,所以段地址左移4位再加上偏移地址便可以得到20位的中断例程地址。
下面是IA-32 CPU相应中断和异常的全过程:
- 将代码段寄存器CS和指令指针寄存器(EIP)的低16位压入堆栈
- 将标志寄存器EFLAGS的低16位压入堆栈
- 清除标志寄存器的IF标志,以禁止其他中断
- 清除标志寄存器的TF,RF,AC标志
- 使用向量号n作为索引,在IVT中找到对应的表项(n*4+IVT表基地址)
- 将表项中的段地址和偏移地址分别装入CS和EIP寄存器中,并开始执行对应的代码
- 中断例程总是以IRET指令结束。IRET指令会从堆栈中弹出前面保存的CS,IP和标志寄存器值,然后返回执行被中断的程序。
3.7 中断
中断通常是由CPU外部的输入输出设备(硬件)所触发的,供外部设备通知CPU“有事情要处理”,因此又叫中断请求。中断请求的目的是希望CPU暂时停止执行 当前正在执行的程序,转去执行中断请求所对应的中断处理例程。
中断机制为CPU和外部设备间的通信提供了一种高效的方法,有了中断机制,CPU就可以不用去频繁的查询外部设备的状态了,因为外部设备有事需要处理时,他可以发出中断请求通知CPU。
在硬件级,中断是由一块专门芯片来管理的,通常称为中断控制器。他负责分配中断资源和管理各个中断源发出的中断请求。为了便于标识各个中断请求,中断管理器通常用IRQ后面加上数字来表示不同路的中断请求信号。
3.8 函数调用约定
cdecl
cdecl调用约定又称为C调用约定,是c/c++语言缺省的调用约定。参数按照从右至左的方式入栈,函数本身不清理栈,此工作有调用者负责,返回值在eax中。由于由调用者清理栈,所以允许可变参数函数存在。
stdcall
stdcall很多时候被称为pascal调用约定。pascal语言是早期很常见的一种教学用计算机程序设计语言,其语法严谨,参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在eax中。
fastcall
fastcall的调用方式运行相对快,因为它通过寄存器来传递参数。它使用ecx和edx传送两个双字或更小的参数,剩下的参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在eax中。
naked
naked是一个很少见的调用约定,一般不建议使用。编译器不会给这种函数增加初始化的清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果,此调用约定必须跟declspec同时使用,例如声明一个函数,如_declspec(naked) int add(int a,int b);
pascal
这是pascal语言的调用约定,跟stdcall一样,参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在 eax中,vc已经废弃了这种调用方式,因此在写vc程序时,建议使用stdcall。
thiscall
这是c++语言特有的一种调用方式,用于类成员函数的调用约定。如果参数确定,this指针存放于ecx寄存器,函数自身清理堆栈;如果参数不确定,this指针在所有参数入栈后再入栈,调用者清理栈。Thiscall不是关键字,程序员不能使用。参数按照从右至左的方式r入栈。
第四章:文件格式
4.1 ELF文件格式
目标文件有三种类型:
可重定位文件( Relocatable File) 包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
可执行文件( Executable File) 包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。
共享目标文件( Shared Object File) 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。
目标文件全部是程序的二进制表示,目的是直接在某种处理器上直接执行。
ELF文件是运行在unix平台下的可执行文件,在学习其文件格式前,首先了解一下文件中使用的数据表示方式。如下表所示:
名称 | 大小 | 对齐 | 目的 |
Elf32_Addr | 4 | 4 | 无符号程序地址 |
Elf32_Half | 2 | 2 | 无符号中等正数 |
Elf32_Off | 4 | 4 | 无符号文件偏移 |
Elf32_Sword | 4 | 4 | 有符号大整数 |
Elf32_Word | 4 | 4 | 无符号大整数 |
Unsigned char | 无符号小正数 |
目标文件格式
目标文件既要参与程序链接,又要参与程序执行。出于方便性和效率考虑,目标文件格式提供了两种并行视图,分别反应了这些活动的不同需求。
如上图所示,在文件开始处是一个ELF头部,用来描述整个文件的组织。节区部分包含链接视图的大量信息:指令,数据,符号表,重定位信息等等。
程序头部表,如果存在的话,告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
节区头部表包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称,节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可有可无。
注意:尽管头部显示的各个组成部分是有顺序的,实际上除了ELF头部表以外,其他节区和段都没有规定的顺寻。
ELF Header部分
文件的最开始几个字节给出如何解释文件的提示信息。这些信息独立于处理器,也独立于文件中的其余内容。ELF Header部分可以用下面的数据结构表示:
#define EI_NIDENT 16
typedef struct{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
}Elf32_Ehdr;
e_ident:目标文件标识,标志此文件是一个ELF文件。
e_type:目标文件的类型,取值取下
名称 | 取值 | 含义 |
ET_NONE | 0 | 未知目标文件格式 |
ET_REL | 1 | 可重定位文件 |
ET_EXEC | 2 | 可执行文件 |
ET_DYN | 3 | 共享目标文件 |
ET_CORE | 4 | Core文件(转储格式) |
ET_LOPROC | 0xff00 | 特定处理器文件 |
ET_HIPROC | 0xffff | 特定处理器文件 |
ET_LOPROC和ET_HIPROC之间的取值用来标识与处理器相关的文件格式。
e_machine:给出文件的目标体系结构类型,取值如下:
名称 | 取值 | 含义 |
EM_NONE | 0 | 未指定 |
EM_M32 | 1 | AT&T WE 32100 |
EM_SPARC | 2 | SPARC |
EM_386 | 3 | Intel 80386 |
EM_68K | 4 | Motorola 68000 |
EM_88K | 5 | Motorola 88000 |
EM_860 | 7 | Intel 80860 |
EM_MIPS | 8 | MIPS RS3000 |
特定处理器的ELF名称会使用机器名来进行区分。
e_version:目标文件版本。
e_entry:程序入口的虚拟地址。如果目标文件没有程序入口,可以为0.
e_phoff:程序头部表格( Program Header Table)的偏移量(按字节计算)。如果文件没有程序头部表格,可以为 0。
e_shoff:节区头部表格( Section Header Table) 的偏移量(按字节计算)。 如果文件没有节区头部表格,可以为 0。
e_flags:保存与文件相关的, 特定于处理器的标志。 标志名称采用 EF_machine_flag的格式。
e_ehsize :ELF 头部的大小(以字节计算)。
e_phentsize 程序头部表格的表项大小(按字节计算)。
e_phnum 程序头部表格的表项数目。可以为 0。
e_shentsize 节区头部表格的表项大小(按字节计算)。
e_shnum 节区头部表格的表项数目。可以为 0。
e_shstrndx:节区头部表格中与节区名称字符串表相关的表项的索引。 如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF。
节区:
节区中包含目标文件中的所有信息,除了:ELF头部,程序头部表格,节区头部表格。节区满足以下条件:
- 目标文件中的每个节区都有对应的节区头部描述它,反过来,有节区头部不意味着有节区。
- 每个节区占用文件中一个连续字节区域(这个区域可能长度为 0)。
- 文件中的节区不能重叠,不允许一个字节存在于两个节区中的情况发生。
- 目标文件中可能包含非活动空间( INACTIVE SPACE)。这些区域不属于任何头部和节区,其内容未指定。
节区头部表格:
ELF 头部中, e_shoff 成员给出从文件头到节区头部表格的偏移字节数; e_shnum给出表格中条目数目; e_shentsize 给出每个项目的字节数。 从这些信息中可以确切地定位节区的具体位置、长度。
每个节区头部可以用如下数据结构描述:
typedef struct{
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
}Elf32_Shdr;
各个成员含义如下:
成员 | 说明 |
sh_name | 给出节区名称。是节区头部字符串表节区( Section Header StringTable Section)的索引。名字是一个 NULL 结尾的字符串。 |
sh_type | 为节区的内容和语义进行分类。 |
sh_flags | 节区支持 1 位形式的标志,这些标志描述了多种属性。此字段定义了一个节区中包含的内容是否是可以修改、是否可以执行等信息。如果一个标志位被设置,则该值取值为1.未定义的各位都设置为0. |
sh_addr | 如果节区将出现在进程的内存映像中, 此成员给出节区的第一个字节应处的位置。否则,此字段为 0。 |
sh_offset | 此成员的取值给出节区的第一个字节与文件头之间的偏移。不过,SHT_NOBITS 类型的节区不占用文件的空间, 因此其 sh_offset 成员给出的是其概念性的偏移。 |
sh_size | 此成员给出节区的长度(字节数)。除非节区的型是SHT_NOBITS,否则节区占用文件中的 sh_size 字节。类型为SHT_NOBITS 的节区长度可能非零, 不过却不占用文件中的空间。 |
sh_link | 此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。 |
sh_info | 此成员给出附加信息,其解释依赖于节区类型。 |
sh_addralign | 某些节区带有地址对齐约束。例如,如果一个节区保存一个doubleword, 那么系统必须保证整个节区能够按双字对齐。 sh_addr对 sh_addralign 取模,结果必须为 0。目前仅允许取值为 0 和 2的幂次数。数值 0 和 1 表示节区没有对齐约束。 |
sh_entsize | 某些节区中包含固定大小的项目, 如符号表。 对于这类节区, 此成员给出每个表项的长度字节数。 如果节区中并不包含固定长度表项的表格,此成员取值为 0。 |
特殊节区:
很多节区中包含了程序和控制信息。下面的表格中给出了系统使用的节区以及他们的类型和属性。
名称 | 类型 | 属性 | 含义 |
.bss | SHT_NOBITS | SHF_ALLOC +SHF_WRITE | 包含将出现在程序的内存映像中的为初始化数据。 根据定义, 当程序开始执行, 系统将把这些数据初始化为 0。 此节区不占用文件空间。 |
.comment | SHT_PROGBITS | 无 | 包含版本控制信息。 |
.data | SHT_PROGBITS | SHF_ALLOC +SHF_WRITE | 这些节区包含初始化了的数据, 将出现在程序的内存映像中。 |
.data1 | SHT_PROGBITS | SHF_ALLOC +SHF_WRITE | 这些节区包含初始化了的数据, 将出现在程序的内存映像中。 |
.debug | SHT_PROGBITS | 无 | 此节区包含用于符号调试的信息。 |
.dynamic | SHT_DYNAMIC | 此节区包含动态链接信息。 节区的属性将包含 SHF_ALLOC 位。是否 SHF_WRITE 位被设置取决于处理器。 | |
.dynstr | SHT_STRTAB | SHF_ALLOC | 此节区包含用于动态链接的字符串, 大多数情况下这些字符串代表了与符号表项相关的名称。 |
.dynsym | SHT_DYNSYM | SHF_ALLOC | 此节区包含了动态链接符号表。 |
.fini | SHT_PROGBITS | SHF_ALLOC +SHF_EXECINSTR | 此节区包含了可执行的指令, 是进程终止代码的一部分。 程序正常退出时, 系统将安排执行这里的代码。 |
.got | SHT_PROGBITS | 此节区包含全局偏移表。 | |
.hash | SHT_HASH | SHF_ALLOC | 此节区包含了一个符号哈希表。 |
.init | SHT_PROGBITS | SHF_ALLOC +SHF_EXECINSTR | 此节区包含了可执行指令, 是进程初始化代码的一部分。 当程序开始执行时, 系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码。 |
.interp | SHT_PROGBITS | 此节区包含程序解释器的路径名。 如果程序包含一个可加载的段, 段中包含此节区, 那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0。 | |
.line | SHT_PROGBITS | 无 | 此节区包含符号调试的行号信息, 其中描述了源程序与机器指令之间的对应关系。 其内容是未定义的。 |
.note | SHT_NOTE | 无 | 此节区中包含注释信息,有独立的格式。 |
.plt | SHT_PROGBITS | 此节区包含过程链接表( procedure linkagetable) 。 | |
.relname | SHT_REL | 这些节区中包含了重定位信息。 如果文件中包含可加载的段, 段中有重定位内容, 节区的属性将包含 SHF_ALLOC 位,否则该位置 0。 传统上 name 根据重定位所适用的节区给定。 例如 .text 节区的重定位节区名字将是: .rel.text 或者 .rela.text。 | |
.relaname | SHT_RELA | 同上 | |
.rodata | SHT_PROGBITS | SHF_ALLOC | 这些节区包含只读数据, 这些数据通常参与进程映像的不可写段。 |
.rodata1 | SHT_PROGBITS | SHF_ALLOC | 同上 |
.shstrtab | SHT_STRTAB | 此节区包含节区名称。 | |
.strtab | SHT_STRTAB | 此节区包含字符串, 通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段, 段中包含符号串表, 节区的属性将包含SHF_ALLOC 位,否则该位为 0。 | |
.symtab | SHT_SYMTAB | 此节区包含一个符号表。 如果文件中包含一个可加载的段, 并且该段中包含符号表, 那么节区的属性中包含SHF_ALLOC 位, 否则该位置为 0。 | |
.text | SHT_PROGBITS | SHF_ALLOC +SHF_EXECINSTR | 此节区包含程序的可执行指令。 |
字符串表:
字符串表节区包含以NULL结尾的字符序列,通常称为字符串。ELF目标问件通常使用字符串来表示符号和节区名称。对字符串的引用通常以字符串在字符串表中的下标给出。
一般,第一个字节(索引为0)定义为一个空字符串。类似的,字符串表的最后一个字节也定义为NULL,以确保所有字符串都以NULL结尾。
例如:对于各个节区而言,节区头部的sh_name成员包含其对应的节区头部字符串表节区的索引,此节区由ELF头的e_shstrndx成员给出。
符号表:
目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。符号表索引是对此数组的索引。索引0表示表中的第一表项,同时也作为未定义符号的索引。
符号表项的格式如下:
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_sym;
各个字段的含义如下:
字段 | 说明 |
st_name | 包含目标文件符号字符串表的索引, 其中包含符号名的字符串表示。 如果该值非 0, 则它表示了给出符号名的字符串表索引, 否则符号表项没有名称。注:外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称。 |
st_value | 此成员给出相关联的符号的取值。 依赖于具体的上下文, 它可能是一个绝对值、一个地址等等。 |
st_size | 很多符号具有相关的尺寸大小。 例如一个数据对象的大小是对象中包含的字节数。如果符号没有大小或者大小未知,则此成员为 0。 |
st_info | 此成员给出符号的类型和绑定属性。 |
st_other | 该成员当前包含 0,其含义没有定义。 |
st_shndx | 每个符号表项都以和其他节区间的关系的方式给出定义。 此成员给出相关的节区头部表索引。某些索引具有特殊含义。 |
程序头部
可执行文件或者共享目标文件的程序头部是一个结构数组,没有结构描述了一个段或者系统准备程序执行所必须的其他信息。目标文件的“段”包含了一个或者多个“节区”,也就是“段内容”。程序头部仅对于可执行文件和共享目标文件有意义。
可执行目标文件在ELF头部的e_phentsize和e_phnum成员中给出其自身程序头部的大小,程序头部的数据结构如下图:
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_phdr;
各个字段说明如下:
p_type:此数组元素描述的段的类型,或者如何解释此数组元素的信息。
p_offset:此成员给出从文件头到该段第一个字节的偏移。
p_vaddr:此成员给出段的第一个字节将被放到内存中的虚拟地址。
p_paddr:此成员仅用于与物理地址相关的系统中。因为 System V 忽略所有应用程序的物理地址信息,此字段对与可执行文件和共享目标文件而言具体内容是未指定的。
p_filesz:此成员给出段在文件映像中所占的字节数。可以为 0。
p_memsz:此成员给出段在内存映像中占用的字节数。可以为 0。
p_flags:此成员给出与段相关的标志。
p_align:可加载的进程段的 p_vaddr 和 p_offset 取值必须合适, 相对于对页面大小的取模而言。 此成员给出段在文件中和内存中如何对齐。数值 0 和 1 表示不需要对齐。否则 p_align 应该是个正整数, 并且是 2 的幂次数, p_vaddr 和 p_offset 对 p_align取模后应该相等
4.2 MACH-O文件格式
Mach-o格式是OS X系统上的可执行文件格式,类似于Windows的PE与linux的ELF。每个mach-o文件头包含一个mach-o头,然后载入命令(Load Commands),最后是数据块(Data)。下面来对整个mach-o的格式进行详细分析。
Mach-o文件的格式如下图所示:
由如下几部分组成:
Header:保存了mach-o的一些基本信息,包括了平台,文件类型,LoadCommands的个数等等。
LoadCommands:这一段紧跟Header。加载mach-o文件时会使用这里的数据来确定内存的分布。
Data:每一个segment的具体数据都保存在这里,这里包含了具体的代码,数据等等。
Header
Headers的定义可以在开源的内核代码中找到。
/** The 32-bit mach header appears at the very beginning of the object file for* 32-bit architectures.*/struct mach_header {uint32_t magic; /* mach magic number identifier */cpu_type_t cputype; /* cpu specifier */cpu_subtype_t cpusubtype; /* machine specifier */uint32_t filetype; /* type of file */uint32_t ncmds; /* number of load commands */uint32_t sizeofcmds; /* the size of all the load commands */uint32_t flags; /* flags */}; /* Constant for the magic field of the mach_header (32-bit architectures) */#define MH_MAGIC 0xfeedface /* the mach magic number */#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */ /** The 64-bit mach header appears at the very beginning of object files for* 64-bit architectures.*/struct mach_header_64 {uint32_t magic; /* mach magic number identifier */cpu_type_t cputype; /* cpu specifier */cpu_subtype_t cpusubtype; /* machine specifier */uint32_t filetype; /* type of file */uint32_t ncmds; /* number of load commands */uint32_t sizeofcmds; /* the size of all the load commands */uint32_t flags; /* flags */uint32_t reserved; /* reserved */}; /* Constant for the magic field of the mach_header_64 (64-bit architectures) */#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */ |
根据mach_header和mach_header_64的定义,很明显可以看出,Headers的主要作用是帮助系统迅速的定位mach-o文件的运行环境,文件类型。
Magic:0xfeedface是32位,0xfeedfacf是64位。
Cputype,cpusubtype:确定CPU的平台与版本(ARM-V7)
Filetype:文件类型(执行文件,库文件,core,内核扩展…)
Ncmds,sizeofncmds:Load Commands的个数和长度
Flags:dyld加载时需要的标志位
Reserved:只有64位的时候才存在的字段,暂时没有用
Load Commands
Load_Command的数据结构如下:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
Load Commands 直接就跟在Header后面,所有command占用内存的总和在Mach-O Header里面已经给出了。在加载过Header之后就是通过解析LoadCommand来加载接下来的数据了。
Segment&Section
加载数据时,主要加载的就是LC_SEGMET或者LC_SEGMENT_64。LC_SEGMET和LC_SEGMENT_64的数据结构如下所示:
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
可以看出,这里大部分的数据是用来帮助内核将Segment映射到虚拟内存的。主要要关注的是nsects字段,标示了Segment中有多少section。section是具体有用的数据存放的地方。
Section的数据结构如下:
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
除了同样有帮助内存映射的变量外,在了解mach-o格式的时候,只需要知道不同的Section有着不同的作用就可以了。
Section | 作用 |
_text | 代码 |
_cstring | 硬编码的字符串 |
_const | Const关键词修饰过的变量 |
_DATA._bss | Bss段 |
4.3 PE文件格式
PE是Portable Executable Format(可移植的执行体)简写,它是目前Windows平台的主流可执行文件格式。
MS-DOS头部
每个PE文件是以一个DOS程序开始的,有了它,一旦程序运行在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ header之后的DOS stub(DOS块)。DOS stub实际上是一个有效的EXE,平常把DOS MZ头与DOS stub合称为DOS文件头。
PE文件的第一个字节起始于一个传统的MS-DOS头部,被称作IMAGE_DOS_HEADER。其结构如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
其中有两个值比较重要,分别是e_magic合e_lfanew。e_magic字段需要被设置为值5A4D,再ASCII表示法里,它的ASCII值为“MZ”。e_lfanew字段是真正的PE文件头的相对偏移,其指出真正PE头的文件偏移位置,它占用4字节,位于文件开始偏移3Ch字节中。
PE文件头
紧跟着Dos stub的是PE文件头(PE Header),PE Header是PE相关结构NT映像头(IMAGE_NT_HEADERS)的简称,其中包含很多PE装载器用到的重要字段。知性体再支持PE文件结构的操作系统中执行时,PE装载器从IMAGE_DOS_HEADER结构中的e_lfanew字段里找到PE Header的起始偏移量,加上基址得到PE文件头的指针。
PNTHeader=ImageBase+dosHeader->e_lfanew
实际上又两个版本的IMAGE_NT_HEADER结构,一个是为32位的可执行文件准备的,另一个是64位版本,在后面的讨论中不做考虑,他们几乎没有区别。
IMAGE_NT_HEADER由三个字段组成:
IMAGE_NT_HEADERS STRUCT
{
DWORD Signature; //PE文件头标志,为ASCII的“PE”,+0h
IMAGE_FILE_HEADER FileHeader; //+4h
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //+18h
}
Signature字段被设置为00004550h,ASCII码字符是“PE00”.“PE\0\0”是PE文件头的开始,DOS头部的e_lfanew字段正是执行“PE\0\0”。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //+04h
WORD NumberOfSections; //+06h 文件的区块数目
DWORD TimeDateStamp; //+08h
DWORD PointerToSymbolTable; //+0Ch
DWORD NumberOfSymbols; //+10h
WORD SizeOfOptionalHeader; //+14h IMAGE_OPTIONAL_HEADER32结构大小
WORD Characteristics; //+16h 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
IMAGE_FILE_HEADER(映像文件头)结构包含了PE文件的一些基本信息,最重要的一个域指出了IMAGE_OPTIONAL_HEADER的大小。
Machine:可执行文件的目标CPU类型。PE文件可以在多种机器上使用,不同平台指令机器码是不同的,下表所示是几种典型的机器类型标志。
机器 | 标志 |
Intel i386 | 14Ch |
MIPS R3000 | 162h |
MIPS R4000 | 166h |
Alpha AXP | 184h |
Power PC | 1F0H |
NumberOfSections:区块数目,块表紧跟在IMAGE_NT_HEADERS后面
TimeDateStamp:表明文件是何时被创建的。
PointerToSymbolTable:COFF符号表的问啊金偏移位置
NumberOfSymbols:如果有COFF符号表,它代表其中的符号数目。
SizeOfOptionalHeader:紧跟着IMAGE_FILE_HEADER后面的数据的大小。在PE文件中,这个数据结构叫IMAGE_OPTIONAL_HEADER,其大小依赖于32位还是64位文件,对于32位文件,这个域通常是00E0h;对于64位文件,这个域是00F0h。这些值要求的最小值,较大的值也可能出现。
IMAGE_OPTIONAL_HEADER结构
可选映像头(IMAGE_OPTIONAL_HEADER)是一个可选的结构,但实际上IMAGE_FILE_HEADER结构不足以定义PE文件属性,因此可选映像头中定义了更多的数据,完全不必考虑两个结构区别在哪里,两者连起来就是一个完整的PE文件头结构。IMAGE_OPTIONAL_HEADER32结构如下:
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
+18h WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
+1Ah BYTE MajorLinkerVersion; // 链接程序的主版本号
+1Bh BYTE MinorLinkerVersion; // 链接程序的次版本号
+1Ch DWORD SizeOfCode; // 所有含代码的节的总大小
+20h DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
+24h DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h DWORD AddressOfEntryPoint; // 程序执行入口RVA
+2Ch DWORD BaseOfCode; // 代码的区块的起始RVA
+30h DWORD BaseOfData; // 数据的区块的起始RVA
//
// NT additional fields. 以下是属于NT结构增加的领域。
//
+34h DWORD ImageBase; // 程序的首选装载地址
+38h DWORD SectionAlignment; // 内存中的区块的对齐大小
+3Ch DWORD FileAlignment; // 文件中的区块的对齐大小
+40h WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
+42h WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
+44h WORD MajorImageVersion; // 可运行于操作系统的主版本号
+46h WORD MinorImageVersion; // 可运行于操作系统的次版本号
+48h WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
+4Ah WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
+4Ch DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
+50h DWORD SizeOfImage; // 映像装入内存后的总尺寸
+54h DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小
+58h DWORD CheckSum; // 映像的校检和
+5Ch WORD Subsystem; // 可执行文件期望的子系统
+5Eh WORD DllCharacteristics; // DllMain()函数何时被调用,默认为 0
+60h DWORD SizeOfStackReserve; // 初始化时的栈大小
+64h DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
+68h DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
+6Ch DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
+70h DWORD LoaderFlags; // 与调试有关,默认为 0
+74h DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来 // 一直是16
+78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
下面讲解几个比较重要的字段:
AddressOfEntryPoint:程序执行入口RVA,对于DLL,这个入口点是在进程初始化和关闭时以及线程创建/毁灭时被调用。在大多数可执行文件中,这个地址并不直接指向Main,WinMain或DllMain,而是指向运行时库代码并由他来调用上述函数。在DLL中这个域能被设为0,前面提到的通知消息都不能收到。链接器/NOENTRY开关可以设置这个域为0.
ImageBase:文件在内存中首选装入地址。如果有可能(也就是说,目前如果没有其他占据这个块地址,它是正确对齐的并且是一个合法的地址等),加载器试图在这个地址装入PE文件,如果可执行文件是在这个地址转入的,那么加载器将跳过应用基址重定位的步骤。
SectionAlignment:当被装入内存时的区块对齐大小。每个块被装入的地址必定是本字段指定数值的整数倍。默认的对齐尺寸是目标CPU的页尺寸。
FileAlignment:磁盘上PE文件内的区块对齐大小。组成块的原始数据必须保证从本字段的倍数地址开始。对于x86可执行文件,这个值通常是200h或1000h,这是为了保证块总是从磁盘的扇区开始。这个值必须是2的幂,其最小值为200h,并且,如果SectionAlignment小于CPU的页尺寸,这个域必须与SectionAlignment匹配。
Subsystem:一个标明可执行文件所期望的子系统的枚举值,取值如下:
取 值 | Windows.inc中的预定义值 | 含 义 |
0 | IMAGE_SUBSYSTEM_UNKNOWN | 未知的子系统 |
1 | IMAGE_SUBSYSTEM_NATIVE | 不需要子系统(如驱动程序) |
2 | IMAGE_SUBSYSTEM_WINDOWS_GUI | Windows图形界面 |
3 | IMAGE_SUBSYSTEM_WINDOWS_CUI | Windows控制台界面 |
5 | IMAGE_SUBSYSTEM_OS2_CUI | OS2控制台界面 |
7 | IMAGE_SUBSYSTEM_POSIX_CUI | POSIX控制台界面 |
8 | IMAGE_SUBSYSTEM_NATIVE_WINDOWS | 不需要子系统 |
9 | IMAGE_SUBSYSTEM_WINDOWS_CE_GUI | Windows CE图形界面 |
Data Directory[16]:数据目录表,由数个相同IMAGE_DATA_DIRECTORY结构组成,指向输出表,输入表,资源块等数据。IMAGE_DATA_DIRECTORY结构的定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据块的起始RVA
DWORD Size; //数据块长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
数据目录列表如下:
索 引 | 索引值在Windows.inc中的预定义值 | 对应的数据块 |
0 | IMAGE_DIRECTORY_ENTRY_EXPORT | 导出表 |
1 | IMAGE_DIRECTORY_ENTRY_IMPORT | 导入表 |
2 | IMAGE_DIRECTORY_ENTRY_RESOURCE | 资源 |
3 | IMAGE_DIRECTORY_ENTRY_EXCEPTION | 异常(具体资料不详) |
4 | IMAGE_DIRECTORY_ENTRY_SECURITY | 安全(具体资料不详) |
5 | IMAGE_DIRECTORY_ENTRY_BASERELOC | 重定位表 |
6 | IMAGE_DIRECTORY_ENTRY_DEBUG | 调试信息 |
7 | IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 版权信息 |
8 | IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 具体资料不详 |
9 | IMAGE_DIRECTORY_ENTRY_TLS | Thread Local Storage |
10 | IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 具体资料不详 |
11 | IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 具体资料不详 |
12 | IMAGE_DIRECTORY_ENTRY_IAT | 导入函数地址表 |
13 | IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 具体资料不详 |
14 | IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | 具体资料不详 |
15 | 未使用 |
区块表:
紧跟着IMAGE_NT_HEADER后的是区块表,他是一个IMAGE_SECTION_HEADER结构数组。每个IMAGE_SECTION_HEADER结构包含了它所关联区块的信息,如位置,长度,属性,该数组的数目由IMAGE_NT_HEADERS.FileHeader.NumberOfSections指出。
IMAGE_SECTION_HEADER结构定义如下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //8个字节的区块名
union { //区块尺寸
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; //区块的RVA地址
DWORD SizeOfRawData; //文件对齐后的尺寸
DWORD PointerToRawData; //文件偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //区块的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
VirtualSize:指出实际的,被使用的区块大小,是区块在没对齐处理前的实际大小。如果VirtualSize大于SizeOfRawData,那么SizeOfRawData是来自可执行文件初始化数据的大小,与VirtualSize相差的字节用零填充。
VirtualAddress:该块装载到内存中的RVA。这个地址是按照内存页对齐的,它的数值总是SectionAligment的整数倍。
SizeOfRawData:该块在磁盘文件中所占的大小。在可执行文件中,该字段包含经过File Alignment调整后的块的长度。例如:指定FileAlignment的大小为200h,如果VirtualSize中的块的长度为19Ah个字节,这一块应保存的长度为200h个字节。
PointerToRawData:该块在磁盘文件中所占的偏移。程序进编译或汇编后生成原始数据,这个字段用于给出原始数据在文件中的偏移。
- 转自绿盟科技博客”: 原文链接.