APC注入

前言:

  "APC"是"Asynchronous Procedure Call"(异步过程调用)的缩写,它是一种软中断机制,当一个线程从等待状态中苏醒时(线程调用SignalObjectAndWait 、SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx函数时会进入可唤醒状态),它会检测有没有APC交付给自己。如果有,就会执行这些APC过程。APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC称为用户模式APC。APC调用的顺序为先入先出(FIFO)。我们可以使用 QueueUserAPC 函数把一个APC函数压入APC队列中

函数介绍:  

DWORD
WINAPI
QueueUserAPC(
    _In_ PAPCFUNC pfnAPC,        //APC函数的地址
    _In_ HANDLE hThread,            //线程句柄
    _In_ ULONG_PTR dwData        //APC函数的参数
    );

//APC函数原型
VOID NTAPI PAPCFUNC(
    _In_ ULONG_PTR Parameter
    );

实现原理:

  将 QueueUserAPC 函数的第一个参数(函数地址)设置的是LoadLibraryA函数地址;第三个参数(传递参数)设置的是DLL路径,第二个参数要注入的进程的线程句柄,那么执行APC时便会调用LoadLibraryA函数加载指定路径的DLL,完成DLL注入操作。

  注意:一个进程可能包含多个线程,为了确保能够执行插入的APC,应向目标进程的所有线程都插入相同的APC,实现加载DLL的操作

实现流程:

  (1).通过OpenProcess打开目标进程,获取目标进程的句柄

  (2).使用VirtualAllocEx在目标进程中申请空间

  (3).使用WriteProcessMemory函数在刚申请的空间中写入要注入的DLL路径

  (4).获取LoadLibraryA函数地址

  (5).通过CreateToolhelp32Snapshot、Thread32First以及Thread32Next遍历线程快照,获取目标进程的所有线程ID

  (6).遍历获取的线程ID,通过OpenThread函数以THREAD_ALL_ACCESS访问权限打开线程,获取线程句柄

  (7).使用QueueUserAPC向所有线程插入APC函数,参数1是(4)步获取的地址,参数2是(6)步获取的句柄,参数3是(2)步申请空间的首地址

实现代码:

//APC注入
BOOL CInjectDlg::ApcInjectDll(char* pszProcessName, char* pszDllFileName)
{
    DWORD* pThreadId = NULL;
    DWORD dwThreadIdLength = 0;
    //根据进程名获取PID
    DWORD dwProcessId = GetProcessIdByProcessName(pszProcessName);
    if (dwProcessId <= 0)
    {    
        return FALSE;
    }
    //根据PID获取所有相应的线程ID
    DWORD bRet = GetAllThreadIdByProcessId(dwProcessId, &pThreadId, &dwThreadIdLength);
    if (FALSE == bRet)
    {
        return FALSE;
    }
    //打开目标进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
    if (NULL == hProcess)
    {
        MessageBox(L"打开目标进程失败!");
        return FALSE;
    }
    // 在目标进程中申请空间
    LPVOID lpPathAddr = VirtualAllocEx(
        hProcess,                    // 目标进程句柄
        0,                            // 指定申请地址
        strlen(pszDllFileName) + 1,    // 申请空间大小
        MEM_RESERVE | MEM_COMMIT,    // 内存的状态
        PAGE_READWRITE);            // 内存属性
    if (NULL == lpPathAddr)
    {
        MessageBox(L"在目标进程中申请空间失败!");
        CloseHandle(hProcess);
        return FALSE;
    }
    // 3.在目标进程中写入Dll路径
    if (FALSE == WriteProcessMemory(
        hProcess,                    // 目标进程句柄
        lpPathAddr,                    // 目标进程地址
        pszDllFileName,                    // 写入的缓冲区
        strlen(pszDllFileName) + 1,    // 缓冲区大小
        NULL))                // 实际写入大小
    {
        MessageBox(L"目标进程中写入Dll路径失败!");
        CloseHandle(hProcess);
        return FALSE;
    }
    //5.获取LoadLibraryA的函数地址
    //FARPROC可以自适应32位与64位
    FARPROC pFuncProcAddr = GetProcAddress(GetModuleHandle((LPCWSTR)L"kernel32.dll"), "LoadLibraryA");
    if (NULL == pFuncProcAddr)
    {
        MessageBox(L"获取LoadLibrary函数地址失败!");
        CloseHandle(hProcess);
        return FALSE;
    }
    // 遍历线程, 插入APC
    for (int i = 0; i < dwThreadIdLength; i++)
    {
        // 打开线程
        HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadId[i]);
        if (hThread)
        {
            // 插入APC
            QueueUserAPC((PAPCFUNC)pFuncProcAddr, hThread, (ULONG_PTR)lpPathAddr);
            // 关闭线程句柄
            CloseHandle(hThread);
            hThread = NULL;
        }
    }

    return TRUE;
}

