奇怪的函数调用

简介: 奇怪的函数调用

       整理移动硬盘时,发现一个名为 attack 的目录,进去以后发现原来是一段简单的 C 语言代码。代码如下:

#include <stdio.h>
void Attack()
{
    while (1)
    {
        printf("Attack...\r\n");
    }
    getchar();
}
int main(int argc, char* argv[])
{
    int arr[5] = { 0 };
    arr[7] = (int)Attack;
    return 0;
}


       看代码猜测,应该是死循环输出 Attack 字符串,因为毕竟是数组的越界访问(很多一些演示栈溢出的程序,都会用到数组的越界访问、字符串的拷贝等)。直接打开 VS 2015 进行编译、连接、运行,发现运行后什么结果都没有输出。当然了,这应该是被 VS 2015 的编译连接选项所导致的。进行一番设置,然后再进行执行。果然是死循环输出 Attack 字符串。

设置编译连接选项

       类似这样的程序,在之前 VC 6 的编译环境下比较简单,到了高版本的 VS 下就需要设置相应的项目、编译、连接选项了,否则默认的安全选项会导致测试失败。不过好在这样的选项不多。这里逐一来进行设置。

       在项目名上点击鼠标右键,在弹出的对话框上选择“属性”。

       在“属性页”的“常规”选项中将字符集设置为“未设置”,如下图所示。

       在 C/C++ 的“代码生成”选项中,将“安全检查”设置为“禁用安全检查(/GS-)”,如下图所示。

       设置“连接器”下的“高级”选项,“随机地址”设置为“否”,“数据执行保护(DEP)”设置为“否”,如下图所示。


       再次进行编译运行,发现死循环的测试成功了。如下图所示。注意观察右侧的滚动条,往下滚动速度很快。

原因分析

       为什么会产生这样的显现呢?原因就是数组越界的赋值,代码如下:

arr[7] = (int)Attack;

       在 C 语言中,函数名的名称就是函数的首地址。上面的赋值语句是将 arr[7] 的位置赋值为了 Attack 函数的地址。而 arr[7] 又是何物呢?在了解 arr[7] 之前,需要了解的是函数调用与函数的栈帧。


       C 语言在调用函数时,根据函数的调用约定(C 语言的调用约定为 _cdcel)先将参数从右至左依次入栈,然后将返回地址压入栈中。当进入被调用的函数后,会先将 EBP 寄存器入栈,然后将 ESP 寄存器赋值给 EBP,最后通过 sub esp 来抬高栈顶,当作被调用函数的栈空间。EBP 作为基址指针,对当前函数(被调用函数)中的局部变量通过 [EBP - 0xXXX] 来进行访问,而对于调用时栈中的参数,则通过 [EBP + 0xXXX] 来进行访问。通常,[EBP + 4] 是返回地址,[EBP + 8] 是第一个参数的(如果有参数的话)。当函数返回时,通过 add esp 来收回栈空间,然后在执行 retn 指令时,会把栈中的保存的返回地址赋值给 EIP 寄存器,然后从返回地址继续执行代码。


       有了上面的知识点,我们来看一下,上面程序的反汇编代码,代码如下:

004117F0    55              PUSH EBP
004117F1    8BEC            MOV EBP,ESP
004117F3    81EC DC000000   SUB ESP,0DC
004117F9    53              PUSH EBX
004117FA    56              PUSH ESI
004117FB    57              PUSH EDI
004117FC    8DBD 24FFFFFF   LEA EDI,DWORD PTR SS:[EBP-DC]
00411802    B9 37000000     MOV ECX,37
00411807    B8 CCCCCCCC     MOV EAX,CCCCCCCC
0041180C    F3:AB           REP STOS DWORD PTR ES:[EDI]
0041180E    C745 E8 0000000>MOV DWORD PTR SS:[EBP-18],0
00411815    33C0            XOR EAX,EAX
00411817    8945 EC         MOV DWORD PTR SS:[EBP-14],EAX
0041181A    8945 F0         MOV DWORD PTR SS:[EBP-10],EAX
0041181D    8945 F4         MOV DWORD PTR SS:[EBP-C],EAX
00411820    8945 F8         MOV DWORD PTR SS:[EBP-8],EAX
00411823    B8 04000000     MOV EAX,4
00411828    6BC8 07         IMUL ECX,EAX,7
0041182B    C7440D E8 5A104>MOV DWORD PTR SS:[EBP+ECX-18],test.0041105A
00411833    33C0            XOR EAX,EAX
00411835    52              PUSH EDX
00411836    8BCD            MOV ECX,EBP
00411838    50              PUSH EAX
00411839    8D15 50184100   LEA EDX,DWORD PTR DS:[411850]
0041183F    E8 19FAFFFF     CALL test.0041125D
00411844    58              POP EAX
00411845    5A              POP EDX
00411846    5F              POP EDI
00411847    5E              POP ESI
00411848    5B              POP EBX
00411849    8BE5            MOV ESP,EBP
0041184B    5D              POP EBP
0041184C    C3              RETN

       以上反汇编代码来自 OD,如下图所示。

       在上面 0041180E 到 0041181D 的位置处,是对 arr 数组进行初始化的过程。对应代码的如下:

