1.X86汇编初步认识

这里来看两个例子

加减乘除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
ASSUME CS:CODES,DS:DATAS ;关联一下啦

DATAS SEGMENT ;定义⼀个DATAS段
X DW 3 ;给字变量X赋值,X占16位
Y DW 2 ;给字变量Y赋值,Y占16位
STR1 DB 'X = $' ;⽤于输出的表达式字符串,下同理
STR2 DB 'Y = $'
STR3 DB 'X + Y = $'
STR4 DB 'X - Y = $'
STR5 DB 'X * Y = $'
STR6 DB 'X / Y = $'
STR7 DB '...$' ;余数的表达形式,如:5/2=2...1
DATAS ENDS ;DATAS段结束

CODES SEGMENT ;定义⼀个CODES段
START: ;程序开始标号处
MOV AX,DATAS ;先将段DATAS中⽴即数存到通⽤寄存器AX中作为中转
MOV DS,AX ;将⽴即数送到段寄存器DS中

;输出"X = "
LEA DX,STR1 ;调⽤字符串STR1开始有效地址(偏移地址),存放在寄存器DX中
MOV AH,09H ;调⽤DOS系统9号功能:显示字符串
INT 21H ;调⽤DOS功能中断
;输出X的值
MOV DX,X ;将X的值存放在DX寄存器中
ADD DL,'0' ;把数字变成字符输出,因为汇编中只能输出字符;0的ASCII值是30H,数字加上'0'后变为字符
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断
;输出回⻋换⾏
MOV DL,10 ;输出回⻋换⾏,回⻋键ACSII值为10
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断

;输出"Y = "
LEA DX,STR2 ;调⽤字符串STR2开始有效地址(偏移地址),存放在寄存器DX中
MOV AH,09H ;调⽤DOS系统9号功能:显示字符串
INT 21H ;调⽤DOS功能中断
;输出Y的值
MOV DX,Y ;将Y的值存放在DX寄存器中
ADD DL,'0' ;把数字变成字符输出,因为汇编中只能输出字符;0的ASCII值是30H,数字加上'0'后变为字符
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断
;输出回⻋换⾏
MOV DL,10 ;输出回⻋换⾏,回⻋键ACSII值为10
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断

;输出"X + Y = "
LEA DX,STR3 ;调⽤字符串STR3开始有效地址(偏移地址),存放在寄存器DX中
MOV AH,09H ;调⽤DOS系统9号功能:显示字符串
INT 21H ;调⽤DOS功能中断
;输出X+Y的值
MOV DX,X ;将X的值存放在DX中
ADD DX,Y ;将X和Y相加,结果存放在DX中
ADD DL,'0' ;把数字变成字符输出,因为汇编中只能输出字符;0的ASCII值是30H,数字加上'0'后变为字符
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断
;输出回⻋换⾏
MOV DL,10 ;输出回⻋换⾏,回⻋键ACSII值为10
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断

;输出"X - Y = "
LEA DX,STR4 ;调⽤字符串STR4开始有效地址(偏移地址),存放在寄存器DX中
MOV AH,09H ;调⽤DOS系统9号功能:显示字符串
INT 21H ;调⽤DOS功能中断
;输出X-Y的值
MOV DX,X ;将X的值存放在DX中
SUB DX,Y ;将X和Y相减,结果存放在DX中
ADD DL,'0' ;把数字变成字符输出,因为汇编中只能输出字符;0的ASCII值是30H,数字加上'0'后变为字符
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断
;输出回⻋换⾏
MOV DL,10 ;输出回⻋换⾏,回⻋键ACSII值为10
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断

