0day安全
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.2 定位shellcode

3.2.1 栈帧移位与jmp esp

回忆2.4节中的代码植入实验,当我们可以用越界的字符完全控制返回地址后,需要将返回地址改写成shellcode在内存中的起始地址。在实际的漏洞利用过程中,由于动态链接库的装入和卸载等原因,Windows进程的函数栈帧很有可能会产生“移位”,即shellcode在内存中的地址是会动态变化的,因此像2.4节中那样将返回地址简单地覆盖成一个定值的做法往往不能让exploit奏效,如图3.2.1所示。

要想使exploit不至于10次中只有2次能成功地运行shellcode,我们必须想出一种方法能够在程序运行时动态定位栈中的shellcode。

回顾2.4节代码植入实验中在verify_password函数返回后栈中的情况,如图3.2.2所示。

(1)实线体现了代码植入的流程:将返回地址淹没为我们手工查出的shellcode起始地址0x0012FAF0,函数返回时,这个地址被弹入EIP寄存器,处理器按照EIP寄存器中的地址取指令,最后栈中的数据被处理器当成指令得以执行。

(2)虚线则点出了这样一个细节:在函数返回的时候,ESP恰好指向栈帧中返回地址的后一个位置!

图3.2.1 栈帧移位示意图

图3.2.2 溢出发生时栈、寄存器与代码之间的关系

一般情况下,ESP寄存器中的地址总是指向系统栈中且不会被溢出的数据破坏。函数返回时,ESP所指的位置恰好是我们所淹没的返回地址的下一个位置,如图3.2.3所示。

提示:函数返回时,ESP所指位置还与函数调用约定、返回指令等有关。例如,retn3与retn4在返回后,ESP所指的位置都会有所差异。

图3.2.3 使用“跳板”的溢出利用流程

由于ESP寄存器在函数返回后不被溢出数据干扰,且始终指向返回地址之后的位置,我们可以使用图3.2.3所示的这种定位shellcode的方法来进行动态定位。

(1)用内存中任意一个jmp esp指令的地址覆盖函数返回地址,而不是原来用手工查出的shellcode起始地址直接覆盖。

(2)函数返回后被重定向去执行内存中的这条jmp esp指令,而不是直接开始执行shellcode。

(3)由于esp在函数返回时仍指向栈区(函数返回地址之后),jmp esp指令被执行后,处理器会到栈区函数返回地址之后的地方取指令执行。

(4)重新布置shellcode。在淹没函数返回地址后,继续淹没一片栈空间。将缓冲区前边一段地方用任意数据填充,把shellcode恰好摆放在函数返回地址之后。这样,jmp esp指令执行过后会恰好跳进shellcode。

这种定位shellcode的方法使用进程空间里一条jmp esp指令作为“跳板”,不论栈帧怎么“移位”,都能够精确地跳回栈区,从而适应程序运行中shellcode内存地址的动态变化。

本节实验将把4.4节代码植入实验中的password.txt文件改造成上述思路的exploit,并加入安全退出的代码避免点击消息框后程序的崩溃。

题外话:1998年,黑客组织“Cult of the Dead Cow”的Dildog在Bugtrq邮件列表中以Microsoft Netmeeting为例首次提出了利用jmp esp完成对shellcode的动态定位,从而解决了Windows下栈帧移位问题给开发稳定的exploit带来的重重困难。毫不夸张地讲,跳板技术应该算得上是Windows栈溢出利用技术的一个里程碑。

3.2.2 获取“跳板”的地址

我们必须首先获得进程空间内一条jmp esp指令的地址作为“跳板”。通过第1章对PE文件和Win_32平台下进程4GB的虚拟内存空间的学习,我们应当明白除了PE文件的代码被读入内存空间,一些经常被用到的动态链接库也将会一同被映射到内存。其中,诸如kernel.32.dll、user32.dll之类的动态链接库会被几乎所有的进程加载,且加载基址始终相同。

2.4节实验中的有漏洞的密码验证程序已经加载了user32.dll,所以我们准备使用user32.dll中的jmp esp作为跳板。获得user32.dll内跳转指令地址最直观的方法就是编程序搜索内存。

   #include <windows.h>
#include <stdio.h>
#define DLL_NAME "user32.dll"
main()
{
     BYTE* ptr;
     int position,address;
     HINSTANCE handle;
     BOOL done_flag = FALSE;
     handle=LoadLibrary(DLL_NAME);
     if(!handle)
     {
        printf(" load dll erro !");
        exit(0);
     }
     ptr = (BYTE*)handle;
     for(position = 0; !done_flag; position++)
     {
        try
        {
           if(ptr[position] == 0xFF && ptr[position+1] == 0xE4)
           {
                //0xFFE4 is the opcode of jmp esp
                int address = (int)ptr + position;
                printf("OPCODE found at 0x%x\n",address);
           }
        }
        catch(...)
        {
          int address = (int)ptr + position;
          printf("END OF 0x%x\n", address);
          done_flag = true;
        }
     }
}

