本人从开始学习逆向已经有一段时间了,感觉逆向是一门很深的学问,需要长时间的积累和足够的耐心和细心。
下面是我个人的一些总结,还请大家指点。学习逆向是需要一定的汇编基础,学习汇编就想是学习一门外语,它的指令就像是单词一样,只有理解了这些单词的意思才可以理解汇编的代码的含义,由于汇编器/反汇编器的不同,现在x86汇编代码主要分为Intel和AT&T汇编,这两者在语法方面存在一定的差异,建议先从Intel汇编开始,它主要是在PC端,而且参考的资料较多。(本文部分内容来自网络)
一、基础的单位和字节序的介绍
bit 位(指的是 0 or 1)
byte 字节 1byte = 8 bit
word 字 1 word = 2 byte
dword (即double word)
1KB=1024B
1MB=1024KB
1GB=1024MB
1TB=1024GB
字节序:
大端序(Big endain):内存地址低位储存数据的高位,
小端序(Little endain):内存地址高位储存数据的高位(x86是基于Intel8086处理器的小端体系结构)
MZ 对应的是5A 4D,这里是十六进制,每两位代表一个字节。
二、寄存器
2、寄存器(Register):CPU 内部用来存放数据的一些小型储存区域,暂存指令、数据和地址。
一开始寄存器是8位的(也就是1个字节),到了8086的时期变成16位(像ax,bx),再到80386的时期发展为32位(eax,ebx等),再到现在主流的64位cpu采用的是两个32位的寄存器一起使用。它的规律可见下图
由这张图,我们可以发现寄存器是兼容的从 * al–>ax–>eax*
寄存器有分以下几类:
2.1、通用寄存器:(寄存器前面的E,代表extended,扩展)
在windows保护模式下的x86体系结构有8个通用寄存器
EBP,ESI,EDI,ESP:主要用作保存内存地址的指针
EAX,EBX,ECX,EDX(0-FFFFFFFF):主要用在算数运算指令中,常用来保存常量与变量的值。
部分通用寄存器的用途:
ECX:循环计数器
ESI:字符串/内存操作的源`
EDI:字符串/内存操作的目标
EBP:栈帧基准
ESP:(栈顶指针)指示栈区域的栈顶地址,指向当前进程的栈空间地址`。
EIP(instuction pointer):扩展的指令指针寄存器,总是指向下一条要被执行的指寄存针器。
2.1、标志位寄存器(Flag Register)
(IA-32)事实上所有的标志位归并与一个32位的标志位寄存器,也就是说有32个不同的标志位。
每个标志位都有两个属性:置1或置0
在逆向分析的过程中,你真正需要关心的标志位只有三个,也就是cmp指令能修改的那三个:Z/O/C。
因为跳转指令是否成立是于这三个标志寄存器的值有关的
Z标志位(zero flag),这个标志位是最常用的,运算结果为0时候,Z标志位置1,否则置0。
O标志位(溢出标志 overflow flag),在运行过程中,有符号整数溢出时,OF置为1。
C标志位(进位标志 carry flag),无符号整数溢出时,CF置为1,记录运算时从最高有效位产生的进位值。例如执行加法指令时,最高有效位有进位时置1,否则置0。
通常在选择、循环等语句中需要用到cmp和text指令去改变标志寄存器的值,以此来判断是否跳转
2.3、段寄存器(Segment)
CPU访问内存单元时要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间。我们将这个唯一的地址称为物理地址。
8086CPU(外部)有20位地址总线,可传送20位地址,寻址能力为1M。8086内部为16位结构,它只能传送16位的地址,表现出的寻址能力却只有64KB。
8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址
地址加法器合成物理地址的方法是:
**物理地址=段地址×16+偏移地址**
段地址和偏移地址都是16位的,'段地址 x 16'即向左移了4位(2^4).
一个数据的X进制形式左移1位,相当于乘以X。
由6种寄存器组成,分别为
CS(code segment)
SS(stack segment)
DS(data segment)
ES(extra segment)
FS(data segment)
GS( data segment)`
ES,FS,GS存放程序使用的附加数据段的段基址
三、基础的指令(汇编指令不区分大小写)
几个概念
立即数:以常量的形式出现在指令中,只能作为源操作数。
寄存器数:将数据存放在寄存器中,指令直接使用寄存器名。
内存操作数:将数据放在内存中,指令中使用内存地址。、如:[BX]
3.1算数运算指令
ADD DEST,SRC //右加到左
SUB DEST,SRC //DEST = DEST - SRC
INC DEST //加一
DEC DEST //减一
NOG //NULL
imul src //带符号乘
idiv src //带符号除
mul src 乘
div src 除
dec dest 自减
inc dest 自加
int 中断指令
指令有很多,不用死记,用到时查就好了
3.2.逻辑运算和关系运算指令
AND dest,src //按位与运算,后值赋给dest
OR dest,src //按位或运算
XOR dest,src //按位异或
NOT dest //按位取反
text dest, src //这个指令和and指令差不多,对两个操作数进行按位的‘与’运算,但在逻辑与操作后,对两个操作数的内容均不进行修改,仅对标志位重新置位。
CMP dest,src // 和SUB指令相似,只是不将dest-src的值放入dest
3.3 基础的指令
CALL XXXX //调用XXXX地址处的函数
JMP XXXX // 跳转到XXXX地址处
PUSH XXXX //保存XXXX到栈(入栈)
POP XXXX //弹出XXXX地址(出栈)
RETN XXXX //跳转到栈中保持的地址础的结构.
条件跳转指令有很多,通常情况下,都与CMP和TEXT匹配出现,但条件跳转指令是看标志位的值的,具体的条件跳转指令表可以自行百度
操作符 offert :取得标号的偏移地址
mov ax,offert start ;相当于 mov ax,0
x = [abc] 是指取abc 所在的地址的值,赋给x;
四、基础的结构
4.1、栈(Stack)
(1)暂时保存函数内的局部变量(2)调用函数时传递参数(3)保存函数返回后的地址
FILO,由下向上扩展
向栈压入数据,栈顶指针减小,向低地址移动
4.2、栈帧(Stack Frame)技术:(每个函数的每次调用,都有它自己独立的一个栈帧)
用EBP表示栈区域的基地址,函数被调用是保存ESP的值,函数返回时再把值返回ESP,保证栈不会崩溃
栈帧的基本结构如下
PUSH EBP //函数开始 (使用EBP前先把已有值保存到栈中)
MOV EBP,ESP //保存当前ESP到EBP中
... //这是函数体部分
… //ESP的变化与EBP无关,可以安全访问函数的局部变量、参数
MOV ESP, EBP //将函数的起始地址返回到ESP中
POP EBP //函数返回前弹出保存在栈中的EBP值
RETN //函数终止
一段简单的代码/汇编代码
1: #include
2: long add(long a,long b)
3: {
00401020 push ebp //栈帧开始
00401021 mov ebp,esp
00401023 sub esp,48h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-48h] //取出此函数可用栈空间首地址放入edi
0040102C mov ecx,12h
00401031 mov eax,0CCCCCCCCh //局部变量初始化
00401036 rep stos dword ptr [edi] //根据ecx的值,将eax中的内容,以dw为单位写到edi指向的内存中
4:long x = a,y = b;
00401038 mov eax,dword ptr [ebp+8]
0040103B mov dword ptr [ebp-4],eax
0040103E mov ecx,dword ptr [ebp+0Ch]
00401041 mov dword ptr [ebp-8],ecx
5:return (x+y);
00401044 mov eax,dword ptr [ebp-4]
00401047 add eax,dword ptr [ebp-8] //x+y
6:}
0040104A pop edi
0040104B pop esi
0040104C pop ebx
0040104D mov esp,ebp
0040104F pop ebp
00401050 ret
/*
...
...
*/
7:int main()
8:{
00401060 push ebp //栈帧
00401061 mov ebp,esp
00401063 sub esp,48h
00401066 push ebx
00401067 push esi
00401068 push edi
00401069 lea edi,[ebp-48h]
0040106C mov ecx,12h
00401071 mov eax,0CCCCCCCCh
00401076 rep stos dword ptr [edi]
9:long a = 1,b = 2;
00401078 mov dword ptr [ebp-4],1
0040107F mov dword ptr [ebp-8],2
10: printf("%d\n",add(a,b));
00401086 mov eax,dword ptr [ebp-8]
00401089 push eax //eax = 2 = [ebp-8]
0040108A mov ecx,dword ptr [ebp-4]
0040108D push ecx //ecx = 1 = [ebp-4]
0040108E call @ILT+0(add) (00401005) //调用add()函数
00401093 add esp,8
00401096 push eax
00401097 push offset string "%d\n" (0042201c)
0040109C call printf (004010d0)
004010A1 add esp,8
11: return 0;
004010A4 xor eax,eax //将eax清零
12:
13: }
004010A6 pop edi
004010A7 pop esi
004010A8 pop ebx
004010A9 add esp,48h //降低栈顶esp,释放局部变量空间
004010AC cmp ebp,esp //检测栈平衡,
004010AE call __chkesp (00401150) //进入栈平衡错误检测函数
004010B3 mov esp,ebp
004010B5 pop ebp
004010B6 ret
od中的代码
栈计算机中常见的结构,但是也存在着一些问题,如栈的溢出,常常会被人利用。
断点:设置断点后,调试运行到断点处将会暂停
返回地址:执行CALL命令进入被调用的函数之前,CPU会先把函数的返回地址压入栈
XOR:两个相同的值进行XOR运算结果为0,常用于寄存器的初始化操作。(相同的值连续执行2次XOR运算即变回原值)
以上所述的只是一些在逆向的过程中常见的部分基础内容,还有许多不足,在开始学习逆向的时候是较困难的,由于有大量陌生的东西,但是只要去多接触,是可以掌握的,就拿基础的汇编的一些语句来说,根本不用去死记,熟悉了就好了,就我自己来说就是平时多去写程序,先看汇编代码,然后在用工具去逆向。