在分享加载器的编写逻辑之前,还是想和大家聊下目前市面上防病毒软件和EDR的检测原理,因为免杀最终对抗的就是这两类产品,只有了解检测原理,才能更针对性的进行绕过。如果有任何个人理解、描述不对的地方,还请大佬们及时指正!

一、杀毒软件和EDR检测原理

    杀毒软件和EDR检测病毒主要是有以下几种方式:

    1)静态检测:对可执行文件做特征扫描或做Hash计算,扫描其中的字符串或字节数据,再与病毒特征库做匹配,如果匹配中则对可执行文件做隔离或清除。

    2)启发式扫描:分析文件特征,代码结构等来判断是否存在恶意行为。

    3)沙箱:在隔离环境中运行恶意程序,查看软件是否有敏感行为,比如操控注册表,建立网络连接,可疑的文件访问,可执行内存分配等。

    4)API Hook:一些AV和EDR会Hook住恶意代码经常调用的Win32 API,以便监测软件的行为。例如,如果一个可执行文件调用了分配可读写执行内存,将Shellcode写入分配的内存,创建线程执行分配的内存空间地址代码的API,那么多半这个软件就会被贴上恶意软件的标签。

    5)内存扫描:一些比较厉害的商业EDR会执行内存扫描,比如MDE,Cortex,Crowd Strike。这一检测方式的效果是非常好的。因为无论Shellcode如何加密,要在内存中执行就必须在加载进入内存中后解密,这时候的Shellcode就相当于赤裸在EDR的扫描下,可以类比CS生成Raw格式的shellcode后,无保护的放在有AV或者EDR的机器上。如果没有实施一些内存扫描规避技术,就几乎会被检测出来。

二、规避思路

    前三种的检测方法都有一些比较好的绕过方法,比如针对静态检测,我们可以采取Shellcode与加载器分离的方式,并且将Shellcode以某种方法加密,这样就不会存在比较显著的特征。

    针对启发式扫描,我们可以将参考的代码简单做下混淆,修改函数名和变量名,并可以添加一些无关代码;动态寻址我们要调用的函数,这样要调用的函数名就不会出现在IAT表中。

    针对沙箱,我们可以使用休眠,载入虚假dll,检查内存,CPU数量来检测执行环境是否在沙箱中,如果在沙箱中则停止执行。

    后两种检测方法的规避技术针对不同的AV和EDR就可能会出现不同的效果。针对API Hook,我们可以使用一些不常见的Win32 API, 或者unhook来规避,但这种方法针对一些AV可能还有些用处,针对EDR效果就收效甚微了。EDR需要使用Direct Syscall或者Indirect Syscall的方式来规避API Hook.

    针对内存扫描,需要自定义用户定义的反射型加载器,这个可能会留到后面单独一篇来讨论。

    总结来说,要在AV或者EDR无感的条件下执行CS 的Beacon,需要做到:

    1) Shellcode与Loader分离,并将Shellcode做加密,规避静态查杀

    2) 混淆函数名,变量名,动态加载API,规避启发式扫描

    3) 增加检测沙箱的技术,如检测内存,CPU数量等,规避沙箱检测

    4) 调用生僻API,unhook API或者直接进行syscall来规避API Hook。

    5) 自定义C2 profile(这个也可以减少一些内存和行为特征), 自定义用户定义的反射型加载器来规避内存中的扫描,比如让Beacon 在休眠时自动加密,在执行解密;在休眠时修改内存保护属性,变为读写;在执行时变为读写执行等。

三、实现细节

