长亭百川云 - 文章详情

老生常谈杀软特性 免杀数字你也行

Ting丶

39

2024-08-24

#首先感谢Gatsby师傅最近一直和我一起深入研究免杀、也跟我讲了一些杀软的特性。一起学习、一起成长的过程确实很有趣。

#当有个朋友一起学习研究,一起进步的速度是飞快的。

#WingBy小密圈简介在文末。

简介

#文章耗时5小时,总计6300字

这篇文章主题无疑是免杀,但是不会仅仅针对某一钟技术或者思路,可能会同时讨论多种技术。

读完你会收获这些内容:

免杀学习的方法、免杀的一些小的,但是很有威力的知识点。

一些学习免杀、写免杀🐎、对抗杀软的一些技术的普遍误区。

一些老生常谈技术的升级版。

不再畏惧某数字杀软。

免杀的宗旨

所谓免杀,字面意思免受杀软的查杀。正常的杀软,不会杀什么程序?那当然是正常程序(hello world除外....),然而我们较难的去伪造成正常程序,因为他本质上就是木马,当然你可以使用patch技术,向正常程序植入你的shellcode,这样看起来正常多了。

那么如果是一个他看不穿的程序呢,他无法判断这个程序是不是恶意的?那么他会杀吗,我个人觉得是不一定杀的,因为按照常识每天都会有各种各样的程序被编译出来,他不可能每个程序都认识,他如果不认识就杀,那除了服务器谁会装那玩意儿。因此我喜欢对程序做一些处理,尽量让杀软无法判断我的程序是做什么用的,尽量让他不认识我。

第一问:杀软如何去认识一个程序?

首先是比对md5,比对可执行程序的静态资源,某绒就是”典型“的例子。他会比对PE文件的一些字段例如.data .text等字段有没有什么可疑的字符有则杀,这里推荐使用工具virtest进一步验证这个理论。

一次有趣的经历,我写了一个常见的unhook ntdll,一落地杀了,我跟朋友说,他不会杀这段unhook ntdll代码吧,后面我继续写,就加了个函数,火绒又不杀了。他可能就是比对hash,有比对到近似的地方就杀了。

对于PE文件,他还会比对你所使用的IAT表、导入表。所谓IAT表(Import Address Table)如图所示记录了每个导入函数的名字和所在的dll名称,可以看到这个程序,使用了这些危险的API,当然这些API也会受到杀软重点关注,杀软会对这些API进行挂钩,通俗来讲就是监控这些api在做什么。有的程序莫名其妙被杀可能就是这个原因,下面第三问会有一个典型案例。

第二问:如何让他不知道我的程序是做什么的?

既然杀软会监控我们使用的导入函数,那么我们就不直接对函数进入导入。例如我们不会直接使用virtualloc,进行开辟内存空间。那么导入函数表就不会有这个API了,这样会不会就增加了杀软的可信度呢。假设一个极端情况,我们的程序一个api都没有或者很少很杂,也没有被打×的API,那么他是不是就会认为这个程序是不认识的呢。知道这个道理,还过不了静态吗。

这里随便展开几种隐藏导入函数的方式

让杀软不认识你

那当然要做的就是隐藏导入函数

第一种 API自实现  

对于memcpy我们可以这样实现 进行替换,这样导入函数表里不就没有这个函数了吗

1PVOID Mymemcpy(PVOID Destination, CONST PVOID Source, SIZE_T Length) {
2    PBYTE D = (PBYTE)Destination;
3    PBYTE S = (PBYTE)Source;
4
5    while (Length--)
6        *D++ = *S++;
7
8    return Destination;
9}

第二种 老生常谈 GetProcAddress+GetModuleHandle

通过GetProcAddress 和 GetModuleHandle获取函数  举例如下

1typedef HANDLE(WINAPI* pCreateThread)(
2    LPSECURITY_ATTRIBUTES   lpThreadAttributes,
3    SIZE_T                  dwStackSize,
4    LPTHREAD_START_ROUTINE  lpStartAddress,
5    __drv_aliasesMem LPVOID lpParameter,
6    DWORD                   dwCreationFlags,
7    LPDWORD                 lpThreadId
8    );
9
10pCreateThread MyCreateThread = (pCreateThread)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "CreateThread");

