可执行文件
软件逆向工程分析的对象是程序,即一个或多个可执行文件。
1、可执行文件的形成过程(编译和链接)
(1)用户将一组用高级语言编写的源代码作为编译器输入
(2)编译器解析输入,并为每个源代码文件产生对应的汇编代码
(3)汇编器接收编译器生成的汇编代码,并继续执行汇编操作,将生成的每份机器代码临时存于各对象文件中
(4)现已生成了多个对象文件,但最后的目标是生成一个可执行文件。于是链接器参与其中,连接分散的各对象文件,经过处理融合成完整的程序。然后按照可执行文件的格式,填入各种指定程序运行环境的参数,最后形成一个完整的可执行文件。
注:在每步过程中或多或少伴随着信息的丢失(如编译阶段会丢弃源代码的注释信息,汇编时会丢弃汇编代码中的label名称,链接时可能丢弃函数名,类型名等)
2、不同格式的可执行文件
Windows——PE文件
Linux——ELF文件
PE文件
由DOS头,PE文件头,节表及各节数据组成,(需要引用外部的动态链接库时有)导入表,(自己可以提供函数给其他程序来动态链接,常见于DLL文件,有)导出表
ELF文件
由ELF头、各节数据、字表、字符串段、符号表组成
节
是程序中各部分的逻辑划分,一般有特定名称
如.text/.code代表代码节
.data代表数据节
在运行时,可执行文件的各节会被加载到内存的各位置,为了方便管理和节省开销,一个或多个节会被映射到一个段(Segment)。段的划分是根据这部分内存需要的权限(读、写、执行)来进行的。如果在相应的段内进行了非法操作,如在只能读取和执行的代码段进行了写操作,就会产生段错误
汇编语言
1、寄存器、内存和寻址
寄存器
x86-32
#通用寄存器:EAX EBX ECX EDX ESI EDI
#栈顶指针寄存器ESP(内存存放着一个指针,这个指针永远指向系统栈最上面一个栈帧的栈顶。)、栈底指针寄存器EBP(其内存存放着一个指针,这个指针永远指向系统栈上面一个栈帧的底部。)
#指令计数器EIP(保存下一条即将执行的执行的地址)
#段寄存器CS DS SS ES FS GS
x86-64
#通用寄存器:RAX RBX RCX RDX RSI RDI R8~R15
#栈顶指针寄存器ESP、栈底指针寄存器EBP
#指令计数器EIP(保存下一条即将执行的执行的地址)
#段寄存器CS DS SS ES FS GS
x16
#通用寄存器:AX BX CX DX SI DI
#栈顶指针寄存器ESP、栈底指针寄存器EBP
#指令计数器EIP(保存下一条即将执行的执行的地址)
#段寄存器CS DS SS ES FS GS
R8~R15进行拆分时的命名规则:R8d(低32位),R8w(高16位),R8b(低16位)
标志寄存器
AF:辅助进位标志——运算结果在第3位进位的时候置1
PF:奇偶校验标志——运算结果的最低有效字节有偶数个1时置1
SF:符号标志——有符号整型的符号位为1时置1,代表这是一个负数
ZF:零标志位——运算结果全为0时置1
OF:溢出标志位——运算结果在被操作数是有符号数且溢出时置1
CF:进位标志——运算结果向高位以上进位时置1,用来判断无符号数的溢出
寻址
常见指令
跳转指令
2、汇编与反汇编
汇编是直接翻译汇编语句为对应的机器代码,并直接将相邻的各条语句放在一起
反汇编是将机器代码编译回汇编语言
注:冯诺依曼架构模糊了代码与数据的区别界限,在代码节中可能穿插跳转表,常量池(ARM),普通常量数据,甚至恶意的干扰数据等。所以直接一条条连续地向下解析指令往往会出现问题
3、调用约定
为编译器创立了一些规定各函数之间的参数传递的约定,称为调用约定
(1)x86 32位
#__cdecl:参数从右向左依次压入栈中,调用完毕,由调用者负责将这些压入的参数清理掉,返回值置于EAX中
#__stdcall:参数从右向左依次压入栈中,调用完毕,由被调用者负责将这些压入的参数清理掉,返回值置于EAX中(Windows中很多API都用这种方式)
#_thiscall:为类方法专门优化的调用约定,将类方法的this指针放在ECX中,其余参数压入栈中
#_fastcall:为加速调用而生的调用约定,将第1个参数放在ECX中,第2个参数放在EDX中,然后将后续的参数从右至左压入栈中
(2)x86 64位
Microsoft x64:在Windows上使用,依次(从左至右)将前4个参数放入RCX、RDX、R8、R9,剩下的参数从右至左压入栈中
SystemV x64:在Linux、MacOS上使用,使用RDI、RSI、RCX、RDX、R8、R9,传递前6个参数,剩下的从右至左压入栈中
局部变量
局部变量有“易失性”:一旦函数返回,所有局部变量失效。
因此,将局部变量存放在栈上,在每次函数被调用时,程序从栈上分配一段空间,作为存储局部变量的区域。
把每个函数自己的这一片区域称为帧,由于这些帧都在栈上,又被称为栈帧(由局部变量,父栈帧的值,返回地址,参数四部分构成)。
栈的内存区域并不一定是固定的,随着每次调用的路径不同,栈帧的位置也会不同。
虽然栈的内容随着进栈、出栈会一直不断变化,但是一个函数中每个局部变量相对于该函数栈帧的偏移都是固定的,可以引入一个寄存器ebp(称为栈指针)专门存储当前栈帧的位置。
ebp栈指针
程序在函数初始化阶段赋值ebp为栈帧中间的某个位置,这样可以用ebp引用所有的局部变量。由于上一层的父函数也要使用ebp,因此要在函数开始时先保存ebp,再赋值ebp为自己的栈帧的值。ebp再初始化后实际上指向的是父栈帧地址的存储位置。
流程汇编代码
1 | push ebp |