所有关注攻击性安全社区的人都会在过去两年中一次又一次地遇到Userland hooking
, Syscalls
, P/Invoke
/D-Invoke
等术语。我自己也遇到了一些我不完全理解的博客文章和工具。我有时觉得我需要从头开始积累知识。由于我在很多情况下不需要这些“新”技术,我把这些课题的研究推迟了几个月。
随着安全事件的日益增多,越来越多的企业建立了安全运营中心(SOC)或计算机应急响应小组(CERT)。另一个术语是“网络防御中心”。这些单位的主要目的是分析新出现的安全事件,并查明和阻止潜在的攻击者。除了SIEM之外,EDR系统也越来越多地被用于分析。与此同时,EDR绕道话题对我们进攻性的安全人员来说变得越来越重要。长话短说:为了能够复制和使用公共技术,我现在必须自己深入研究这些话题。我认为激励自己的最好方法就是写一篇关于这个话题的博客文章。这些工具和技术,实际上已经出版了,比我在这篇文章中要提到的参考文献要古老得多。它们以前已经被恶意软件在野外积极使用。这篇博文将是我发现的公开工具/技术的总结。我强烈建议你阅读这里链接的所有其他博客文章。它们包含了更多的信息和背景知识。在深入讨论主要主题之前,我们必须先了解一些Windows操作系统体系结构的基础知识,以及有关汇编代码的一小部分内容。请跳过这部分。
如果您正在编写一个独立于编程语言的程序,则很可能使用编译器从相应的源代码构建程序。源代码片段基本上被翻译成机器语言,即01010011 00110011 01100011 011010101 01110010 00110011
这样的最终二进制代码,可以由CPU直接执行:
一些编译器(例如gcc)在转换为机器代码之前会生成汇编代码。汇编代码指令实际上与机器代码具有一对一的映射关系。因此,这是最接近机器码的代码,例如:
通过IDA Pro或Ghidra反汇编程序,您还可以从已编译的源代码中获得汇编代码。
程序员通常不想重新发明轮子,所以基本函数是从现有库中导入的。例如,printf()
是用C语言从库stdio.h
导入的。例如,Windows开发人员正在使用应用程序编程接口(API),API也可以导入到程序中。所谓的Win32 API是有文档记录的,由几个库文件(DLL文件)组成,这些文件位于C:\windows\system32\
文件夹中,例如kernel32.DLL
、User32.DLL
等:
NTDLL.dll
不是Win32 API的一部分,也没有正式的文档。
Windows操作系统具有两种不同的特权级别,这些特权级别是为了保护操作系统免受例如已安装的应用程序导致的崩溃而实现的。Windows系统上安装的所有应用程序均以所谓的用户模式运行。 内核和设备驱动程序以所谓的内核模式运行。用户模式下的应用程序无法访问或操作内核模式下的内存部分。由于内核补丁保护,AV/EDR系统只能在用户模式下监视应用程序行为。用户模式中的最后一个实例是NTDLL.dll
中的Windows API函数。如果调用了NTDLL.dll中的任何功能,则CPU接下来将切换到内核模式,AV/EDR就不再能够监视该模式。NTDLL.dll的单个功能称为Syscalls
。
作为模拟攻击者,我们在哪里需要或使用Windows API?例如,如果我们要将特定的字节(例如shellcode
)写入进程,则可以使用以下C#代码片段从文件kernel32.dll
导入WriteProcessMemory
:
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);
在此处可以找到一个如何使用kernel32.dll
函数将shellcode写入远程进程的示例。
我们大多数人最常使用的是PE-Loaders。在大多数情况下,我们希望尽可能长时间地保留注入的内存,以免在磁盘上留下任何痕迹,也不会出现AV-Evasion。因此,必须从内存中加载Mimikatz或任何其他C编写的工具,这是通过PE-Loader完成的。 Powersploits Invoke-ReflectivePEInjection
或Casey Smith
的C#PE-Loader
大量使用Windows API函数,例如kernel32.dll
中的CreateRemoteThread
,GetProcAddress
,CreateThread
。
Last but not least - 取决于您使用的Command&Control(C2)框架-他们中的大多数使用WindowsAPI函数作为他们的模块。
但是Win32 API文件中包含的功能(例如kernel32.dll
,User32.dll
等)没有直接转换为机器代码,而是从本地API NTDLL.dll映射到其他功能。例如,来自kernel32.dll的writeProcessMemory
将从NTDLL.dll解析为NtProtectVirtualMemory
-> NtWriteVirtualMemory
-> NtProtectVirtualMemory
。
第一个Syscall
NtProtectVirtualMemory
为该进程设置新权限并使其可写
第二个NtWriteVirtualMemory
实际上写入字节
第三个调用恢复该进程的旧权限。
因此,本机API NTDLL.dll是操作系统前面的最后一个实例。
自从NTDLL.dll函数成为最后一个实例以来,AV/EDR可以监视攻击者或恶意软件的可疑活动,市场目前通用这个玩法。他们将自定义DLL文件注入到每个新进程中。您可以找到DLL文件,这些文件是通过Sysinternals procexp64.exe
从AV/EDR加载到进程中的。您需要检查“View”菜单中的“Show Lower Pane”按钮,然后检查该按钮以显示已加载的DLL:
选择首选过程后,您将在“下部窗格”视图部分中看到已加载的DLL文件。在这种情况下,我们看到由McAfee AV的CMD.EXE加载的DLL的文件:
Powershell.exe
从McAfee注入了更多的DLL,这很可能是因为它监控了更多的用例。
如您所见,McAfee注入了三个DLL文件,其中一个被称为“ Thin Hook Environment
”-最有可能监视Windows API调用的DLL。
因此,这些加载的DLL文件将监控为特定Windows API调用注入它们的过程。在我的上一篇博客文章中,我以签名更改,运行时加密和解密等形式撰写了有关AV-Evasion的文章。如果我们对shellcode进行加密并在运行时对其进行解密以将其写入到远程进程中,则可以调用writeProcessMemory
,该函数在幕后有时会调用NtWriteVirtualMemory
。AV/EDR可能的目标之一是查看攻击者在运行时准确加载到内存中的内容。因此他们可以监视NtWriteVirtualMemory
调用。但是如何进行“监视”呢?
如果程序从kernel32.dll加载了类似NtWriteVirtualMemory
的函数,则将kernel32.dll的副本放入内存。AV/EDR通常会处理此文件的内存中副本,并将自己的代码添加到特定功能中,例如NtWriteVirtualMemory
。当程序调用该函数时,将首先执行AV/EDR附加代码,例如在NtWriteVirtualMemory
的情况下,将对字节进行分析,然后将shell写入远程进程。通过使用此技术,他们可以看到明文shellcode字节,因为此时它们已被解密。 通过修补API函数将自己的代码嵌入内存中的AV/EDR的技术称为Userland-Hooking
。
通过加载自定义的Invoke-Mimikatz版本(就像我在第二篇博客文章通过手动修改第二部分绕过AMSI并在系统上启用防御程序一样),内存扫描器在解密和PE加载后从内存中捕获了Mimikatz。如果您再次查看该代码-首先完成解密,然后再运行PE-Loader。现在我们知道,PE-Loader调用了几个潜在的可疑Windows API调用。这些调用会触发内存扫描器。因此,避免调用将根本不进行内存扫描。
到目前为止,我已经知道Userland-Hooking
技术已经公开,它以某种方式unhooking,将其重新修补到内存中,修补AV/EDR的DLL或避免通过使用直接Syscall加载Windows API函数。
@SpecialHoang和MDsec在2019年初发布了博客文章,解释了如何通过修补补丁来绕过AV/EDR软件:
https://medium.com/@fsx30/bypass-edrs-memory-protection-introduction-to-hooking-2efb21acffd6 https://www.mdsec.co.uk/2019/03/silencing-cylance-a-case-study-in-modern-edrs/
如果您的植入程序或工具从kernel32.dll或NTDLL.dll加载某些功能,则库文件的副本将加载到内存中。AV / EDR供应商通常会从内存中的副本中修补某些功能,并将JMP汇编程序指令放在代码的开头,以将Windows API功能重定向到AV / EDR软件本身的某些检查代码。因此,在调用真实的Windows API函数代码之前,需要进行分析。如果此分析没有导致可疑/恶意行为,并且返回了干净的结果,则随后将调用原始Windows API函数。如果发现恶意软件,则Windows API调用将被阻止,否则该进程将被终止。我从ired.team盗的图,这可能有助于理解该过程:
这两篇博文都侧重于绕过EDR软件CylancePROTECT
并为此特定软件构建PoC代码。通过修补来自内存中被操纵的NTDLL.dll的其他JMP指令,Cylance的分析代码将永远不会被执行。因此,无法进行检测/阻止:
此技术的一个缺点是,您可能必须为每个不同的AV/EDR更改补丁。它们不太可能在同一点的相同功能之前都放置一条附加的JMP指令。他们很可能会hook不同的功能,并可能在其补丁程序中使用其他位置。如果您已经知道目标环境中已安装了哪种AV/EDR解决方案,则可以使用此技术,并且可以通过打补丁来绕过保护措施。
我还找到了一个包含AV/EDR供应商及其相应的hook Windows API函数的PDF文件的存储库,如果您感兴趣,请在此处查看:
https://github.com/D3VI5H4/Antivirus-Artifacts
Outflanknl在2019年6月19日的博客文章中发布了一个名为Dumpert的工具,他们在其中解释了如何使用直接系统调用绕过Userland-Hooking
。我不会覆盖博客文章中的所有详细信息,而仅总结最重要的事实以理解该主题。这里使用的技术的目标是在运行时不从ntdll.dll加载任何函数,而是直接使用相应的汇编代码来调用它们。通过反汇编ntdll.dll文件,可以获取其中包含的每个函数的汇编代码。
这里的一个问题是,在Windows OS版本之间,有时甚至在Service Pack /内部版本号之间,汇编代码有时有所不同。Google项目Zero对这些差异进行了一些研究,以便可以在链接的网站上查找它们。通过为所有OS版本嵌入所有不同的汇编代码版本,可以在运行时检查基础操作系统,并为所需的Windows API函数选择正确的汇编代码。可以使用ASM文件通过Visual Studio将汇编程序代码嵌入C项目中。因此,Dumpert项目正在使用ASM文件,该文件在每个Windows版本的汇编代码中都包含所有必要的Windows API函数:
https://github.com/outflanknl/Dumpert/blob/master/Dumpert-DLL/Outflank-Dumpert-DLL/Syscalls.asm
要使用此技术,您需要了解项目所需的确切NTDLL.dll函数,并通过反汇编为它们提取相应的汇编代码。之后,您需要构建一个ASM文件,其中包含针对不同Windows OS版本的所有不同偏移量。听起来很复杂。
使用此技术也有一些缺点:
但是使用这种技术将使我们能够绕开Userland-Hooking。此技术独立于不同的供应商。他们都根本看不到任何Windows API函数导入或调用。No function imports -> no patch/hook by the AV/EDR software -> stealth/bypass.
随着SysWhispers工具的发布,使用相应的C-Header文件创建自定义ASM文件变得更加容易。卸载ntdll.dll的手动开销被省去了。通过执行单个python脚本,可以轻松构建ASM和Header-File:
大约1个月前发布了SysWhispers2,它减少了ASM文件的大小,并在每一代中使用了随机的函数名称哈希。将来将不推荐使用第一个版本,因此您应该使用受支持的版本2。
Dumpert,Syswhispers和Syswhispers2当前仅支持x64 Syscall。如果您需要x86 Syscall,则在Github上发布了SysWhispers2_x86。
如果您不想用C语言编写工具/植入物,也可以使用NimlineWhispers来动手,后者可为ASM文件和Nim-Code头文件建立文件。@ ajpc500还写了一篇不错的博客文章,介绍如何使用NimlineWhispers通过Nim进行Shellcode注入。在此处查看博客文章。我自己玩过Nim syscall Shellcode Injection PoC,它的工作原理很吸引人!请注意,使用默认的NTDLL.dll函数名称将导致以明文形式包含它们的二进制文件,可通过任何hexeditor看到该二进制文件:
在与@IKalendarov谈论NimlineWhispers时,他发现启用了云保护的Windows Defender成功执行了Shellcode,但是引发了警告,指出随后检测到防御逃避:
我发现,通过重命名ASM文件中的Windows API函数,当然也可以重命名shellcode注入代码中的Windows API函数,可以轻松绕过此检测。例如,NtAllocateVirtualMemory
变为NtAVM,依此类推。如果您的Shellcode本身或其背后的代码包含任何Windows API函数导入-可以再次检测到。因此,shellcode加载程序和shellcode本身应使用Syscall来保持未被Userland-Hooks检测到。
@TheRealWover发布了一个名为D/Invoke的C#库。首先,它被添加到SharpSploit,但是后来在TheWover上发布了一个nuget包,可以在这里的任何VisualStudio项目中导入。2020年6月也有相应的博客文章。如果您主要使用C#编码,那么实际上这是您进行Userland-Hooking绕过的最简单方法。我只是从TheWovers帖子中挑选一小部分,因为此博客文章会通过解释所有内容而爆炸。如果您是这个主题的新手,他的博客文章可能有点“繁重”。我不懂第一次读它的一半。@ Jean_Maes_1994发布了一个博客文章,在此总结了通过D / Invoke使用的所有技术。生成的PoC代码DInvisibleRegistry
可用于查找不同的D/Invoke实现方法,并且在我看来非常有用和易于理解
P/Invoke基本上是从Windows库文件静态导入API调用的默认方式。从上面显示的kernel32.dll导入WriteProcessMemory
是P/Invoke方法。通过使用此方法,AV / EDR系统可以修补Windows库文件(如NTDLL.dll)的内存副本
与P / Invoke相比,D / Invoke在运行时手动加载Windows API函数,并使用指向其在内存中位置的指针来调用该函数。在编写时,AV / EDR挂钩未检测到运行时手动加载库文件的情况,因此它们不会修补新导入的功能,并且在没有 hook/patch的情况下仍保持原始状态。
有三种不同的方法可以避免通过D / Invoke进行Userland-Hooking
:
DInvoke.Data.PE.PE_MANUAL_MAP mappedDLL = new DInvoke.Data.PE.PE_MANUAL_MAP();
mappedDLL = DInvoke.ManualMap.Map.MapModuleToMemory(@"C:\Windows\System32\ntdll.dll");
DInvoke.Data.PE.PE_MANUAL_MAP mappedDLL = DInvoke.ManualMap.Overload.OverloadModule(@"C:\Windows\System32\ntdll.dll");
IntPtr pAllocateSysCall = DInvoke.DynamicInvoke.Generic.GetSyscallStub("NtAllocateVirtualMemory");
NtAllocateVirtualMemory fSyscallAllocateMemory = (NtAllocateVirtualMemory)Marshal.GetDelegateForFunctionPointer(pAllocateSysCall, typeof(NtAllocateVirtualMemory));
对于这三种方法中的每一种,您还需要为代码中的每个Windows API函数创建非托管的委托。我不会在这里介绍整个过程,因为您可以阅读@TheRealWover或@ Jean_Maes_1994中的链接博客文章。
最初,我计划展示如何将P / Invoke CreateRemoteThread C#shellcode注入PoC移植到D / Invoke Syscall版本中。我在摆弄所有需要的所有NTDLL.dll函数,例如NtOpenProcess,NtAllocateVirtualMemory,NtWriteVirtualMemory和CreateThreadEx,但不幸的是无法成功使我的Shellcode执行正常。这是因为我以前从未使用过那些NTDLL.dll函数,并且一直在为“哪个值应放在哪个函数参数中”,“哪个kernel32.dll函数解析为哪个ntdll.dll函数”而苦苦挣扎,并在许多晚上深思熟虑试图使它起作用。同时,@_RastaMouse只用了几天,他就发表了一篇完整的博客文章,内容涉及这个主题: https://offensivedefence.co.uk/posts/dinvoke-syscalls/
我的PoC可以处理他博客文章中的信息。只需自己阅读即可。
我们了解到,AV / EDR系统挂接NTDLL.dll的特定功能,以将其自己的代码放入其中进行分析。ired.team上有一篇很好的简短文章,它解释了如何将NTDLL.dll的新副本从磁盘映射到内存,将.text部分从新副本复制到内存中已挂接文件的.text部分,因此 通过覆盖钩子撤消钩子:
还包括用于unhooking过程的C ++ PoC代码以及分步指南。如果您还没有读完,请继续阅读。
再说一次-如果有人不太熟悉C / C ++编码-我最近玩过OffensiveNim
,而OffensiveNim
存储库包含一个名为clr_host_cpp_embed_bin.nim的模板,我们可以在其中嵌入纯C ++代码。我们可以使用此模板,并将ired.team网站中的C ++ PoC嵌入其中,并且在Nim中有一个可以正常工作的NTDLL.dll取消对PoC的绑定:
when not defined(cpp):
{.error: "Must be compiled in cpp mode"}
# Stolen from https://www.ired.team/offensive-security/defense-evasion/how-to-unhook-a-dll-using-c++
{.emit: """
#include <iostream>
#include <Windows.h>
#include <winternl.h>
#include <psapi.h>
int test()
{
HANDLE process = GetCurrentProcess();
MODULEINFO mi = {};
HMODULE ntdllModule = GetModuleHandleA("ntdll.dll");
GetModuleInformation(process, ntdllModule, &mi, sizeof(mi));
LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll;
HANDLE ntdllFile = CreateFileA("c:\\windows\\system32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);
PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew);
for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++) {
PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {
DWORD oldProtection = 0;
bool isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtection);
memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize);
isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, oldProtection, &oldProtection);
}
}
CloseHandle(process);
CloseHandle(ntdllFile);
CloseHandle(ntdllMapping);
FreeLibrary(ntdllModule);
return 1;
}
""".}
proc unhook(): int
{.importcpp: "test", nodecl.}
when isMainModule:
var result = unhook()
echo "[*] Assembly executed: ", bool(result)
# Every code from here is not hooked / detected from Windows API imports at runtime anymore
如果您正在寻找unhooking NTDLL.dll的独立于语言的解决方案,我可以推荐@slaeryans Shellycoat shellcode。
通过首先注入此shellcode(可以用任何语言完成),都可以完成替换已钩住的NTDLL.dll的.text部分的相同过程。注入Shellycoat之后,您可以注入您的植入代码,钩子将不再检测到该代码。Slaeryan还介绍了使用Pros&Cons在回购中如何解除NTDLL.dll的钩挂的不同方法,值得一读。
@EthicalChaos采用了绕过EDR系统的新方法。这在两篇博客文章中进行了解释:让我们创建EDR并绕过第一部分,让我们创建EDR并绕过第二部分-同样是从2020年6月开始,使用最终的工具SharpBlock。
与以前相比,SharpBlock使用的方法有所不同。它正在创建一个新进程,并使用Windows调试API侦听LOAD_DLL_DEBUG_EVENT事件。SharpBlock正在寻找要通过调试API加载EDR的DLL,并修补此新注入的DLL的Entrypoint,以便它仅返回TRUE,而不执行其他任何操作。因此,目标DLL将不执行任何操作并退出->再也没有钩子/补丁。
SharpBlock使我们能够指定目标DLL文件名或描述来修补其入口点 在为这篇博文使用SharpBlock时,我尝试使用以下命令阻止McAfees EpMPThe.dll:
SharpBlock.exe -d "McAfee Endpoint Thin Hook Environment" --disable-bypass-amsi -e "C:\Windows\System32\cmd.exe" --disable-bypass-etw --disable-header-patch -w
这导致以下行为
我向@EthicalChaos询问了导致此失败块的可能原因,他告诉我,这很可能是针对SharpBlock的第一个保护机制。特别是第123行无法执行,即WriteProcessMemory函数。
如上所述,WriteProcessMemory将从NTDLL.dll解析为NtProtectVirtualMemory
和NtWriteVirtualMemory
,并且通过McAfee接缝来阻止进程通过NtProtectVirtualMemory更改其将DLL的内存保护钩到RWX或通过NtWriteVirtualMemory
写入。因此,Sharpblock本身与EpMPThe.dll挂钩,并且由于Userland-Hook而无法修补McAfee挂钩DLL。这个博客是关于Userland-Hooking
绕过方法的-一种方法是使用直接Syscall而不是API导入,对吗?
使用D / Invokes方法GetSyscallStub @EthicalChaos更改了WriteProcessMemory函数,以在另一个分支中定向Syscall。在该分支中,无需钩子即可直接调用NtProtectVirtualMemory和NtWriteVirtualMemory,以便SharpBlock修补McAfee再次成功hook DLL的McAfee:
And - tada - the DLL is not loaded anymore:
@ am0nsec和@smelly__vx发布了另一种使用直接Syscall进行Shellcode执行的技术。 他们发布了用c编写的PoC代码以及.NET Core编写的PoC。
就我从“仅”略读官方论文所了解的范围而言,从NTDLL.dll或其他库文件中检索函数的正确Syscall的方法是不同的。因此,它们不是直接从文件中提取的。但是我必须承认,这篇论文是用“沉重的”语言写的-因此,对于像我这样并不十分精通此学科的人来说,很难理解。我要在几个月后再读一次,也许我会更好地理解该方法-有时等待并阅读其他文章/论文是关键。