现在当我和我的红队朋友谈及进程注入的话题时,回答通常是“是的……但是……”。因为检测的风险超过了在宿主进程中“寄生”的需要。典型的进程注入技术过于突出,而且注入往往与恶意活动有关。有时,我喜欢培养这种“反病毒软件bypass”的爱好,并使用恶意 shellcode 来对抗当今最好的端点保护来实现进程注入。
因此,在这篇文章中,我将介绍可以使用哪些绕过技术组合来实现零检测或零告警的进程注入。
1.自定义版本GetProcAddress()
WINAPI
许多规避技术依赖于函数的使用GetProcAddress()
来获取函数的虚拟内存地址。例如,获取函数内存地址以Nt*
执行直接系统调用的典型方法(如结合直接系统调用和 sRDI 绕过 AV/EDR中https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/首次演示的那样)是:
GetProcAddress
(
GetModuleHandle
(
L"ntdll.dll"
),
L"NtWriteProcessMemory"
);
上面一行中的所有内容都是用于检测的签名。"NtWriteProcessMemory"
字符串和的组合"ntdll.dll"
是可疑的,尤其是作为GetModuleHandle
和的参数GetProcAddress
。两者都用于解析函数的内存位置,旨在绕过导出的(和潜在的)API 函数的使用。如今,GetProcAddress
这是一项受到严密监控的功能。
为了绕过这两种检测技术,我们可以使用我们自己的实现,GetProcAddress()
该实现使用进程的PEB
结构来获取可执行文件中(导出的)函数的内存地址。其中PEB
包含有关进程、加载模块 (DLL)、函数等的大量信息,我们可以自己读取这些信息以获取它们的内存位置。获取 的内存位置,向下ntdll.dll
移动到给我们一个我们可以直接调用的函数内存地址列表。有多种实现方式,大多数都归结为相同的原理。PEB
AddressOfNames
WINAPI
我们将使用前面描述的方法混淆的字符串。你还可以考虑使用 API 调用的哈希值进行查找https://www.ired.team/offensive-security/defense-evasion/windows-api-hashing-in-malware。
2.使用硬件断点的系统调用
这种绕过钩子的相对较新的绕过技术我首先在@EthicalChaos的帖子In-Process Patchless AMSI Bypass中发现。他的帖子概述了如何使用硬件断点和矢量异常处理程序 (VEH) 绕过 EDR 挂钩,从而避免对(恶意活动的指示器)ntdll.dll
进行内存修补。ntdll.dll
该技术相当简单:
- 注册一个VEH来处理断点触发的异常。VEH 在引发异常的线程中处理,并且 VEH 可以访问相应的线程上下文(包括所有寄存器)。
- 在要拦截执行的内存地址上设置断点,即
WINAPI NtWriteProcessMemory
. 设置DR7
寄存器会导致操作系统调用已注册的 VEH。GetThreadContext
(
myThread
,
&
ctx
);
/* get thread context */
ctx
.
Dr0
=
(
UINT64
)
&
bp_addr
;
/* address you want to break on */
ctx
.
Dr7
|=
(
1
<<
0
);
/* set first bit in DR7 */
ctx
.
Dr7
&=
~
(
1
<<
16
);
/* clear 16 an 17th bit */
ctx
.
Dr7
&=
~
(
1
<<
17
);
SetThreadContext
(
myThread
,
&
ctx
)
/* set the thread context, putting the breakpoint in place */
- 然后可以从 VEH 接管控制流,并绕过 EDR 挂钩。
@Dec0ne创建了HWSyscalls,其中实现了上述步骤。在 VEH 中,它使用HalosGate在检测到挂钩时解析系统调用号 (SSN) WINAPI
(例如,指令中地址后的下一条指令JMP
)。作为一个很好的补充,HWSyscalls 会将RIP
(指令指针)指向syscall; ret
中的一条指令ntdll.dll
,使返回地址 ( RAX
) 指向ntdll.dll
内存,而不是直接来自加载程序的可执行内存(直接系统调用的指示)。
HWSyscalls 是一个易于集成的模块。我们将在加载器中使用它。
3.无线程注入
下一个新技术,也是这个加载器真正的明星,是@EthicalChaos的无线程进程注入。这种技术只需要VirtualAlloc
, WriteProcessMemory
(和 VirtualProtect
)并且避免使用NtCreateThread(因此我认为是“无线程”)。最后一次调用的失败打破了典型的进程注入检测组合。它是这样的:
- 在远程进程中找到一个足够大的内存位置(“memory hole”或“code cave”),可以容纳我们生成的shellcode和trampoline
- 将shellcode加stub写入code cave。存根将用作trampoline。
JMP
在常用ntdll
功能(例如)之后添加指令NtOpen
。- 等待一个合法的线程调用
NtOpen
,按照JMP
指令执行我们的shellcode。 - trampoline重定向控制流回到合法
NtOpen
指令,以继续进程执行并避免崩溃。
ThreadlessInject存储库https://github.com/CCob/ThreadlessInject中提供了更多详细信息。
4. 绕过常见的恶意模式
这实际上只是重复先前解释的相同技术。其中一项关键检测技术是VirtualAlloc
and WriteProcessMemory
(或 Nt equivalents)调用约 300KB 内存(植入shellcode )。将这些内存操作分块可以绕过DripLoader在两年前引入的检测。所以我们的加载器中也使用这种技术。
5. 休眠绕过
在大部分时间里,植入程度将处于休眠状态,等待下一次 C2 连接。一旦我们成功执行了植入shellcode,在休眠时将其隐藏在内存中是规避 EDR 的关键。已经有一些新的休眠绕过实现,但没有太多的公开,所以让我们扩展一下这个话题。
大多数现在休眠绕过实施都是基于Austin Hudson的 FOLIAGE 技术。其中之一,5pider的Ekko可能是当今使用最广泛的实现。
Ekko(类似于 FOLIAGE,但使用排队计时器而不是排队APC)使用线程池将休眠混淆任务托管给工作线程。工作线程处理主线程(信标所在的位置)的休眠混淆,并在植入程序继续执行时提醒主线程。具体为以下步骤:
- 创建一个新的
Event
和一个来TimerQueue
对混淆操作进行排队。hEvent
=
CreateEventW
(
0
,
0
,
0
,
0
);
hTimerQueue
=
CreateTimerQueue
();
- 使用创建当前(主)线程的快照
RtlCaptureContext
并将其保存在&CtxThread
(WaitForSingleObject
调用只是等待RtlCaptureContext
完成保存快照)。if
(
CreateTimerQueueTimer
(
&
hNewTimer
,
hTimerQueue
,
RtlCaptureContext
,
&
CtxThread
,
0
,
0
,
WT_EXECUTEINTIMERTHREAD
) ) {
WaitForSingleObject
(
hEvent
,
0x32
);
- 然后 Ekko 定义了 6 个不同的上下文结构,每个结构都包含要执行的混淆操作:
memcpy
(
&
RopProtRW
,
&
CtxThread
,
sizeof
(
CONTEXT
) );
// 1. Set memory protection to RW
memcpy
(
&
RopMemEnc
,
&
CtxThread
,
sizeof
(
CONTEXT
) );
// 2. Encrypt memory image, multi-byte RC4 without needing memory allocations
memcpy
(
&
RopDelay
,
&
CtxThread
,
sizeof
(
CONTEXT
) );
// 3. Delay (sleep) for specified amount of time, using WaitForSingleObject on something that does not become alertable
memcpy
(
&
RopMemDec
,
&
CtxThread
,
sizeof
(
CONTEXT
) );
// 4. Decrypt the memory image
memcpy
(
&
RopProtRX
,
&
CtxThread
,
sizeof
(
CONTEXT
) );
// 5. Set memory protection to RX
memcpy
(
&
RopSetEvt
,
&
CtxThread
,
sizeof
(
CONTEXT
) );
// 6. Call SetEvent to alert our main thread that the worker thread is finished.
- 将上述所有调用排队到线程池中,供工作线程执行并在完成时提醒主线程:
CreateTimerQueueTimer
(
&
hNewTimer
,
hTimerQueue
,
NtContinue
,
&
RopProtRW
,
100
,
0
,
WT_EXECUTEINTIMERTHREAD
);
CreateTimerQueueTimer
(
&
hNewTimer
,
hTimerQueue
,
NtContinue
,
&
RopMemEnc
,
200
,
0
,
WT_EXECUTEINTIMERTHREAD
);
CreateTimerQueueTimer
(
&
hNewTimer
,
hTimerQueue
,
NtContinue
,
&
RopDelay
,
300
,
0
,
WT_EXECUTEINTIMERTHREAD
);
CreateTimerQueueTimer
(
&
hNewTimer
,
hTimerQueue
,
NtContinue
,
&
RopMemDec
,
400
,
0
,
WT_EXECUTEINTIMERTHREAD
);
CreateTimerQueueTimer
(
&
hNewTimer
,
hTimerQueue
,
NtContinue
,
&
RopProtRX
,
500
,
0
,
WT_EXECUTEINTIMERTHREAD
);
CreateTimerQueueTimer
(
&
hNewTimer
,
hTimerQueue
,
NtContinue
,
&
RopSetEvt
,
600
,
0
,
WT_EXECUTEINTIMERTHREAD
);
内存扫描不需要钩子或其他RWX
Ekko 在 Cobalt Strike 4.7+ sleepmask 套件中实现,我建议启用它。此外,可以考虑添加ETW 和 AMSI 的无补丁绕过。
对于注入 PoC,我们将使用Kyle Avery的 Cobalt Strike 反射加载器AceLdr,它为我们实现了上述内容。此外,它还会在我们休眠时通过使用NtSetContextThread
“namazso 的 x64 返回地址欺骗器”(强烈推荐他的 DEF CON 30 演讲内容)指向随机的其他线程上下文来欺骗返回地址。
就这样,绕过(至少一个)行业领先的 EDR 产品的检测。而且全部在纯 C 里.exe
,没有其它花哨的语言、运行时、可以使用晦涩的文件扩展名或任何东西。只需“双击即可”。
原文始发于微信公众号(军机故阁):bypass 行业最优质的EDR
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论