//根据进程名获取PID
DWORD CInjectDlg::GetProcessIdByProcessName(char* pszProcessName)
{
    //1.创建进程快照
    HANDLE hSnap = CreateToolhelp32Snapshot(
        TH32CS_SNAPPROCESS,            //遍历进程快照1
        0);                            //进程PID
    if (NULL == hSnap)
    {
        MessageBox(L"创建进程快照失败!");
        return 0;
    }

    //2.获取第一条进程快照信息
    PROCESSENTRY32  stcPe = { sizeof(stcPe) };
    if (Process32First(hSnap, &stcPe))
    {

        //3.循环遍历进程Next
        do {

            //获取快照信息
            USES_CONVERSION;
            CString ProcessName = A2T(pszProcessName);
            if (!lstrcmp(stcPe.szExeFile, ProcessName))
            {
                //4.关闭句柄
                CloseHandle(hSnap);
                return stcPe.th32ProcessID;
            }

        } while (Process32Next(hSnap, &stcPe));

    }
    
    //4.关闭句柄
    CloseHandle(hSnap);
    return 0;
}

//根据PID获取所有相应的线程ID
BOOL CInjectDlg::GetAllThreadIdByProcessId(DWORD dwProcessId, DWORD** ppThreadId, DWORD* pdwThreadIdLength)
{
    DWORD* pThreadId = NULL;
    //统计线程个数
    DWORD dwThreadIdLength = 0;
    //默认情况下,一个线程的栈要预留1M的内存空间, 
    //而一个进程中可用的内存空间只有2G,所以理论
    //上一个进程中最多可以开2048个线程
    DWORD dwBuffLength = 2048;

    //申请内存
    pThreadId = new DWORD[dwBuffLength];
    if (pThreadId == NULL)
    {
        MessageBox(L"申请内存失败!");
        return FALSE;
    }
    //将申请的控件初始化为0
    RtlZeroMemory(pThreadId, (dwBuffLength * sizeof(DWORD)));
    //1.创建线程快照
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (NULL == hSnap)
    {
        MessageBox(L"创建线程快照失败!");
        delete[] pThreadId;
        pThreadId = NULL;
        return FALSE;
    }

    //2.第一次遍历线程
    THREADENTRY32 th32 = { sizeof(th32) };
    if (Thread32First(hSnap, &th32))
    {
    //3.循环遍历线程
        do
        {
            //判断该线程是否属于这个进程
            if (th32.th32OwnerProcessID == dwProcessId)
            {
                pThreadId[dwThreadIdLength] = th32.th32ThreadID;
                dwThreadIdLength++;
            }

        } while (Thread32Next(hSnap, &th32));

        CloseHandle(hSnap);
        *ppThreadId = pThreadId;
        *pdwThreadIdLength = dwThreadIdLength;
        return TRUE;
    }
    else
    {
        MessageBox(L"创建线程快照失败!");
        delete[] pThreadId;
        pThreadId = NULL;
        CloseHandle(hSnap);
        return FALSE;
    }

}
原文地址:https://www.cnblogs.com/ndyxb/p/12890800.html