StackOverflow

本篇文章主要是手动分析栈的溢出情况,并且去掉了相关的检查代码,我们下面就是通过简单的示例,来分析整个流程,首先看代码如下:

代码文件为:test_stack.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
29
30
31
32
33
34
35
36
37
#define _CRT_SECURE_NO_WARNINGS
//by MoreWindows
#include <windows.h>
#include <conio.h>
#include <stdio.h>
#include <process.h>
#include <string.h>


void foo(const char* input)
{
char buf[4]; //buf 占4字节,后4字节为ebp,再后4个字节为返回地址。
strcpy(buf, input); //传入的字符串去覆盖返回地址,从而使用程序执行bar()函数
}

void bar(void)
{
printf("Augh! This program have been hacked by MoreWindows!\n");
_getch();
exit(0);//由于这时的ebp已经破坏了, 所以在这直接退出程序. 不然会弹出错误对话框
}

int main(int argc, char* argv[])
{
printf("Address of main = %p\n", main);
printf("Address of foo = %p\n", foo);
printf("Address of bar = %p\n", bar);

//构造字符串,前8个填充字符,再跟一个bar()函数的地址。
char szbuf[50] = "12341234";
DWORD* pbarAddress = (DWORD*)&szbuf[8];
*pbarAddress = (DWORD)bar;

foo(szbuf);

return 0;
}

1. 准备编译环境

  • visual studio 2019
  • Debug/x86
  • 使用C编译器编译
  • 去掉栈检查,JMC等

1. 配置

指定C编译器:

去掉基本运行时检查

具体的rtc检查,生成的函数,可以参考文章:https://blog.csdn.net/magictong/article/details/6306820

去掉缓冲区检查

去掉整个作用就是去掉我们在反汇编中经常能看到的函数:___security_cookie

去掉JMC选项

JMC选项一个Debug调试过程中的对于PDB文件的一种修饰,可以查看文章:https://learn.microsoft.com/en-us/cpp/build/reference/jmc?view=msvc-170

上面的配置都完成之后,我们就可以生成程序了,然后调试程序,就可以看见反汇编结果。

2. 反汇编并调试

调试过程中的反汇编结果:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
; ----------------------main主函数---------------------------

int main(int argc, char* argv[])
{
005B1920 push ebp
005B1921 mov ebp,esp
005B1923 sub esp,78h
005B1926 push ebx
005B1927 push esi
005B1928 push edi
printf("Address of main = %p\n", main);
005B1929 push offset _main (05B12DFh)
005B192E push offset string "Address of main = %p\n" (05B7B70h)
005B1933 call _printf (05B10CDh)
005B1938 add esp,8
printf("Address of foo = %p\n", foo);
005B193B push offset _foo (05B127Bh)
005B1940 push offset string "Address of foo = %p\n" (05B7B8Ch)
005B1945 call _printf (05B10CDh)
005B194A add esp,8
printf("Address of bar = %p\n", bar);
005B194D push offset _bar (05B11F9h)
005B1952 push offset string "Address of bar = %p\n" (05B7BA8h)
005B1957 call _printf (05B10CDh)
005B195C add esp,8

//构造字符串,前8个填充字符,再跟一个bar()函数的地址。
char szbuf[50] = "12341234";
005B195F mov eax,dword ptr [string "12341234" (05B7BC4h)]
005B1964 mov dword ptr [szbuf],eax
005B1967 mov ecx,dword ptr ds:[5B7BC8h]
005B196D mov dword ptr [ebp-30h],ecx
005B1970 mov dl,byte ptr ds:[5B7BCCh]
005B1976 mov byte ptr [ebp-2Ch],dl
005B1979 push 29h
005B197B push 0
005B197D lea eax,[ebp-2Bh]
005B1980 push eax
005B1981 call _memset (05B114Fh)
005B1986 add esp,0Ch
DWORD* pbarAddress = (DWORD*)&szbuf[8];
005B1989 mov eax,1
005B198E shl eax,3
005B1991 lea ecx,szbuf[eax]
005B1995 mov dword ptr [pbarAddress],ecx
*pbarAddress = (DWORD)bar;
005B1998 mov eax,dword ptr [pbarAddress]
005B199B mov dword ptr [eax],offset _bar (05B11F9h)

foo(szbuf);
005B19A1 lea eax,[szbuf]
005B19A4 push eax
005B19A5 call _foo (05B127Bh) ; -------------------- ①
005B19AA add esp,4 ; ------------------------------ ②

return 0;
005B19AD xor eax,eax
}

;---------------------foo函数------------------------------

void foo(const char* input)
{
005B1880 push ebp
005B1881 mov ebp,esp
005B1883 sub esp,44h
005B1886 push ebx
005B1887 push esi
005B1888 push edi
char buf[4]; //buf 占4字节,后4字节为ebp,再后4个字节为返回地址。
strcpy(buf, input); //传入的字符串去覆盖返回地址,从而使用程序执行bar()函数
005B1889 mov eax,dword ptr [input]
005B188C push eax
005B188D lea ecx,[buf] ; ------------------------- ③
005B1890 push ecx
005B1891 call _strcpy (05B120Dh)
005B1896 add esp,8
}

; --------------------bar函数----------------------------

