安全搜索进程内存空间

作者:lisl03

本文发表在黑防2008的2期上,按照他们要求的间隔时间已经过了,所以贴上来。本文设计了用于演示的程序,一下子没有找到怎么发附件,所以需要的读者就向邮箱[email protected]发 application mail吧。

ShellCode的编写好像是一个永远说不完的话题。在11期《黑客防线》上我写了《再谈绕过卡巴斯基主动防御的ShellCode编写》的文章。小弟最近在编写一个漏洞的利用程序时,遇到了在进程空间中安全搜索ShellCode的 问题。小弟在网上搜索了一番也没有什么有用的东西。在为这个问题困扰了许久之后,小弟请教了一位大虾后才知道原来国外的一些大牛们早就作过这方面的深入研 究了。在参阅了一些相关的资料后,小弟终于解决了如何安全搜索内存空间的问题。好东西不敢独享,把他写出来献给如我一样正在艰难学习中的菜鸟,大虾看了就飘过吧!

很多情况下在编写ShellCode的时候我们都会面临着如何安全地搜索进程空间,找到我们的可爱的没有被破坏得ShellCode的问题。例如,有些溢出程序对编码有着特殊得要求,比如会将0x2F(‘/’)转化为0x5C(‘\’)等等的情况,甚至还有不能出现0xFF的情况。这个时候可能不管你怎么变形都无法符合要求;有些文件格式型的溢出由于文件结构自身的特点,在溢出点附近不可能放入大得ShellCode。溢出产生的时候,我们都必须用一个简短的Search Code查找到原始的ShellCode后,再执行这个功能ShellCode来实现我们的溢出利用。这种通过设置一定得标志字节,由小ShellCode找在内存中查找大ShellCode的方法有一个有趣得叫法:Egg-Hunt.。

在栈溢出利用时,EIP开始在堆栈里执行后,由于堆栈在内存中的地址往往比较低:如0x0012XXXX,而原始ShellCode却在0x07BAXXXX远的高地址。直接搜索内存的时候会由于:违规访问未分配内存、用户断点、 浮点异常等等问题而造成系统的崩溃。于是在我们好不容易获得CPU控制权之际,却得到了如下的报错提示框,刚刚得到的CPU控制权就这样丢失了。

图一、内存搜索错误


图二、异常详细信息

因此必须实现对进程空间的安全搜索,最终找到实现功能的ShellCode,并在那里安全着陆。对于这样Egg-Hunt的ShellCode的编写有如下三个基本要求:
搜索程序的鲁棒性:这样的要求主要体现在搜索程序必须要有处理“非法地址”访问的能力,才能够安全地搜索进程地址空间,否则对无效地址的访问将导致被溢出进程的崩溃。

搜索程序的简洁性:汇编代码的体积是编写搜索程序时需要考虑的一个非常重要的方面,因为绕过溢出环境对Search Code大小的限制是我们使用这种功两段式ShellCode的原因。

搜索程序的高效性:溢出的时候我们当然希望能够尽快跳转到ShellCode去执行我们希望的操作。否则将留下一个不能得到及时更新的屏幕将会是溢出特征的一个最好提示。

在以上三个对Egg-hunt的要求中,程序的鲁棒性是最重要的,因为在执行对进程地址空间搜索的时候,非法地访问毕竟是我们最常遇到的问题。我们当然不希望由于搜索内存失败而造成程序的崩溃,使我们丧失了执行ShellCode的机会。
那么下面我们将主要探讨一些在Windows平台下,常用的安全搜索内存空间的方法。

一、利用SHE机制安全搜索内存空间:
Windows系统中的SEH机制是大家在进行溢出攻击时常常利用的一种机制。在这里我们在执行进程空间搜索之前首先注册一个异常处理函数,用它来捕获并完成对“非法地址访问”及各种“运行时异常”的处理,以提高搜索程序的鲁棒性,使我们能够顺利地找到进程空间中的ShellCode。

为了对这样的方法的原理有更好的理解,我们再将Windows 系统中异常处理方面的内容再复习一下。
1、Windows的结构化异常处理(SEH)
Windows在创建线程时,操作系统会为每个线程分配TEB结构,并且将FS段选择器指向当前线程的TEB数据结构(结构的定义可以参见参考资料)。

Typedef struct _NT_TIB {
Struct _EXCEPTION_REGISTERATION_RECORD * ExceptionList;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
Union {
PVOID FiberData;
ULONG Version;
};
PVOID ArbitaryUserPointer;
Struct _NT_TIB * Self;
} NT_TIB;

偏移为0的_EXCEPTION_REGISTERATION_RECORD主要用于处理SHE,因此我们使用FS:[0]也就能够访问SEH。其结构定义如下:

EXCEPTION_REGISTERATION Struct
Prev dd ? 前一个EXCEPTION_REGISTERATIONj结构
Hander dd ? 异常处理例程入口
_EXCEPTION_REGISTERATION end };

其中prev指向前一个_EXCEPTION_REGISTERATION的指针,因此线程的异常处理例程形成了一个链状结构。当系统处理异常时就查找异常处理链表,调用相应的异常处理函数执行对程序异常的处理。

2、异常信息
当一个异常发生时,操作系统向异常处理的线程堆栈中压入3个结构,这三个结构是:EXCEPTION_RECORD,CONTEXT, EXCEPTION_POINTERS。
1、 EXCEPTION_ RECORD结构
EXCEPTION_ RECORD结构包含了有关最近发生异常的详细信息,这些信息独立于CPU。其结构如下:

EXCEPTION_ RECORD STRUCT {
+0 DWORD ExceptionCode ;异常代码
+4 DWORD ExceptionFlags ;异常标志
+8 struct EXCEPTION_RECORD ;指向另一个EXCEPTION_ RECORD的指针
+C PVOID ExceptionAddress ;异常发生的地址
+10 DWORD NumberParatemeters ;与异常联系的参数数量(0-15)
+14 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]
} EXCEPTION_ RECORD

其中,ExceptionCode字段定义了产生异常的原因。图二中表示程序引发了一个EXCEPTION_ACCESS_VIOLATION异常,异常代码为0xC0000005,查阅该异常代码后我们知道程序读写了一个没有可读写属性的地址。

3、CONTEXT结构
CONTEXT结构包含了特定处理器的寄存器数据,系统使用CONTEXT结构来执行各种内部操作。该结构可参考WinNt.h的定义,在X86平台下该结构定义为CONTEXT86,后面将直接用CONTEXT代指CONTEXT86。因为由于该结构比较长,出于篇幅考虑只列出来结构的部分成员:

Typedef struct _ CONTEXT86{
……
//通用寄存器
+9C DWORD Edi;
+A0 DWORD Esi;
+A4 DWORD Ebx;
+A8 DWORD Edx;
+AC DWORD Ecx;
+B0 DWORD Eax;
//控制寄存器
+B4 DWORD Ebp;
+B8 DWORD Eip;
+BC DWORD SegCs;
+C0 DWORD EFlags;
+C4 DWORD Esp;
+C8 DWORD SegSs;
……
} CONTEXT86;

CONTEXT结构非常重要,通过修改CONTEXT结构中的成员,可以使程序能够在异常时执行相应的处理工作,使程序能够继续执行。在Windows Xp系统中,系统调用NTDLL.DLL模块中的KiUserExceptionDIspatcher函数来执行异常处理。该函数最终将程序中注册的异常处理函数的地址装入ECX,然后调用执行。此时堆栈的分布情况如下:

Esp+0x0 -> *EXCEPTION_RECORD
Esp+0x4 -> *_EXCEPTION_REGISTRATION( 简称ERR)
Esp+0x8 -> *CONTEXT record
Esp+0xC -> *Param

如果注册的异常处理函数将CONTEXT结构中保存的EIP值修改到一个“安全的地方”,那么执行完异常处理函数后,程序将返回到“安全代码”中去执行。

下面的例子程序将在执行搜索内存的操作之前向异常处理链表中注册一个异常处理函数,用于在发生地址访问错误的时候修正程序的执行路径,修改EIP的值为发生访问违例(执行内存比较)的下一条指令地址。这样程序将能够安全、顺利地执行完搜索内存的操作。

为了弄清楚异常发生时系统的状态我们进行如下跟踪分析:
① 在Ollydbg中打开程序Seh.exe,由于搜索的起始地址为0x200000将会导致一个访问异常。我们OllyDbg的命令窗口中输入bp 0x7C9237BD,在Windows Xp系统将要调用异常处理函数时断下来观察堆栈的状态(Windows 2000 中是 0x77F8E43E)。此时程序的状态如图三所示:


图三、KiUserExceptionDIspatcher函数调用异常处理时的堆栈状态

此时Esp的值为0x12FBF0,该地址指向EXCEPTION_RECORD结构,结构偏移为0x0处保存了异常代码0xC0000005,偏移为0xC处保存了异常发生的地址0x401017,该指令是我们执行模式匹配的指令scasd。Esp+4(0x12FFBC)保存了指向下一个异常处理结构的指针。Esp+8(0x12FCF0)保存了指向CONTEXT结构的指针,该结构偏移为0xB8处保存了异常发生时EIP寄存器的值0x401017,偏移为0x9C处保存了Edi寄存器的值0x200000。CONTEXT保存的各寄存器的状态可参见图四。

