0x01 环境

系统:Windows 10
编译环境:MSVC x86
关键函数:QueueUserAPC 函数

0x02 线程 APC 队列简介

就是在正常函数之外的另外一个函数,类似于SEH在进程遇到异常的地位,默认情况下,创建线程时不会创建这个队列,当调用 QueueUserAPC函数时,就会为这个线程创建这个队列。创建 APC 队列的函数,一般使用 Wait 函数族或者 SleepEx 函数等带有 bAlertable 参数的函数进入一种假"警惕"的状态, 进入 Alertable 状态的线程,系统调度器会在线程函数本身处于"警惕"(暂停等待状态)时,调用线程 APC 队列中的函数。

0x03 原理及步骤

1.原理

在一个进程中,执行到SleepEx()或WaitForSingleObjectEx()时,系统就会产生一个“中断”,当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数。我们就可以利用QueueUserAPC()这个API,以此去执行我们设定的代码,进而完成DLL注入的目的。

2.步骤

0.通过目标窗口名获取PID
1.利用快照枚举所有的线程找到与PID相等的TID
2.开辟远程内存空间,写入Dll的路径
3.插入我们的DLL即可

0x04 代码

#include <Windows.h>
#include <Tlhelp32.h>
#include <iostream>

// 用于保存线程 TID 的指针
ULONG *pThreadId = (ULONG * )malloc(sizeof(ULONG));

// 根据进程名称获取 PID
// ProcessName 进程名称
ULONG GetProcessIdByProcessName(char* ProcessName)
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    //拍快照
    if (INVALID_HANDLE_VALUE == hSnapshot)
    {
        return 0;
    }

    PROCESSENTRY32 pi;
    pi.dwSize = sizeof(PROCESSENTRY32); //初始化成员
    BOOL bRet = Process32First(hSnapshot, &pi);
    while (bRet)
    {
        char Name[MAX_PATH] = { 0 };
        sprintf_s(Name, "%ls", pi.szExeFile);
        if (strcmp(Name, ProcessName) == 0)
        {
            return pi.th32ProcessID; // 返回PID
        }

        bRet = Process32Next(hSnapshot, &pi);
    }

    return 0;
}

// 根据 PID 获取获取相应的线程 TID
// ProcessId    进程 PID
// pThreadId   数组用于保存线程TID
// ThreadIdLen 用于返回数组实际长度
BOOL GetThreadIdByProcessId(ULONG ProcessId, ULONG* pThreadId, ULONG* ThreadIdLen)
{
    HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
    THREADENTRY32 te32 = { sizeof(THREADENTRY32) };
    ULONG Number = 0;

    // 把所有线程拍一个快照
    hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (hThreadSnap == INVALID_HANDLE_VALUE)
        return(FALSE);

    // 在使用 Thread32First 前初始化 THREADENTRY32 的结构大小.
    te32.dwSize = sizeof(THREADENTRY32);

    // 现在获取系统线程列表, 并显示与指定进程相关的每个线程的信息
    do {
        // 比对是否为该进程线程
        if (te32.th32OwnerProcessID == ProcessId)
        {
            // 是的话保存到线程数组中
            *pThreadId = te32.th32ThreadID;
            break;
        }
    } while (Thread32Next(hThreadSnap, &te32));

    if (!Number)
        return FALSE;

    return TRUE;
}

// APC注入
// ProcessName 要注入的进程名称
// DllName     要注入的DLL名称
BOOL ApcInjectDll(char* ProcessName, char* DllName)
{
    ULONG ProcessId = 0;
    ULONG IsFlag = TRUE;
    HANDLE hProcess = NULL, hThread = NULL;
    ULONG DllNameLen = strlen(DllName) + 1;
    PVOID pBaseAddr = NULL;
    ULONG dwRet = 0;
    PVOID pLoadLibraryA = NULL;
    ULONG ThreadIdsLen = 0;

    do
    {
        // 根据进程名称获取 PID
        ProcessId = GetProcessIdByProcessName(ProcessName);
        if (0 >= ProcessId)
        {
            printf("GetProcessIdByProcessName Error: %d\n", GetLastError());
            IsFlag = FALSE;
            break;
        }
        // 根据 PID 获取获取所有相应的线程 TID
        IsFlag = GetThreadIdByProcessId(ProcessId, pThreadId, &ThreadIdsLen);
        if (!IsFlag)
        {
            printf("GetAllThreadIdByProcessId Error: %x\n", GetLastError());
            IsFlag = FALSE;
            break;
        }
        // 打开注入线程
        hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
        if (hProcess == NULL)
        {

            printf("OpenProcess Error: %x\n", GetLastError());
            IsFlag = FALSE;
            break;
        }
        // 在注入的进程中申请空间
        pBaseAddr = VirtualAllocEx(hProcess, NULL, DllNameLen, MEM_COMMIT | MEM_RESERVE,
                PAGE_EXECUTE_READWRITE);
        if (pBaseAddr == NULL)
        {
            printf("VirtualAllocEx Error: %x\n", GetLastError());
            IsFlag = FALSE;
            break;
        }
        // 向申请的空间中写入 Dll 路径数据
        WriteProcessMemory(hProcess, pBaseAddr, DllName, DllNameLen, &dwRet);
        if (DllNameLen != dwRet)
        {
            printf("WriteProcessMemory Error: %x\n", GetLastError());
            IsFlag = FALSE;
            break;
        }

        hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, *pThreadId);
        if (hThread != NULL)
        {
            // 插入 APC
            if (!QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)pBaseAddr))
            {
                printf("QueueUserAPC Error: %x\n", GetLastError());
            }
            // 关闭线程句柄
            CloseHandle(hThread);
            hThread = NULL;
            printf("插入 APC 成功!\n");
        }

    } while (FALSE);

    CloseHandle(hProcess);
    return IsFlag;
}

int main()
{
    // 注入我们想要执行的DLL
    ApcInjectDll((char*)"TestExe.exe", (char*)"./Dll1.dll");
    system("pause");
    return 0;
}

0x05 总结及相关文章

总的来说APC注入对目标进程是有要求的,需要对方进入“警惕”状态下的,文章中的代码仅向目标进程的一个线程进行注入,如果有需要可以添加提权和枚举所有线程的操作。其次也在其他网站上看到APC队列的由来好像是和驱动文件有关系,所以在内核层面应该会有不一样的APC注入。
学习文章:
UserMode下的APC插入
APC异步过程调用
常见注入手法第二讲,APC注入(注意此方法中的示范代码并不能成功注入,个人认为是因为MFC消息机制等原因,原理是相似的)