void bar(void)
{
005B1810 push ebp
005B1811 mov ebp,esp
005B1813 sub esp,40h
005B1816 push ebx
005B1817 push esi
005B1818 push edi
printf("Augh! This program have been hacked by MoreWindows!\n");
005B1819 push offset string "Augh! This program have been ha@"... (05B7B30h)
005B181E call _printf (05B10CDh)
005B1823 add esp,4
_getch();
005B1826 call dword ptr [__imp___getch (05BB174h)]
exit(0);//由于这时的ebp已经破坏了, 所以在这直接退出程序. 不然会弹出错误对话框
005B182C push 0
005B182E call dword ptr [__imp__exit (05BB178h)]
}

分析:

我们运行程序到调用 foo 函数, 如下图:

暂时的输出如下:

通过上图,我们要记住一点,调用 foo 函数后面的地址,这个是汇编 call _foo 的返回地址,第一张图 的位置: 005B19AA add esp,4

我们运行程序,进去到 foo 函数,如下图:

我们通过查看 esp 寄存器,查看栈中的数据:

能看到 esp 地址的数据的第一值是 aa 19 5b 00 也就是 005b19aa 这个地址就是我们上面看到的 call _foo 函数的下一个地址,也就是 call 之后,压入栈的返回地址。

我们继续运行程序,我们要获取buf变量的位置,因为是局部变量,本身变量存在于栈中,下图:

我们获取到的 ecx 寄存的值就是 buf 的缓冲区地址:008FF710

我们通过上面,知道 的位置就是返回地址的位置:0x008FF718 aa 19 5b 00 ?.[.

的位置就是 buf 缓存区位置:0x008FF710, 其中的值是随意填充的,没有影响。

我们继续运行程序,如下图:

我们完成了 strcpy 函数的执行,然后观察现在 buf 的位置的数值情况,如下图:

我们看到 buf 地址的位置已经背填充了值,本身的程序代码是这样的:

1
2
3
4
5
6
//构造字符串,前8个填充字符,再跟一个bar()函数的地址。
char szbuf[50] = "12341234";
DWORD* pbarAddress = (DWORD*)&szbuf[8];
*pbarAddress = (DWORD)bar;

foo(szbuf);

buf 本身填充的数据有两个部分组成,一个是字符数据:"12341234", 一个是 bar 函数的地址,这个地址我们通过最上面的输出,可以看到如下:

1
2
3
printf("Address of main = %p\n", main);
printf("Address of foo = %p\n", foo);
printf("Address of bar = %p\n", bar);

我们继续看上面的执行完成后的内存栈图,

注意观察这三个地址:0x008FF718, 0x008FF714, 0x008FF710

  • 0x008FF710: buf的位置

  • 0x008FF714: ?

  • 0x008FF718: 存放调用函数foo的返回地址(可以查看上面的说明)不过这个地址目前已经被 bar 的函数地址覆盖了:f9 11 5b 00

第2个地址:0x008FF714 是什么呢? 在 buf 和 返回地址之间这个压入栈的数据是什么呢?其实也是很好理解,我们从 foo 函数的汇编就可以看出来,如下:

1
2
3
4
5
6
7
8
void foo(const char* input)
{
005B1880 push ebp ; ------------ 压入ebp
005B1881 mov ebp,esp
005B1883 sub esp,44h
005B1886 push ebx
005B1887 push esi
005B1888 push edi

其实这个地方就是 push ebp 的操作。所以在返回地址和buf之间的位置。

那么我们继续执行,看看会出现什么效果?

首先执行到 foo 函数的最后,如下:

1
2
3
4
5
6
7
8
9
005B1891  call        _strcpy (05B120Dh)  
005B1896 add esp,8
}
005B1899 pop edi
005B189A pop esi
005B189B pop ebx
005B189C mov esp,ebp
005B189E pop ebp
005B189F ret ; ------------------ ret 返回函数

我们查看 ret 汇编指令的说明(Intel IA-32文档):

因为是32位程序,所以 ret 执行指令最终执行后是:EIP ← Pop(); 说明会把栈返回的数据返回给 EIP 寄存器,这个也就是代码执行的下一个执行存储的寄存器。

因为上面已经说明了, 把返回地址修改为了 bar 函数的地址:0x008FF718 f9 11 5b 00

那么 foo 返回后,也就是要执行 bar 函数了,这个也是能看到的确实是执行了 bar 函数:

1
2
3
4
5
6
void bar(void)
{
printf("Augh! This program have been hacked by MoreWindows!\n");
_getch();
exit(0);//由于这时的ebp已经破坏了, 所以在这直接退出程序. 不然会弹出错误对话框
}

我们继续执行,看看能不能输出 printf 函数,如下图:

已经看到了,确实执行了, 并输出了信息。

3. 总结

我们通过上面的分析,就是一个完整的栈溢出漏洞的程序执行过程, 但要注意,我们去掉的几乎所有的栈检查和运行时检查,这里只是做为一个演示,观察怎么通过溢出覆盖了返回地址的。后续的文档,我们还会基于这个文章,做payload的分析工作,通过输出的方式弹出对话框。


StackOverflow
https://xxxxnnxxxx.github.io/2023/07/06/StackOverflow/
作者
xxxxnnxxxx
发布于
2023年7月6日
许可协议