jmp esp对应的机器码是0xFFE4,上述程序的作用就是从user32.dll在内存中的基地址开始向后搜索0xFFE4,如果找到就返回其内存地址(指针值)。

如果您想使用别的动态链接库中的地址(如“kernel32.dll”、“mfc42.dll”等),或者使用其他类型的跳转地址(如call esp、jmp ebp等),也可以通过对上述程序稍加修改而轻易获得。

除此以外,还可以通过OllyDbg的插件轻易地获得整个进程空间中的各类跳转地址。您可以到看雪论坛的相关版面下载到这个插件(OllyUni.dll),并把它放在OllyDbg目录下的Plugins文件夹内,重新启动OllyDbg进行调试,在代码框内单击右键,就可以使用这个插件了,如图3.2.4所示。

图3.2.4 用OllyDbg的插件搜索“跳板”的地址

搜索结束后,单击OllyDbg中的“L”按钮,就可以在日志窗口中查看搜索结果了。

3.2.3 使用“跳板”定位的exploit

仍然使用2.4节中的代码作为攻击目标,实验环境如表3-2-1所示

表3-2-1 实验环境

说明:函数调用地址和跳转地址依赖于系统补丁,需要在实验时重新确定。确定的方法在实验指导中有详细说明。

运行我们自己编写程序搜索跳转地址得到的结果和OllyDbg插件搜到的结果基本相同,如图3.2.5所示。

图3.2.5 OllyDbg搜出的“跳板”与程序搜出的“跳板”地址

题外话:跳转指令的地址将直接关系到exploit的通用性。事实上,kernel32.dll与user32.dll在不同的操作系统版本和补丁版本中也是有所差异的。最佳的跳转地址位于那些“千年不变”且被几乎所有进程都加载的模块中。

这里不妨采用位于内存0x77DC14CC处的跳转地址jmp esp作为定位shellcode的“跳板”。

在制作exploit的时候,还应当修复2.4节中shellcode无法正常退出的缺陷。为此,我们在调用MessageBox之后,通过调用exit函数让程序干净利落地退出。

这里仍然用dependency walker获得这个函数的入口地址。如图3.2.6所示,ExitProcess是kernel32.dll的导出函数,故首先查出kernel32.dll的加载基址0x7C800000,然后加上函数的偏移地址0x0001CDDA,得到函数入口最终的内存地址0x7C81CDDA。

图3.2.6 计算ExitProcess函数的入口地址

写出的shellcode的源代码如下所示。

   #include <windows.h>
int main()
{
     HINSTANCE LibHandle;
     char dllbuf[11] = "user32.dll";
     LibHandle = LoadLibrary(dllbuf);
     _asm{
         sub sp,0x440
         xor ebx,ebx
         push ebx // cut string
         push 0x74736577
         push 0x6C696166//push failwest
         mov eax,esp //load address of failwest
         push ebx
         push eax
         push eax
         push ebx
         mov eax,0x77D804EA // address should be reset in different OS
         call eax //call MessageboxA
         push ebx
         mov eax,0x7C81CDDA
         call eax //call exit(0)
     }
}

为了提取出汇编代码对应的机器码,我们将上述代码用VC6.0编译运行通过后,再用OllyDbg加载可执行文件,选中所需的代码后可直接将其dump到文件中,如图3.2.7所示。

通过IDA Pro等其他反汇编工具也可以从PE文件中得到对应的机器码。当然,如果熟悉intel指令集,也可以为自己编写专用的由汇编指令到机器指令的转换工具。

现在我们已经具备了制作新exploit需要的所有信息。

图3.2.7 从PE文件中提取shellcode的机器码

(1)搜索到的jmp esp地址,用作重定位shellcode的“跳板”:0x77DC14CC。

(2)修改后并重新提取得到的shellcode,如表3-2-2所示。

表3-2-2 shellcode及注释

按照2.4节中对栈内情况的分析,我们将password.txt制作成如图3.2.8所示的形式。

图3.2.8 在输入文件中部署shellcode

现在再运行密码验证程序,怎么样,程序退出的时候不会报内存错误了吧。虽然还是同样的消息框,但是这次植入代码的流程和2.4节中已有很大不同了,最核心的地方就是使用了跳转地址定位shellcode,进程被劫持的过程如图3.2.3中我们设计的那样。