int arr[5] = { 0 };

       可以看到,C 语言的一行代码,对应到汇编就有好多行。在 0041182B 处也是一行赋值语句,代码如下:

MOV DWORD PTR SS:[EBP+ECX-18],test.0041105A

     EBP + ECX - 18,此处 ECX 的值为 1C,1C - 18 = 4,那么此处相当于是如下代码:

MOV DWORD PTR SS:[EBP + 4], test.0041105A

       回顾我们前面提到的,[EBP + 4] 的位置处保存着返回地址,也就是调用当前函数的函数的下一条指令。比如,A 函数中调用了 B 函数,当 B 函数执行完成后,会接着执行 A 函数中,调用 B 函数处的下一条指令。而此时,返回地址被覆盖为 0041105A,那么,这个 0041105A 是什么值?回顾上面的代码,代码如下:

arr[7] = (int)Attack;

       0041105A 是 Attack 函数的首地址。那么当 main 函数返回时,相当于调用了 Attack 函数。而 Attack 函数中是一个死循环。

观察内存变化

  看一下代码执行到 0041180E 时 ebp 的情况,如下图所示。

571f715156133cc397ddab7b0d643a28.png



       此时,可以看到 [ebp + 4] 中的值是 00411FCE,然后再观察 [ebp - 18] 到 [ebp - 8] 内存中的值都为 cc。然后,将代码执行到 00411823 处,观察 ebp 的情况,如下图所示。


27157f35bf49d0febc3affa6d50bccff.png

       可以看到 [ebp - 18] 到 [ebp - 8] 的栈空间都被初始化为了 0。接着继续执行代码,到 00411833 的地址处,再次观察 ebp 的情况,如下图所示。

       可以看到,[ebp + 4] 的栈地址处的值被修改了,接着将代码执行向下执行,执行到 0041184C 后,也就是执行完 retn 后观察 EIP 寄存器的值,如下图所示。


       可以看到,此时 EIP 的值为 0041105A,而反汇编代码处是一个跳表的位置。在当前位置接着在单步一下,如下图所示。

       从图中可以看到,在注释位置有一个“attack...”字符串的提示,从这点就可以看出,该段反汇编代码是 Attack() 函数了。


       到此,整个程序的执行就说清楚了。


总结


       这种程序虽小,但是考察的是对函数调用时内存结构相关的知识。虽然简单的,但还是很有意思的。

相关文章
|
25天前
|
C语言
【C语言】全局搜索变量却找不到定义?原来是因为宏!
使用条件编译和 `extern` 来管理全局变量的定义和声明是一种有效的技术,但应谨慎使用。在可能的情况下,应该优先考虑使用局部变量、函数参数和返回值、静态变量或者更高级的封装技术(如结构体和类)来减少全局变量的使用。
31 5
|
5月前
|
Python
python语法错误变量未定义
【7月更文挑战第9天】
99 1
|
7月前
|
存储 C++
面试题:C++函数调用的过程?
面试题:C++函数调用的过程?
85 0
|
C语言
【C语言】刨析数组作为函数参数时可能会出现的问题以及对应的解决方法
【C语言】刨析数组作为函数参数时可能会出现的问题以及对应的解决方法
72 0
|
存储 C++ Python
关于函数参数传递,80%人都错了
实际的输出我想大家都尝试过了吧,应该是选项二:
Go中都是值传递,切记! 你所了解的引用传递等知识经验从今天开始彻底抛弃!
Go中都是值传递,切记! 你所了解的引用传递等知识经验从今天开始彻底抛弃!
|
编译器 C语言 C++
C语言数组越界造成的死循环例子,当你得到了这个意想不到的结果的时候,你肯定不知道为什么,看你还敢不敢越界访问数组了
C语言数组越界造成的死循环例子,当你得到了这个意想不到的结果的时候,你肯定不知道为什么,看你还敢不敢越界访问数组了
124 0
|
存储 编译器 C语言
【C语言】汇编角度剖析函数调用的整个过程
【C语言】汇编角度剖析函数调用的整个过程
C中使用errno查看函数调用的错误
C中使用errno查看函数调用的错误
79 0
解决办法:C代码中明明有,为什么编译时提示未定义的引用
解决办法:C代码中明明有,为什么编译时提示未定义的引用
385 0