• 详解最实用的几种dll注入方式


    目录

    概念

    注入方式

    钩子注入

    原理

    示例代码

    代码注解

    运行截图

    远线程注入

    原理

    示例代码 

    突破Session 0 隔离的远线程注入 

    示例代码 

    *入口点注入

    原理

    参考代码

     APC注入

    一、APC注入

    二、API

    三、实现

    四、示例代码


    概念

    要谈dll注入,首先则要了解dll ,对dll的概念和使用不熟悉的读者可移步 dll概念和使用方式详解

    所谓DLL注入就是将一个DLL放进某个进程的地址空间里,让它成为那个进程的一部分。这样该进程和dll共享同一内存空间,这样dll可以使用该进程的所有资源,随时监控程序运行。通常,我们将需要实现的功能封装生成dll文件,然后将其注入到某一进程中,从而在该进程中添加或扩展我们需要的功能。因此dll注入技术被广泛运用在恶意攻击、游戏外挂、木马等程序中。

    注入方式

    钩子注入

    原理

    利用函数SetWindowsHookEx来实现注入。该函数将一个应用程序定义的挂钩处理函数安装到挂钩链中去,可以通过安装挂钩处理过程来对系统的某些类型事件进行监控,这些事件与某个特定的线程或系统中的所有事件相关。如果我们需要向某一进程注入dll,则首先获取该进程中某一线程ID,在dll文件中封装我们的挂钩处理函数,对该线程设置钩子,当触发类型事件时,线程则调用dll文件中的挂钩函数。此时,如果线程想要成功地调用该挂钩函数,必须先加载该dll文件,这样也就实现了我们的dll注入了。

    函数原型

    HHOOK SetWindowsHookExA(
        [in] int       idHook,
        [in] HOOKPROC  lpfn,
        [in] HINSTANCE hmod,
        [in] DWORD     dwThreadId
    );

    [in] lpfn

    指向挂接过程的指针。如果 dwThreadId 参数为零或指定由其他进程创建的线程的标识符,则 lpfn 参数必须指向 DLL 中的挂接过程。否则,lpfn 可以指向与当前进程关联的代码中的挂接过程。

    [in] hmod

    DLL 的句柄,其中包含 lpfn 参数所指向的挂钩过程。如果 dwThreadId 参数指定由当前进程创建的线程,并且挂接过程位于与当前进程关联的代码中,则必须将 hMod 参数设置为 NULL

    [in] dwThreadId

    挂接过程要与之关联的线程的标识符。对于桌面应用程序,如果此参数为零,则挂钩过程将与在与调用线程相同的桌面上运行的所有现有线程相关联

    示例代码

    dll文件

    1. #include "pch.h"
    2. #include
    3. BOOL APIENTRY DllMain( HMODULE hModule,
    4. DWORD ul_reason_for_call,
    5. LPVOID lpReserved
    6. )
    7. {
    8. switch (ul_reason_for_call)
    9. {
    10. case DLL_PROCESS_ATTACH:
    11. case DLL_THREAD_ATTACH:
    12. case DLL_THREAD_DETACH:
    13. case DLL_PROCESS_DETACH:
    14. break;
    15. }
    16. return TRUE;
    17. }
    18. HHOOK hhk = 0;
    19. LRESULT CALLBACK KeyboardProc(
    20. int code,
    21. WPARAM wParam,
    22. LPARAM lParam
    23. )
    24. {
    25. if (code == HC_ACTION)
    26. {
    27. if (wParam == VK_HOME && (lParam & (1<<30)))
    28. MessageBoxA(NULL, "您按下了HOME键", "HOME",MB_OK);
    29. }
    30. return CallNextHookEx(hhk, code, wParam, lParam);
    31. }
    32. extern "C" _declspec(dllexport) HOOKPROC getDllPoint(void)
    33. {
    34. return KeyboardProc;
    35. }

    设置钩子

    1. typedef HOOKPROC(*func)(void);
    2. void CInjectTestDlg::OnBnClickedButton1()
    3. {
    4. HWND hwnd = ::FindWindowA(NULL, "陈子青注入工具2.0");
    5. DWORD pid;
    6. DWORD thread_id = ::GetWindowThreadProcessId(hwnd, &pid);
    7. HMODULE mod = LoadLibraryA("myDll.dll");
    8. func proc = (func)::GetProcAddress(mod, "getDllPoint");
    9. HOOKPROC fp=proc();
    10. HHOOK hhk= ::SetWindowsHookEx(WH_KEYBOARD, fp, mod, thread_id);
    11. }

    代码注解

    首先获取窗口线程,加载dll文件获取getDllPoint函数,该函数返回钩子处理函数的地址,然后给窗口线程设置键盘钩子,当触发键盘事件时,该线程调用myDll.dll文件中的处理函数,那么该线程必须先加载该dll文件,这时也就等同于我们将myDll.dll注入到该程序中了。

    运行截图

    点击注入,当按下HOME键时,弹出信息框。

    远线程注入

    原理

    远线程注入比较好理解,通过CreateRemoteThread函数在需要注入的进程中创建一个自己的线程,在该线程中调用系统的LoadLibraryA加载dll文件即可。

    LoadLibraryA函数Kernel32.dll中,因此可以通过加载Kernel32.dll以来获取LoadLibraryA函数地址,其函数的参数就是dll文件的路径。此时可以通过VirtualAllocEx函数在该进程中创建一块内存用来保存需要被注入dll文件路径。

    CreateRemoteThread函数原型

    HANDLE CreateRemoteThread(
      [in]  HANDLE                 hProcess,
      [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
      [in]  SIZE_T                 dwStackSize,
      [in]  LPTHREAD_START_ROUTINE lpStartAddress,
      [in]  LPVOID                 lpParameter,
      [in]  DWORD                  dwCreationFlags,
      [out] LPDWORD                lpThreadId
    );

    [in] hProcess

    要在其中创建线程的进程的句柄。句柄必须具有PROCESS_CREATE_THREADPROCESS_QUERY_INFORMATIONPROCESS_VM_OPERATIONPROCESS_VM_WRITEPROCESS_VM_READ访问权限,并且在某些平台上没有这些权限可能会失败。有关详细信息,请参阅进程安全性和访问权限

    [in] lpThreadAttributes

    指向SECURITY_ATTRIBUTES结构的指针,该结构指定新线程的安全描述符,并确定子进程是否可以继承返回的句柄。如果 lpThreadAttributes 为 NULL,则线程将获得默认的安全描述符,并且句柄无法继承。

    [in] lpStartAddress

    指向类型为 LPTHREAD_START_ROUTINE的应用程序定义函数的指针将由线程执行,并表示远程进程中线程的起始地址。该函数必须存在于远程进程中。有关详细信息,请参阅线程过程

    [in] lpParameter

    指向要传递给线程函数的变量的指针。

    [in] dwCreationFlags

    控制线程创建的标志。

    价值意义

    0

    线程在创建后立即运行。

    CREATE_SUSPENDED

    0x00000004

    线程是在挂起状态下创建的,并且在调用 ResumeThread 函数之前不会运行。

    STACK_SIZE_PARAM_IS_A_RESERVATION

    0x00010000

    dwStackSize 参数指定堆栈的初始保留大小。如果未指定此标志,则 dwStackSize 指定提交大小。

    [out] lpThreadId

    指向接收线程标识符的变量的指针。

    如果此参数为 NULL,则不返回线程标识符。

    VirtualAllocEx函数原型

    LPVOID VirtualAllocEx(
      [in]           HANDLE hProcess,
      [in, optional] LPVOID lpAddress,
      [in]           SIZE_T dwSize,
      [in]           DWORD  flAllocationType,
      [in]           DWORD  flProtect
    ); 

    [in] hProcess

    进程的句柄。该函数在此进程的虚拟地址空间内分配内存。

    句柄必须具有PROCESS_VM_OPERATION访问权限。

    [in, optional] lpAddress

    为要分配的页面区域指定所需起始地址的指针。

    如果要预留内存,该函数会将此地址向下舍入到分配粒度的最接近的倍数。

    如果要提交已预留的内存,则该函数会将此地址向下舍入到最近的页边界。若要确定主计算机上的页面大小和分配粒度,请使用 GetSystemInfo 函数。

    [in] dwSize

    要分配的内存区域的大小,以字节为单位。

    如果 lpAddress 为 NULL,则该函数将 dwSize 向上舍入到下一页边界。

    如果 lpAddress 不是 NULL,则该函数将分配包含从 lpAddress 到 lpAddress+dwSize 范围内的一个或多个字节的所有页面。这意味着,例如,跨越页面边界的 2 字节范围会导致函数同时分配这两个页面。

    [in] flAllocationType

    内存分配的类型。此参数必须包含以下值之一。

    价值意义

    MEM_COMMIT

    0x00001000

    为指定的保留内存页分配内存费用(从内存的总大小和磁盘上的分页文件)。该函数还保证当调用方稍后最初访问内存时,内容将为零。除非/直到实际访问虚拟地址,否则不会分配实际的物理页。

    要一步到位地保留和提交页面,请使用 调用 VirtualAllocExMEM_COMMIT | MEM_RESERVE

    尝试通过指定不MEM_RESERVE和非 NULL lpAddress 的MEM_COMMIT来提交特定地址范围将失败,除非已保留整个范围。生成的错误代码为 ERROR_INVALID_ADDRESS

    尝试提交已提交的页不会导致函数失败。这意味着您可以提交页面,而无需首先确定每个页面的当前提交状态。

    如果 lpAddress 指定安全区内的地址,则必须MEM_COMMIT flAllocationType

    MEM_RESERVE

    0x00002000

    保留进程的一系列虚拟地址空间,而不在内存或磁盘上的分页文件中分配任何实际的物理存储。

    您可以通过使用MEM_COMMIT再次调用VirtualAllocEx来提交保留页面。要一步到位地保留和提交页面,请使用 调用 VirtualAllocExMEM_COMMIT | MEM_RESERVE

    其他内存分配函数(如 malloc 和 LocalAlloc)在释放保留内存之前无法使用保留内存。

    MEM_RESET

    0x00080000

    指示不再对 lpAddress 和 dwSize 指定的内存范围中的数据感兴趣。不应从分页文件中读取或写入分页文件。但是,稍后将再次使用内存块,因此不应将其取消提交。此值不能与任何其他值一起使用。

    使用此值并不能保证使用MEM_RESET操作的范围将包含零。如果希望范围包含零,请取消提交内存,然后重新提交。

    当您使用MEM_RESET时,VirtualAllocEx 函数将忽略 fProtect 的值。但是,您仍必须将 fProtect 设置为有效的保护值,如PAGE_NOACCESS

    如果您使用MEM_RESET并且内存范围映射到文件,则VirtualAllocEx将返回错误。共享视图只有在映射到分页文件时才可接受。

    MEM_RESET_UNDO

    0x1000000

    MEM_RESET_UNDO应仅在之前已成功应用MEM_RESET的地址范围上调用。它指示调用方对 lpAddress 和 dwSize 指定的指定内存范围内的数据感兴趣,并尝试反转MEM_RESET的影响。如果函数成功,则意味着指定地址范围内的所有数据都完好无损。如果函数失败,则地址范围内至少有一些数据已替换为零。

    此值不能与任何其他值一起使用。如果在之前未MEM_RESET的地址范围上调用MEM_RESET_UNDO,则行为未定义。指定MEM_RESET时,VirtualAllocEx 函数将忽略 flProtect 的值。但是,您仍必须将 flProtect 设置为有效的保护值,如PAGE_NOACCESS

    Windows Server 2008 R2、Windows 7、Windows Server 2008、Windows Vista、Windows Server 2003 和 Windows XP: MEM_RESET_UNDO标志在 Windows 8 和 Windows Server 2012 之前不受支持。

     

    此参数还可以指定以下值,如所示。

    价值意义

    MEM_LARGE_PAGES

    0x20000000

    使用大页面支持分配内存。

    大小和对齐方式必须是大页最小值的倍数。若要获取此值,请使用 GetLargePageMinimum 函数。

    如果指定此值,则还必须指定MEM_RESERVEMEM_COMMIT

    MEM_PHYSICAL

    0x00400000

    保留可用于映射地址窗口化扩展插件 (AWE) 页的地址范围。

    此值必须与MEM_RESERVE一起使用,不得与其他值一起使用。

    MEM_TOP_DOWN

    0x00100000

    在可能的最高地址分配内存。这可能比常规分配慢,尤其是在分配很多时。

    [in] flProtect

    要分配的页面区域的内存保护。如果要提交页,则可以指定任何一个内存保护常量

    如果 lpAddress 指定安全区内的地址,则 flProtect 不能是以下任何值:

    • PAGE_NOACCESS
    • PAGE_GUARD
    • PAGE_NOCACHE
    • PAGE_WRITECOMBINE

    为安全区分配动态内存时,必须PAGE_READWRITEPAGE_EXECUTE_READWRITE flProtect 参数。

    示例代码 

    1. void CInjectTestDlg::OnBnClickedButton2()
    2. {
    3. const char* path = "E:\\工程文件\\myDll\\Debug\\mydll2.dll";//要注入的dll路径
    4. HWND hwnd = ::FindWindowA(NULL, "陈子青注入工具2.0");
    5. DWORD pid;
    6. GetWindowThreadProcessId(hwnd, &pid);
    7. HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    8. LPVOID lpAddr = VirtualAllocEx(hProc,NULL, strlen(path)+1, MEM_COMMIT, PAGE_READWRITE);
    9. WriteProcessMemory(hProc, lpAddr, path,strlen(path), NULL);
    10. HMODULE sysMod = LoadLibraryA("Kernel32.dll");
    11. LPTHREAD_START_ROUTINE fp =(LPTHREAD_START_ROUTINE) GetProcAddress(sysMod, "LoadLibraryA");
    12. CreateRemoteThread(hProc, NULL, NULL,(LPTHREAD_START_ROUTINE)fp, lpAddr, 0, NULL);
    13. }

    突破Session 0 隔离的远线程注入 

    使用ZwCreateThreadEx函数,突破SESSION 0隔离,向system等权限的系统服务器进程中入时,由于系统的SESSION 0隔离机制,导致注入失败;我们可使用ZwCreateThreadEx函数进行远程线程注入。ZwCreateThreadEx函数在ntdll.dll中没有被声明,所以需要使用GetProcAddressntdll.dll中获取函数导出地址。该函数在官网文档里是查询不到的。其函数原型如下

    X64
    DWORD ZwCreateThreadEx(
        PHANDLE                    ThreadHandle,
        ACCESS_MASK                DesiredAccess,
        LPVOID                    ObjectAttributes,
        HANDLE                    ProcessHandle,
        LPTHREAD_START_ROUTINE    lpStartAddress,
        LPVOID                    lpParameter,
        ULONG                    CreateThreadFlags,
        SIZE_T                    ZeroBits,
        SIZE_T                    StackSize,
        SIZE_T                    MaximumStackSize,
        LPVOID                    pUnkown
    )

    X32
    DWORD ZwCreateThreadEx(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        BOOL CreateSuspended,
        DWORD dwStackSize,
        DWORD dw1,
        DWORD dw2,
        LPVOID pUnkown);

    示例代码 

    1. typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)GetProcAddress(hNtdll, "ZwCreateThreadEx");
    2. if (NULL == ZwCreateThreadEx)
    3. {
    4. printf("获取ZwCreateThreadEx函数地址失败!");
    5. CloseHandle(hProcess);
    6. return FALSE;
    7. }
    8. //在目标进程中创建线程
    9. HANDLE hRemoteThread = NULL;
    10. //pfnStartAddress 为loadlibrary函数地址, pBuf 传参
    11. DWORD dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess,
    12. pfnStartAddress, pBuf, 0, 0, 0, 0, NULL);
    13. if (NULL == hRemoteThread)
    14. {
    15. printf("目标进程中创建线程失败!");
    16. CloseHandle(hProcess);
    17. return FALSE;
    18. }

    *入口点注入

    原理

    在以CREATE_SUSPENDED的方式启动程序,进程启动时被挂起,此时该进程无法拿到任何权限和数据,此时修改PE文件的入口点代码,在进程入口点跳转到实现注入的代码段,并在此恢复入口点代码再跳转回入口点,让程序正常运行。

    这样做的好处是:第一我们注入的代码时运行在进程的主线程中的;第二进程在拉起时就被暂定完成注入之后才开始运行,实现“神不知鬼不觉”的效果

    实现过程中涉及如下关键技术点

    ① 获取程序入口点  C++获取程序入口点

    ② 进程的创建挂起  进程的创建与使用

    ③  jmp 远跳 对应机器码 0xE9

    ④ 如何定位写入数据 技巧 :通过 0xCCCCCCCC 来转换

    ⑤ 关于写入函数的问题,函数中涉及调用都是通过地址寻址来调用的,如果直接写入,该地址在对方内存空间中是无效的 。

    ⑥ Jmp调转的相对地址计算 = 跳转的目标地址 - 当前地址 - 0x5

    该方法需要对注入进程的内存进行反复改写和恢复,修正过程中要在对方进程中进行Debug,因此需要一定的逆向基础真正完善,否则程序常常是容易崩溃的,

    下面提供一段入口点注入的代码,仅供参考

    参考代码

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. char dllPath[MAX_PATH] = "E:\\工程文件\\EntryPointDemo\\Debug\\Dll1.dll";
    6. const char exePath[MAX_PATH] = "C:\\Users\\Administrator\\Desktop\\game.exe";
    7. typedef HMODULE(*_loadLibrayA)(LPCSTR);
    8. typedef HMODULE (*_getModuleHandle)(LPCSTR);
    9. typedef BOOL (*_virtualProtect)(LPVOID ,SIZE_T,DWORD,PDWORD);
    10. typedef struct ROMOTE_DATA
    11. {
    12. char path[MAX_PATH];//dll地址
    13. _loadLibrayA loadLibrary;
    14. DWORD entryPoint;
    15. _getModuleHandle getModuleHande;
    16. _virtualProtect virtualProtect;
    17. char code[0x5];
    18. LPVOID injectAddr;
    19. }*PROMOTE_DATA;
    20. PROMOTE_DATA rotData;
    21. DWORD GetEntryPointAddr();
    22. void initRotData()
    23. {
    24. //strcpy_s(rotData->path,dllPath);
    25. memcpy(rotData->path,dllPath,sizeof(dllPath));
    26. HMODULE hMod = LoadLibraryA("Kernel32.dll");
    27. rotData->loadLibrary = (_loadLibrayA)GetProcAddress(hMod, "LoadLibraryA");
    28. //printf("loadLibrary :%x\n", rotData->loadLibrary);
    29. rotData->entryPoint = GetEntryPointAddr();
    30. rotData->getModuleHande = (_getModuleHandle)GetProcAddress(hMod, "GetModuleHandleA");
    31. //printf("getModuleHande:%x\n", rotData->getModuleHande);
    32. rotData->virtualProtect = (_virtualProtect)GetProcAddress(hMod, "VirtualProtect");
    33. //printf("virtualProtect: %x\n", rotData->virtualProtect);
    34. }
    35. DWORD GetEntryPointAddr()
    36. {
    37. HANDLE hFile = CreateFileA(exePath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    38. int szFile = GetFileSize(hFile, NULL);
    39. LPBYTE lpFile = new BYTE[szFile];
    40. int ret = ReadFile(hFile, lpFile, szFile, NULL, NULL);
    41. if (ret == 0)
    42. {
    43. printf("read file error:%d", GetLastError());
    44. return NULL;
    45. }
    46. PIMAGE_DOS_HEADER imgDosHeader = (PIMAGE_DOS_HEADER)lpFile;
    47. PIMAGE_NT_HEADERS imgNtHeader = (PIMAGE_NT_HEADERS)(lpFile + imgDosHeader->e_lfanew);
    48. PIMAGE_OPTIONAL_HEADER imgOptionHeader = &(imgNtHeader->OptionalHeader);
    49. DWORD bRet = imgOptionHeader->AddressOfEntryPoint;
    50. if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile);
    51. if (lpFile != NULL) delete[] lpFile;
    52. return bRet;
    53. }
    54. void InjectFunc()
    55. {
    56. PROMOTE_DATA pData = (PROMOTE_DATA)0xCCCCCCCC;
    57. pData->loadLibrary(pData->path);
    58. char* prevcode = (char*)pData->entryPoint;
    59. for (int i = 0; i < 0x5; i++) prevcode[i] = pData->code[i];
    60. unsigned point = pData->entryPoint;
    61. __asm
    62. {
    63. mov eax, point
    64. jmp eax
    65. }
    66. }
    67. void modifyEntryPoint(PROMOTE_DATA p)
    68. {
    69. DWORD baseAddr= (DWORD)p->getModuleHande(0);
    70. p->entryPoint += baseAddr;
    71. DWORD lwt{};
    72. p->virtualProtect((LPVOID)p->entryPoint, 0x5, PAGE_EXECUTE_READWRITE, &lwt);
    73. char *prevcode = (char*)p->entryPoint;
    74. for (int i = 0; i < 5; ++i) p->code[i] = prevcode[i];
    75. prevcode[0] = 0xE9;//jmp
    76. unsigned distance = (unsigned)( (DWORD)p->injectAddr - p->entryPoint - 0x5);
    77. unsigned *_code = (unsigned*)(p->entryPoint+1);
    78. _code[0] = distance;
    79. }
    80. int main()
    81. {
    82. STARTUPINFOA startUp;
    83. ZeroMemory(&startUp, sizeof(STARTUPINFOA));
    84. PROCESS_INFORMATION proInfo;
    85. ZeroMemory(&proInfo, sizeof(PROCESS_INFORMATION));
    86. int bRet = CreateProcessA(exePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL,
    87. "C:\\Users\\Administrator\\Desktop\\",
    88. &startUp, &proInfo);
    89. if (bRet == 0)
    90. {
    91. printf("createprocess error:%d\n", GetLastError());
    92. return 0;
    93. }
    94. //进行入口点注入
    95. rotData = new ROMOTE_DATA;
    96. DWORD rotAddr=(DWORD)VirtualAllocEx(proInfo.hProcess, NULL, 0x3000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    97. DWORD rotDataAddr = rotAddr + 0x200;
    98. initRotData();
    99. char modifyCode[0x200];
    100. memcpy(modifyCode, InjectFunc, sizeof(modifyCode));
    101. for (int i = 0; i < 0x200; i++)
    102. {
    103. unsigned* pcode = (unsigned*)(&modifyCode[i]);
    104. if(pcode[0] == 0xCCCCCCCC)
    105. {
    106. pcode[0] = (unsigned)rotDataAddr;
    107. break;
    108. }
    109. }
    110. DWORD injectAddr = rotAddr + 0x1000; //注入dll的函数
    111. rotData->injectAddr = (LPVOID)injectAddr;
    112. WriteProcessMemory(proInfo.hProcess, (LPVOID)rotDataAddr, rotData, 0x200, NULL);
    113. WriteProcessMemory(proInfo.hProcess, (LPVOID)injectAddr, modifyCode, 0x200, NULL);
    114. DWORD rotFuncAddr = rotAddr + 0x2000; //写入远程函数
    115. WriteProcessMemory(proInfo.hProcess, (LPVOID)rotFuncAddr, modifyEntryPoint, 0x500, NULL);
    116. HANDLE hProcHnd = CreateRemoteThread(proInfo.hProcess, NULL, 0,
    117. (LPTHREAD_START_ROUTINE)rotFuncAddr, (LPVOID)rotDataAddr, 0, NULL);
    118. if (hProcHnd != INVALID_HANDLE_VALUE) WaitForSingleObject(hProcHnd, INFINITE);
    119. else printf("CreateRemoteThread error : %d", GetLastError());
    120. //注入完成后恢复进程运行
    121. ResumeThread(proInfo.hThread);
    122. return 0;
    123. }

     APC注入

    一、APC注入

    什么是APC?

    [原创]小Win,点一份APC(Apc机制详解)(一)-编程技术-看雪论坛-安全社区|安全招聘|bbs.pediy.com
    每一个线程都有自己的APC队列,使用QueueUserAPC函数把一个APC函数压入APC队列中。当处于处于用户模式的APC压入线程APC队列后,该线程并不直接调用APC函数,除非该线程处于可通知状态,调用的顺序为先入先出(FIFO)。

    二、API

    QueueUserAPC函数
    QueueUserAPC function (processthreadsapi.h) - Win32 apps | Microsoft Docs

    三、实现

    一个进程包含多个线程,为了确保能够执行插入的APC,应向目标进程的所有线程都插入相同的APC,实现加载DLL的操作
    实现APC注入的具体流程如下:
    首先,通过OpenProcess函数打开目标进程,获取目标进程的句柄。
    然后,通过调用WIN32 API函数CreateToolhelp32Snapshot、Thread32First以及Thread32Next遍历线程快照,获取目标进程的所有ID。
    接着,调用VirtualAllocEx函数在目标进程中申请内存,并通过WriteProcessMemory函数向内存中写入DLL的注入路径。
    最后,遍历获取的线程ID,并调用OpenThread函数以THREAD_ALL_ACCESS访问权限打开线程,获取线程句柄。并调用QueueUserAPC函数向线程插入APC函数,设置APC函数的地址为LoadLibraryA函数的地址,并设置APC函数参数为上述DLL路径地址。

    四、示例代码

    1. #include "stdafx.h"
    2. #include "ApcInject.h"
    3. void ShowError(char *pszText)
    4. {
    5. char szErr[MAX_PATH] = { 0 };
    6. ::wsprintf(szErr, "%s Error[%d]\n", pszText);
    7. ::MessageBox(NULL, szErr, "ERROR", MB_OK | MB_ICONERROR);
    8. }
    9. // 根据进程名称获取PID
    10. DWORD GetProcessIdByProcessName(char *pszProcessName)
    11. {
    12. DWORD dwProcessId = 0;
    13. PROCESSENTRY32 pe32 = { 0 };
    14. HANDLE hSnapshot = NULL;
    15. BOOL bRet = FALSE;
    16. ::RtlZeroMemory(&pe32, sizeof(pe32));
    17. pe32.dwSize = sizeof(pe32);
    18. // 获取进程快照
    19. hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    20. if (NULL == hSnapshot)
    21. {
    22. ShowError("CreateToolhelp32Snapshot");
    23. return dwProcessId;
    24. }
    25. // 获取第一条进程快照信息
    26. bRet = ::Process32First(hSnapshot, &pe32);
    27. while (bRet)
    28. {
    29. // 获取快照信息
    30. if (0 == ::lstrcmpi(pe32.szExeFile, pszProcessName))
    31. {
    32. dwProcessId = pe32.th32ProcessID;
    33. break;
    34. }
    35. // 遍历下一个进程快照信息
    36. bRet = ::Process32Next(hSnapshot, &pe32);
    37. }
    38. return dwProcessId;
    39. }
    40. // 根据PID获取所有的相应线程ID
    41. BOOL GetAllThreadIdByProcessId(DWORD dwProcessId, DWORD **ppThreadId, DWORD *pdwThreadIdLength)
    42. {
    43. DWORD *pThreadId = NULL;
    44. DWORD dwThreadIdLength = 0;
    45. DWORD dwBufferLength = 1000;
    46. THREADENTRY32 te32 = { 0 };
    47. HANDLE hSnapshot = NULL;
    48. BOOL bRet = TRUE;
    49. do
    50. {
    51. // 申请内存
    52. pThreadId = new DWORD[dwBufferLength];
    53. if (NULL == pThreadId)
    54. {
    55. ShowError("new");
    56. bRet = FALSE;
    57. break;
    58. }
    59. ::RtlZeroMemory(pThreadId, (dwBufferLength * sizeof(DWORD)));
    60. // 获取线程快照
    61. ::RtlZeroMemory(&te32, sizeof(te32));
    62. te32.dwSize = sizeof(te32);
    63. hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    64. if (NULL == hSnapshot)
    65. {
    66. ShowError("CreateToolhelp32Snapshot");
    67. bRet = FALSE;
    68. break;
    69. }
    70. // 获取第一条线程快照信息
    71. bRet = ::Thread32First(hSnapshot, &te32);
    72. while (bRet)
    73. {
    74. // 获取进程对应的线程ID
    75. if (te32.th32OwnerProcessID == dwProcessId)
    76. {
    77. pThreadId[dwThreadIdLength] = te32.th32ThreadID;
    78. dwThreadIdLength++;
    79. }
    80. // 遍历下一个线程快照信息
    81. bRet = ::Thread32Next(hSnapshot, &te32);
    82. }
    83. // 返回
    84. *ppThreadId = pThreadId;
    85. *pdwThreadIdLength = dwThreadIdLength;
    86. bRet = TRUE;
    87. } while (FALSE);
    88. if (FALSE == bRet)
    89. {
    90. if (pThreadId)
    91. {
    92. delete[]pThreadId;
    93. pThreadId = NULL;
    94. }
    95. }
    96. return bRet;
    97. }
    98. // APC注入
    99. BOOL ApcInjectDll(char *pszProcessName, char *pszDllName)
    100. {
    101. BOOL bRet = FALSE;
    102. DWORD dwProcessId = 0;
    103. DWORD *pThreadId = NULL;
    104. DWORD dwThreadIdLength = 0;
    105. HANDLE hProcess = NULL, hThread = NULL;
    106. PVOID pBaseAddress = NULL;
    107. PVOID pLoadLibraryAFunc = NULL;
    108. SIZE_T dwRet = 0, dwDllPathLen = 1 + ::lstrlen(pszDllName);
    109. DWORD i = 0;
    110. do
    111. {
    112. // 根据进程名称获取PID
    113. dwProcessId = GetProcessIdByProcessName(pszProcessName);
    114. if (0 >= dwProcessId)
    115. {
    116. bRet = FALSE;
    117. break;
    118. }
    119. // 根据PID获取所有的相应线程ID
    120. bRet = GetAllThreadIdByProcessId(dwProcessId, &pThreadId, &dwThreadIdLength);
    121. if (FALSE == bRet)
    122. {
    123. bRet = FALSE;
    124. break;
    125. }
    126. // 打开注入进程
    127. hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
    128. if (NULL == hProcess)
    129. {
    130. ShowError("OpenProcess");
    131. bRet = FALSE;
    132. break;
    133. }
    134. // 在注入进程空间申请内存
    135. pBaseAddress = ::VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    136. if (NULL == pBaseAddress)
    137. {
    138. ShowError("VirtualAllocEx");
    139. bRet = FALSE;
    140. break;
    141. }
    142. // 向申请的空间中写入DLL路径数据
    143. ::WriteProcessMemory(hProcess, pBaseAddress, pszDllName, dwDllPathLen, &dwRet);
    144. if (dwRet != dwDllPathLen)
    145. {
    146. ShowError("WriteProcessMemory");
    147. bRet = FALSE;
    148. break;
    149. }
    150. // 获取 LoadLibrary 地址
    151. pLoadLibraryAFunc = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
    152. if (NULL == pLoadLibraryAFunc)
    153. {
    154. ShowError("GetProcessAddress");
    155. bRet = FALSE;
    156. break;
    157. }
    158. // 遍历线程, 插入APC
    159. for (i = 0; i < dwThreadIdLength; i++)
    160. {
    161. // 打开线程
    162. hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadId[i]);
    163. if (hThread)
    164. {
    165. // 插入APC
    166. ::QueueUserAPC((PAPCFUNC)pLoadLibraryAFunc, hThread, (ULONG_PTR)pBaseAddress);
    167. // 关闭线程句柄
    168. ::CloseHandle(hThread);
    169. hThread = NULL;
    170. }
    171. }
    172. bRet = TRUE;
    173. } while (FALSE);
    174. // 释放内存
    175. if (hProcess)
    176. {
    177. ::CloseHandle(hProcess);
    178. hProcess = NULL;
    179. }
    180. if (pThreadId)
    181. {
    182. delete[]pThreadId;
    183. pThreadId = NULL;
    184. }
    185. return bRet;
    186. }

  • 相关阅读:
    『Halcon与C#混合编程』002_读取图像、显示图像
    母婴行业数字化发展趋势:内容多元化、服务定制化、人群全覆盖
    echart在折线显示横纵(横纵线沿着折线展示)
    电力电子转战数字IC——题目合集+持续更新进度
    基于LADRC自抗扰控制的VSG三相逆变器预同步并网控制策略(Simulink仿真实现)
    Oracle 面试题及答案整理,最新面试题
    【ElM分类】基于麻雀搜索算法优化ElM神经网络实现数据分类附代码
    中秋将至,通过代码实现嫦娥奔月庆祝佳节
    【JavaEE进阶系列 | 从小白到工程师】JavaEE的可变参数使用
    异步FIFO设计的仿真与综合技术(2)
  • 原文地址:https://blog.csdn.net/weixin_40582034/article/details/125630395