2022年,工业级Endpoint绕过蓝图
原文地址
正文
两年前我成为一个全职红队人员。这是一个我内心十分喜欢的专业。就在几周前,我开始找寻一个新的副业,我决定开始捡起我的红队爱好——开始研究绕过端点保护方案。
这篇文章中我会列出一些技术用于绕过企业级端点保护方案。出于教育目的,我不会放出相关源码。为了让广大安全行业受众能理解文中的内容,我不会详细讲解每种技术的细节。但是我会引用其他的人的文章,帮助读者深入理解其细节。
在模拟对抗中,初始访问阶段最核心的挑战就是绕过企业级EDR。商业的C2框架提供了不可修改的shellcode和二进制给红队人员使用,但是这些大部分都被工业级端点保护给特征了。为此就需要将shellcode的静态特征和行为特征给混淆掉。
这篇博客中会涉及如下的技术用于最终执行我们的恶意shellcode,或者说是实现shellcode加载器:
- Shellcode 加密
- 减少熵值
- 绕过AV沙盒
- 导入表混淆
- 禁用ETW
- 绕过常见的恶意API调用模式
- 直接系统调用,或者说规避syscall的监控
- 移除ntdll.dll中的hook
- 伪造线程调用栈
- beacon内存加密
- 自定义反射式加载
- Malleable profile中的OpSec 配置
1. Shellcode 加密
首先是比较基础也尤为重要的话题,那就是shellcode静态混淆。我写的加载器中使用了XOR和RC4加密算法,因为这两个算法易于实现,不会留下太多的加密活动执行特征。如果是AES加密算法的话,会在导入表留下特征,会增加样本的可疑性。例如早些版本的loader就触发了Windows Defender的检测,因为导入表存在CryptDecrypt
,CryptHashData
,CryptDeriveKey
函数。
上图的dumpbin显示了使用了AES加密算法的二进制的导入表信息。
2. 减少熵值
许多AV/EDR解决方案在评估一个未知的二进制时会考虑熵值。由于我们对shellcode进行了加密,就会导致熵值比较高。这是二进制代码被混淆的一个明显指标。
有很多办法用于减少熵值,最简单有效的两种是这样的:
- 添加一些熵值较低的资源到二进制中,比如说图像。
- 增加一些字符串,例如英文目录,或者一些字符串输出"strings C:\ProgramFiles\Google\Chrome\Application\100.0.4896.88\chrome.dll”
一种更优雅的方案是实现一种算法将shellcode混淆(编码/加密)成英文单词。这可以说是一个一石二鸟的方案。
3. 绕过AV沙箱
许多EDR会将二进制在一个沙箱中运行几秒用于探测行为。为了避免影响用户的体验,EDR不会运行二进制太长的时间。我曾见过Avast使用了长达30s的时间用于探测二进制行为。因此我们可以利用这种限制,延迟执行我们的shellcode。我喜欢通常我喜欢去计算一个大质数。更进一步的话,可以使用这个质数用于加密密钥。
4. 导入表混淆
有时需要避免可疑的API出现在导入表中。简单的说这个表就是你在二进制中使用的其他库的Windows API。通常有VirtualAlloc
、VirtualProtect
、WriteProcessMemory
、CreateRemoteThread
、SetThreadContext
等。使用dumpbin /export <binary.exe>
将会列出所有的导入表项。在大多数情况下,如果只有少量的可疑API调用,我们会直接使用系统调用来绕过EDR hook。
当然我们也可以动态获取函数地址,采用函数指针去调用。
1
2
3
4
5
6
7
|
typedef
BOOL
(WINAPI
*
pVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);
pVirtualProtect fnVirtualProtect;
unsigned char sVirtualProtect[]
=
{
'V'
,
'i'
,
'r'
,
't'
,
'u'
,
'a'
,
'l'
,
'P'
,
'r'
,
'o'
,
't'
,
'e'
,
'c'
,
't'
,
0x0
};
unsigned char sKernel32[]
=
{
'k'
,
'e'
,
'r'
,
'n'
,
'e'
,
'l'
,
'3'
,
'2'
,
'.'
,
'd'
,
'l'
,
'l'
,
0x0
};
fnVirtualProtect
=
(pVirtualProtect) GetProcAddress(GetModuleHandle((LPCSTR)sKernel32), (LPCSTR)sVirtualProtect);
/
/
call VirtualProtect
fnVirtualProtect(address, dwSize, PAGE_READWRITE, &oldProt);
|
使用字符数组而不是字符串来增加字符串的提取难度。虽然这个方法绕不了hook,但是能降低IAT表的可疑性。
5. 禁用ETW
许多EDR使用ETW来进行扩展,尤其是Microsoft ATP。 ETW允许对一个进程的功能和WINAPI调用进行追踪。ETW由内核组件和用户模式组件构成,主要用于注册系统调用或者其他系统操作的回调。用户模式的部分在ntdll.dll中,由于ntdll已经加载到每个进程中,因此我们可以充分使用这个dll来控制ETW的功能。有很多方法来绕过用户模式ETW。最常用的方法是patch掉EtwEventWrite函数,这个函数用于写入ETW事件。首先是找到这个函数的地址,然后替换第一条指令,修改为返回成功(SUCESS)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
void disableETW(void) {
/
/
return
0
unsigned char patch[]
=
{
0x48
,
0x33
,
0xc0
,
0xc3
};
/
/
xor rax, rax; ret
ULONG oldprotect
=
0
;
size_t size
=
sizeof(patch);
HANDLE hCurrentProc
=
GetCurrentProcess();
unsigned char sEtwEventWrite[]
=
{
'E'
,
't'
,
'w'
,
'E'
,
'v'
,
'e'
,
'n'
,
't'
,
'W'
,
'r'
,
'i'
,
't'
,
'e'
,
0x0
};
void
*
pEventWrite
=
GetProcAddress(GetModuleHandle((LPCSTR) sNtdll), (LPCSTR) sEtwEventWrite);
NtProtectVirtualMemory(hCurrentProc, &pEventWrite, (PSIZE_T) &size, PAGE_READWRITE, &oldprotect);
memcpy(pEventWrite, patch, size
/
sizeof(patch[
0
]));
NtProtectVirtualMemory(hCurrentProc, &pEventWrite, (PSIZE_T) &size, oldprotect, &oldprotect);
FlushInstructionCache(hCurrentProc, pEventWrite, size);
}
|
上面的方法,在我测试的两个EDR上仍然有效。
6. 绕过常见的可疑API调用模式
大多数行为检测最终都是基于检测恶意模式。其中一个模式是在短时间内特定的WINAPI调用的顺序。第4节中简要提到的可疑WINAPI调用通常是用来执行shellcode的,因此被严重监控(如
VirtualAlloc、WriteProcess、CreateThread模式用于内存分配和写入约250KB的shellcode并执行)。然而这些调用也被用于良性活动,因此EDR解决方案的挑战是如何区分良性和恶意调用。Filip Olszak写了一篇很棒的博文,利用延迟
和较小的内存分配和写入块来混合良性的WINAPI调用行为。简而言之,他的方法调整了一个典型的shellcode加载器的以下行为:
- 取代了一次性写入大约250kb的shellcode,而是先分配一块小于64kb的内存,然后标记为NO_ACCESS。接着再将同样大小的shellcode写入分配的内存。
- 在上述每项操作之间引入延迟。这将增加执行shellcode所需的时间,但也会使连续执行的表现出更多的模式。
这种技术的一个缺陷是要确保你找到的内存位置能够容纳你的整个shellcode在连续的内存页中。Filip的DripLoader实现了这个概念。
我构建的loader不会注入其他进程,而是使用NtCreateThread
在自己的进程空间中执行shellcode。因为一个未知的进程去写入其他进程通常会认为是一种突出的可疑活动。使用加载器自身去执行能更好地绕过检测模式。缺点就是任何崩溃的后渗透模块将会崩溃掉加载器进程,从而导致整个植入的崩溃。持久化技术和BOF可以帮助克服这个缺点。
7. 直接系统调用&绕过syscall标记
loader使用了直接系统调用绕过EDRs对ntdll的hook。我在这里避免讲解太多关于直接系统调用是如何工作的细节,因为这篇博客的目的不在于此。关于细节,可参考上面引用的Outflank。
简而言之,直接系统调用意味着直接调用API对应的内核系统调用。比如说对于VirtualAlloc的调用对应的就是NtAlocateVirtualMemory。这样就绕过了EDR对VirtualAlloc的hook。
为了能直接调用系统,我们需要从ntdll获取系统调用的syscall id,然后使用函数签名,并传递正确的参数到栈上去调用对应的syscall。关于这点,有很多现成的工具,比如说SysWhispers2和SysWhispers3。从绕过的方面来看,这个思路有两个问题:
- 二进制中存在的syscall指令很容易被静态地检测
- syscall的返回地址不是ntdll里的模块地址。也就是说我们二进制syscall的返回地址在ntdll地址之外。syscall的调用不是来自ntdll这种行为就很可疑了。
为了克服这些问题,我们可以做如下工作:
- 实现egg hunter机制。将系统调用指令替换为egg(也就是一些随机的可识别的pattern),在运行时,通过在内存中搜索这些egg,并使用ReadProcessMemory和WriteProcessMemory替换成syscall指令。这个技术已经被klezVirus实现过。
- 搜索ntdll.dll中的syscall指令,然后在准备好相应的参数后跳转到ntdll模块去执行syscall. 这样rip将会指向ntdll.dll内存区域。
上述技术在SysWhiper3中都有体现。
8. 移除ntdll.dll中的hook
另外一种绕过EDR hook的方式就是覆写已经被加载的ntdll。ntdll是Windows进程第一个加载的DLL,然后EDR的DLL紧接着才会在我们执行代码之前加载,完成对相应的函数的hook。如果我们重写加载一份ntdll的拷贝,那么EDR的hook就会被覆盖掉。MDSec研究之后实现了RefleXXion C++库。RelfeXXion使用直接的系统调用NtOpenSection和NtMapViewOfSection获得了一个干净的ntdlll.dll,它是之前注册表路径加载的\KnownDlls\ntdll.dll。然后通过覆写ntdll.dll的.text节区,把edr的hook给清除掉了。
我推荐使用这个库来绕过EDR对syscall的标记。
9. 伪造线程调用栈
接下来的两节涵盖了两种技术,用于绕过对我们shellcode的内存检测。由于植入的beacon在大多数情况下都在睡眠中等待来自其操作者传入任务。在这段时间里植入的beacon很容易受到EDR内存扫描技术的影响。我们先将第一种规避技术——伪造线程调用堆栈。当implant在睡眠时,他的线程返回地址指向我们申请的用于存放shellcode的内存。通过检查一个线程的返回地址,就很容易被EDR发现存在可疑进程,从而shellcode也会被定位到。为了避免这种情况,我们需要切断线程的返回地址和shellcode直接的联系。一种思路是我们对Sleep
函数进行hook,当这个函数被beacon shellcode调用的时候,我们修改返回地址为0。然后调用原来的Sleep
函数。当Sleep函数返回时,我们将正确的返回地址修改回去,保证线程正确继续执行。Mariusz Banach在ThreadStackSpoofer项目里实现了这一技术。这个仓库提供了更多关于这个技术的细节以及缺陷。
我们可以通过下面两张截图中观看到伪造线程调用堆栈后的效果。
一个是默认beacon的线程调用栈:
一个是spoofed beacon的线程调用栈:
10. beacon内存加密
另外一种规避检查的技术是当implant在睡眠时,将可执行内存区域进行加密。通过对Sleep
函数进行hook,如果调用者的内存区域是MEM_PRIVATE
和EXECUTABLE
的,那么就将其进行XOR加密,当Sleep
函数返回时则进行内存解密。
还有一种思路是注册VEH处理NO_ACCESS
访问异常,加密内存段,修改权限为RX。然后在睡眠之前,将内存属性改为NO_ACCESS
。当Sleep
函数返回时就会触发异常访问。VEH接收到异常后进行相应的解密,恢复正确的内存属性。这种技术可用于规避对Sleep hook的检测。
Mariusz Banach在项目ShellcodeFluctuation里实现了这个技术。
11. 自定义反射式加载
我们的beacon shellcode最终是在DLL中执行的话,就需要一个加载器。很多C2框架都使用了Stephen Fewer的ReflectiveLoader。关于反射式DLL加载,有很多写的非常好的文章,而且Stephen Fewer的代码注释和文档也很清晰。简而言之,反射式加载主要做如下工作:
- 解析一些来自kernel32.dll的必要函数地址,例如
VirtualAlloc
,LoadLibraryA
- 将DLL写入内存
- 构建DLL的导入表,这样DLL才能调用ntdll.dll和kernel32.dll的函数。
- 加载其他额外的库以及解析相应的导入函数地址
- 调用DLL入口点
Cobalt Strike 也增加了类似自定义方式去内存反射式加载DLL。Bobby 和 Santiago写了一个非常隐蔽的加载器——BokuLoader,它使用了Cobalt Strike的UDRL。这个技术我也在我的加载器中进行了使用。BokuLoader实现了几种绕过技术:
- 对GetProcAddress的限制,通常EDR会hook这个函数。
- 绕过AMSI和ETW
- 仅使用直接系统调用
- 仅使用
RW
和RX
的内存,不使用RWX
即(EXECUTE_READWRITE
)的内存。
- 将beacon的DLL headrs从内存中移除
12. Malleable profile中的OpSec 配置
建议的Malleable profile配置是这样的:
1
2
3
4
5
6
7
|
set
startrwx
"false"
;
set
userwx
"false"
;
set
cleanup
"true"
;
set
stomppe
"true"
;
set
obfuscate
"true"
;
set
sleep_mask
"true"
;
set
smartinject
"true"
;
|
总结
当然这只是入侵EDR端点系统的第一步,这并不意味着EDR的方案就全线溃败。后渗透活动使用的模块,也可能会导致EDR报警。一般来说,要么运行BOF,要么通过implant的SOCKS代理功能建立隧道。也许还得考虑将EDR的hook还原,避免检测到unhook操作,当然可能还得移除ETW/AMSI补丁。
这就是一个猫和老鼠的游戏,无疑猫会变得越来越强。
引用文章地址
PE导入表
可疑的API列表
ETW深入研究和攻击面
在用户模式绕过ETW
绕过注入检测
DripLoader
Fork&Run 成为历史
BOF
Outflank 直接系统调用
SysWhispers2
SysWhipers3
SysWhispers is dead, long live SysWhispers
RefleXXion
ThreadStackSpoofer
ShellcodeFluctutation
ReflectiveLoader
BokuLoader
AMSI
Halo's Gate - twin sister of Hell's Gate