这里GetModuleHandle和GetProcAddress可能会挂钩 或是重点关注 那么也可以通过其他方式获取 第三种就是

第三种遍历kenerl32.dll获取函数

这种你需要对PE较为熟悉才看的懂代码,网上也有现成的代码这里就不展开了。可以看到里面基本没有其他的windows API

第四种API hash

这种有个好处,就是你可以不用直接在程序中硬编码“virtualloc”这样的字符。像第二种kernel32.dll、CreateThread这样的字符会硬编码在程序中,如果人工分析就会比较警觉了。

原理大致也是遍历dll  但是加了一个hash 那么你可以提取计算virtualloc的hansh然后放进来遍历查找这个API

第五种syscall 直接系统调用

这里也不展开讲了,比较syscall的“门”也有很多,了解的师傅肯定知道,比如最开始的地狱之门、然后光明之门。。。。

通俗来讲,首先syscall是一个不可拆分的原指令(好像是叫这个来着)他不会被杀软挂钩,每一个0环的APi(底层的API,如virtualloc,最终进入ntdll调用的就是底层的NtAllocateVirtualMemory,可以看到他名字有个Nt)都有一个SSN,即系统调用号,如果我们获取了他的系统调用号,就可以直接通过系统调用号来获取这个底层API达到脱钩的效果,在导入函数表中也不会有这个API。syscall 又分直接系统调用、间接系统调用 如下我就是用的一个间接系统调用。同时获取到SSN和syscall的地址,通过call或者jmp来进行跳转到syscall的地址,再进行调用该API。当然他也有他的缺点也就不展开了 汇编示例代码如下

1CustomNtAllocateVirtualMemory PROC
2    mov eax, SSN系统调用号
3    call syscall地址
4    ret
5CustomNtAllocateVirtualMemory ENDP

。。。。。。。。。。。。。。

每一种方式都是可以进一步研究和加强的,效果也各有不同。

隐藏导入函数表做好了,就可以让他不知道你在做什么了。当然此外最好不让他比对出一些字符或者hash。

第三问:为啥360要杀hellc world?难道真的是杀疯了?

当然不是,他也有他的理由的,首先我们编译程序,通常会默认带有一个叫CRT库的东西。这玩意会造成的后果就是你的可执行程序的导入函数表里会有很多你压根没用到的API。即使你只是printf了一个hello world,但是杀软认为你调用了很多危险的、打×的windows API,再加上这个hello world被多次云传,比对到了hash,于是认为这是一个恶意程序。这样解释,是不是跟我前面讲的就呼应上了。

第四问:如何不让他比对到hash?

当然就是改资源(icon、签名后面展开)、改静态的一些字符、改函数代码等等使用virtest咯 = 没见过你

总结:看完上面的,你可能对360等杀软的查杀,心里多少有个底了吧。总结一下就是让杀软不认识你、不知道你用了哪些api、不知道你是做什么的、没见过你 = 无法判断是不是恶意程序。

过动态扫描的话这里也就不展开了,需要对内存、堆栈进行处理了。 如果师傅有兴趣请移步WingBy小密圈。

目前以及讲了如果过最新火绒的内存扫描了,而且使用的是CS,并不是其他特征交少的C2。(最新火绒免杀上线不难,但是执行命令不报CS、记录里不报CS远控、过最新火绒6内存扫描、可能有的师傅还没尝试哦,卡巴EDR没能查杀的,却被火绒检测到了CS特征.....)

常见免杀误区

在讲几个常见免杀误区的案例之前,先来讲讲一些,目前大家普遍认为是正确的观点。

1.某绒是MD5比对器

2.国内杀软不存在内存扫描

3.某数字就是乱杀,hello world都不放过

4.某数字不认识你的程序就杀

5.某数字一见到无效签名就杀

6.上线万岁,上了线就是免杀了

7.一些常见技术实现代码的错误使用(或老技术未公开的升级版)

