学习笔记逆向工程核心原理02.小段标记法IA-32寄存器栈abexcrackme栈帧
【学习笔记】《逆向工程核心原理》02.小段标记法、IA-32寄存器、栈、abex‘crackme、栈帧
思维导图
1. 字节序
- 字节序分为 大端序 与 小端序
- 字符串最后是以 NULL 结尾的
1.1. 大端序与小端序
大端序 :内存地址低位存储数据的高位、内存地址高位存储数据的低位
小端序 :内存地址高位存储数据的高位、内存地址低位存储数据的低位
采用大端序保存多字节数据非常直观,它常用于大型UNIX服务器的RISC系列的CPU中。此外, 网络协议中也经常采用大端序方式 。了解这些,对从事x86系列应用程序的开发人员以及代码逆向分析人员具有非常重要的意义,因为通过网络传输应用程序使用数据时,往往都需要修改字节序。
1.2. 在OllyDbg中查看小端序
代码
#include "windows.h"
BYTE b = 0x12;
WORD w = 0x1234;
DWORD dw = 0x12345678;
char str[] = "abcde";
int main(int argc, char *argv[])
{
BYTE lb = b;
WORD lw = w;
DWORD ldw = dw;
char *lstr = str;
return 0;
}
直接goto到401000
main()
函数处
全局变量b、W、dw、str的地址分别为 413880、413888、413884、41388c 。下面通过OllyDbg的数据窗口来分别查看它们所在的内存区域
可以发现变量w的数据采用小段存储
2.IA-32寄存器
2.1. 什么是CPU寄存器
寄存器(Register)是CPU内部用来存放数据的一些小型存储区域,它与我们常说的RAM(RandomAccessMemory,随机存储器、内存)略有不同。CPU访问(Access) RAM中的数据时要经过较长的物理路径,所以花费的时间要长一些;而寄存器集成在CPU内部,拥有非常高的读写速度。
2.2. IA-32寄存器
IA-32是英特尔推出的32位元架构,属于复杂的指令集架构,它提供了非常丰富的功能,且支持多种寄存器。
通用寄存器(General PurposeRegisters,32位,8个)
段寄存器(Segment Registers,16位,6个)
程序状态与控制寄存器(ProgramStatus and Control Registers,32位,1个)
指令指针寄存器(InstructionPointer,32位,1个)
2.2.1. 通用寄存器
为了实现对低16位的兼容,各寄存器又可以分为高(H:High)低(L:Low)
几个独立寄存器。下面以EAX为例讲解。
EAX:(0~31)32位
AX:(0~15)EAX的低16位
AH:(8~15)AX的高8位
AL:(0~7)AX的低8位
若想全部使用4个字节(32位),则使用EAX;若只想使用2个字节(16位),只
要使用EAX的低16位部分AX就可以了。
AX又分为高8位的AH与低8位的AL两个独立寄存器。借助这种方式,可以根据不同情况把一个32位的寄存器分别用作8位、16位、32位寄存器。后面的程序调试中,我们分析汇编代码就能很容易地理解它们。
EAX:(针对操作数和结果数据的)累加器
EBX:(DS段中的数据指针)基址寄存器
ECX:(字符串和循环操作的)计数器
EDX:(IO指针)数据寄存器
EBP:(SS段中栈内数据指针)扩展基址指针寄存器
ESI:(字符串操作源指针)源变址寄存器
EDI:(字符串操作目标指针)目的变址寄存器
ESP:(SS段中栈指针)栈指针寄存器
此外:EAX一般用在函数返回值中。所有的win32API 函数都会先把返回值保存到EAX再返回
2.2.2. 段寄存器
IA-32的保护模式中,段是一种内存保护技术,它把内存划分为多个区段,并为每个区段赋、范围、访问权限等,以保护内存。
此外,它还同分页技术(Paging)一起用于将虚拟内存变更为实际物理内存。段内存记录在 SDT (SegmentDescriptorTable,段描述符表)中,而段寄存器就持有这些SDT的索引(index)。
CS:Code Segment,代码段寄存器
SS:Stack Segment,栈段寄存器
DS:Data Segment,数据段寄存器
ES:Extra(Data)Segment,附加(数据)段寄存器
FS:DataSegment,数据段寄存器
GS:DataSegment,数据段寄存器
2.2.3. 程序状态与控制寄存器
EFLAGS:FlagRegister,标志寄存器
大小为4字节(32位)由原来的16位FLAGS寄存器拓展而来
学习代码逆向分析技术的初级阶段,只要掌握3个与程序调试相关的标志即可,
分别为 ZF(ZeroFlag,零标志) 、 OF(OverflowFlag,溢出标志) 、 CF(CarryFlag,进位标志) 。
2.2.4. 指令指针寄存器
●EIP:InstructionPointer,指令指针寄存器
指令指针寄存器保存着CPU要执行的指令地址,其大小为32位(4个字节),由原16位IP寄存器扩展而来。程序运行时,CPU会读取EIP中一条指令的地址,传送指令到指令缓冲区后,EIP寄存器的值自动增加,增加的大小即是读取指令的字节大小。
这样,CPU每次执行完一条指令,就会通过EIP寄存器读取并执行下一条指令。
与通用寄存器不同,我们不能直接修改EIP的值,只能通过其他指令间接修改 ,这些特定指令包括JMP、Jcc、CALL、RET。此外,我们还可以通过中断或异常来修改EIP的值。
3. 栈
栈内存在进程中的作用如下:
(1)暂时保存函数内的局部变量。
(2)调用函数时传递参数。
(3)保存函数返回后的地址。
栈其实是一种数据结构,它按照FILO(FirstInLastOut,后进先出)的原则存储数据。
1.1. 栈的特征
一个进程中, 栈顶指针(ESP)初始状态指向栈底端 。执行PUSH命令将数据压人栈时,栈顶指针就会上移到栈顶端。执行POP命令从栈中弹出数据时,若栈为空,则栈顶指针重新移动到栈底端。
换言之, 栈是一种由高地址向低地址扩展的数据结构 ,图中,栈是由下往上扩展的。
由于栈具有这种特征,所以我们常常说“ 栈是逆向扩展的 ”,向栈中压数据就像一层层砌砖,每向上砌一层,砖墙就增高一点儿。
3.1.2. 栈操作实例
调试器加载附件 Stack.exe
可以发现栈顶指针的值为 19FF74 ,下面也可以看到ESP指向的地址及其值
然后我们执行
PUSH 100
观察ESP值的变化
发现ESP变成了 19FF70 少了4个字节。并且当前栈顶指针指向 19FF70 地址,该地址中保存着100这个值。
就是说 执行
PUSH 100
命令时, 数值100被压入栈 ESP随之向上移动。即ESP值减少了4个字节。
然后我们执行
POP EAX
命令
执行后,ESP的值又增加了4个字节,变回了 19FF74 ,栈又变成了原来的初始状态。 并且将原来存储在栈中的数值100 保存到了EAX寄存器中
向栈压入数据时,栈顶指针减小,向低地址移动;从栈中弹出数据时,栈顶指针增加,向高地址移动。
请记住 :栈顶指针在初始状态下指向栈底。这就是栈的特征
4. abex’crackme
运行这个
abex'crackme
这个程序,会提示两个信息
4.1. 开始调试
虽然看不到在搞什么名堂,还是直接用OD进行调试吧
进来后会发现EP代码非常短。这是因为这个程序是用汇编语言编写出来的可执行文件
使用VC++、VC、Delphi等开发工具编写程序时,除了自己编写的代码外,还有一部分启动函数是由编译器添加的,经过反编译后,代码看上去就变得非常复杂。
但是如果直接使用汇编语言编写程序,汇编代码会直接变为反汇编代码。观察图6-3中的代码可以看到,
main
直接出现在EP中,简洁又直观,充分证明了这是一个直接用汇编语言编写的程序。
分析代码可以发现 在第一个弹框点击确认后,程序会调用
GetDriveType()
API,获取C驱动器的类型。 (大部分都是HDD类型),然后操作它使之被识别为CD-ROM类型。然后就会输出 成功后的提示
4.1.1. 破解
我们使用f8去一步一步执行,可以发现在
JE short 0040103D
处因为条件不满足导致无法跳转到
40103D
从而进入了错误的判断当中
这里我们对
je short 40103d
反汇编 把
je
改成
jmp即可
然后运行即可进入正确的代码逻辑中
4.1.2. 将参数压入栈
这里介绍一下函数调用时将函数参数压入栈中的方法
我们查看
40100-40100E
之间的命令
可以发现调用
messageboxwA
函数之前使用了4个PUSH命令,把函数需要的参数
逆序
压入栈
然后这是C语言的函数调用代码
MessageBox(NULL, "Make me think your HD is a CD-Rom.", "abex' 1stCrackme", MB_OK|MB_APPLMODAL);
可以发现两者传入参数的顺序是不同的。
函数调用时的参数顺序(正序)与参数入栈时的顺序(逆序)相反。
这里就是因为栈的结构是 FILO 先进后出,即出入栈会交换顺序
我们是要先把参数入栈,然后再出栈传递给函数进行调用。所以需要逆序入栈,从而使得是正序出栈
我们把程序执行到EIP=
40100E
处观察栈窗口
从这个栈中取出数据就是正序的了。也就可以正确的传递给函数参数进行调用
个人理解:因为栈是向低地址延申的。所以低地址是栈顶,高地址是栈底。所以出来的顺序就是
NULL
Make me ....
也就是正序的了。
5. 栈帧
作用 : 栈帧在程序中用于声明局部变量、调用函数。
栈帧就是利用EBP寄存器访问栈内局部变量、参数、函数返回地址等的手段
与ESP寄存器不同:ESP承担栈顶指针的作用。而EBP寄存器则负责行驶栈帧指针的职能
程序运行中,ESP寄存器的值随时变化,访问栈中函数的局部变量、参数时,若以ESP值为基准编写程序会十分困难,并且也很难使CPU引用到准确的地址。
所以,调用某函数时, 先要把用作基准点(函数起始地址)的ESP值保存到EBP,并维持在函数内部 。
这样,无论ESP的值如何变化,以EBP的值为基准(base)能够安全访问到相关函数的局部变量、参数、返回地址,这就是EBP寄存器作为栈帧指针的作用。
●最新的编译器中都带有一个“优化”(Optimization)选项,使用该选项编译简单的函数将不会生成栈帧。
●在栈中保存函数返回地址是系统安全隐患之一,攻击者使用缓冲区溢出技术能够把保存在栈内存的返回地址更改为其他地址。
5.1. 调试示例:stackframe.exe
源代码
#include "stdio.h"
long add(long a, long b)
{
long x = a, y = b;
return (x + y);
}
int main(int argc, char* argv[])
{
long a = 1, b = 2;
printf("%d\n", add(a, b));
return 0;
}
调试器加载程序 并goto到 401000处
5.1.1. 开始执行main()函数&生成栈帧
int main(int argc, char* argv[]){}
函数
main
是程序开始执行的地方,在
main
函数的起始地址(401020)处,按F2键设置个断点,然后按F9运行程序,程序运行到
main
函数的断点处暂停。
然后观察栈的变化
当前
ESP
的值为
19ff2c
,
EBP
的值为
19ff70
。切记地址
401250
保存在
ESP(19ff2c
)中,它是
main
函数执行完毕后要返回的地址。
观察代码可以发现
main
函数 一开始运行就会生成与其对应函数的栈帧
这里
PUSH EBP
就是把EBP的值压入栈。
main
函数中 EBP为栈帧指针,用来把EBP之前的值备份到栈中。(
main
函数执行完毕,返回之前,该值会再次恢复)
下一条命令
MOV EBP,ESP
就是把ESP的值 传递给EBP。换言之,从这条命令开始,EBP就有和ESP相同的值,直到
main
函数执行完毕。EBP的值始终不变。 也就是说,我们通过EBP就可以安全访问到存储在栈中的函数参数与局部变量了
执行完
PUSH EBP
与
MOV EBP,ESP
后,函数
main
的栈帧就生成了(设置好了EBP)
然后在栈窗口 选择 地址->相对EBP
然后调试到
mov ebp,esp
可以发现当前EBP值与ESP值相同。 EBP处的数值是
19FF70
他是
main
函数开始执行时EBP持有的初始值。
5.1.2. 设置局部变量
分析源代码中的变量声明与赋值语句
long a = 1, b = 2;
在
main
函数中,上述代码用于在栈中为局部变量
a b
分配空间,并赋初始值
如何分配空间?
SUB ESP,0x8
这里就是把ESP减去8个字节(因为a b是两个长整形,每个占用4个字节)开辟空间,以便将它们保存在栈中。
当为这两个变量开辟好空间后,在
main
函数内部,无论ESP的值怎么变化。变量a b的栈空间都不会受影响。由于EBP的值在
main
函数内部是固定不变的。所以,我们可以通过以它为基准进行访问函数的局部变量了。
通过指令
mov dword ptr ss:[ebp-0x4],0x1
mov dword ptr ss:[ebp-0x8],0x2
来设置值。
在界面中可能会显示
那是因为反汇编工具会将
ebp
作为基址,并对局部变量进行标注,以提高可读性
其中
local.1
表示一个局部变量。这里就是
a
变量
DWORDPTRSS:[EBP-4]
语句中,SS是StackSegment的缩写,表示栈段。由于Windows中使用的是段内存模型(Segment MemoryModel),使用时需要指出相关内存属于哪一个区段。其实,32位的WindowsOS中,SS、DS、ES的值皆为0,所以采用这种方式附上区段并没有什么意义。因EBP与ESP是指向栈的寄存器,所以添加上了SS寄存器。请注意,“DWORDPTR”与“SS:”等字符串可以通过设置OllyDbg的相应选项来隐藏。
执行完这两天命令后,查看栈内的情况可以发现已经将变量的值存储到栈内了
5.1.3. add函数参数传递与调用
源代码中调用了add函数 并且打印了返回值
printf("%d\n", add(a, b));
观察这5行代码,它表示了调用add()函数的全过程。
401000
处的函数就是
add
函数,函数
add
接受
a
b
这两个长整型参数。所以调用
add
函数之前 需要把这两个参数压入栈中。
值得注意的是:这里入栈的顺序与
add
函数的参数顺序正好相反。 这就是参数的逆向存储即先入栈b 再入栈a
然后进入add函数内部进行分析
在执行函数之前,CPU会先把函数的返回地址入栈,用作函数执行完毕后的返回地址。
观察代码可以发现函数
add
在
40103c
处被调用。他的下一条命令地址是
401041
这就是
add
函数的返回地址。
可以发现此时CPU也把函数返回地址压入栈中了
5.1.4. 开始执行add函数&生成栈帧
add
函数前两行
long add(long a, long b)
{
函数开始执行时,栈中会单独生成与其对应的栈帧
执行后就会把原来EBP的值备份到栈中,然后把ESP的值传递给EBP
5.1.5. 设置add函数的局部变量(x,y)
源代码
long x=a, y=b
这里声明了两个长整型变量 x y并用a b 赋值给他们
首先开辟空间
SUB EBP,0x8
然后赋值
、
执行后的栈内情况
5.1.6. add运算
源代码
return (x+y);
用于返回两个局部变量之和
执行后eax的值就是3
5.1.7. 删除函数add的栈帧&函数执行完毕(返回)
对应原代码
return (x+y);
}
执行完加法运算后,要返回函数
add
在此之前,需要先删除
add
函数的栈帧
mov esp,ebp
这里与开始生成栈帧的命令
mov ebp,esp
向对应
生成栈帧时: 把函数开始执行时的ESP值放入EBP
删除栈帧时:把放入EBP中的ESP值还给ESP
执行完上面的命令后,地址
401003
处的SUB ESP,0x8
就会失效,即函数add
的两个局部变量x,y不在有效
然后
pop ebp
恢复函数
ADD
开始执行时备份到栈中的EBP值。它与
401000
处的
PUSH EBP
命令对应。 EBP值恢复为
19FF28
它是
main
函数的EBP值,到此
add
函数的栈帧就被删除了
可以看到此时ESP的值为
19ff14
此地址的值是
401041
它是执行
call 401000
命令时CPU存储到栈中的返回地址
然后执行retn命令后。存储在栈中的返回地址即被返回, 此时调用栈也已经完全返回到被调用到
add
函数之前的状态
5.1.8. 从栈中删除函数add的参数(整理栈)
现在,函数执行流已经返回到
main
函数中
然后执行
ADD esp,0x8
即回收空间。不需要a b参数了
这里与前面开辟空间的
SUB esp,0x8
相对应
被调函数执行完毕后, 函数的调用者(Caller) 负责清理存储在栈中的参数,这种方式称为 cdecl 方式;
反之, 被调用者(Callee) 负责清理保存在栈中的参数,这种方式称为 stdcall 方式。
这些函数调用规则统称为调用约定(CallingConvention),这在程序开发与分析中是一个非常重要的概念
5.1.9. 调用printf函数
对应源代码
printf("%d\n",add(a,b));
由于上面的
printf
函数有2个参数,大小为8个字节(32位寄存器+32位常量=64位=8字节),
所以在
40104F
地址处使用
ADD
命令,将ESP加上8个字节,把函数的参数从栈中删除。函数
printf
执行完毕并通过
ADD
命令删除参数后 栈内情况
5.1.10. 设置返回值
对应源代码
return 0;
main
函数使用该语句设置返回值0
汇编代码
xor eax,eax
异或清0 且比
MOV eax,0
更快
5.1.11. 删除栈中&main函数终止
对应源代码
return 0;
}
主函数终止,同
add
函数一样,需要先从栈中删除其对应的栈帧
对应汇编代码
MOV ESP,EBP
POP EBP
执行后,
main
函数栈帧被删除。其局部变量,a b 也不再生效。
此时与
main
函数开始的栈内情形是一样的
然后执行
retn
程序流跳转到返回地址
401250
处,该地址指向Visual C++的启动函数区域。随后执行进程终止代码。
6. 设置OllyDbg选项
6.1. 反汇编选项
Alt+O
6.2. 分析1选项
关闭后就不会显示
Local.1
这种形式了
笔记仅记录自己学习。里面有大量书中原文。如有侵权,请联系我删除。