逆向工程核心原理
作者:互联网
一.熟悉调试器
1.设置“大本营”的四种方法
每次重新运行调试器,都会回到程序的入口点,为方便使用,可以设置某个重要的点(地址),使调试可以快速转到设置点上。
(1)Goto命令
记录设置大本营的地址,执行Go to(Ctrl + G)命令,输入地址,使光标定位到该地址,按F4,让调试流运行到该处。
(2)设置断点
按F2在大本营设置断点,设置后调试运行到断点处将会暂停。也可在Breakpoint对话框双击断点跳转。
(3)注释
按键盘上的 ";" 键 ,可以在指定地址添加注释。同样鼠标右键菜单中选择Search for—User defined comment,可以看到用户的所有注释,双击地址可定位。
(4)标签
按键盘上的 ":" 键 ,可以在指定地址添加标签。同样鼠标右键菜单中选择Search for—User defined labels菜单可打开标签窗口
2.快速查找指定代码的四种方法
(1)代码执行法
若程序功能非常明确时,可以逐条执行来查找需要查找的位置。
(2)字符串检索法
Search for—All referenced text strings
查看相应字符串所在地址
(3)API检索法
①在调用代码中设置断点
Search for—All intermodular calls
当应用程序向显示器画面输出内容时,需要在程序内部调用Win32 API。当我们能够推测出来程序运行调用的API时,可以查找所有的API,寻找地址。
②在API代码中设置断点
Search for—Name in all calls
当不能列出API函数时,可以向DLL代码库添加断点。因为当编写的应用程序执行操作时,必然会使用操作系统提供的API想OS提出请求,然后被调用API对应的系统DLL文件会加载到应用程序的内存中,Alt+M打开内存映射窗口,找到系统库函数,右键Name in all modules可以显示该函数所有API,查找函数。
3.“打补丁”修改字符串
"打补丁"不仅可以修复bug,还能向程序中添加新功能。对象可以是文件、内存,还能是程序的代码、数据等。
修改字符串的两种方法
①直接在缓冲区修改字符串
选中16进制代码部分,Ctrl+E打开编辑模式,修改字符串。注意,当修改范围超过原有字符串,可能损失后面的数据。
-
保存更改到可执行文件
上面的修改是暂时的,若想永久保存,要把更改后的程序保存为一个可执行文件。
在dump窗口选中更改后的字符串,右键菜单中选择Copy to executable file,打开Hex窗口,继续右键选择Save file,输入文件名,保存为.exe文件。
②在其他内存区域新建字符串并传递给消息函数
字符串以参数形式传递给函数,此时传递的是所在区域的首地址,若改变字符串地址,消息框就可以变成更改后的字符串。在内存中选择NULL填充区域,Ctrl+E填充字符串,将填充后的首地址传递给函数,光标定位到要修改的首地址处,按空格键打开Assemble窗口,修改地址即可。
二.小端序标记法
字节序
字节序是多字节数据在计算机内存中存放的字节顺序,主要分为小端序和大端序。
举例来说,数值0x2211
使用两个字节储存:高位字节是0x22
,低位字节是0x11
。
大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
小端字节序:低位字节在前,高位字节在后,即以
0x1122
形式储存。
三.IA-32寄存器
1.CPU寄存器
寄存器是CPU内部用来存放数据的一些小型存储区域,与RAM(随机存储器)不同,CPU访问RAM要经过较长的物理路径,话费时间更长;寄存器在CPU内部,具有更快的读写速度。
2.IA-32寄存器
包括基本程序运行寄存器、控制寄存器、内存管理寄存器、调试寄存器。
基本程序运行寄存器
-
通用寄存器(32位,8个)
-
段寄存器(16位,6个)
-
程序状态与控制寄存器(32位,1个)
-
指令指针寄存器(32位,1个)
(1)通用寄存器
用于传送和暂存数据,也可参与算数逻辑运算,并保存运算结果。
为了实现对低16位的兼容,各寄存器又可以分为高(High)和低(Low)。
以EAX为例:
-
EAX:(0~31)32位
-
AX:(0~15)EAX的低16位
-
AH:(8~15)AX的高8位
-
AL:(0~7)AX的低8位
其余寄存器如图所示:
各寄存器的名称及作用:
EAX:(针对操作数和结果数据的)累加器
EBX:(DS段中的数据指针)基址寄存器
ECX:(字符串和循环操作的)计数器
EDX:(I/O指针)数据寄存器
上述寄存器只要用在算术运算(ADD、SUB、XOR、OR等)指令中。此外,ECX也可在循环命令中循环次数,每循环一次,ECX减1;EAX一般用在函数返回值中,所有Win32API函数都会先把返回值保存到EAX再返回。
EBP:(SS段中栈内数据指针)扩展基址指针寄存器
ESI:(字符串操作源指针)源变址寄存器
EDI:(字符串操作目标指针)目的变址寄存器
ESP:(SS段中栈指针)栈指针寄存器
(2)段寄存器
段是一种内存保护技术,它把内存划分为多个区段,并为每个区段赋予起始地址、范围、访问权限等,以保护内存。此外,它还同分页技术一起用于将虚拟内存变更为实际物理内存。
段寄存器总共由6种寄存器组成,分别为CS、SS、DS、ES、FS、GS,每个寄存器的大小为16位,即2个字节。每个段寄存器指向的段描述符与虚拟内存结合,形成一个线性地址,借助分页技术,线性地址最终转换为实际的物理地址。
CS:代码段寄存器
SS:栈段寄存器
DS:数据段寄存器
ES:附加(数据)段寄存器
FS:数据段寄存器
GS:数据段寄存器
顾名思义,CS用来存放应用程序代码所在段的段基址,SS用于存放栈段的段基址,DS用于存放数据段的段基址,ES、FS、GS来存放程序使用的附加数据段的段基址。
(3)程序状态与控制寄存器
EFLAGS:Flag Register,标志寄存器
大小为4字节,32位,由原来的16位FLAGS寄存器扩展而来。每一位都有意义,每一位的值或为1或为0,代表On/Off或True/False。目前只需掌握三个标志:ZF(Zero FLag零标志),OF(Overflow Flag)溢出标志、CF(Carry Flag,进位标志)。
ZF:若运算结果为0,则结果为1(True),否则其值为0(False)
OF:有符号整数(signed integer)溢出时,OF值被置为1。
CF:无符号整数(unsigned integer)溢出时,其值被置为1。
(4)指令指针寄存器
EIP:Instruction Pointer,指令指针寄存器
指令指针寄存器保存着CPU要执行的指令地址,大小为32位(4个字节)。程序运行时,CPU会读取EIP中一条指令的地址,传送指令到指令缓冲区,EIP寄存器的值自动增加,增加的大小是读取指令的字节大小。
四、栈
1.栈
栈内存在进程中的作用如下
(1)暂时保存函数内的局部变量
(2)调用函数时传递参数
(3)保存函数返回后的地址
栈作为一种数据结构,按照先进后出的原则存储数据。
栈的特性
一个进程中,栈顶指针(ESP)初始状态指向栈底端。执行PUSH命令将数据压入栈时,栈顶指针就会上移到栈顶端,栈顶指针减小,执行POP命令从栈中弹出数据时,若栈为空,则栈顶指针重新移动到栈底端,栈顶指针增大。所以,栈是一种由高地址向低地址扩展的数据结构。
五.栈帧
栈帧是利用EBP寄存器访问栈内局部变量、参数、函数返回地址等的手段。栈顶指针ESP寄存器会在程序运行中发生变化,若以ESP为基准访问数据,会产生其他问题。所以,把ESP的值保存在EBP中,以EBP为基准访问数据,这就是栈帧的作用。
六、函数调用约定
函数调用约定是对函数如何传递参数的一种约定。调用函数前先把参数压入栈再传递给参数,栈是定义在进程中的内存空间,当进程运行时确定栈内存的大小。
Q:函数执行完成后,栈中参数如何处理。
A:不用管。
参数临时储存在栈中,再向栈存放其他值时,原有值会被覆盖掉。且栈内存固定,既不能也没必要释放内存。
Q:函数执行完毕,ESP的值如何变化?
A:ESP值要恢复到函数调用之前,这样可引用的栈大小才不会缩减。
因为栈内存固定,ESP来指示当前栈的位置,若ESP指向栈底,则无法再使用该栈。函数调用后如何处理ESP,是函数调用约定要解决的问题。
主要的函数调用如下:
-
cdecl
-
stdcall
-
fastcall
1.cdecl
cdecl是主要在C语言中使用的方式,调用者负责处理栈。参数由右向左入栈。
调用函数的参数入栈后,调用者函数直接清理其压入栈的函数参数。
2.stdcall
此方式常用于Win32API,调用函数的参数入栈(由右向左)后,调用者函数直接清理其压入栈的函数参数。
好处在于,代码尺寸小,拥有更好的兼容性。
3.fastcall
fastcall与stdcall方式基本类似,当该方式通常会使用寄存器(函数的第一个和第二个参数通过ecx和edx传递)而非栈内存来传递参数。若某函数有四个参数,则前两个参数分别使用ECX、EDX寄存器传递。
七.PE文件
PE(Portable Executable)文件是Windows操作系统下使用的可执行文件格式。PE文件是指32位的可执行文件,64位的可执行文件称为PE+或PE32+,是PE(32)文件的一种扩展形式。
1.PE文件格式
严格的说,OBJ(对象)文件之外的所有文件都是可执行的。DLL、SYS文件不能直接在shell中运行,但可以使用调试器等等执行。
(1)基本结构
从DOS(磁盘操作系统)头到节区头是PE头部分,其下的节区合称PE体。文件中使用偏移,内存中使用VA(Virtual Address,虚拟地址)来表示位置。文件的内容一般可分为代码(.text)、数据(.data)、资源(.rsrc)节,分别保存。
节区:Flash芯片的最小数据存储单位
节区头定义了各节区在文件或内存中的大小、位置、属性等。PE头与各节区的尾部存在一个区域,称为NULL填充。
(2)VA&RVA
VA指进程虚拟内存的绝对地址,RVA(相对虚拟地址)指从某个基准位置(ImageBase)开始的相对地址。
换算关系:RVA+ImageBase=VA
2.PE头
(1)DOS头
DOS文件广泛使用时,微软为了PE文件对DOS文件的兼容性,在PE头的最前面添加了一个IMAGE—DOS—HEADER结构体,来扩展已有的DOS EXE头。
IMAGE—DOS—HEADER结构体大小为40字节,必须知道两个重要成员。e_magic与e_lfanew.
e_magic:DOS签名(4D5A=>ASCII值"MZ")
e_lfanew:指示NT头的偏移(不同文件拥有可变值)
所有PE文件在开始部分都有DOS签名(“MZ”),e_lfanew值指向NT头所在位置。
(2)DOS存根
DOS存根在DOS头下方,是可选项,且大小不固定。
DOS存根的内容是当我们的程序在DOS环境中运行时执行的代码, 也就是给一个提示信息:This is program cannot be run in DOS mode
, 那我们是可以随便将其内容修改为自己想填充的东西, 反正不会影响在window os中的运行, 但记住这个大小是不能修改的, 会影响后面指令索引地址跟着出错, 最后程序崩溃
(3)NT头(IMAGE—NT—HEADERS)
此结构体由三个成员组成,第一成员为签名结构体,值为50450000h(“PE”00)另外两个成员分别为文件头与可选头结构体。
(4)文件头
表现文件大致属性的IMAGE—FILE—HEADERS结构体。结构体有以下4种重要成员。
#1.Machine
每个CPU都拥有唯一的Machine码,兼容32位Intel x86芯片的Machine码为014c。
#2.NumberOfSection
用来指出文件中存在的节区数量。该值必须大于0,且当定义的节区数量与实际节区不同时,会发生运行错误。
#3.SizeOfOptionalHeader
IMAGE—NT—HEADERS结构体的最后一个成员是IMAGE—OPTIONAL—HEADER32结构体。
SizeOfOptionalHeader成员用来指出IMAGE—OPTIONAL—HEADER32结构体的长度。
#4.Characteristics
用来标识文件的属性,文件是否是可运行的状态、是否为DLL文件等信息。
(5)可选头
IMAGE—OPTIONAL—HEADER32是结构体中最大的,需要关注下列成员。
#1.Magic
为IMAGE—OPTIONAL—HEADER32结构体时,Magic码为10B;为IMAGE—OPTIONAL—HEADER64结构体时,Magic码为20B。
#2.AddressOfEntryPoint
AddressOfEntryPoint持有EP的RVA值,该值指出程序最先执行的代码起始地址。
#3.ImageBase
32位系统进程虚拟内存范围是0~FFFFFFFF,ImageBase指出文件的优先装入地址。EXE、DLL文件被装载到用户内存的0~7FFFFFFF中,SYS文件被载入内核内存的80000000~FFFFFFFF中。
#4.SectionAlignment,FileAlignment
PE文件的Body部分划分为若干节区,FileAlignment指定了节区在磁盘文件中的最小单位,而SectionAlignment则指定了节区在内存中的最小单位。
#5.SizeOfImage
加载PE文件到内存时,SizeOfImage指定了PE Image在虚拟内存中所占空间的大小。
#6.SizeOfHeader
用来指出整个PE头的大小。
#7.Subsystem
该Subsystem值用来区分系统驱动文件(*.sys)与普通的可执行文件(*.exe,*.dll)。
#8.NumberOfRvaAndSizes
用来指定DataDirectory数组的个数
#9.DataDirectory
是由IMAGE_DATA_DIRECTORY结构体构成的数组。
(5)节区头
PE文件中的code(代码)、data(数据)、resource(资源)等按照属性分类在不同节区,然后把各节区属性记录在节区头中(节区属性中有文件/内存的起始位置、大小、访问权限等)。
IMAGE_SECTION_HEADER
节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区。
重要成员如下:
3.RVA to RAW
PE文件加载到内存时,每个节区需要准确完成内存地址与文件偏移间的映射,称为RVA to RAW
公式:RAW=RVA - VirtualAddress(内存中节区起始位置) + PointerToRawData(磁盘中节区起始位置)。
4.IAT(Import Address Table,导入地址表)
IAT保存的内容与Windows操作系统的核心进程、内存、DLL结构有关,简言之,IAT是一种表格,用来记录正在使用哪些库中的哪些函数。
(1)DLL(动态链接库)
为了提高计算机效率, 引入了DLL概念。描述如下:
-
不把库包含到程序中,单独组成DLL文件,需要时调用;
-
内存映射技术使加载后的DLL代码、资源在多个进程中实现共享;
-
更新库时只要替换相关DLL文件即可,简便易行。
加载DLL方式实际有两种:一种是“显式链接”,程序使用DLL加载,使用完毕后释放内存;另一种是“隐式链接”,程序开始时即一同加载DLL,程序终止时再释放占用的内存。IAT提供的机制与隐式链接有关。
(2)IMAGE_IMPORT_DESCRIPTOR
IMAGE_IMPORT_DDESCRIPTOR结构体记录着PE文件要导入哪些库文件。
注意
Import:导入,向库提供服务(函数)
Export:导出,从库向其他PE文件提供服务(函数)
执行一个普通程序时往往需要导入多个库,导入多少库就存在多少IMAGE_IMPORT_DDESCRIPTOR结构体,这些结构体形成了数组,且结构体数组最后以NULL结构体结束。
重要成员如下:
5.EAT
在Windows操作系统中,库是问了方便程序调用而组合起来的包含函数的集合体。EAT是一种核心机制,应用程序只有通过EAT才能准确求得从库中导出函数的起始地址。与IAT一样,特定结构体IMAGE_EXPORT_DESCRIPTOR(在PE头中)保存导出信息,且PE文件中仅有一个用来说明库EAT的结构体。
用来说明IAT的IMAGE_IMPORT_DESCRIPTOR以数组存在,且拥有多个成员,是因为PE文件可以同时导入多个库
重要成员
GetProcAddress()
从库中获得函数地址的API为GetProcAddress()函数,该API引用EAT来获取指定API的地址。
八.运行时压缩
1.数据压缩
任何文件(数据)都由0或1组成,只要有合适的压缩算法,就能缩减大小。若压缩后文件能100%恢复,称为无损压缩;若不能恢复原状,称为有损压缩。
2.运行时压缩器
运行时压缩器是针对可执行文件而言的,可执行文件内部含有解压缩代码,文件在运行瞬间于内存中解压缩后执行。
运行时压缩文件也是PE文件,内部含有原PE文件与解码程序
把普通PE文件创建成运行时压缩文件的实用程序称为压缩器,经反逆向技术特别处理的压缩器称为保护器。
(1)压缩器
PE压缩器指可执行文件压缩器,是PE文件的专用压缩器
目的:
-
缩小PE文件的尺寸
便于传输和保存
-
隐藏PE文件内部代码与资源
可以隐藏PE文件内的代码及资源(字符串、API名称字符串)
(2)保护器
PE保护器是一类保护PE文件免受代码逆向分析的实用程序。它们不像普通的压缩器一样仅对PE文件进行运行时压缩,还应用了多种防止代码逆向分析的工具。
使用目的
-
防止破解
-
保护代码与资源
不仅可以保护PE文件本身,还可在文件运行时保护进程内存,防止打开Dump窗口。
3.运行时压缩的文件
以notepad.exe与notepad_upx.exe为例
-
PE头大小一样
-
节区名称改变(.text——>UPX0、.data——>UPX1)
-
第一个节区的RawDataSize(磁盘文件中节区所占大小) = 0(文件大小为0)
-
资源节区(.rsrc)大小几乎无变化
在第一个节区中RawDataSize为0,即第一节区在磁盘文件中不存在,但第一节区VirtualSize为0010000,即在内存中存在。由此可知,经过UPX压缩后的PE文件在运行瞬间将压缩的代码解压到内存中的第一节区中,解压缩代码和压缩的源代码都在第二节区,文件运行时首先执行解压缩代码,将处于压缩状态的源代码解压到第一节区,解压结束后再运行源文件的EP代码。
4.快速查找UPX OEP的方法
OEP:源文件的EP为OEP
(1)在POPAD指令后的JMP指令处设置断点
UPX压缩器的特征之一是,其EP代码被包含在PUSHAD/POPAD指令之间,并且跳转到OEP代码的JMP指令紧接着出现在POPAD指令之后,只要在JMP指令处设置好断点,运行后就能直接找到OEP。
(2)在栈中设置硬件断点
该方法也利用UPX的PUSHAD/POPAD指令的特点。在执行PUSHAD命令后,EAX到EDI寄存器的值依次被存储到栈,对该栈地址设置硬件断点,当执行POPAD指令时会访问该内存地址来获取寄存器的值,从而触发断点。
九.基址重定位表
1.PE重定位
基址重定位表(Base Relocation Table),记录PE重定位时需要修改的硬编码地址的位置。
一般地,向进程的虚拟内存加载PE文件(EXE、DLL、SYS)时,文件会被加载到PE头的ImageBase所指的地址处。若加载的文件为DLL或SYS,且ImageBase位置加载了其他DLL或SYS文件时,则会进行PE重定位。
PE重定位是指PE文件无法加载到ImageBase所在位置时,加载到其他地址所发生的处理行为。
在进程创造好之后,EXE文件会首先加载到内存当中,因而无需考虑重定位的问题。
系统的DLL实际不会发生重定位,因为同一系统的kernel32.dll、user32.dll等会被加载到自身固有的ImageBase。
基址重定位表(以notepad.exe为例)
位于PE头的DataDirectory数组
基址重定位表的地址为RVA 2F000
第一个成员为VirtualAddress,实际是RVA值。
第二个成员SizeOfBlock,指重定位块的大小。
最后一项是TypeOffset数组,不属于结构体成员,是以注释形式存在,表示在该结构体下会出现WOED类型的数组。
TypeOffset值为2个字节,16位大小,由4位Type与12位的Offset合成的。
高4位用作Type,PE文件常见值为3,64位的PE+文件中常见值为A。
低12位是真正的位移。
2.PE重定位操作原理
1、在应用程序中查找硬编码的地址位置;
换算等式:VirtualAddress+Offset=RVA
用RVA查找寻找硬编码地址
2、读取值后,减去ImageBase(VA转换为RVA);
3、加上实际加载地址(RVA转换为VA)。
查找硬编码的地址的位置,会使用到重定位表,它是记录硬编码地址偏移的列表。
标签:逆向,文件,核心,PE,地址,内存,寄存器,原理,节区 来源: https://www.cnblogs.com/zzy-AVA/p/16404248.html