8.老技术用不了,早就被杀软秒了

第七、八点我会举几个遇到的案例来阐述,篇幅会较大。我先来给大家解释前面几个。都是我的个人观点,结合生活常识、以及自己多个样本的测试。所以不一定正确,但是师傅们可以作为参考、觉得有误可以与我讨论,一起进步。不喜勿喷啦。

第一个误区

某绒我认为主要是MD5比对,但是在某绒最新的第六代,也有了很强的内存扫描,其中测试的一个样本,免杀了卡巴的EDR,却被某绒的内存扫描标记为CS。但是他不告警,在记录了却能看到他检测出是CS,所以某绒显示无风险不难,难在他的内存扫描,不识别为远控,以及上线后的正常执行命令等。

第二个误区

现在当然就是错误的了,某绒明显最新一代有了内存扫描,数字的话我个人觉得也是有的。

第三个误区

当然前面已经解释了数字杀hello world的科学原理。

第四个误区

经过我多个样本的测试,以及尝试可知,明显是错误的,他要是不认识就杀。。。。

第五个误区

这个误区,确实很多人是这么认为的,包括一些圈内比较出名的师傅。一个群的师傅应该知道这个事情哈哈哈。

起因是我上周公众号发布了一个工具叫做Ting_More。


用来对可执行程序进行批量加资源和签名。有的师傅觉得这玩意没用、因为是使用sigthief加的签名,是属于无效签名、360见了就会秒。有的师傅认为这玩意儿早就有了、烂大街了都,确实批量加资源的icon的确实是有了,但是集成了批量加签名的我没找到有公开出来的,所以就自己动手丰衣足食了,完了分享出来了,反倒是被泼了冷水。。。。。

回到主题,360真的专杀无效签名?见了就秒的吗?事实证明并非如此。

我在使用VS编译完成后,没有做修改VS的编译配置属性、混淆、加壳等等任何处理。我就是直接使用我的工具TIng_More批量生成了230个马,对其进行批量加资源以及调用sigthief进行加签名(无效签名)

然后我立马放入360环境(14大版本)下,批量云传,由于文件较多过了几分钟之后发现230个杀了83个,也就相当于过了一半以上, 然后我随便拿了一个剩下的,尝试运行,于是在360面前成功上线了。

最终推翻了这个误区,那么我们分析一下这背后的科学原理是什么呢

我认为首先是hash,我进行随机加资源、随机加签名、连程序的名字也是随机的这一系列操作无非就是改头换面,让杀软认为这是一个新的程序而不是已知的hash。而同样的代码也就是本质,都是一样的,只是相貌不同就被认为是正常的程序了,我目前只能通过hash比对来进行解释了。所以你的程序即使一开始刚哥编译出来的、被360杀了,不妨试试这款Ting_More工具,让你的程序改头换面,可能就过了360了。之前的没过可能就是因为代码结构相似,被上传次数多了,被加进了hash库中所以识别了。所以有的师傅修改VS的配置就把360过了,就是因为编译环境的改变导致可执行程序的hash改变了。

第六个误区

上了线就是免杀了吗?当然不是火绒上线多简单、执行命令试试哈哈哈是否不产生告警呢,冷门C2就算啦。

此外提权、dump密码、横向等一些操作能否免杀呢?也是很值得深入学习、研究的。 还是欢迎加入小密圈、里面的东西真的很干、很用心!!!!

老生常谈的误区

这一部分就来解释第7/8个误区吧

这里先通俗的讲一下VEH异常处理的原理

VEH异常处理想要达到的效果其实就是让shellcode在要执行的时候赋予可执行权限,在不执行的时候赋予不可执行权限,绕过杀软内存扫描。毕竟杀软不会扫描所有内存,通常主要扫描具备可执行权限的内存区域。当不执行的时候,也就是CS进行休眠的时候,给他不可执行就绕过了。过程大抵是这样的

首先会注册一个异常处理事件

1//创建事件
2hEvent = CreateEvent(NULL, TRUE, false, NULL);
3//改写VEH函数列表,使处理函数作为优先级最高
4AddVectoredExceptionHandler(1, &PvectoredExceptionHandler);

