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); 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 idaapiimport idautilsimport binascii 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} " )