《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.5-1.6)

1.5. 函数序言和结尾

函数开头的一些指令序列。通常是这样的:

push        ebp
mov     ebp, esp
sub     esp, X

首先保存EBP中的值,把EBP的值设置为ESP的值,然后在栈上为局部变量分配一些空间。EBP中的值在整个函数执行的时候都会原封不动地保存在栈中。然后EBP会用作访问局部变量。

函数结尾的指令通常释放栈中之前分配的空间,恢复EBP寄存器中的值,并将控制权返回给调用方。

mov     esp, ebp
pop     ebp
ret     0

1.5.1. 递归

序言和结尾会对递归的性能产生不好的影响。具体后面会解释。

1.6. 栈

栈实际上就是用ESP和EBP指示的一块内存。SP指向栈底。PUSH指令会减小SP,POP指令会增加SP。这很好理解,因为栈从高地址向低地址增长。栈底实际上就是栈所在内存的开始(最高地址)。

ARM支持递减栈,也支持递增栈。比如STMFD/LDMFD, STMED/LDMED这些指令是用来处理递减栈的,STMFA/LDMFA, STMEA/LDMEA指令用来处理递增栈。

1.6.1. 为什么栈是向低地址生长的?

因为将内存分成了两部分,一部分是堆,一部分是栈。由于不能确定这两部分多大,所以把堆从低地址开始,栈从高地址开始,相对增长。

1.6.2. 栈用来干什么?

(1)保存函数的返回地址

x86

CALL指令相当于:PUSH address_after_call / JMP operand

RET指令相当于:POP tmp / JMP tmp

一个很简单的溢出栈的方法是:

void f()
{
    f();
};
ARM

返回地址存在LR(link register)寄存器中。

有的时候我们可以看见代码序言中有PUSH R4-R7, LR以及代码结尾中有POP R4-47, PC,就是把这些寄存器(包括LR)全部存在了栈中。

叶函数:不调用其他函数。可能不会用到栈。

(2)传递函数参数

在x86中,最常见的传参方式叫做cdecl。

push arg3
push arg2
push arg1
call f
add esp, 12

压栈、压栈、压栈,调用函数,然后恢复栈指针到push参数前。

其他传参方式

不使用栈的传参方法。一种常见方法是使用全局变量传参。但是如果这样的话,就不能实现递归函数。并且线程不安全。

MS-DOS有一种用寄存器传参的方法。类似于Linux和Windows下的系统调用。

(3)局部变量存储

减小栈指针的值就可以为局部变量分配栈空间。

x86: alloca()函数

这个函数有点像malloc(),但不是分配堆空间,而是分配栈上的空间。分配的空间不需要free()这样的函数来释放,只需要改变ESP的值(增加)就行了。alloca()函数的实现就是减小ESP的值,然后设置ESP指向分配的块就行。

给的一个例子使用了_snprintf()函数,类似printf(),但是会把结果输出到stdout,即写入buf缓冲区。

MSVC

注意调用函数alloca()的参数是用EAX传递的。

GCC + Intel syntax

在不调用外部函数的情况下完成了这个功能。其实就是直接sub esp, 600这样的语句。

GCC + AT&T syntax
movl    $3, 20(%esp)
mov     DWORD PTR [esp+20], 3

两种不同风格的比较,其功能是一样的。

(4)(Windows) SEH

SEH也是存储在栈上的。

(5)缓冲区溢出保护

(6)栈中数据的自动解分配

局部变量和SEH都是在使用后自动释放的。但是在堆中就必须手动释放了。

1.6.3. 典型的栈布局

在函数还未执行的时候,栈中的数据分布是这样的:

ESP指向返回地址。

往ESP上面看,即低地址部分,从上到下分别是局部变量2,局部变量1,保存的EBP。每个内容是4byte。

往ESP下面看,即高地址部分,从上到下分别是参数1,参数2,参数3。

我们知道栈是从高地址向低地址生长,所以这个模型里,栈向上增长。

1.6.4. 栈中的噪音

给出了一个例子,例子中实现了两个函数,一个f1(),定义了a、b、c三个变量,分别赋值为1,2,3。一个f2(),定义了a、b、c三个变量但没有初始化,还调用了printf()输出这三个变量。先调用f1再调用f2。编译时会warning没有初始化的变量,但是输出时却输出了123。这是为什么?

用OllyDbg查看。f1()分配变量后,放在0x1FF860-0x1FF858这12个byte上。而在f2中,a,b,c三个值依然被分配在这12个byte的地址上。

这个例子说明什么?虽然函数返回了,但是函数调用期间的参数和局部变量依然留在栈上。如果不覆盖,那么这个值是可以获取的。其实这样非常不安全,有可能造成信息泄露。有一种方法是调用函数后把栈清空,但是这样造成额外开销非常不划算。

MSVC 2013

前面使用MSVC2010编译的,现在用MSVC2013编译,发现输出的值颠倒了,变成3,2,1。发现在f2中分配的变量顺序与分是相反的。

1.6.5. 练习

(1)http://challenges.re/51

用MSVC2008编译的结果如下:

int main()
{
004113A0 55               push        ebp  
004113A1 8B EC            mov         ebp,esp 
004113A3 81 EC C0 00 00 00 sub         esp,0C0h 
004113A9 53               push        ebx  
004113AA 56               push        esi  
004113AB 57               push        edi  
004113AC 8D BD 40 FF FF FF lea         edi,[ebp+FFFFFF40h] 
004113B2 B9 30 00 00 00   mov         ecx,30h 
004113B7 B8 CC CC CC CC   mov         eax,0CCCCCCCCh 
004113BC F3 AB            rep stos    dword ptr es:[edi] 
printf("%d %d %d\n");
004113BE 8B F4            mov         esi,esp 
004113C0 68 3C 57 41 00   push        41573Ch 
004113C5 FF 15 BC 82 41 00 call        dword ptr ds:[004182BCh] 
004113CB 83 C4 04         add         esp,4 
004113CE 3B F4            cmp         esi,esp 
004113D0 E8 66 FD FF FF   call        0041113B 
return 0;
004113D5 33 C0            xor         eax,eax 
}

结果打印出来的值是175700436、0、2147348480。

开启完全优化选项并使用Release版本:

int main()
{
printf("%d %d %d\n");
00401000 68 F4 20 40 00   push        4020F4h 
00401005 FF 15 A0 20 40 00 call        dword ptr ds:[004020A0h] 
0040100B 83 C4 04         add         esp,4 
return 0;
0040100E 33 C0            xor         eax,eax 
}

还是会打印出来一些值,但是与上一次的不一样了。

(2)http://challenges.re/52

应该是打印出当前日历时间。time()是32位函数,time64是64位的。因为32位的函数所表示的时间不能晚于2038年1月18日19时14分07秒。

其实不同的语言的大致步骤是一样的。用寄存器或栈存放调用time函数所需的参数,调用time函数,返回值保存在某一寄存器。用这个寄存器的参数再去调用printf。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值