其中 PvectoredExceptionHandler 的异常处理逻辑是这样的  

1LONG NTAPI PvectoredExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo) {
2    if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0xc0000005 && is_Exception(ExceptionInfo->ContextRecord->Rip)) {
3        VirtualProtect(BASE_ADDRESS, dwSize_Gloab, PAGE_EXECUTE_READ, &Beacon_Protect);
4        return EXCEPTION_CONTINUE_EXECUTION;
5    }
6    return EXCEPTION_CONTINUE_SEARCH;
7}

首先if判断是否是0xc00005异常,也就是没有可执行权限的异常debug的时候长这样

然后判断发生该异常的内存区域是否在我们的shellcode所存在的内存区域中,如果是则VirtualProtect设置可执行权限。

既然要判断发生该异常的内存区域是否在我们的shellcode所存在的内存区域,那么我们就要给原始的virtualloc挂钩(如果CS开辟内存用的是这个API的话),也就是hook virtualloc。这样好让每次开辟内存空间的时候我们可以获取他的地址和空间大小。有的师傅问我不就一开始执行shellcode之前开辟一次吗?当然不是CS在运行的时候也会调用VirtualAlloc开辟内存空间,如果你不hook,就无法获取CS开辟的空间的地址。以后每次CS调用VirtualAlloc的时候就会当我们自己设置的VirtualAlloc里面运行也就是NewVirtualAlloc

既然休眠的时候我们要内存进行保护,也要对sleep进行hook,然后CS一旦调用Sleep,就会进入我们的newSleep中,开启内存的保护。重点来了大家的NewSleep通常是这样写的

1VOID WINAPI NewSleep(DWORD dwMilliseconds)
2{
3	UnHookSleep();
4	Sleep(dwMilliseconds);
5	HookSleep();
6	SetEvent(hEvent);
7}

这里的SetEvent用来重置事件

由于我们要不断进行内存保护,所有我们会另起一个线程  无限循环 来开启内存保护

1DWORD WINAPI SetProtect(LPVOID lpPatameter)
2{
3	while (true)
4	{
5		WaitForSingleObject(hEvent, INFINITE);
6		VirtualProtect(BASE_ADDRESS, dwSize_Gloab, PAGE_NOACCESS, &Beacon_Protect);
7		ResetEvent(hEvent);
8	}
9	return 0;
10}
11
12HANDLE hThread = CreateThread(NULL, 0, SetProtect, NULL, 0, NULL);

这里WaitForSingleObject是用来阻塞的,当调用了SetEvent之后就不再阻塞开始使用VirtualProtect赋予不可执行权限。

所以刚异常处理完添加可执行权限了,在sleep的时候就给他添加不可执行权限,进行内存保护。这就是异常处理的大致过程,那么在我今天测试的时候,我发现了一个问题,上面NewSleep通常是这样写的

1VOID WINAPI NewSleep(DWORD dwMilliseconds)
2{
3	UnHookSleep();
4	Sleep(dwMilliseconds);
5	HookSleep();
6	SetEvent(hEvent);
7}

代码从上到下,首先unhook,防止死循环一直调用给钩住的sleep,然后再重新hook,好让下一次cs休眠的时候能再次进入这里。然后setEvent回到SetProtect不再阻塞进行

之前用VEH,看了网上几个VEH的源码,其中SetProtect大致都长这样VirtualProtect赋予不可执行权限。在我调试的时候确实是这个效果,每次都是sleep完毕了,再去进行内存保护。这里给大家看看效果

首先我这里代码的一些进行print打印

内存保护处

异常处理处

newsleep处

运行效果如下

验证了我的猜想,那么既然都休眠完,再去调用内存保护,是不是就违背了一开始使用这个技术的初衷

后面在网上找了几个VEH的文章和代码,基本上都是这样写的

某公众号

某公众号

某公众号

某博客网站

那么正确的 使用方法应该是这样的

只需要把setEvent放在真正sleep之前就可以了。

