在越来越多的攻防演练项目中,上线机器至C2(Command & Control)以及钓鱼打开内网入口点都需对其所使用的落地文件进行处理,因此免杀是这类项目中至关重要的一环。
目前,红队人员经常使用CobaltStrike这款优秀的C2,所以他们往往要对CobaltStrike所生成的payload进行免杀处理,继而开始下一步的渗透过程。同时,由于shellcode的混淆比较灵活,加载也方便,是现在主流的免杀上线方式。当然,这里所指的shellcode不只是CobaltStrike所生成的。
下文将会介绍对ShellCode进行免杀上线的一些基本方式、技巧以及思路。首先介绍了shellcode的概念以及处理常见C2所生成shellcode特征;其次介绍了几种隐藏敏感API的方式,分别是:回调函数、IAT隐藏、PEB隐藏以及Syscall;然后说明了反沙盒的思路以及使用LLVM编译器达到较好的反调试效果;最后,延伸了如何处理PE文件的免杀思路以及项目。
一、SHELLCODE
shellcode是一串位置无关的机器码,可以直接在内存中执行。目前,红队人员对于shellcode的免杀是最常见的,也非常灵活,配合一些实现“PE to shellcode”的工具还可以对很多可执行文件作免杀处理。
红队人员经常使用的C2所生成的shellcode有很多被杀软标记的特征,如果直接把CobaltStrike的shellcode放进源代码中,那么编译出来的可执行文件一定会被杀软静态查杀。因此,如果对shellcode特征进行规避,那么就要让AV/EDR无法识别文件中这串十六进制数据所存在的已标记特征。常见方法是对shellcode进行加密,然后进入内存进行解密执行,这样做的目的就是避免shellcode的静态特征。另外,分离免杀也是为了解决这一问题,它将shellcode与加载器分离开,再通过各种通信协议把shellcode传输回加载器。但分离免杀主要使用各类通信传输的方法,而不需考虑shellcode混淆,在实战中面对多种复杂情况时通用性不强,所以不在本文讨论范围内。
因此,笔者对于如何处理shellcode有如下两种方法。
-
可以自己实现一些混淆、加密的功能,然后将混淆后的shellcode放入代码中,再在加载器中编写解密函数。例如,简单地使用异或对shellcode进行处理,同时相应加载器中的解密代码也使用异或,只需key一样即可。但实际上,不建议使用这类简单、易还原的混淆方法。 -
可以直接调用开源的加密代码进行修改使用,如加密项目: WjCryptLib
。
二、API调用隐藏
2.1 执行API
内存中的shellcode可直接用指针执行或内联汇编等方式执行。如指针执行:
((void(*)())Memory)();
或者创建线程执行:
hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)shellcode, NULL, NULL, &dwThreadId );
WaitForSingleObject(hThread, INFINITE);
除以上这2种及其他常见执行方式外,实际上还存在可供选择的方式。和常见的php的回调函数类似,Windows当中也有很多回调函数,我们可以使用这个特点来执行shellcode,进而缓解常见的执行函数的检测,如EnumFonts
,参考项目:AlternativeShellcodeExec
。
如果使用进程注入这一方式,那么可供调用的API会更多,如常见的创建远程线程注入
、APC inject
、EarlyBird APC queue inject
等。当然,要进行进程注入,就需获取对应进程的Handle,然后在对应的进程空间中申请一块空间去执行shellcode,这一过程一般涉及到如下几个API:OpenProcess
、VirtualAlloc
、WriteProcessMemory
、QueueUserAPC
、CreateRemoteThread
等。
很多免杀框架、工具常用的方法有PPID伪装
、Process Hollowing
等,Process Hollowing
也叫做进程镂空,主要思路为卸载合法进程的内存,写入恶意代码,接着伪装成合法进程,篇幅限制没法给出核心代码,具体涉及到一些PEB的知识。
2.2 隐藏IAT调用API
在PE结构中,导入表(IAT)中声明了这一PE文件会载入哪些模块及其中的函数地址。例如,当我们执行一个PE文件,它调用一个API时,操作系统常常会去kernel32.dll
这一动态链接库中调用。当我们查看PE文件的IAT时,可以看到导入地址情况。例如,我们的程序使用了MessageBox
这一API,接着查询对应PE文件的IAT,可以看到会从USER32.dll
去调用:
如果编写过shellcode加载器的红队人员,可能会知道我们加载shellcode的时候,脱离不了一些内存分配、创建线程的API。对于防护软件来说,既然所有PE文件都有导入表,那么可以理所当然地把IAT中的导入函数情况作为一个评估文件风险的维度,如同时使用Virtualalloc
、CreateThread
或使用Virtualalloc
分配RW属性的空间后,再使用VirtualProtect
改变属性为可执行(这也是目前大家经常使用的小技巧)。如果同时出现了这些API,很多AV则可能会直接判定为高风险文件。
补充:分配内存的函数还有
HeapAlloc
,或者在PE文件中新增加一个section的方式等等也会缓解检测。
因此,我们可以使用GetProcAddress
函数,从kernel32.dll
中获取这几个函数地址,自定义函数指针并调用它们。这样,就可以在IAT中隐藏几个敏感的API调用,示例代码:
1.
typedef LPVOID(WINAPI* ImportVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
2.
ImportVirtualAlloc CustomVirtualAlloc = (ImportVirtualAlloc)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualAlloc");
更进一步,如我们上面所提到的在调用kernel32.dll
导出的函数Virtualalloc
后,其实会到NTDLL.dll
寻找更低级别的函数去调用。NTDLL.dll
可以看作是进入内核的一道门。我们还可以调用比VirtualAlloc
更低级别的函数NtAllocateVirtualMemory
,这个函数就是在NTDLL.dll
中被导出的,这类函数也称为Native API
。更多未导出API参阅:http://undocumented.ntinternals.net。
还有一种情况,即我们用到的GetModuleHandle
和GetProcAddress
如果也被杀软所监控的话,那我们还可以手动解析PEB来将这两个函数进行处理,过程大致如下:
-
先找到PEB模块基址;
-
再从PEB中找到
PEB_LDR_DATA
结构; -
接着定位PEB_LDR_DATA结构内存在的双向链表
InInitializationOrderModuleList
(模块初始化装载顺序); -
然后从该链表获得DLL的基地址,再循环从这个DLL的导出函数中找到我们要使用的函数。
关于几个关键的结构地址偏移不同系统位数情况我总结如下:
x86下:
fs寄存器(0x18)存放着当前线程的线程环境块(TEB)
TEB+0x30处指向PEB结构
PEB+0x0c处指向PEB_LDR_DATA结构
PEB_LDR_DATA+0x1c处为InInitializationOrderModuleList
x64下:
gs寄存器(0x30)存放着当前线程的线程环境块(TEB)
TEB+0x60处指向PEB结构
PEB+0x18处指向PEB_LDR_DATA结构
PEB_LDR_DATA+0x30处为InInitializationOrderModuleList
虽x86和x64环境下的偏移位置有所不同,但获取所需InInitializationOrderModuleList
结构的思路是一样的,即TEB -> PEB -> PEB.Ldr -> PEB_LDR_DATA
。
-
通过汇编代码直接定位PEB地址:
.CODE
GetPeb PROC
mov rax,gs:[60h]
ret
GetPeb ENDP
END
-
获得Kernel32.dll的地址,x64代码如下图:
接着将代码运行结果,结合windbg验证所取函数地址,如图:
现在,我们已经获得了Kernel32.dll
的地址,然后再编写代码实现从这个PE文件的导出表中遍历需要的函数即可,这需要对PE结构有一定的了解,这里不作过多说明。
这样去处理API调用也达到了一样甚至更好的效果。
2.3 Syscall
Syscall,系统调用。这项方法在目前的免杀技术中比较流行。为了了解系统调用的真正含义,需要对操作系统有一定的了解,如果读者对早期的操作系统MS-DOS有了解的话,可能会知道一个简单的应用程序崩溃可能会导致整个操作系统崩溃,操作系统机制并没有限制各个内存区域的访问。随着所谓的保护模式的操作系统出现,其引入了一些保护措施,使用几个运行级别来隔离运行的程序,从而保护操作系统免受影响。在Windows系统上,我们见到的应用程序在用户态下运行,也叫做ring3;驱动和内核在内核态下运行,也叫做ring0。
使用这些手段可以确保应用程序无法直接访问关键的系统资源,当应用程序需要执行一些特权操作时候,处理器首先要切换到ring0将执行流切换到内核态,这其实就是系统调用。
大部分AV/EDR为了性能考虑,都在ring3层(用户态)进行Hook,过程一般是通过修改一些重点监控API的汇编地址,使其jmp到另一个检测函数,检测通过再jmp回原始函数地址。我们直接使用ring0层的系统调用就可以绕过此类Hook,下面将举例说明系统调用。
在使用ProcessMoniter监控notepad.exe程序打开一个txt文件时,查看Windows API CreateFile的调用:
上图显示了上述操作的调用堆栈,可以看到会从user-mode逐步进入kernel-mode。
我们还可以用windbg查看NtCreateFile
的详细的汇编调用。首先,NTDLL.dll
中的NtCreateFile负责在堆栈上设置相关的调用参数;其次,将系统调用号移动到eax寄存器中;最后,执行syscall指令。CPU将进入内核模式(ring 0),内核使用SSDT查找对应的系统调用号调用,将user-mode中的参数复制到kernel-mode,并执行内核调用的API(一般为“Zw”开头),执行的返回值再返回到user-mode中。
所以,可以直接使用syscall去绕过user-mode下的HOOK,但系统调用号在不同的操作系统版本中都不相同,我们可以根据对应版本的系统调用号用汇编写出对应的汇编代码(如上图),再创建引用,就可以在项目中直接调用。当然,也有很多自动化工具可以帮助生成代码,如项目SysWhispers
等。
目前还有相当多的方法去对抗EDR和杀软,如流行的禁止非微软签名的DLL注入
或者替换NTDLL
的UNHOOK方式等。我们还可以搜索这类方式去了解原理以及阅读代码,按需组合到自己的加载器即可。
三、Anti SandBox
此部分不过多介绍沙箱这一概念,其检测逻辑就是模拟运行恶意文件,从而分析恶意文件的行为。那么,反沙箱就是当我们的恶意文件进入沙箱被分析时,不被察觉出其恶意行为。我们常常要做的就是,识别当前环境为沙箱后立即退出程序或运行一些无意义代码。要实现上述功能,就需在代码中加一些判断去决定程序是否继续执行,这里的判断条件是最重要的。以下将大致介绍三种反沙箱思路。
首先,最为粗暴有效的方式就是你的恶意代码只能在你指定的机器上运行,所以可以寻找一些指定机器所独有的
特征,如在指定目录放置标志文件,机器的hostname、MAC地址等。
其次,可以添加更为通用的反沙箱手段。这在钓鱼攻击中最为有效,如简单判断内存磁盘、内存大小等,当然也不宜添加过多,因为进行反沙箱的代码有时也会被沙箱所识别,所以可能也需对所调用的查询系统信息等API进行处理。
这里要注意把通用反沙箱识别技术以不通用的方式来实现。
最后,如果针对钓鱼攻击所使用的加载器,可以利用上述思路组合出来的进程注入手段,进而添加一些反沙箱甚至反VM手段,因为我们钓鱼攻击的目标一般都为物理PC,具体公开的思路及代码推荐参考项目al-khaser
。
四、反调试
反调试没有绝对好的方式,我们所能做的只能是增加分析人员的调试成本。
4.1 检测进程
可以在程序中获取当前环境所存在的进程,再判断其中是否包含一些常见的调试分析工具的进程,如若存在则推出程序,不过这种办法也只是略微增加了逆向分析的难度而已。
4.2 LLVM
实质上,这属于一种反汇编、反编译的方式,但是做好了这种方法,也就加大了调试人员的调试成本,也起到了我们想要做到的效果。
LLVM也是一种编译器,与传统编译器(比如GCC)不同的是:传统编译器的处理工作是为三段式的,可以分为前端(Frontend)、优化器(Optimizer)、后端(Backend)。Frontend负责解析源代码,将其翻译为抽象语法树;Optimizer对其进行优化,最后Backend负责将优化后的代码转换为目标机器码。而LLVM的Frontend会产生语言无关的中间代码LLVM Intermediate Representation (LLVM IR),Optimizer对LLVM IR处理后再由Backend转换为目标平台的机器码。
用来操作LLVM IR生成过程的框架称为LLVM Pass,对于LLVM只介绍一些简单的概念,因为我们的目的只是使用其混淆代码,所以只需达到会使用即可。
比如OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度(实质上OLLVM是属于LLVM的一个Pass)
Ollvm混淆主要分成三种模式,这三种模式主要是流程平坦化
,指令替换
,以及控制流伪造
。
流程平坦化 :这个模式主要通过将if-else语句替换成do-while语句,然后通过switch语句来对流程的控制,这样就能模糊基本块之间的前后关系。
指令替换 :这个模式主要通过使用更复杂的指令序列来替换一些标准的二元运算符,从而增加逆向的难度。
控制流伪造 :这个模式主要是会在一个简单的运算中外包好几层if-else的判断,从而增加逆向的难度。
例如下图是经过处理后的程序逻辑,变得十分复杂,图片来自吾爱论坛@Rimao
五、免杀PE文件
此小节将介绍对于无源码文件的免杀加载方式。常用的做法有使用PE加载器去解析PE文件再进行加载。还记得在第一小节我们提到了“配合一些PE to shellcode的工具还可以对很多可执行文件做免杀处理。”
那么,我们如何用现有的shellcode加载器对PE文件进行免杀处理,如果我们已经对上述四个部分非常了解,可以自己实现免杀性以及稳定性较强的加载器,那么我们还可以进一步的使加载器发挥更强大的作用。
由于我们实现的只是加载shellcode的代码,那么只需要把PE文件转换为shellcode就可以用加载器加载。那么先来思考如何才能加载PE文件,答案是PE Loader,因此只要用shellcode实现一个PE Loader就可以满足我们的需求。实现这一加载器主要要做的有内存对齐,修复IAT表,修复重定位表等,这里需要对PE结构、PEB、TEB等结构非常了解才可以实现,笔者也不敢说全部了解,常常各类免杀工具或是框架会使用现有的开源项目donut
、PE2Shellcode
、pe_to_shellcode
等实现这个功能。
最后,我们使用上述提到的方式,可以将PE文件转换为shellcode,然后就是常规的加载执行。
总结
本文大致分为:shellcode的处理、API的处理、反沙盒、调试以及PE免杀这四大部分。介绍了基于真实对抗项目的大部分需求相应的解决办法。内容大都来自于对公开的知识进行学习和总结,以及优秀的前辈们的开源技术。尽可能得一些值得了解的知识点说的直白明了,最重要的是读者可以根据所提到的相关技术进一步引申学习。其实不论是利用任何语言,都可以利用上述思路编写出自己的免杀方式。当然,利用一些当前较火的GO、Nim等语言可能会有一些静态方面规避的特性,会减轻一些对抗的“痛苦”。不过“万变不离其宗”,如果能熟练掌握上述提到的所有东西以及学习相关代码,相信写出一个漂亮的shellcode加载器也是非常容易的。对shellcode进行免杀上线至C2,只是后渗透过程当中的一小部分以及第一步,后续要做的大量行为规避、绕过操作也相当有趣并且值得思考学习。
参考
https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/
https://www.52pojie.cn/thread-1369130-1-1.html
原文始发于微信公众号(黑客在思考):「免杀对抗」怎样实现一个基础的shellcodeloader