通过对内存访问异常的处理可以使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。