图四、异常发生时CONTEXT结构的状态

②按下F7后执行预定义的异常处理函数。该函数将修正CONTEXT中保存的EIP的值为指令add ebx, 01000h在内存中的地址(0x0040101C)。当程序恢复执行时,将从该指令继续执行内存空间的搜索操作。修改后的CONTEXT状态可参见图五。

图五、修正后CONTEXT结构的状态

通过对内存访问异常的处理可以使ShellCode能够安全地执行搜索功能,最终找到我们的“Egg”。

本文中附带的程序实际上只是一个用于说明的代码,由于没有判断异常产生的原因因此在实际应用中有一定的问题。聪明的读者也许会发现如果在OllyDbg中调试该程序会失败,仔细分析后我们发现,导致该程序调试失败的原因是执行单步跟踪时系统也会产生一个异常EXCEPT_SINGLE_STEP(参看此时结构EXCEPTION_ RECORD中保存的异常代码为0x80000004)。由于我们的异常处理程序进行区别处理,因此程序执行失败。

另外,由于Windows XP中对异常处理的安全性进行了增强,如果异常处理函数在堆栈中,系统即认为是不安全的,因此我们的异常处理例程不会得到执行。

二、使用IsBadReadPtr函数安全搜索内存空间:
IsBadReadPtr是模块Kernel32.dll中导出的一个函数,其功能是用于判读进程是否拥有对指定内存地址段的读权限。如果具有对某段地址空间的读权限则返回0,否则返回非0。该函数原型如下:

BOOL IsBadReadPtr(
CONST VOID *lp, // address of memory block
UINT ucb // size of block
);

使用这种方法进行搜索时,首先调用API函数IsBadReadPtr判断进程是否具有对指定地址范围的读权限,如果可读则执行标志位判断操作,否则继续寻找下一个地址空间。

细心的读者也许已经注意到程序中有两次重复的scasd,jnz search_loop指令,这样做的目的主要是由于指令中出现过0x50905090。因此我们将搜索标志设置为8字节,即两次重复的0x50905090,以保证程序能真正找到ShellCode而不是匹配代码段中的标志字节后停下来。实际上,在使用SHE进行查找时也处理了类似的问题,在上一种方法中是使用以下指令来完成两次匹配的过程。

mov ecx, 2h
repe scasd

另外,使用IsBadReadPtr进行搜索时会感觉到搜索速度比较慢。笔者对此进行了跟踪,发现当访问没有读权限的空间时,调试器会提示“访问违例在Kernerl32中,根据请求已忽略”。调试器会因此出现短暂的停顿,造成了搜索速度要慢一些。查阅了一定的资料后,造成这个问题的原因是由于对内存地址访问的竞争引起的。

三、使用NtDisplayString安全搜索内存空间:
NtDisplayString是Windows NT、Windows 2000/Xp系统下提供的一个Native API函数,因此这种方法也不适用于Windows 9x系统。系统内核用该函数将字符串输出到屏幕(Text-mode),系统蓝屏时输出到屏幕的字符就是通过该函数完成的。该函数原型如下:

NTSYSAPI NTSTATUS NTAPI NtDisplayString(
IN PUNICODE_STRING String
)

我们之所以能使用该函数用于执行安全内存空间搜索的主要原因是:该函数从唯一的一个参数中读取数据并且没有写操作。如果函数参数指向的地址不可读,那么函数将返回内存访问异常(0xc000000005)的错误代码。
另外需要说明的是,int 2E是用于请求实现于 ntoskrnl.exe 的内部Native API 函数的软件中断。执行NtDisplayString调用的代码片段如下:

push edx
push 43h
pop eax
int 2eh

随同INT 2Eh传入的是EAX 和 EDX 的两个寄存器参数。Edx保存的是待校验的地址指针的值,EAX 中保存的是函数入口表的索引ID。系统处理 INT 2eh 时,根据EAX的值确定每个调用将被分配到那个函数。程序根据调用后的返回值是否为0xC0000005来判断该地址是否可读,搜索程序的其他流程与上一种方法基本相同,在这里就不详细说了。

四、总结
从上面描述的三种搜索内存的方法看,使用NtDisplayString来搜索内存空间是速度最快,占用字节数最少,鲁棒性最好的方法。尽管该方法不能在Win 9x下使用,但是毕竟目前使用Win 9x的用户已经很少了。笔者在实际编写ShellCode时最后也选用了这种方法。
安全搜索内存的问题已经到此算是成功解决了,通过对这个问题的研究,总结起来感觉还是学到了不少东西。同时也体会到一些黑客前辈们对技术的不懈追求,在这里谨向他们表示敬意。

小提示:编译本文附带的演示文件时请使用Masm32。

相关日志

发表评论