;输出"X * Y = "
LEA DX,STR5 ;调⽤字符串STR5开始有效地址(偏移地址),存放在寄存器DX中
MOV AH,09H ;调⽤DOS系统9号功能:显示字符串
INT 21H ;调⽤DOS功能中断
;输出X*Y的值
MOV AX,X ;MUL乘法指令中⼀个乘数在AL寄存器中
MUL Y ;Y为另⼀个乘数,X*Y的结果存放在了AX寄存器中
MOV DX,AX ;将AX中的乘积内容送到DX中⽤于输出
ADD DL,'0' ;把数字变成字符输出,因为汇编中只能输出字符;0的ASCII值是30H,数字加上'0'后变为字符
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断
;输出回⻋换⾏
MOV DL,10 ;输出回⻋换⾏,回⻋键ACSII值为10
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断
;输出"X / Y = "
LEA DX,STR6 ;调⽤字符串STR6开始有效地址(偏移地址),存放在寄存器DX中
MOV AH,09H ;调⽤DOS系统9号功能:显示字符串
INT 21H ;调⽤DOS功能中断
;输出X/Y的商值
XOR DX,DX ;做16位除法前需要将DX清零
MOV AX,X ;DIV除法指令中16位被除数在AX寄存器中
DIV Y ;Y为除数,X/Y的结果16位商存放在AX中,余数存放在DX中,(如果是8位,商存放在AL中,余数在AH中)
MOV DX,AX ;将AX中的商值内容送到DX中⽤于输出
ADD DL,'0' ;把数字变成字符输出,因为汇编中只能输出字符;0的ASCII值是30H,数字加上'0'后变为字符
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断
;输出"..."
LEA DX,STR7 ;调⽤字符串STR7开始有效地址(偏移地址),存放在寄存器DX中
MOV AH,09H ;调⽤DOS系统9号功能:显示字符串
INT 21H ;调⽤DOS功能中断
;输出X/Y的余数值
XOR DX,DX ;由于DL中值已被覆盖,重新进⾏⼀次除法运算
MOV AX,X ;DIV除法指令中16位被除数在AX寄存器中
DIV Y ;Y为除数,X/Y的结果16位商存放在AX中,余数存放在DX中,(如果是8位,商存放在AL中,余数在AH中)
ADD DL,'0' ;把数字变成字符输出,因为汇编中只能输出字符;0的ASCII值是30H,数字加上'0'后变为字符
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断

MOV AH,4CH ;调⽤DOS系统4C号功能:结束程序
INT 21H ;调⽤DOS功能中断
CODES ENDS ;CODES段结束
END START ;汇编程序运⾏结束

其实这里面的代码大部分都是认识的

这里来简单的分析几个

1
2
;输出"X = "
LEA DX,STR1 ;调⽤字符串STR1开始有效地址(偏移地址),存放在寄存器DX中
  • lea 肯定是一个指令咯–他得到的是数据的偏移地址,和offset不一样的是:offset是伪指令,为什么会使用这个DX呢?这个神仙来了也不知道,秘密都藏在下面的中断中,肯定这个中断中的参数是指定了DX【我也没查,忙猜的,猜测下面的第九号中断遇到$就停】

image-20230506160341028

1
2
MOV AH,09H ;调⽤DOS系统9号功能:显示字符串
INT 21H ;调⽤DOS功能中断
  • 这里就是调用一个中断的过程,STR1 DB ‘X = $’ ;⽤于输出的表达式字符串,下同理

  • emmm 这个不太懂,这个 $ 符号感觉就和C语言中的占位符差不多,姑且这么认为吧。

1
MOV DL,10 ;输出回⻋换⾏,回⻋键ACSII值为10
  • 都说啦,回车的ACSII是10,配合着下面的中断就能回车啦
1
2
3
4
5
;输出Y的值
MOV DX,Y ;将Y的值存放在DX寄存器中
ADD DL,'0' ;把数字变成字符输出,因为汇编中只能输出字符;0的ASCII值是30H,数字加上'0'后变为字符
MOV AH,02H ;调⽤DOS系统的02号功能:显示⼀个字符
INT 21H ;调⽤DOS功能中断
  • 这里中间add dl ,’0’看似加的是0,其实暗藏玄机,其实加的是ASCII码,也就是30h,看下面这三张图片你会更清楚

image-20230506160535772

image-20230506160633074

1
DIV Y ;Y为除数,X/Y的结果16位商存放在AX中,余数存放在DX中,(如果是8位,商存放在AL中,余数在AH中)
  • 这里是div 指令,详细介绍可以去看8086汇编。里面肯定讲过

ok 言归正传。

image-20230506160718324

发现这里生成的exe文件其实不是PE文件。用IDA pro 打开就可以看到它的反汇编的代码。这里就不累赘了

一个windows程序的汇编语言逆向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <Windows.h>
#include <tchar.h>
int APIENTRY _tWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
if(lstrcmp(lpCmdLine, _T("2012")) == 0){
MessageBox(GetActiveWindow(),
_T("Hello! 2012"), _T("MESSAGE"), MB_OK);
}else{
MessageBox(GetActiveWindow(),
_T("Hello! Windows"), _T("MESSAGE"), MB_OK);
}
return 0;
}

