对于《逆向工程实战》的1.3/1.4/1.7/1.9节练习的尝试(x86/x64)。
一、基础知识补充
1.C函数调用过程原理及函数栈帧分析——
A.栈
栈是一种LIFO形式的数据结构,所有的数据都是后进先出。
这种形式的数据结构正好满足我们调用函数的方式: 父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。
栈支持两种基本操作,push和pop。
push将数据压入栈中。
pop将栈中的数据弹出并存储到指定寄存器或者内存中。
pop操作后,栈中的数据并没有被清空,只是该数据我们无法直接访问。
栈的生长方向是从高地址到低地址的,也就是说,栈基址ebp的值是大于或等于(栈初始化)栈顶地址esp的。
(1)push操作示例:push $0x50
也就是说,push操作首先会将esp的值减4,然后再将0x50存入此时的栈顶(esp指向的位置)。
(2)pop操作示例:pop eax
pop操作会将此时esp指向的位置处的值先出栈,然后再将esp的值加4。
B.栈帧
栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。
栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。
在x86-32bit中,我们用 %ebp 指向栈底,也就是基址指针;用 %esp 指向栈顶,也就是栈指针。
下面是一个栈帧的示意图:
一般来说,我们将 %ebp 到 %esp 之间区域当做栈帧(也有人认为该从函数参数开始,不过这不影响分析)。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。
C.实例
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);
...
}
(1)函数调用
汇编代码如下——
_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) ;
栈分布如下图——
(2)函数返回
_MyFunction:
push %ebp
movl %esp, %ebp //这两条指令建立了一个新的函数栈帧,通常被称为函数序言
movl -12(%esp), %esp
...
mov %ebp, %esp
pop %ebp
ret
2.关于栈这种数据结构的编程题目
(1)实现栈的push、pop序列;
https://www.cnblogs.com/ttltry-air/archive/2012/08/14/2638855.html
(2)栈&队列面试题之实现一个栈;
1.3
01: mov edi, [ebp+8] //取局部变量到edi中,该变量存在[ebp+8]处
02: mov edx, edi
03: xor eax, eax //eax=0
04: or ecx, 0FFFFFFFFh //ecx=ffffffffh(即-1)
05: repne scasb //循环查询edi中是否有与eax中的值相同的内容,这里eax=0,所以可以推断是查找字符串的结尾数据,故存在[ebp+8]处的应该是一个字符串。
06: add ecx, 2
07: neg ecx //这两步计算之后,ecx中存放的数值就是[ebp+8]处字符串的长度(不包括结尾处的'0'),可以假设ecx的数值验证。
08: mov al, [ebp+0ch] //取局部变量到al中,该变量存在[ebp+0c]处
09: mov edi, edx //edx中存放的是字符串[ebp+8]的地址
10: rep stosb //将eax中的值初始化到es:[edi] 指向的地址,一次初始化一个byte,循环执行的次数为ecx中的值(即edi中字符串的长度),故可推出[ebp+0c]处的变量是和[ebp+8]处字符串长度相同的字符串数据。
11: mov eax, edx //将初始化赋值后的字符串变量[ebp+8]赋值给eax作为返回值
第6行和第7行也可以用以下两行代码代替:
not ecx //得到搜索次数,也就是字符串的完整长度
dec ecx //-1得到字符串不包含末尾0的长度
综上,此段汇编代码完成了字符串变量初始化赋值的操作。
1.4
1.(1)根据所学的CALL和RET指令,描述你将如何读出EIP的值?
不是很理解题目的要求,因为call和ret指令都具有设置EIP的值的能力,而在实际程序执行过程中,eip的值是在不断变化的,也就是每执行一条指令,EIP的值都会发生变化,所以感觉实时读取EIP的值是无法通过CALL和RET实现的。
可以考虑在ret
指令执行前执行pop eax
,此时eax中的内容就是即将存入EIP寄存器的内容。
(2)为什么不能用MOV EAX, EIP来实现?
在用mov指令进行寄存器与寄存器之间的数据传送时;
代码段寄存器CS及指令指针寄存器EIP不参加数的传送;
其中CS可以作为源操作数参加传送,但不能作为目的操作数参加传送;
eip是特殊寄存器不能直接对其进行赋值操作。
2.把EIP设为0xAABBCCDD,给出至少两种代码序列:
(1)
push AABBCCDDh
ret
(2)
call AABB CCDDh
(3)
jmp AABB CCDDh
3.在前面的例子函数addme中,如果在执行RET之前没有正确恢复栈指针,会出现什么情况?
程序会转到此时ESP所指地址空间继续执行。
4.前面介绍过的所有调用惯例中,返回值都是储存在32位寄存器(EAX)中。如果返回值用一个32位寄存器放不下怎么办?写一个程序来验证你的答案。不同编译器采用的机制也有所不同吗?
对于小于4个字节的数据函数将返回值存储在eax中;
5~8个字节对象的情况调用惯例都是采用eax和edx的联合返回方式进行,eax存放低32位,edx存放高32位。
https://blog.csdn.net/qq405180763/article/details/39252693
第一题
- 在尝试第一题的过程中,在分析过程中画出包括参数和局部变量在内的栈布局时遇到了问题,发现自己的基础还不够扎实,于是在基础部分中对于栈上的数据操作以及函数调用时的栈操作进行了学习。
- 在分析汇编代码的栈数据操作时的经验积累:
如上图所示,在执行完call指令,即调用完函数返回之后,此时无论执行call指令之前esp指向栈空间的哪个地方,返回之后esp都是位于ebp-130h的位置(正如红框框出来的DllMain函数最开始的布局一样)。
注意push入栈操作时esp值要减4,pop出栈时要加4。
第三题
参考链接:
不同的函数调用约定对于函数名的描述方式也不同(C编译器的函数名修饰规则如下):
对于
__stdcall
调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数,例如_functionname@number
。__cdecl
调用约定仅在输出函数名前加上一个下划线前缀,例如_functionname
。__fastcall
调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,例如@functionname@number
。
第四题
strlen——计算字符串的长度
mov edi, [ebp+8] //假设[ebp+8]中存放的是待计算长度的字符串变量
mov edx, edi
xor eax, eax //eax=0
or ecx, 0FFFFFFFFh //ecx=ffffffffh(即-1)
repne scasb //循环查询edi中是否有与eax中的值相同的内容,这里eax=0,所以可以推断是查找字符串的结尾数据。
not ecx
dec ecx //此时ecx中存放的就是字符串的长度
mov eax, ecx
strchr——查找某字符在字符串中首次出现的位置
mov edi, [ebp+8] //假设[ebp+8]中存放的是待处理的字符串变量
mov edx, edi
mov eax, 61h //待查找的字符’a’
or ecx, 0FFFFFFFFh //ecx=ffffffffh(即-1)
repne scasb //循环查询edi中是否有与eax中的值相同的内容。
not ecx //此时ecx中存放的就是‘a’在字符串中的位置
mov eax, ecx
memcpy——复制 src 所指的内存内容的前 num 个字节到 dest 所指的内存地址上
mov ecx, num //num
mov edx, [ebp+8] //dest
mov al, [ebp+0ch] //src
mov edi, edx //src
rep stosb //将eax中的值初始化到es:[edi] 指向的地址,一次初始化一个byte,循环执行的次数为ecx中的值。
mov eax, edx //将初始化赋值后的字符串变量[ebp+8]赋值给eax作为返回值
- memset——将指定内存的前n个字节设置为特定的值
——同上,实现功能的关键语句:rep stosb
strcmp——比较字符串(区分大小写)
mov ebx, _String1 //第一个字符串的基址
mov edx,_String2 //第二个字符串的基址
xor esi, esi //初始化索引值loc_123:
mov eax, _String1[esi4] //取字符并比较
sub eax,_String2[esi4]
inc esi
jz loc_123 //如果相等则继续比较下一个字符
… //如果不相等此时eax中存的就是返回值(两个字符的差值)
- strset——将字符串中的所有字符都设置为一个指定字符(strset() 不会生成新字符串,而是修改原有字符串,因此它只能操作字符数组,不能操作字符串指针指向的字符串,因为字符串指针指向字符串常量,常量不能被修改)
mov edi, _String
mov edx, edi
xor eax, eax
or ecx, 0FFFFFFFFh
repne scasb
not ecx
dec ecx //先判断字符串数组的长度,此时ecx中存放的就是字符串的长度
mov edx, _String //字符串数组的基址
mov al, 'a'
mov edi, edx //src
rep stosb //将eax中的值初始化到es:[edi] 指向的地址,一次初始化一个byte,循环执行的次数为ecx中的值。
mov eax, edx //将初始化赋值后的字符串赋值给eax作为返回值
第五题(待续)
1.9
1.给出两种x64上获得指令指针的方法;
- 使用x64上特有的RIP寻址,即
mov rax,qword ptr cs:loc_xxx
或mov rax, [rip]
; - 在ret指令执行前pop栈顶数据;
参考链接
1.基础部分: