逆向工程核心原理

2022/6/23 23:25:01

本文主要是介绍逆向工程核心原理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一.熟悉调试器

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)。

查找硬编码的地址的位置,会使用到重定位表,它是记录硬编码地址偏移的列表。



这篇关于逆向工程核心原理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程