关于VEH还有几个误区我这里再举一个例子,竟然我们就是要让运行报错0xc0005,那么为啥我们在执行shellcode之前还有给他加可执行权限呢?如图

有的公众号也是这样处理的

某公众号

某公众号

某公众号

当然也有是正确处理的公众号

其实我绝对后面再加执行权限可以把VEH用的更加娴熟,效果会更好

那么举这几个案例,没有别的目的,不是为了抹黑,如果上面截图的公众号主看到啦,麻烦高抬贵手,莫要生气嘿嘿,咱们只是发现问题纠正错误,一起学习、一起成长,当然这几个公众号其他文章质量还是不错滴。

总结:说到这里,其实我是想告诉正在自学免杀的师傅们、在学习一个项目的时候、一定要多去调试代码看看效果、多去思考可能有其他的改进方法来提升效果,而不该直接把代码扣下来,直接就用了,一定要多看几个项目。Github真是好地方。 当然VEH还有一些我和Gatsby师傅一起讨论出来的升级版,如有兴趣请移步WingBy小密圈,后期定会更新。

说到改进我再来个举例子,还是拿VEH进行思考。

我们每次只有Sleep的时候才会进行内存保护,那我们能不能每次一设置可执行权限之后,马上就给他加上不可执行权限呢,马上进行内存保护,这样shellcode还能正常运行吗?经过我的实验,答案是能的,只要你的setevent设对了地方就行。这里我选择放在这里。(看来刚刚就漏了)

思路就是每次添加可执行权限之后就停止阻塞,又给他添加可执行权限。效果如图 可以发现即使没睡眠的时候也是在做内存保护的

那么师傅问为什么这样了shellcode还是能正常上线呢。

我个人的理解是shellcode汇编代码执行到没有权限的时候会阻塞等待,一旦有权限了他就会继续执行,而不是会立马跳到setprotect里面添加不可执行权限了。

360真的很可怕?

首先来一段小插曲,一天我的好哥们Gatsby,不想再做世界首富了,给我发了个他写

的免杀源码,里面用到的技术大抵是老生常谈的资源加载执行shellcode、老生常谈shellcode加密解密、老生常谈API隐藏(导入函数表隐藏)、老生常谈VEH异常处理、老生常谈unhook ntdll,hook virtuallco,然后过了核晶......,过鲲鹏不知道他用的是不是这个源码也没问。然后我就看了一下,问题出在压根没有调用hookvirtualloc,在整个程序里面,但是他又有注册异常事件

仔细阅读了上面的内容的师傅应该就发现了,都没有去hook virtualloc,我怎么获取新开的内存地址,怎么给他添加不可执行权限进行内存保护?

然而就是这段代码,编译过后使用Ting_More批量生成免杀马,过了360并且正常上线使用。也就是文章一开始的那个截图

于是Gatsby也羞羞的红了脸,他以为他给的代码调用了,实际上没有调用。甚至只有了资源加载 shellcode加密就过了360

上面的截图是20号的效果,4天过后也就是8月24号中午12点,距离文章发布不到1小时的测试效果,依然是全部免杀最新版360安全卫士(右下脚有时间)

好了剩下最后一个问题

老技术用不了,早就被杀软秒了?

看了这篇文章,相信师傅们心中已经有了答案了。整个篇幅用到最多的一个词语“老生常谈”。、

20年的黑暗之门syscallHell's Gate ,能否还能过360?试试就知道咯。

有的师傅直接复制粘贴代码,完了编译、360落地无于是得出结论,老技术早就被秒了。

看完这篇文章了,师傅们也该知道为啥被秒了。其实就是同一个项目被多次云传了,静态特征早已被标记了。不妨使用Ting_More改头换面、或者加一些其他代码、技术进行处理试试呢。

最后还是很自信的宣传一下我们的知识星球。

我们的星球涵盖了红蓝攻防、企业/公益SRC、各类证书挖掘包括但不限于EDU、CNVD等、Java/PHP0Day代码审计、免杀对抗、云安全攻防、CTF、安全开发等等,还有很多高质量的文章,以及秘密工具。别的星球有的我们未必没有。

领取优惠卷现价119,学生89。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2