测试代码

代码很简单,就是把函数指针转换成int类型储存在数组的边界之外

数组在C语言中以连续的方式储存在堆栈中,所以声明数组时必须确定长度,但是在调用数组时编译器不会检查数组的边界

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
#include<iostream>

void func() {
while (1) {
std::cout<<"缓冲区溢出"<<std::endl;
}
}

int check() {
int arr[8] = {1,2,3,4,5,6,7,8};
arr[10] = (int)&func;
return 0;
}

void main() {

__asm {
mov eax, eax
mov eax, eax
mov eax, eax
}

check();
getchar();

return;
}


直接运行结果如下


分析

可以看到func()函数并没有被调用但还是执行了,在高级语言层面上无法得知原因,只能通过反汇编分析堆栈平衡。

关键处反汇编:

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
push        ebp  
mov ebp,esp
sub esp,0E8h
push ebx
push esi
push edi
lea edi,[ebp-28h]
mov ecx,0Ah
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
mov ecx,97F029h
call 0097138E
10: int arr[8] = {1,2,3,4,5,6,7,8};
mov dword ptr [ebp-24h],1
mov dword ptr [ebp-20h],2
mov dword ptr [ebp-1Ch],3
mov dword ptr [ebp-18h],4
mov dword ptr [ebp-14h],5
mov dword ptr [ebp-10h],6
mov dword ptr [ebp-0Ch],7
mov dword ptr [ebp-8],8
11: arr[10] = (int)&func;
mov eax,4
imul ecx,eax,0Ah
mov dword ptr [ebp+ecx-24h],971352h
12: return 0;
xor eax,eax
13: }
push edx
mov ecx,ebp
push eax
lea edx,ds:[009724A4h]
call 00971230
pop eax
pop edx
pop edi
pop esi
pop ebx
add esp,0E8h
cmp ebp,esp
call 00971294
mov esp,ebp
pop ebp
ret

从反汇编可以得知从EBP-0X28到EBP之间为缓冲区,对每个字节填充0XCC,vs的编译器生成的局部变量是从EBP-8开始的,所以想要造成缓冲区溢出应该将下标取10

mov dword ptr [ebp+ecx-24h],971352h这条指令将函数地址被储存到了EBP+4,EBP指向的内存储存的是堆栈提升前的EBP,而EBP+4储存的是函数调用前的下一步地址

当执行到

1
2
3
mov         esp,ebp  
pop ebp
ret

堆栈平衡过程中,ESP会还原成EBP的值,然后弹出原EBP的值都是正常的(ESP此时指向的地址储存着原EBP的值)

而当执行RET指令时,ESP弹出后+4,此时ESP正是指向那个函数的地址

RET和CALL指令都会修改EIP寄存器,最终RET指令将ESP的值写入到EIP然后跳转