如果在函数调用过程中,堆栈操作无法实现平衡,覆盖了返回的地址,函数就无法返 回了。
例3-11溢出代码的实现。 #include
test(); label1:
cout<<\label2: _asmnop; _asmnop; _asmnop; _asmnop;
cout<<\return0; }
inttest() {
inttest[2]; inttemp;
cout< for(inti=0;i<=3;i++) { cin>>temp; test[i]=temp; } cout<<&temp; cout<<&test[0]; cout<<&test[1]; cout<<&test[2]; cout<<&test[3]; cout< cout<<\return0; } 在test函数中,定义了一个可以存储2个32位整数的test数组,然而下面的循环 代码却向这个地址写入了4个元素。后面的两个元素无情地覆盖了返回的地址,导致 函数执行后无法返回。test函数内部定义了两个局部变量,test数组元素占用堆栈8 个字节,而temp占了4个字节。这样堆栈中存储的元素自上而下依次为temp、test[0]、 test[1]、test[2]和函数返回的地址。由于堆栈指针是基于内存线性分布的,for循环顺 序写入了4个元素,却把函数返回的地址无情覆盖了。其中test[3]存放着test返回的 地址。 由此可以通过改变参数的输入改变函数的执行流程。这样可以推而广之,如果溢 出了更多的代码,可以通过查找函数的返回地址保存在哪个堆栈指针区域,精心修改 这个区域的值,使其指向一个特殊的指令。比如jmpesp,这样函数返回后,将跳转到 这个地址运行,而jmpesp指令可以使用堆栈的数据来当作指令运行。为了对齐,可 以在溢出代码中加入一些特殊指令比如nop指令。 当然溢出情况最常见的是字符串数组溢出。出现这种情况的根本原因是没有对参 数中的字符串长度作限制,导致多出来的字符串中包含了恶意代码。由于C字符串一 般都是采用零结尾的,因此字符串操作函数一见到零,就认为字符串结束了,而精心 编写的溢出代码不可避免地会碰到指令中包含零的情况。为了不希望把指令截断,有 时还需要对这些指令进行特殊编码,比如和一个值进行异或操作。注入代码进入一个 陌生的环境中运行,还需要解决诸多的问题,比如API函数调用等。总之,溢出代码 的编写需要比较高的编程技能。 对于注入到Win32应用程序进程中的代码而言,其代码的实现单靠一些汇编指令 是远远不够的,它只有通过调用WindowsAPI函数才能完成必要的功能。比如向 Windows添加用户,设置用户密码,把用户加入到管理员组,打开一个Shell窗口, 启动Telnet服务,调用Socket函数接收和发送数据等。完成这些功能都需要调用 WindowsAPI函数。然而注入的代码是无法直接知道这些API的函数地址的,甚至注 入的进程根本就没有使用这些函数的动态链接库,所以注入代码和创建远程线程一样, 需要知道这些函数的入口地址,必要的情况下还需要手工加载动态链接库。 加载一个动态链接库需要调用Windows中的LoadLibrary和GetProcAddress函数, 这两个函数是Kernel32.DLL的输出函数,而几乎所有的Win32应用程序都会使用这 个动态链接库。我们只要得到这个动态链接库的基地址。得到这个动态链接库的基地 址之后,就可以通过遍历这个库的export表得到它提供的LoadLibrary和 GetProcAddress函数地址。有了这两个函数,就可以加载任何的动态链接库,并调用 它提供的输出函数。 搜索Kernel32基地址的方法有很多,但是这些方法一般都是通过它提供的API 函数间接得到的。比如说提供了kernel32.dll的一个特殊的UnhandledExceptionFilter 输出函数。这个函数的地址比较特殊,它可以通过下面的汇编指令获得:movesi,fs:0 lodsd retry: cmp[eax],0xffffffff jeexit//如果到达最后一个节点(它的pfnHandler指向UnhandledExceptionFilter) moveax,[eax]//否则往后遍历,一直到最后一个节点 jmpretry exit: 有了这个地址之后,就可以通过下面的代码得到kernel32.dll的基地址了。 下面是完整代码: #include __inline__declspec(naked)unsignedintGetKernel32() { __asm { pushesi pushecx movesi,fs:0 lodsd retry: cmp[eax],0xffffffff jeexit//如果到达最后一个节点(它的pfnHandler指向UnhandledExceptionFilter) moveax,[eax]//否则往后遍历,一直到最后一个节点 jmpretry exit: moveax,[eax+4] FindMZ: andeax,0xffff0000//根据PE执行文件以64k对界的特征加快查找速度 cmpwordptr[eax],'ZM'//根据PE可执行文件特征查找KERNEL32.DLL的基址 jneMoveUp//如果当前地址不符合MZ头部特征,则向上查找 movecx,[eax+0x3c] addecx,eax cmpwordptr[ecx],'EP'//根据PE可执行文件特征查找KERNEL32.DLL的基址 jeFound//如果符合MZ及PE头部特征,则认为已经找到,并通过Eax返回给调用者 MoveUp: deceax//准备指向下一个界起始地址 jmpFindMZ Found: popecx popesi ret } } voidmain(void) { printf(\getch(); } 这个程序运行后会得到一个77E60000的输出结果,这个结果是和Kernel32在这 个编译后程序的导入基地址相符的。如图3-5所示。 有了模块加载的基地址,就可以根据函数名得到具体函数的入口地址了。实现这 个目的需要了解Win32的存储结构。查找步骤如下: (1)定位到PE文件头(基地址)。 (2)从PE文件头部的可选文件头(OptionHeader)得到datadirectory(数据目录)的第 一项,得到导出表的地址VirtualAddress。 (3)从AddressOfNames字段指示的函数名称表的第一项开始,将每一项的函数名 与GetProcAddress函数名进行比较,直到找到匹配函数为止。 (4)根据匹配项在输出函数表中的索引值,在AddressOfFunctions中查找函数的入 口地址。 Movedx,edi Assumeedx:ptrImage_DOS_HEADER Addedx,[edx].e_lfanew AssumeEdx:ptrIMAGE_NT_HEADERS Movedx,[edx].OptionHeader.DataDireectory.VirtualAddress AddEdx,hKernel32;//hKernel32为上面查到的函数基地址。 Assumeedx:ptrImage_Export_Directory Movebp,[edx].AddressOfNames Addebp,hKernel32 Xoreax,eax .Repeat movecx,14;//GetProcAddress函数的长度 movedi,[ebp] addedi,hKernel32 leaesi,szGetProcAddress;//函数名称 repzcomsb .IfZero? .Break .Endif