运行这个程序

image-20230506160844833

看到的是弹出的 hello windows。

但是我们看这个源代码哈,是可以弹出Hello! 2012的emmm 我这里看不懂代码。

image-20230506160838139

看到的是 在cmd 中 运行这个程序,并且有一个2012 就会弹出这个窗口

真实中,我们是看不到源程序代码的,所以这里我们去用IDA反汇编一下

进入用IDA打开后

1
按下空格健

image-20230506160831172

看到这里其实是有2个分支的。现在就去看看这个分支语句如何才能满足条件

1
再次按下空格

就能看到相对应的汇编代码啦

image-20230506160823715

其实这里简单的看一下,所以在传入2012的时候就能进入宁外一个分支。

当然也可以F5来看看伪代码

image-20230506160814479

okokokokok ,上面我们简单的介绍了2个汇编代码和反汇编之后的样子。现在我们来学正儿八经的

2.常用的汇编指令

首先这些指令都可以在Masm的集成器中去学习,下面我们简单介绍一下逆向常用的指令

MOV

移动

要注意的就是,前后俩个东西的宽度要一样

ADD

加法

也知道,宽度一样

SUB

减法

AND

按位与—->&

【只要有一个0,就是0】

OR

按位或—->||

【只要有一个1就是1】

xor

异或—–> ^

【一样就是0,不一样就是1】

NOT

非—–>~

【相当于取反吧 0变成1,1变成0】

MOVS

数据传送,只能内存传内存。

movs edi指定的内存地址,esi指定的内存地址。在传送的时候默认用的是esi和edi寄存器,并且用后esi和edi的值会自增和自减去。

1
2
3
MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI] //简写为:MOVSB
MOVS WORD PTR ES:[EDI], WORD PTR DS:[ESI] //简写为:MOVSW
MOVS DWORD PTR ES:[EDI], DWORD PTR DS:[ESI] //简写为:MOVSD

在使用MOVS的时候,首先要把esi和edi的值改为相对应的内存地址。

edi中放的是源数据地址,edi中放的是将要传送到哪里的地址。

edi和esi中的值,是根据数据宽度来自动增加的。B->1,W->2,D->4

注意:当flag寄存器中,DF=0的时候,esi和edi自增,DF=1的时候,esi和edi自减

STOS

表示将:AL/AX/EAX的值存储到edi指定的内存地址中。

同样的,edi寄存器的值会自动增加或者减少。还是取决于DF位

1
2
3
STOS BYTE PTR ES:[EDI] //简写为:STOSB
STOS WORD PTR ES:[EDI] //简写为:STOSW
STOS DWORD PTR ES:[EDI] //简写为:STOSD

REP

表示循环

rep movs 或者 rep stos

表示循环执行movs指令或者stos指令。循环的次数取决于ecx中的值。每次执行,ecx寄存器的值都会自动减1。知道0,就不在循环

1
2
3
4
5
6
REP MOVSB
REP MOVSW
REP MOVSD
REP STOSB
REP STOSW
REP STOSD

堆栈相关的指令

首先,我们要知道的是:站指针是从大到小的。回想一下8086中,栈指针永远都是指向栈顶的,也就是下面的【内存大的位置】。和盒子一样,盒子的底部就能看作是栈顶指针,永远都指向底部,当我们向盒子里面放入书后,就相当于底部往上抬了一本书的位置,而抬起来位置的大小也取决于书的大小,也就是数据宽度。

PUSH

压入栈。

PUSH 通用寄存器/内存地址/立即数

把里面的值放入栈指针所指向的内存地址

用了过后,esp会减小,比如:esp-4,也就是往上抬了

POP

释放数据

POP /通用寄存器/内存地址

把栈里面的值按照相同的数据宽度,取出来放入对应的通用寄存器或者内存地址中

用了过后,esp会增加,比如:esp+4,取出书后,底部又往下了。


修改eip的指令

eip,指向的是CPU要指向的指令的地址。

JMP

跳转指令,强行跳转

JMP 寄存器/内存/立即数

跳转到某个地方开始执行代码。

CALL

调用某个地址

CALL 寄存器/内存/立即数

就和调用函数一样,它首先会吧下一条指令的地址压入栈。等会调用玩后再调回来继续执行指令,也就是PUSH EIP

RET

返回

配合着CALL使用的,当函数弄完后,POP EIP ,吧EIP的值返回到和之前的一样,继续去执行之前的代码

