缓冲区溢出进击

    添加时间:2013-8-12 点击量:

    缓冲区溢出进击


     


    缓冲区溢出(Buffer Overflow)是策画机安然范畴内既经典而又古老的话题。跟着策画机体系安然性的加强,传统的缓冲区溢出进击体式格式可能变得不再奏效,响应的介绍缓冲区溢出道理的材料也变得“公共化”起来。此中看雪的《0day安然:软件漏洞解析技巧》一书将缓冲区溢出进击的道理论说得简洁了然。本文参考该书对缓冲区溢出道理的讲解,并连络实际的代码实例进行验证。不过即便如此,完成一个简单的溢出代码也须要解决很多书中无法涉及的题目,尤其是面对较新的具有安然特点的编译器——比如MSVisual Studio2010。接下来,我们连络具体代码,遵守对缓冲区溢出道理的循序渐进地懂得体式格式去发掘缓冲区溢出背后的底层机制。


    一、代码 <=> 数据


    顾名思义,缓冲区溢出的含义是为缓冲区供给了多于其存储容量的数据,就像往杯子里倒入了过量的水一样。凡是景象下,缓冲区溢出的数据只会破损法度数据,造成不测终止。然则若是有人精心计表情关溢出数据的内容,那么就有可能获得体系的把握权!若是说用户(也可能是黑客)供给了水&#8212;&#8212;缓冲区溢出进击的数据,那么体系供给了溢出的容器&#8212;&#8212;缓冲区。


    缓冲区在体系中的发挥解析情势是多样的,高等说话定义的变量、数组、布局体等在运行时可以说都是保存在缓冲区内的,是以所谓缓冲区可以更抽象地懂得为一段可读写的内存区域,缓冲区进击的终极目标就是体系能履行这块可读写内存中已经被蓄意设定好的恶意代码。遵守冯&#183;诺依曼存储法度道理,法度代码是作为二进制数据存储在内存的,同样法度的数据也在内存中,是以直接从内存的二进制情势上是无法区分哪些是数据哪些是代码的,这也为缓冲区溢出进击供给了可能。




    1 过程地址空间分布


    1是过程地址空间分布的简单默示。代码存储了用户法度的所有可履行代码,在法度正常履行的景象下,法度计数器(PC指针)只会在代码段和操纵体系地址空间(内核态)内寻址。数据段内存储了用户法度的全局变量,文字池等。栈空间存储了用户法度的函数栈帧(包含参数、局部数据等),实现函数调用机制,它的数据增长标的目标是低地址标的目标。堆空间存储了法度运行时动态申请的内存数据等,数据增长标的目标是高地址标的目标。除了代码段和受操纵体系保护的数据区域,其他的内存区域都可能作为缓冲区,是以缓冲区溢出的地位可能在数据段,也可能在堆、栈段。若是法度的代码有软件漏洞,恶意法度会&#8220;指导&#8221;法度计数器从上述缓冲区内取指,履行恶意法度供给的数据代码!本文解析并实现栈溢出进击体式格式。


    二、函数栈帧


    栈的首要功能是实现函数的调用。是以在介绍栈溢出道理之前,须要弄清函数调用时栈空间产生了如何的变更。每次函数调用时,体系会把函数的返回地址(函数调用指令后紧跟指令的地址),一些关键的存放器值保存在栈内,函数的实际参数和局部变量(包含数据、布局体、对象等)也会保存在栈内。这些数据统称为函数调用的栈帧,并且是每次函数调用都邑有个自力的栈帧,这也为递归函数的实现供给了可能。




    2 函数栈帧


    如图所示,我们定义了一个简单的函数function,它接管一个整形参数,做一次乘法操纵并返回。当调用function(0)时,arg参数记录了值0入栈,并将call function指令下一条指令的地址0 x00bd16f0保存到栈内,然后跳转到function函数内部履行。每个函数定义都邑有函数头和函数尾代码,如图绿框默示。因为函数内须要用ebp保存函数栈帧基址,是以先保存ebp本来的值到栈内,然后将栈指针esp内容保存到ebp。函数返回前须要做相反的操纵&#8212;&#8212;将esp指针恢复,并弹出ebp。如许,函数内正常景象下无论如何应用栈,都不会使栈落空均衡。


    sub esp,44h指令为局部变量开辟了栈空间,比如ret变量的地位。理论上,function只须要再开辟4字节空间保存ret即可,然则编译器开辟了更多的空间(这个题目很诡异,你感觉呢?)。函数调用停止返回后,函数栈帧恢复到保存参数0时的状况,为了对峙栈帧均衡,须要恢复esp的内容,应用add esp,4将压入的参数弹出。


    之所以会有缓冲区溢出的可能,主如果因为栈空间内保存了函数的返回地址。该地址保存了函数调用停止后后续履行的指令的地位,对于策画机安然来说,该信息是很敏感的。若是有人恶意批改了这个返回地址,并使该返回地址指向了一个新的代码地位,法度便能从其它地位持续履行。


    三、栈溢出基起原根蒂根基理


    上边给出的代码是无法进行溢出操纵的,因为用户没有&#8220;插足&#8221;的机会。然则实际上很多法度都邑接管用户的外界输入,尤其是当函数内的一个数组缓冲区接管用户输入的时辰,一旦法度代码未对输入的长度进行合法性搜检的话,缓冲区溢出便有可能触发!比如下边的一个简单的函数。






    void fun(unsigned char data)
    {
        unsigned char buffer[BUF_LEN];
        strcpy((char)buffer,(char)data);//溢出点
    }

    这个函数没有做什么有&#8220;意义&#8221;的工作(这里主如果为了简化题目),然则它是一个典范的栈溢出代码。在应用不安然的strcpy库函数时,体系会盲目地将data的全部数据拷贝到buffer指向的内存区域。buffer的长度是有限的,一旦data的数据长度跨越BUF_LEN,便会产生缓冲区溢出。




    3 缓冲区溢出


    因为栈是低地址标的目标增长的,是以局部数组buffer的指针在缓冲区的下方。当把data的数据拷贝到buffer内时,跨越缓冲戋戋域的高地址项目组数据会&#8220;覆没&#8221;底本的其他栈帧数据,按照覆没数据的内容不合,可能会有产生以下景象:


    1、覆没了其他的局部变量。若是被覆没的局部变量是前提变量,那么可能会改变函数底本的履行流程。这种体式格式可以用于简单的软件验证。


    2、覆没了ebp的值。批改了函数履行停止后要恢复的栈指针,将会导致栈帧落空均衡。


    3、覆没了返回地址。这是栈溢出道理的核心肠点,经由过程覆没的体式格式批改函数的返回地址,使法度代码履行&#8220;不测&#8221;的流程!


    4、覆没参数变量。批改函数的参数变量也可能改变当前函数的履行成果和流程。


    5、覆没上级函数的栈帧,景象与上述4点类似,只不过影响的是上级函数的履行。当然这里的前提是包管函数能正常返回,即函数地址不克不及被随便批改(这可能很麻烦!)。


    若是在data本身的数据内就保存了一系列的指令的二进制代码,一旦栈溢出批改了函数的返回地址,并将该地址指向这段二进制代码的其实地位,那么就完成了根蒂根基的溢出进击行动。




    4 根蒂根基栈溢出进击


    经由过程策画返回地址内存区域相对于buffer的偏移,并在对应地位机关新的地址指向buffer内部二进制代码的其实地位,便能履行用户的自定义代码!这段既是代码又是数据的二进制数据被称为shellcode,因为进击者经由过程这段代码打开体系的shell,以履行随便率性的操纵体系号令&#8212;&#8212;比如病毒,安装木马,开放端口,格局化磁盘等恶意操纵。


    四、栈溢出进击


    上述过程固然理论上能完成栈溢出进击行动,然则实际上很难实现。操纵体系每次加载可履行文件到过程空间的地位都是无法猜测的,是以栈的地位实际是不固定的,经由过程硬编码覆盖新返回地址的体式格式并不成靠。为了能准断定位shellcode的地址,须要借助一些额外的操纵,此中经典的是借助跳板的栈溢出体式格式。


    按照前边所述,函数履行后,栈指针esp会恢复到压入参数时的状况,在图4中即data参数的地址。若是我们在函数的返回地址填入一个地址,该地址指向的内存保存了一条特别的指令jmp esp&#8212;&#8212;跳板。那么函数返回后,会履行该指令并跳转到esp地点的地位&#8212;&#8212;即data的地位。我们可以将缓冲区再多溢出一项目组,覆没data如许的函数参数,并在这里放上我们想要履行的代码!如许,不管法度被加载到哪个地位,终极都邑回来履行栈内的代码。




    5 借助跳板的栈溢出进击


    借助于跳板的确可以很好的解决栈帧移位(栈加载地址不固定)的题目,然则跳板指令从哪找呢?&#8220;荣幸&#8221;的是,在Windows操纵体系加载的多量dll中,包含了很多如许的指令,比如kernel32.dllntdll.dll,这两个动态链接库是Windows法度默认加载的。若是是图形化界面的Windows法度还会加载user32.dll,它也包含了多量的跳板指令!并且更&#8220;神奇&#8221;的是Windows操纵体系加载dll时辰一般都是固定地址,是以这些dll内的跳板指令的地址一般都是固定的。我们可以离线搜刮出跳板履行在dll内的偏移,并加上dll的加载地址,便获得一个实用的跳板指令地址!




    //查询dll内第一个jmp esp指令的地位
    int findJmp(chardll_name)
    {
        char handle=(char)LoadLibraryA(dll_name);//获取dll加载地址
        forint pos=0;;pos++)//遍历dll代码空间
        {
            if(handle[pos]==(char0 xff&&handle[pos+1]==(char0 xe4//寻找0 xffe4 = jmp  esp
            {
                return (int)(handle+pos);
            }
        }
    }

    这里简化了搜刮算法,输出第一个跳板指令的地址,读者可以拔取其他更合适地位。LoadLibraryA库函数返回值就是dll的加载地址,然后加上搜刮到的跳板指令偏移pos便是终极地址。jmp esp指令的二进制默示为0 xffe4,是以搜刮算法就是搜刮dll内如许的字节数据即可。


    固然如此,上述的进击体式格式还不敷好。因为在esp后持续追加shellcode代将上级函数的栈帧覆没,如许做并没有什么益处,甚至可能会带来运行时题目。既然被溢出的函数栈帧内供给了缓冲区,我们还是把核心的shellcode放在缓冲区内,而在esp之后放上跳转指令转移到底本的缓冲区地位。因为如许做使代码的地位在esp指针之前,若是shellcode中应用了push指令便会让esp指令与shellcode代码越来越近,甚至覆没自身的代码。这显然不是我们想要的成果,是以我们可以强迫举高esp指针,使它在shellcode之前(低地址地位),如许就能在shellcode内正常应用push指令了。




    6 调剂shellcode与栈指针


    调剂代码的内容很简单:




    add esp,-X
    jmp esp

    第一条指令举高了栈指针到shellcode之前。X代表shellcode肇端地址与esp的偏移。若是shellcode从缓冲区肇端地位开端,那么就是buffer的地址偏移。这里不应用sub esp,X指令主如果避免X的高位字节为0的题目,很多景象下缓冲区溢出是针对字符串缓冲区的,若是呈现字节0会导致缓冲区截断,从而导致溢出失败。


    第二条指令就是跳转到shellcode的肇端地位持续履行。(又是jmp esp!)


    经由过程上述体式格式便能获得一个较为稳定的栈溢出进击。


    五、shellcode机关


    shellcode本质是指溢出后履行的能开启体系shell的代码。然则在缓冲区溢出进击时,也可以将全部触发缓冲区溢出进击过程的代码统称为shellcode,遵守这种定义可以把shellcode分为四项目组:


    1、核心shellcode代码,包含了进击者要履行的所有代码。


    2、溢出地址,是触发shellcode的关键地点。


    3、填充物,填充未应用的缓冲区,用于把握溢出地址的地位,一般应用nop指令填充&#8212;&#8212;0 x90默示。


    4、停止符号0,对于符号串shellcode须要用0结尾,避免溢出时字符串异常。


    前边一向在环绕溢出地址评论辩论,并解决了shellcode组织的题目,而核心的代码如何机关并未说起&#8212;&#8212;即进击成功后做的工作。其实一旦缓冲区溢出进击成功后,若是被进击的法度有体系的root权限&#8212;&#8212;比如体系办事法度,那么进击者根蒂根基上可以随心所欲了!然则我们须要清楚的是,核心shellcode必须是二进制代码情势。并且shellcode履行时是在长途的策画机上,是以shellcode是否能通用是一个很错杂的题目。我们可以用一段简单的代码实例来申明这个题目。


    缓冲区溢出成功后,一般大师都邑开启一个长途的shell把握被进击的策画机。开启shell最直接的体式格式便是调用C说话的库函数system,该函数可以履行操纵体系的号令,就像我们在号令行下履行号令那样。假如我们履行cmd号令&#8212;&#8212;在长途策画机上启动一个号令提示终端(我们可能还不克不及和它交互,然则可以在这之前建树一个长途管道等),这里仅作为实例测试。


    为了使system函数调用成功,我们须要将&#8220;cmd&#8221;字符串内容压入栈空间,并将其地址压入作为system函数的参数,然后应用call指令调用system函数的地址,完成函数的履行。然则如许做还不敷,若是被溢出的法度没有加载C说话库的话,我们还须要调用WindowsAPI Loadlibrary加载C说话的库msvcrt.dll,类似的我们也须要为字符串&#8220;msvcrt.dll&#8221;开辟栈空间。


     



    xor ebx,ebx ;//ebx=0

    push 0 x3f3f6c6c ;//ll??
    push 0 x642e7472 ;//rt.d
    push 0 x6376736d ;//msvc
    mov [esp+10],ebx ;//?->0
    mov [esp+11],ebx ;//?->0
    mov eax,esp ;//msvcrt.dll地址
    push eax ;//msvcrt.dll
    mov eax,0 x77b62864 ;//kernel32.dll:LoadLibraryA
    call eax ;//LoadLibraryA(msvcrt.dll)
    add esp,16

    push 0 x3f646d63 ;//cmd?
    mov [esp+3],ebx ;//?->\0
    mov eax,esp;//cmd地址
    push eax ;//cmd
    mov eax,0 x774ab16f ;//msvcrt.dll:system
    call eax ;//system(cmd)
    add esp,8

    上述汇编代码本质上是如下两个函数调用语句:




    Loadlibrary(&#8220;msvcrt.dll&#8221;);
    system(&#8220;cmd&#8221;);

    不过在机关这段汇编代码时须要重视不克不及呈现字节0,为了填充字符串的停止字符,我们应用已经初始化为0ebx存放器庖代。别的,在对库函数调用的时辰须要提前策画出函数的地址,如Loadlibrary函数的0 x77b62864。策画体式格式如下:




    int findFunc(chardll_name,charfunc_name)
    {
        HINSTANCE handle=LoadLibraryA(dll_name);//获取dll加载地址
        return (int)GetProcAddress(handle,func_name);
    }

    这个函数地址是在本地策画的,若是被进击策画机的操纵体系版本差别较大的话,这个地址可能是错误的。不过在《0day安然:软件漏洞解析技巧》中,作者供给了一个更好的体式格式,感爱好的读者可以参考该书供给的代码。是以机关一个通用的shellcode并非十分轻易,若是想让进击变得有效的话。


    六、汇编说话主动转换


    写出shellcode后(无论是简单的还是通用的),我们还须要将这段汇编代码转换为机械代码。若是读者对x86汇编十分熟悉的话,选择手工敲出二进制代码的话也未尝不成。不过我们都能让策画机帮助做完这些事,既然开辟景象供给了编译器,用它们辅佐何乐而不为呢?既不消OllyDbg对象,也不实用其他的第三方对象,我们写一个简单的函数来完成这个工作。


     



    //将内嵌汇编的二进制指令dump到文件,style指定输出数组格局还是二进制情势,返回代码长度
    int dumpCode(unsigned charbuffer)
    {
        goto END ;//略过汇编代码
    BEGIN:
        __asm
        {
            //在这里定义随便率性的合法汇编代码
            
        }
    END:
        //断定代码局限
        UINT begin,end;
        __asm
        {
            mov eax,BEGIN ;
            mov begin,eax ;
            mov eax,END ;
            mov end,eax ;
        }
        //输出
        int len=end-begin;
        memcpy(buffer,(void)begin,len);
            //四字节对齐
        int fill=(len-len%4)%4;
        while(fill--)buffer[len+fill]=0 x90;
        //返回长度
        return len+fill;
    }

    因为C++是支撑嵌入式汇编代码的,是以在函数内的汇编代码都邑被整成编译为二进制代码。实现二进制转换的根蒂根基思惟是读取编译器最毕生成的二进制代码段数据,将数据导出到指定的缓冲区内。为了锁定嵌入式汇编代码的地位和长度,我们定义了两个标签BEGINEND。这两个标签在汇编说话级别会被解析为实际的线性地址,然则在高等说话级是无法直接应用这两个标签值的,只能应用goto语句跳转应用它们。然则我们可以顺水推舟,应用两个局部变量在汇编级记录这两个标签的值!




    //断定代码局限
    UINT begin,end;
    __asm
    {
        mov eax,BEGIN ;
        mov begin,eax ;
        mov eax,END ;
        mov end,eax ;
    }

    如许就可以获得嵌入式汇编的代码局限了,应用memcpy操纵将代码数据拷贝到目标缓冲区即可(后边还用nop指令将代码遵守四字节对齐)。不过我们还须要重视一个题目,嵌入式汇编在函数履行时也会履行,这显然不成以,我们只是把它算作数据罢了(是数据?还是代码?),是以在函数开端的处所我们应用goto语句直接跳转到嵌入式会变语句的结尾&#8212;&#8212;END标签!


    七、进击测试


    遵守上述内容,信赖不难机关出一个简单的shellcode并进击之前提供的漏洞函数。然则若是应用VS2010测试的话可能会碰着很多题目。经过多量的调试和材料查询,我们须要设置三处VS的项目属性。


    1、设备->设备属性->C/C++->根蒂根基运行时搜检=默认值,避免被检测栈帧失衡。


    2、设备->设备属性->C/C++->缓冲区安然搜检=否,避免辨认缓冲区溢出漏洞。


    3、设备->设备属性->链接器->高等->数据履行保护(DEP)=否,避免客栈段不成履行。


    从这三处设置看来,今朝的编译器已经针对缓冲区溢出进击做了多量的保护工作(显然这会降落法度的履行机能,是以容许用户设备),使得传统的缓冲区溢出进击变得没那么&#8220;猖獗&#8221;了,然则在策画机安然范畴,&#8220;道高一尺,魔高一丈&#8221;,总有人会找到更隐蔽的进击体式格式让编译器开辟者措手不及。本文除了解析缓冲区溢出进击的道理之外,更读者能从中感触感染到代码安然的首要性,并连络编译器供给的安然功能让本身的代码加倍安然高效。

    文艺不是炫耀,不是花哨空洞的文字堆砌,不是一张又一张的逆光照片,不是将旅行的意义转化为名牌包和明信片的物质展示;很多时候它甚至完全不美——它嘶吼、扭曲,它会痛苦地抽搐,它常常无言地沉默。——艾小柯《文艺是一种信仰》
    分享到: