EDR Series
翻译:
- EDR Series : How EDR Hooks API Calls (Part-1) (cyberwarfare.live)
- EDR Series : Function Re-casting (Part-2) (cyberwarfare.live)
EDR Series
How EDR Hooks API Calls (Part-1)
EDR 是目前比较热门的话题,本文对 McAfee EDR Hook API 的方式进行一些技术分析。
我们知道多数 EDR 在用户层进行 hook(多数是在 ntdll 中)。本文中我们讲讨论 EDR 如何 hook 这些 Dll 中的 API。我们本文以 McAfee EDR 为例,可能不适用于其它 EDR 产品。
文末有所有的函数流程来帮助理解。
图中我们可以看到 3 个与 McAfee 相关的 DLL 被注入到了当前进程中:
- mfedeeprem32.dll - McAfee Deep remediation Injected
- mfehcinj.dll - McAfee HookCore Injected Environment
- mfehcthe.dll - McAfee HookCore Thin Hook Envrionment
现在的问题是,“哪个 DLL 是最先加载的?”,“它们是一起加载进来的吗?还是一个 DLL 加载了其它 DLL?”。接下来我们使用 x64dbg 寻找这些问题的答案。
在 x64dbg 中启用 DLL Entry 和 DLL Load 的断点:
现在我们重新开始i程序,可以在 DLL 加载时触发断点。我们一直 continue,直到发现其中一个 McAfee DLL。在几个断点之后我们发现 mfehcinj.dll 被加载了,这是第一个加载的 DLL。
我们观察它的导出表,可以看见 4 个导出函数,其中一个是初始化函数:
在 Initialize
函数中我们可以看到对 ThinHookInterface
的调用。ThinHookInterface
基本上时 加载 mfehcthe.dll 然后尝试从中获取GetThinHookInterface
函数的地址并进行一些操作。
在ThinHookInterface
中我们可以看到使用LoadLibararyA
加载 mfehcthe.dll。然后获取GetThinHookInterface
函数并调用它。
在调用GetThinHookInterface
之后,检查返回值是否是 0。如果不是则执行到loc_10007240
,执行一些步骤后调用sub_10006a60
(resolve_ldr_x_addr
)。
进入resolve_ldr_x_addr
,我们可以看到它使用sub_100088a0
(GetProcAddress_Dyn
)获取 ntdll 中的两个函数LdrLoadDll
和LdrResolveDelayLoadedAPI
。
实际上,GetProcAddress_dyn
函数分配了一些内存并将一些数据和获得的函数地址放在一起:
现在我们会有些疑惑为什么将这些获取到的函数地址放到堆上,这是因为这些函数还没有被调用过。经过一些操作后我们回到了 Initialize 函数。再下面几步我们可以看到函数sub_10003550
(mfedeeprem32_and_loading_proc_address
)的调用。简单来说,这个函数加载 mfedeeprem32.dll 并且解析了一些函数地址。
如果我们仔细观察sub_10003550
来查找堆函数sub_10002a90
的调用,我们可以看到这个函数是非常大的。它做了很多操作例如创建命名管道,加载 DLL,解析函数地址等等,其中包含很多函数调用。
然而在这里,我们只关心两件事:
- 被加载的 DLL
- 其它函数是如何解析并存放到堆中的
我们可以看到loc_10002ED1
这个位置,mfedeeprem32.dll 被加载:
一些创建命名管道和配置的操作之后,它调用了 mfedeeprem32.dll 中的函数sub_1000A380
。这个函数真正解析了一些函数地址并放到了堆中。
mfedeeprem32.sub_100A380
调用mfedeeprem32.sub_1000A150
mfedeeprem32.sub_1000A150
中存在一个循环来解析所有的函数mfedeeprem32.sub_1000A150
调用mfehcinj.sub_10003B40
mfehcinj.sub_10003B40
调用sub_100088A0
(GetProc_address_Dyn
)来解析函数地址并且存放到堆中。
以下是所有解析的 API 函数地址(包括最初解析的两个 API):
我们可以看到表中的 19 个 API 是系欸后存储在内存中的。这些完成后我们回到 Initialize
函数。过一段时间后我们进入sub_10005490
,这是 hook 开始的地方。
这里有一些可以再 API Hook 图中仔细查看的函数调用。
在sub_10005490
中调用了mfehcthe.sub_10004FC0
。
如果我们深入查看这个函数,我们可以进行到mfehcthe.sub_10001650
,在这里两个函数被放到内存中。这两个函数与 EDR 的 hook 函数有关,我们稍后会证明这一点。
在mfehcthe.sub_10001650
中,我们可以看到分配了内存并设置为 0xcc。
上面提到两个函数被使用 memcpy 复制进了新分配的内存
在函数mfehcthe.sub_100068F0
中,另一个函数被写入内存(在偏移 0x660 处),这是 hook 后进入的函数,这个函数将调用以上的两个函数。
我们假设mini_shellcode_1
在地址 0xBB0000,hook 后的函数/代码将在偏移 0x660 即 0xBB0660。
我们可以在调试器中确认:
目前为止,EDR hook 函数的代码已经放进了内存中,但是用户层的 patch 还没有应用。需要进行 Jump patch 来在执行 syscall 前检查行为是否可疑。
在函数mfethe.sub_10006180
中进行了 patch。
在mfehcthe.sub_10006180
中我们可以看到调用了 WriteProcessMemory,其中 BaseAddress 是要 Hook 的函数地址,buffer 是 5 字节的 jmp 命令。BaseAddress 是从之前保存的地址中取得的,也就是说有 19 个 BaseAddress。Jump patch 是在调用mfehcthe.sub_10004FC0
之前计算的。
如果我们仔细查看调试器,我们可以确认此时 BaseAddress 是LdrLoadDll
的地址并且 buffer 是 jmp hook。
并且我们也可以确认有 5 字节被覆盖成了 jmp 指令
而在进行 patch 之前一切都是正常的
Patch 之后,我们可以看到 jmp 到的地址是 0xBB0660,这就是 hook 函数的地址。
然后 19 个函数全部都被 hook。
总结起来,19 个函数被查找地址,解析后存放到内存中。然后两个函数被复制进内存,这两个函数用来检查调用 API 的代码。然后,另一个函数被放到内存中偏移 0x660 处。
这三个函数被放到了内存中的相同区域中。第三个函数(0x660)中有一些动态解析的内容,比如第一个函数的地址(mini_shellcode_1)。
第二个函数将在第一个函数中被调用。然后一切都准备好了,需要将 jmp patch 计算出来并应用到这 19 个函数中,这些 jmp 将指向内存中偏移 0x660 的函数
Function Re-casting (Part-2)
在查看 McAfee EDR 时,我们注意到在进程创建时会加载一些 DLL,我们害发现这些 DLL 会进行一些加载库,解析函数地址,写内存等操作,这是我们关心的。所以我们可以利用 EDR 中这些 DLL 的导出函数来做这些事情。首先我们需要查找可能有用的函数。在进行一些逆向后我们发现了允许我们加载库并且获取函数地址的函数。我们可以看到这三个参数都是可以完全控制的:
Param_1 = Module Name
Param_2 = Function Name
Param_3 = FARPROC Pointer
以上函数尝试使用GetModuleHandleA
获取模块的句柄(如果模块已经存在),然后调用GetProcAddress
;如果不存在则先加载模块然后调用GetProcAddress
,这正是我们要找的函数,而且所有的参数都可控。这个函数很容易控制,但是不是所有函数都是这样。过了一会我们发现一个可以创建线程和远程线程的函数。在以下函数中我们可以控制 Param 123 和RtlCreateUserThread
和CreateRemoteThreadEx
的地址。
Param_1 = handle
Param_2 = thread routine/function
Param_3 = parameters for thread function
如果全局变量RtlCreateUserThread
没有设置且CreateRemoteThreadEx
设置了,它将会调用CreateRemoteThreadEx
,反之一样。
目前还剩下一件事就是控制全局变量。全局变量是静态的,我们可以用偏移加上模块基地址来获取。
1 | RtlCreateUserThread = BaseAddress of mfehcinj.dll + 0x7fb44 |
所以我们可以复制两个函数的地址到这些全局变量的位置,记得想要调用一个的时候要将另一个清零。
McAfee 状态
Shellcode 执行
注意:这只是一个利用 McAfee 注入的 DLL 的 POC,只能用于 McAfee 环境。由于这种方式依赖了 McAfee 环境所以分析人员很难静态分析字符串加密的二进制文件。然而它也有一些缺点。