3.1 Shellcode加载函数编写

    分享了一些规避的方法和原理之后,接下来我们一起看看Shellcode加载函数具体代码编写的逻辑和代码实现。我们先看看一个常规进程注入的加载函数编写逻辑,然后可以用本文使用的加载函数编写逻辑作对比,从中体验一些免杀的逻辑和想法:

    1) 定义加密的Shellcode数组

    2) 调用CreateToolhelp32Snapshot创建进程快照,遍历进程数组,通过进程名找到需要注入的进程ID

    3) 调用OpenProcess打开目标进程,获得进程句柄

    4) 调用VirtualAllocEx在目标进程中分配可读写执行内存

    5) 解密Shellcode

    6) 调用WriteProcessMemory将Shellcode写入进在目标进程分配的内存中

    7) 调用CreateRemoteThread在目标进程中创建线程执行写入的Shellcode

    与常规的加载函数编写思路不同的是,这里我们主要是用到了Section Memory映射的技术,关于Section的一些概念,大家可以参考链接:https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/section-objects-and-views。简而言之,Section就是可以在进程之间共享的内存。因为共享的特性,我们就可以不用使用WriteProcessMemory API来将Shellcode注入进远端进程,具体实现细节如下:

    1.首先针对Shellcode,我们需要做到加载器与Shellcode分离,让加载器从磁盘上加载Shellcode进入内存中,并且解密。

    1)定义一个异或的函数,方便加解密:

    2) 将Shellcode加载进入内存中并解密:


    2.动态调用ZwCreateSection函数创建Section内存分段。因为ZwCreateSection是Native API, 并没有头文件包含该函数的定义,因此我们需要定义该函数的原型,并且动态获取该函数的地址,然后调用:


    3.动态调用NtMapViewOfSection,将分段内存映射到当前地址空间(因篇幅原因,后续只展示一些关键代码):


    4.将Shellcode复制进入Section中:


    5.再次调用NtMapViewOfSection,将Section映射到远程进程地址空间中:


    6.调用延迟执行的函数NtDelayExecution,改变API调用序列:


    7.在远程进程中创建线程执行Shellcode,并暂缓执行:


    7.在远程进程中创建线程执行Shellcode,并暂缓执行:

 基本上,通过使用section创建的加载函数编写思路就是如此。大家可以发散自己的思维,进一步修改调用的API。比如,使用NtMapViewOfSection将Section映射进远程进程地址空间时,我们可以不将内存权限设置为可读写执行即PAGE_EXECUTE_READWRITE,可以先设置为PAGE_READWRITE,然后再使用NtProtectVirtualMemory将内存权限改回原来的读写执行。总之思路方法很多,大家可以多尝试。今天这篇文章就先介绍调用生僻的API来规避行为检测,动态分析,可能对于有些AV或者EDR来说,这种方法已经不再那么有效。后续作者会再介绍一些unhook API或者通过syscall调用的方式来规避探测,这种方法就会有效的得多。

3.2 沙箱规避

    沙箱规避的方法很多,作者经常使用的有延时执行和载入虚假的DLL,参考代码如下:

更多沙箱规避的代码可以参考:

https://github.com/Arvanaghi/CheckPlease/tree/master/C

沙箱规避完成后,我们就可以结合之前DLL Sideloading + DLL Proxying来编写完整的加载器代码。

主要的逻辑就是:

1) 在DLL Proxying的DLL中,定义Shellcode加载函数

2) 在DllMain函数中应用沙箱检测

3) 当沙箱检测通过后,创建线程执行Shellcode加载函数

可参考如下代码,还是以前两篇文章提及到的DismCore.dll为例:

3.3用户定义的反射加载器

     用户定义的反射加载器可以简单理解为CS生成的Shellcode。前面提到过,无论Shellcode如何加密,要在内存中执行就必须解密后再执行,这种情况下Shellcode就是赤裸的。因此修改Shellcode在内存中的特征才可以规避内存检测。这里就不再详细讨论原理和实现细节了,大家可以使用这个工具,站在巨人的肩膀上:

https://github.com/kyleavery/AceLdr

使用方法非常简单,只需要在CS的Script Manager中载入该脚本即可:

 然后重新生成Shellcode就可以奏效,目前测试该工具内存规避的效果,免费版的MDE无感。

     文章最后:基本上目前针对Shellcode Loader的免杀方法就是这一套组合拳,需要优化的地方就是API Hook的规避和内存扫描规避,免杀涉及的内容很多很广,如果有任何不准确、不完全、不清晰的地方欢迎大家留言指正,希望各位大佬们轻喷。