Bypass EDR

1.内存动态检测

1.1 检测模式

1.1.1 模式匹配

基于特征进行检测。代表性的工具有YARA,以及针对CobaltStrike的检测工具 BeaconEye。

BeaconEye工具主要针对Beacon的配置文件在堆内存中的某一刻一定是解密状态的,甚至配置文件中可以获得TeamServer的哈希。

1.1.2 内存IOCs

基于可疑内存属性的检测。如私有内存(VirtualAlloc)的可执行属性,线程地址未处于映像内存中。

1.1.3 栈回溯

基于睡眠的检测,如Hook住Sleep等函数,在Beacon睡眠时进行栈回溯扫描。

代表工具:BeaconHunter、Hunt-Sleeping-Beacons、MalMemDetect

2.绕过内存检测

2.1 加密堆

2.1.1 加密算法选择

使用单一异或是过不了YARA的,而自己实现RC4等加密算法可能会产生额外的可执行代码块。

系统API SystemFunction032是个不错的RC4加密选择。

2.1.2 基础堆加密

LockdExeDemo -> 存在不稳定的情况

Secondary Heap

Sleep Mask -> cs官方提供,但是需要可执行存根进行错误处理,可能被检测。

2.1.3 基于异常处理

Shellcode Fluctuation -> 将内存设置不可执行,并注册veh异常处理程序,在执行到不可执行内存时进行修复。

2.1.4 基于ROP

Gargoyle -> 使用APC,但是只做了32位的poc

YouMayPasser -> Gargoyle的64位实现

DeepSleep

ROP可能会出现在版本更新后,在进程中找不到gadget的情况。

FOLIAGE -> 不使用ROP,使用NtContinue完成

工作原理:

1.获取KsecDD驱动句柄来执行加密操作

2.获取当前线程句柄以便于操作线程Context

3.创建一个新的线程以获取APC队列

4.创建一个Event来防止新的线程退出

5.拷贝新线程的Context到一个单独的结构体中,后续会重用它

6.对一系列NtContinue APC调用进行排队,每个调用都有一个Context定义睡眠链中的一个步骤,该步骤将返回到NtTestAlert,步骤中做的事情如下:

​ 6.1 等待event防止线程退出

​ 6.2 改变目标内存权限为RW

​ 6.3 通知KsecDD驱动进行加密

​ 6.4 备份原始线程Context

​ 6.5 将线程上下文替换为假的

​ 6.6 调用NtDelayExecution睡眠指定的时间,这个API可能会被检测,可以自己实现,但是又会引发新的问题 -> 我们不想要可执行代码。可以使用WaitForSingleObject的超时参数来模拟休眠。

​ 6.7 通知KsecDD驱动进行解密

​ 6.8 恢复线程Context

​ 6.9 改变目标内存属性为RWX(不要改为RX,因为会造成SMC奔溃)

​ 6.10 结束该新线程

7.强制该新线程进入警醒状态,来执行APC

8.向事件发送信号阻止原始线程继续执行

Ekko与FOLIAGE类似,不过是使用计时器进行APC排序的。

2.1 返回地址伪造

2.1.1 静态伪造

Thread Stack Spoofing -> 将返回地址用0填充

调用NtSetContextThread API来伪造Context结构

2.1.2 动态伪造

Call Stack Spoofers -> 使用了异常处理

x64 Return Address Spoofing

武器库

1.根据hash获取导出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#define HASH_KEY						13
__forceinline DWORD ror(DWORD d)
{
return _rotr(d, HASH_KEY);
}

__forceinline DWORD hash(char* c)
{
register DWORD h = 0;
do
{
h = ror(h);
h += *c;
} while (*++c);

return h;
}

DWORD64 get_export_fun(DWORD dstHash) {
DWORD64 modBase = (DWORD64)LoadLibrary(L"Kernel32.dll");
IMAGE_NT_HEADERS64* inh = (IMAGE_NT_HEADERS64*)(modBase + ((IMAGE_DOS_HEADER*)modBase)->e_lfanew);
IMAGE_DATA_DIRECTORY& idd = inh->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (idd.VirtualAddress == 0)
return 0;

IMAGE_EXPORT_DIRECTORY* ied = (IMAGE_EXPORT_DIRECTORY*)(modBase + idd.VirtualAddress);

DWORD* rvaTable = (DWORD*)(modBase + ied->AddressOfFunctions);
WORD* ordTable = (WORD*)(modBase + ied->AddressOfNameOrdinals);
DWORD* nameTable = (DWORD*)(modBase + ied->AddressOfNames);
//lazy search, there is no need to use binsearch for just one function
for (DWORD i = 0; i < ied->NumberOfFunctions; i++)
{

DWORD curHash = hash((char*)((ULONG_PTR)modBase + nameTable[i]));

if (curHash != dstHash)
continue;
else
return (DWORD64)(modBase + rvaTable[ordTable[i]]);
}
return 0;
}

IDA Python脚本,用于提取Shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import idaapi
import idautils
import binascii

# ida python脚本
# 将指定的函数硬编码全部提取出来

# 指定函数的名称
function_name = "get_export_fun"

# 获取函数对象
function = idaapi.get_name_ea(idaapi.BADADDR, function_name)
if function != idaapi.BADADDR:
# 获取函数的起始地址和结束地址
function_start = idaapi.get_func(function).start_ea
function_end = idaapi.get_func(function).end_ea

# 获取函数的指令地址列表
instructions = list(idautils.Heads(function_start, function_end))

# 输出每条指令的字节
for address in instructions:

bytes_data = idaapi.get_bytes(address, idaapi.get_item_size(address))
disasm = idc.GetDisasm(address)
print(f"// {disasm}")

for dat in bytes_data:
hex_obj = bytes([dat])
hex_string = binascii.hexlify(hex_obj).decode("utf-8")
print(f"__asm __emit(0x{hex_string})")
else:
print(f"找不到函数:{function_name}")

Bypass EDR
http://helloymf.github.io/2023/07/13/bypass-edr/
作者
JNZ
许可协议