Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1549441
  • 博文数量: 157
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 4116
  • 用 户 组: 普通用户
  • 注册时间: 2013-06-14 18:26
文章分类

全部博文(157)

文章存档

2014年(67)

2013年(90)

分类: 网络与安全

2013-07-22 14:39:28

ShellCode的编写好像是一个永远说不完的话题。最近在编写一个漏洞的利用程序时,我遇到了在进程空间中安全搜索ShellCode的问题。在网上搜索了一番也没有什么有用的东西,在为这个问题困扰了许久之后,我请教了一位大虾后才知道,原来国外的一些大牛们早就作过这方面的深入研究了。在参阅了一些相关资料后,终于解决了如何安全搜索内存空间的问题。好东西不敢独享,将其写出来献给如我一样正在艰难学习中的菜鸟,大虾看了就飘过吧!
很多情况下,在编写ShellCode的时候,我们都会面临着如何安全地搜索进程空间,找到我们可爱的没有被破坏的ShellCode的问题。例如,有些溢出程序对编码有着特殊要求,比如会将0x2F(‘/’)转化为0x5C(‘\’)等等的情况,甚至还有不能出现0xFF的情况。这个时候可能不管你怎么变形都无法符合要求;有些文件格式型的溢出,由于文件结构自身的特点,在溢出点附近不可能放入大的ShellCode。溢出产生的时候,我们都必须用一个简短的Search Code查找到原始的ShellCode后,再执行这个功能ShellCode来实现我们的溢出利用。这种通过设置一定的标志字节,由小 ShellCode在内存中查找大ShellCode的方法有一个有趣的叫法:Egg-Hunt。
在栈溢出利用时,EIP开始在堆栈里执行后,由于堆栈在内存中的地址往往比较低,如0x0012XXXX,而原始ShellCode却在 0x07BAXXXX远的高地址。直接搜索内存的时候会由于违规访问未分配内存、用户断点、浮点异常等问题而造成系统崩溃。于是在我们好不容易获得CPU 控制权之际,却得到了如图1和图2所示的报错提示框,刚刚得到的CPU控制权就这样丢失了。
 
图1 内存搜索错误
 
图2 异常详细信息
因此,我们必须实现对进程空间的安全搜索,最终找到实现功能的ShellCode,并在那里安全着陆。对于这样Egg-Hunt的ShellCode的编写,有如下三个基本要求:
搜索程序的鲁棒性:这样的要求主要体现在搜索程序必须要有处理“非法地址”访问的能力,才能够安全地搜索进程地址空间,否则对无效地址的访问将导致被溢出进程的崩溃。
搜索程序的简洁性:汇编代码的体积是编写搜索程序时需要考虑的一个非常重要的方面,因为绕过溢出环境对Search Code大小的限制是我们使用这种功两段式ShellCode的原因。
搜索程序的高效性:溢出的时候,我们当然希望能够尽快跳转到ShellCode去执行我们希望的操作,否则留下一个不能得到及时更新的屏幕,将会是溢出特征的一个最好提示。
在以上三个对Egg-hunt的要求中,程序的鲁棒性是最重要的,因为在执行对进程地址空间搜索的时候,非法的访问毕竟是我们最常遇到的问题。我们当然不希望由于搜索内存失败而造成程序的崩溃,使我们丧失了执行ShellCode的机会。
下面我们将主要探讨一些在Windows平台下,常用的安全搜索内存空间的方法。
 
利用SHE机制安全搜索内存空间
Windows系统中的SEH机制是大家在进行溢出攻击时常常利用的一种机制。在这里,我们在执行进程空间搜索之前首先注册一个异常处理函数,用它来捕获并完成对“非法地址访问”及各种“运行时异常”的处理,以提高搜索程序的鲁棒性,使我们能够顺利地找到进程空间中的ShellCode。
为了对这样的方法的原理有更好的理解,我们再将Windows 系统中异常处理方面的内容复习一下。
1)Windows的结构化异常处理(SEH)
Windows在创建线程时,操作系统会为每个线程分配TEB结构,并且将FS段选择器指向当前线程的TEB数据结构(结构的定义可以参见参考资料)。
view plainprint?

    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]就能够访问SHE了。其结构定义如下:
EXCEPTION_REGISTERATION Struct{
Prev dd ;前一个EXCEPTION_REGISTERATION结构
Hander dd ;异常处理例程入口
_EXCEPTION_REGISTERATION end};
其中prev指向前一个_EXCEPTION_REGISTERATION的指针,因此线程的异常处理例程就形成了一个链状结构。当系统处理异常时就查找异常处理链表,调用相应的异常处理函数执行对程序异常的处理。
2)异常信息
当一个异常发生时,操作系统向异常处理的线程堆栈中压入3个结构,这三个结构是EXCEPTION_RECORD、CONTEXT和EXCEPTION_POINTERS。
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字段定义了产生异常的原因。图2即表示程序引发了一个EXCEPTION_ACCESS_VIOLATION异常,异常代码为0xC0000005,查阅该异常代码后,我们知道程序读写了一个没有可读写属性的地址。
CONTEXT结构包含了特定处理器的寄存器数据,系统使用CONTEXT结构来执行各种内部操作。该结构可参考Winnt.h的定义,在 X86平台下该结构定义为CONTEXT86,后面将直接用CONTEXT代指CONTEXT86。由于该结构比较长,出于篇幅考虑,我只列出结构的部分成员。
view plainprint?

    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)。此时程序的状态如图3所示。
 
图3 KiUserExceptionDIspatcher函数调用异常处理时的堆栈状态
此时ESP的值为0x12FBF0,该地址指向EXCEPTION_RECORD结构,结构偏移为0x0处保存了异常代码 0xC0000005,偏移为0xC处保存了异常发生的地址0x401017,该指令是我们执行模式匹配的指令scasd。 ESP+4(0x12FFBC)保存了指向下一个异常处理结构的指针;ESP+8(0x12FCF0)保存了指向CONTEXT结构的指针,该结构偏移为 0xB8处保存了异常发生时EIP寄存器的值0x401017,偏移为0x9C处保存了EDI寄存器的值0x200000。CONTEXT保存的各寄存器的状态可参见图4。
 
图4 异常发生时CONTEXT结构的状态
按下F7后,执行预定义的异常处理函数。该函数将修正CONTEXT中保存的EIP的值为指令“add    ebx,01000h”在内存中的地址(0x0040101C)。当程序恢复执行时,将从该指令继续执行内存空间的搜索操作。修改后的CONTEXT状态可参见图5。
 
图5 修正后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       scads
另外,使用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来搜索内存空间是速度最快,占用字节数最少,鲁棒性最好的方法。尽管该方法不能在Windows 9x下使用,但毕竟目前使用Windows 9x的用户已经很少了。笔者在实际编写ShellCode时,最后也选用了这种方法。
安全搜索内存的问题到此就算是成功解决了,通过对这个问题的研究,总结起来感觉还是学到了不少东西;同时也体会到了一些黑客前辈们对技术的不懈追求,在这里谨向他们表示敬意。
注:编译本文附带的演示文件时请使用Masm32。
阅读(1560) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~