3.堆栈的详细理解

什么是栈?

说白了,就是一段能被特殊使用的内存空间。【先进后出】

入栈和出栈,不是说把值给清了,本质上是一个copy的过程,只是指针的位置变了,不能访问到罢了,意思就是说pop后,原本内存中的值还是在哪里,而没有被清0。差不多能懂就ok

栈帧是什么?

在我看来就是一种栈的格式。本质上也是栈,这种栈有专门的用途,用来保存函数调用过程中的各种信息【参数,返回地址,本地变量…】栈帧有栈顶和栈底之分,SP指向栈顶。在32位中,%ebp指向栈底,也就是基地址;esp指向栈顶,栈指针咯

image-20230506141014961

我们把这个%ebp到%esp之间的区域当作栈帧。

每调用一个函数,就会产生一个新的栈帧。在函数调用里面,把调用函数的函数称为“调用者”被调用的函数“被调用者”

在这个过程中,调用者需要知道在哪里获取被调用着的返回的值,被调用者返回后%ebp,%esp寄存器的值应该和调用前一致。所以就需要用栈来保存这些数据

【这一段迷迷糊糊的无所谓,实在不行就在学学计算机原理咯,我先学学后面的哈哈哈】

C函数调用过程原理和函数栈帧分析

1
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

啊这……..就不分析啦


1
2
3
4
5
6
7
8
9
10
11
12
13
14
int MyFunction(int x, int y, int z)
{
int a, b, c;
a = 10;
b = 5;
c = 2;
...
}
int TestFunction()
{
int x = 1, y = 2, z = 3;
MyFunction1(1, 2, 3);
...
}

ok,当MyFunction函数被调用的时候,汇编代码大致如下,这个汇编和我学的好像又不太一样….在使用mov的时候

1
2
3
4
5
6
7
_MyFunction:
push %ebp ; //保存%ebp的值
movl %esp, $ebp ; //将%esp的值赋给%ebp,使新的%ebp指向栈顶;这里传参的方向好像不太一样....
movl -12(%esp), %esp ; //分配额外空间给本地变量
movl $10, -4(%ebp) ;
movl $5, -8(%ebp) ;
movl $2, -12(%ebp) ;

image-20230506142839487

ok 这个看得我云里雾里的,我们来理解一下哈

1.首先被调用数的参数按照从从右往左的顺序压入栈中。【这个在win32中看到过,应该懂吧,至于为什么从右往左,因为这个是规定,也又从左往右的~】

2.把原本执行代码的地址压入栈中,也就是要返回的地址压入栈

ok,上面这2步都是调用者负责的,所以这也就是调用者的栈帧

3.把原本的%ebp压入栈中,因为这段空间是临时开辟的,用完这个函数还得回去嘞,所以就需要把之前的指针先保存起来

4.把%esp的值给%ebp,下面就让ebp来充当指针咯,这里讲的轻松,其实一点也不轻松。

这里可以去看看我写的win32汇编中,的某级中,肯定写过堆栈平衡。或者百度一下啦。


这里我也简单讲讲我理解的堆栈平衡

esp:扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针(栈顶指针)
ebp:扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针(栈底指针)。

ebp只是存取某时刻的esp,这个时刻就是进入一个函数内后,cpu会将esp的值赋给ebp,此时就可以通过ebp对栈进行操作,比如获取函数参数,局部变量等,实际上使用esp也可以,只是esp可能会变化,去数据的时候很不方便

首先,这个栈帧,emmm我不是很理解,也许也就是这样定义的吧,定义出这样一个格式,以至于后面可能好将一点或者方便理解

子程序是如何使用传递的参数?这个我们得知道,这样就方便我们理解一下

首先,调用者,把要传递的参数都先压入栈中,因为这个栈也就是内存的一部分三,被调用者就可以去访问这个内存空间,然后从中获取到参数。

所以说,调用者首先把这个参数压入栈中,IP才跳转到这个函数的位置,在这个函数执行完过后,又需要返回到原来代码执行的位置,所以就需要把返回地址也压入栈中。这就是调用者的作用

临时使用的这个堆栈中的数不在有用,所以就需要把栈指针修正到调用前的状态。这个修正是谁修正呢?,调用者和被调用者都可以,和压入参数的方式一样都是有约定的,不同的语言有不同的方式。

所以这个,比如说要调用某个函数

编译后可能是

1
2
3
4
push 参数
push 参数
...
call 某函数

