#DLL注入介绍
将需要执行的shellcode或者DLL注入到目标进程,木马病毒包括说监控程序其实都是这样来监控你的进程。
shellcode注入(其实也就是机器码注入):要先获取kernel32.dll加载基址并根据导出表来获取函数地址。
DLL注入:这里介绍常见的几种注入方式。
1 通过全局钩子注入
首先,windows是基于消息驱动的,所以进程会有消息队列,SetWindowsHookEx就可以截获这些消息,钩子又分为全局钩子和局部钩子。
局部钩子:针对某个线程,比如自己的进程想hook某个消息,就可以用这个API来hook。
全局钩子:针对整个系统。
> HHOOK SetWindowsHookEx(int idHook,HOOKPROC lpfn,HINSTANCE hmod,DWORD dwThreadId);
可以看到当某个消息被触发时,会自动调用lpfn也就是我们的钩子函数,那么这个时候我们把这个钩子函数实现在某个DLL中,那么触发了这些消息的进程都要调用这个函数,那么系统会自动加载该DLL,实现注入。
那么是不是所有进程都有消息队列呢?不是,当程序第一次调用GDI的时候才会创建消息队列,那么对于非GUI程序来说,这种方法就不适用了。这就是这种方式的局限性。
dll文件编码实现:
//钩子回调函数
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;
}
2 远程线程注入
远程线程注入方式需要目标进程调用LoadLibrary来载入我们想注入的DLL,但是我们怎么让目标进程加载我们的DLL呢?其实windows提供了API CreateRemoteThread。
> HANDLE CreateRemoteThread
>
> (
>
> HANDLE hProcess,
> LPSECURITY_ATTRIBUTES lpThreadAttributes,
> SIZE_T dwStackSize,
> LPTHREAD_START_ROUTINE lpStartAddress,
> LPVOID lpParameter,
> DWORD dwCreationFlags,
> LPDWORD lpThreadId
> );
该函数的作用是在目标进程hProcess中创建一个一个线程,并在线程中调用LoadLibrary,加载需要注入的DLL。由于lpStartAddress是线程函数的内存地址,lpParameter是参数。又可以发现LoadLibrary函数的原型和线程函数的函数原型返回值和参数都是一个。由于参数和返回值在内存中其实都是一个指针,所以类型没有关系。(这边只能UNICODE,不说ANSI,大同小异)
> HMODULE WINAPI LoadLibraryW(LPCWSTR lpLibFileName);
>
> DWORD WINAPI ThreadFunc(PVOID pvParam);
我们可以直接这样写,只需要一行代码搞定。
> HADNLE hTread = CreateRemoteThread(hProcessRemote,NULL,0,LoadLibraryW,L"C:\\my.dll",0,NULL);
但是这样还有问题,因为字符串L"C:\my.dll"位于我们自己进程的地址空间中,所以这时候目标进程中LoadLibraryW去访问这个内存地址时候很可能引发违规,把目标进程搞崩溃,这肯定不行的。幸运的是,windows还提供了VirtualAllocEx函数,它可以让一个进程在另一个进程的地址空间分配一块内存,VirtualFreeEx释放这块内存。
> LPVOID VirtualAllocEx( HANDLE hProcess, LPVOID lpAddress,SIZE_T dwSize, DWORD flAllocationType,DWORD flProtect);
>
> BOOL VirtualFreeEx( HANDLE hProcess,LPVOID lpAddress,SIZE_T dwSize,DWORD dwFreeType);
分配了一块内存后,我们还要把字符串从我们进程的地址空间中复制到目标进程的地址空间中。刚好,windows也提供了这些API,可以让一个进程对另一个进程的地址空间进程读写。
> BOOL ReadProcessMemory( HANDLE hProcess, LPCVOID lpBaseAddress,LPVOID lpBuffer,SIZE_T nSize,SIZE_T *lpNumberOfBytesRead);
>
> BOOL WriteProcessMemory(HANDLE hProcess,LPVOID lpBaseAddress,LPCVOID lpBuffer,SIZE_T nSize,SIZE_T *lpNumberOfBytesWritten);
编码实现
int remote_inject_dll(unsigned int ui_pid, const wstring &str_dll)
> {
> HANDLE hProcess = NULL, hThread = NULL;
> hProcess = OpenProcess(
> PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
> FALSE, ui_pid);//打开一个已存在进程对象,并返回进程句柄
>
> if (hProcess == NULL)
> {
> return -1;
> }
>
> int cch = 1 + str_dll.length();
> int cb = cch * sizeof(wchar_t);//宽字符
>
> //在目标进程中申请一块内存,返回分配内存的首地址
> PWSTR pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE);
> if (pszLibFileRemote == NULL)
> {
> CloseHandle(hProcess);
> return -2;
> }
>
> //往这块内存写入该DLL字符串
> if (!WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)str_dll.c_str(), cb, NULL))
> {
> VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);
> CloseHandle(hProcess);
> return -3;
> }
>
> //创建远程线程并执行LoadLibraryW(pszLibFileRemote)
> hThread = CreateRemoteThread(hProcess, NULL, 0, (PTHREAD_START_ROUTINE)LoadLibraryW, pszLibFileRemote, 0, NULL);
> if (hThread == NULL)
> {
>
> VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);
> CloseHandle(hProcess);
> return -4;
> }
>
> WaitForSingleObject(hThread, INFINITE);
>
> VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);//释放内存
> CloseHandle(hThread);
> CloseHandle(hProcess);
> return 0;
> }
注意点:以管理员权限运行程序时,OpenProcess打开高权限进程时,可能打开失败。ZwCreateThreadEx函数在32和64位系统下,函数声明的参数有区别。
3 突破session 0的远程线程注入
使用远程线程注入可以注入普通的应用程序,但是无法注入windows服务程序。因为服务与应用程序是隔离开的,服务都运行在session 0中。和传统的CreateRemoteThread函数实现的远程线程注入DLL的唯一区别在于,突破session 0注入使用的是比CreateRemoteThread更底层的内核函数ZwCreatThreadEx函数来创建远程线程,原理相同。(MSDN查不到这个API,未公开)。
typedef NTSTATUS(__stdcall *Api_ZwCreateThreadEx)(PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, PVOID ObjectAttributes, HANDLE ProcessHandle, PVOID lpStartAddress,
> PVOID lpParameter, ULONG Flags, SIZE_T StackZeroBits, SIZE_T SizeOfStackCommit, SIZE_T SizeOfStackReserve, PVOID lpBytesBuffer);
编码实现:
```cpp
> 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);
>
> 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;
> // 打开注入进程,获取进程句柄
> hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
> if (hProcess == NULL)
> {
> printf("OpenProcess - Error!");
> return-1;
> }
> // 在注入的进程申请内存地址
> dwSize = ::lstrlen((LPCTSTR)pszDllFileName) + 1;
> pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
> if (NULL == pDllAddr)
> {
> printf("VirtualAllocEx - Error!");
> return FALSE;
> }
> //写入内存地址
> if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
> {
> printf("WriteProcessMemory - Error!");
> return FALSE;
> }
> //加载ntdll
> HMODULE hNtdllDll = ::LoadLibrary(L"ntdll.dll");
> if (NULL == hNtdllDll)
> {
> printf("LoadLibrary ntdll - Error!");
> return FALSE;
> }
> // 获取LoadLibraryA函数地址
> pFuncProcAddr = ::GetProcAddress(::GetModuleHandle((LPCTSTR)"Kernel32.dll"), "LoadLibraryW");
> if (NULL == pFuncProcAddr)
> {
> printf("GetProcAddress - Error!");
> return FALSE;
> }
> //获取ZwCreateThreadEx函数地址
> rn TRUE;
>
> }
4 APC注入
APC,全称为Asynchronous Procedure Call,即异步过程调用,是指函数在特定线程中被异步执行,在操作系统中,APC是一种并发机制。
实现原理:
在 Windows系统中,每个线程都会维护一个线程 APC队列,使用QueueUserAPC函数把一个APC函数压入APC队列中,每个线程的APC队列都记录了要求线程执行的一些APC函数。对于用户模式下得APC队列,当线程处于可警告状态(如调用sleep,WaitForSingleObjectEx等函数时),此时才会执行APC队列函数。(队列:先进先出)
我们调用QueueUserAPC插入了一个函数指针(LoadLibrary),当线程执行APC队列函数时,就会去加载我们得dll进行注入。
代码实现:
> 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)
> {
> VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT);
> CloseHandle(hProcess);
> return FALSE;
> }
> // 把Dll的路径复制到内存中
> if (FALSE == ::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) + 1) * sizeof(wzDllFullPath), nullptr))
> {
> VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT);
> CloseHandle(hProcess);
> return FALSE;
> }
> // 获得LoadLibraryW的地址
> PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandle((LPCTSTR)"kernel32.dll"), "LoadLibraryW");
> // 遍历线程, 插入APC
> float fail = 0;
> for (int i = dwThreadIdListLength - 1; i >= 0; i--)
> {
> // 打开线程
> HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]);
> if (hThread)
> {
> // 插入APC
> if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr))
> {
> fail++;
> }
> // 关闭线程句柄
> ::CloseHandle(hThread);
> hThread = NULL;
> }
> }
> printf("Total Thread: %d", dwThreadIdListLength);
> printf("Total Failed: %d", (int)fail);
> if ((int)fail == 0 || dwThreadIdListLength / fail > 0.5)
> {
> printf("Success to Inject APC");
> return TRUE;
> }
> else
> {
> printf("Inject may be failed");
> return FALSE;
> }
> }
注意:APC只适用于多线程方式,不适用于单线程进程。
5 注册表注入
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\
该路径下,AppInit_DLLs键的值为一个或一组DLL(一组的话以空格或逗号分隔,且只有第一个DLL的路径生效),可以将该DLL放到windows的系统目录就不需要指定路径了。为了让系统使用这个表项,还要有一个LoadAppInit_DLLs,并将值设为1。当User32.dll被映射到一个新的进程,会取得上述注册表的值,并调用LoadLibrary来载入AppInit_DLLs对应的DLL。
这个很方便,但是要注意因为是在加载User32.dll的时候注入的,这时候可能还要很多系统DLL没有被载入,如果调用了他们里面的API可能会导致蓝屏。
缺点:同全局钩子一样,只在GUI程序生效,大部分CUI程序不会加载User32.dll。
6 其他
其实还有一些别的注入方式,比如
使用木马注入DLL:把我们知道的某个程序必加载的dll替换成我们自己的,只要我们的dll把原本dll的所有的导出符号也导出就完事儿了。但是局限性大,要每个软件都要搞,而且还可能只适用一个版本。
把DLL作为调试器注入:系统在载入一个被调试程序的时候,会通知调试器,这时候调试器可以将一些代码注入到被调试程序的地址空间。(比如WriteProcessMemory)但是也有局限性,一般都是发的release版本,很麻烦。
参考资料——《windows黑客编程技术详解》、《windwos核心编程》