某函数中的反汇编之后的

1
2
3
push ebp 
mov ebp,esp
...

emmmmm,说了半天好像也是迷迷糊糊的,不要紧,我们来看一个例子

原文链接:https://blog.csdn.net/qq_41683305/article/details/104249224

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "stdio.h"
int function_add(int a,int b);
int main()
{
int a=1,b=1,sum=0;
sum=function_add(a,b);
printf("sum=%d",sum);
return 0;
}
int function_add(int a,int b)
{
return a+b;
}

这个代码的关键点

1
2
int a=1,b=1,sum=0;
sum=function_add(a,b);

把这两句反汇编一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int a=1,b=1,sum=0;
00401038 C7 45 FC 01 00 00 00 mov dword ptr [ebp-4],1 ;偏移地址为[ebp-4]存放1
0040103F C7 45 F8 01 00 00 00 mov dword ptr [ebp-8],1 ;偏移地址为[ebp-8]存放1
00401046 C7 45 F4 00 00 00 00 mov dword ptr [ebp-0Ch],0 ;偏移地址为[ebp-0ch]存放0
--------------------------------------------------------------------------------------------
sum=function_add(a,b);
0040104D 8B 45 F8 mov eax,dword ptr [ebp-8] ;eax的值设置为1
00401050 50 push eax ;压入栈
00401051 8B 4D FC mov ecx,dword ptr [ebp-4] ;ecx的值设置为1
00401054 51 push ecx ;入栈
00401055 E8 AB FF FF FF call @ILT+0(function_add) (00401005)
0040105A 83 C4 08 add esp,8
;由于上面push了2次,dw型的,所以这里需要add esp,8
;作用也就是把指针还原回去呗
0040105D 89 45 F4 mov dword ptr [ebp-0Ch],eax

注意看,这个干啥干啥的时候都是用的这个ebp..这些a b sum 啊啥的,其实都是标号罢了。本质上还是某个内存空间

然后就是去执行这个call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
12:   int function_add(int a,int b)
13: {
004010A0 55 push ebp
004010A1 8B EC mov ebp,esp
004010A3 83 EC 40 sub esp,40h
004010A6 53 push ebx
004010A7 56 push esi
004010A8 57 push edi
004010A9 8D 7D C0 lea edi,[ebp-40h]
004010AC B9 10 00 00 00 mov ecx,10h
004010B1 B8 CC CC CC CC mov eax,0CCCCCCCCh
004010B6 F3 AB rep stos dword ptr [edi]
14: return a+b;
004010B8 8B 45 08 mov eax,dword ptr [ebp+8]
004010BB 03 45 0C add eax,dword ptr [ebp+0Ch]
15:
16: }
004010BE 5F pop edi
004010BF 5E pop esi
004010C0 5B pop ebx
004010C1 8B E5 mov esp,ebp
004010C3 5D pop ebp
004010C4 C3 ret
1
004010A3 83 EC 40             sub         esp,40h

这句话,其实也就是开辟了一段栈空间,这段空间也是人为规定的。本质上还是内存空间,起了一个名字而已

这下能理解了吧,其实这个原文将得真聪明。

okokok

我懂了

ebp=>暂时用一下

esp才是真二八经的栈指针。以至于为什么要使用ebp呢?是因为在子程序中可能还会有push或者pop命令,这样esp的值就会自动改变,就不能指向这个你所需要的参数的位置。这下懂了吧,哈哈哈。

【这个指针的概念也是人为规定上去的,不要被搞迷糊了】

在函数里,都是通过ebp对栈的数据进行操作的,比如获取参数的值,

因为在函数里,esp的值可能是变化的,ebp的值不变,通过ebp来操作数据很方便

最后

1
2
004010C1 8B E5                mov         esp,ebp
004010C3 5D pop ebp

将esp,ebp的值变成调用function_add之前的值,这样看起来只是实现函数的功能,其他并没有啥变化,再使用ret语句,返回去,然后继续往下执行语句

1
0040105A 83 C4 08             add         esp,8

总结:

  1. esp始终指向栈顶,ebp只要在调用函数时,取值为栈顶,这样可方便对数据的操作
  2. 函数调用时,EBP的值入栈,然后ESP的值传给EBP。函数调用结束后,EBP将值传回ESP,ESP又指向了原来的栈顶地址。这样看起来只是实现函数的功能,其他看起来没有变化

ok 如果还不懂,再去别的地方看看吧。