羽夏逆向指引——注入

写在前面 ??此系列是本人一个字一个字码出来的,包括示例和实验截图。可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。

你如果是从中间插过来看的,请仔细阅读 羽夏逆向指引——序 ,方便学习本教程。
简述 ??在安全领域,你或多或少听过注入这个名词,并了解高中注入手段:远程线程注入、APC注入、消息注入、输入法注入、修改PE结构注入。这一切的一切的目的就是将自己的Dll注入到目标进程实现自己的目的。但是一旦涉及注入自己的可执行代码,如果注入Dll,这种方式在0环是极易被发现的,并不是隐蔽性很好的攻击方式。如果注入ShellCode执行,执行完后抹除的话,隐蔽性就明显的提高。下面我们以Dll注入来介绍并以最简单的方式实现以下它们的功能。
远程线程注入 实现
??既然注入Dll,我们就得写一个,如下是其代码:
#include "pch.h"BOOL APIENTRY DllMain( HMODULE hModule, DWORDul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { MessageBox(NULL, L"注入成功!!!By.WingSummer.", L"CnBlog", MB_ICONINFORMATION); break; } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }

??这个Dll的作用就是注入成功之后进行弹窗提示,表示注入成功!我们开始进行一下知识铺垫。
??如何加载Dll呢?我们平时加载的时候会调用LoadLibrary这个函数,如下是函数原型:
HMODULE WINAPI LoadLibraryW( _In_ LPCWSTR lpLibFileName );

??既然是注入,肯定不是我们自己调用。让一个代码执行就需要线程,如果在对方创建线程需要使用如下函数:
HANDLE WINAPI CreateRemoteThread( _In_ HANDLE hProcess, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_T dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_opt_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_opt_ LPDWORD lpThreadId );

??使用LoadLibrary这个函数,需要传参一个字符串地址,而这个地址正好可以用lpParameter提供,但是,这个地址是被注入的进程,我们需要在被注入的程序写一个字符串。可以在被注入程序申请一块内存,其函数原型如下:
LPVOID WINAPI VirtualAllocEx( _In_ HANDLE hProcess, _In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect );

??申请好了地址,就需要写字符串,需要用到的函数如下:
BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, _In_ LPVOID lpBaseAddress, _In_reads_bytes_(nSize) LPCVOID lpBuffer, _In_ SIZE_T nSize, _Out_opt_ SIZE_T* lpNumberOfBytesWritten );

??而在其他程序中申请内存和写内存都需要相应的进程句柄,我们可以打开进程,需要的函数如下:
HANDLE WINAPI OpenProcess( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ DWORD dwProcessId );

??OpenProcess函数的参数dwProcessId表示的是进程ID,这个我们通过输入的方式进行。
??有了以上知识铺垫之后,我们就可以写代码了。但是你可能有疑问,LoadLibrary的地址被注入和注入进程是一样的吗?当然是的。获取函数的时候,我们还需要GetProcAddress函数,其函数原型如下:
FARPROC WINAPI GetProcAddress( _In_ HMODULE hModule, _In_ LPCSTR lpProcName );

??具体代码实现如下:
#include #include using namespace std; #define DllPath L"*:\\****\\DllTest.dll" //根据自己的 Dll 路径来定int main() { HMODULE lib = LoadLibrary(L"kernel32.dll"); if (lib) { FARPROC loadlib = GetProcAddress(lib, "LoadLibraryW"); if (loadlib) { cout << "请输入注入 PID:"; DWORD pid; cin >> pid; HANDLE hprocess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (hprocess) { LPVOID addr = VirtualAllocEx(hprocess, NULL, 4000, MEM_COMMIT, PAGE_READWRITE); if (addr) { if (WriteProcessMemory(hprocess, addr, DllPath, sizeof(DllPath), NULL)) { HANDLE hthread = CreateRemoteThread(hprocess, NULL, NULL, (LPTHREAD_START_ROUTINE)loadlib, addr, 0, NULL); if (hthread) { cout << "注入成功!!!" << endl; CloseHandle(hthread); } CloseHandle(hprocess); } else { cout << "WriteProcessMemory 失败!" << endl; } } else { cout << "VirtualAllocEx 失败!" << endl; } } else { cout << "OpenProcess 失败!" << endl; } } else { cout << "获取 LoadLibraryW 地址失败!" << endl; } } else { cout << "获取 kernel32.dll 地址失败!" << endl; } system("pause"); return 0; }

??如下是实验效果图:
羽夏逆向指引——注入
文章图片

注意事项
  1. 注意程序的位数,64位程序注入64位的DLL,32位注入32位的。
  2. 如果注入高权限的程序,请具有相应的权限。
  3. 如果注入系统服务进程的话,需要通过使用未导出的函数ZwCreateThreadEx,在ntdll里面,需要手动获取。由于会话隔离机制,你无法使用弹窗的形式验证注入成功。
APC 注入 实现
??APC中文名称为异步过程调用,它是Windows十分重要的机制,如果想要学习其内部细节,请自行学习 羽夏看Win系统内核 的APC篇。下面我们重点介绍最小化实现。
??我们利用创建进程的方式来实现,为什么呢?我们来看一下它的函数原型:
BOOL CreateProcessW( [in, optional]LPCWSTRlpApplicationName, [in, out, optional] LPWSTRlpCommandLine, [in, optional]LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional]LPSECURITY_ATTRIBUTES lpThreadAttributes, [in]BOOLbInheritHandles, [in]DWORDdwCreationFlags, [in, optional]LPVOIDlpEnvironment, [in, optional]LPCWSTRlpCurrentDirectory, [in]LPSTARTUPINFOWlpStartupInfo, [out]LPPROCESS_INFORMATION lpProcessInformation );

??在最后一个函数中,里面包含进程句柄和主线程句柄,我向线程发送APC的时候就十分方便。下面我们继续看QueueUserAPC的函数原型:
DWORD QueueUserAPC( [in] PAPCFUNCpfnAPC, [in] HANDLEhThread, [in] ULONG_PTR dwData );

??下面我们来开始写代码:
#include #include using namespace std; #define DllPath L"*:\\****\\DllTest.dll" //根据自己的 Dll 路径来定int main() { HMODULE lib = LoadLibrary(L"kernel32.dll"); if (lib) { FARPROC loadlib = GetProcAddress(lib, "LoadLibraryW"); if (loadlib) { cout << "创建记事本进程开始实验,按任意键继续……"; cin.get(); WCHAR app[] = L"notepad.exe"; STARTUPINFO info = { sizeof(STARTUPINFO) }; PROCESS_INFORMATION pi; BOOL ret = CreateProcess(NULL, app, NULL, NULL, NULL, 0, NULL, NULL, &info, &pi); if (ret) { LPVOID addr = VirtualAllocEx(pi.hProcess, NULL, 4000, MEM_COMMIT, PAGE_READWRITE); if (addr) { if (WriteProcessMemory(pi.hProcess, addr, DllPath, sizeof(DllPath), NULL)) { if (QueueUserAPC((PAPCFUNC)loadlib, pi.hThread, (ULONG_PTR)addr)) { WaitForSingleObjectEx(pi.hThread, -1, TRUE); //触发 APC cout << "注入成功!!!" << endl; } } else { cout << "WriteProcessMemory 失败!" << endl; } } else { cout << "VirtualAllocEx 失败!" << endl; } } else { cout << "创建进程失败!" << endl; }CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { cout << "获取 LoadLibraryW 地址失败!" << endl; } } else { cout << "获取 kernel32.dll 地址失败!" << endl; } system("pause"); return 0; }

??效果图如下:
羽夏逆向指引——注入
文章图片

注意事项
  1. 注意程序的位数,64位程序注入64位的DLL,32位注入32位的。
  2. 里面的相关细节请学习我在文中提到的教程,这些东西并不是一言两语就能说明白的。
消息注入 ??在Windows中大部分的应用程序都是基于消息机制的,它们都有一个消息过程函数,根据不同的消息完成不同的功能。Windows操作系统提供的钩子机制就是用来截获和监视系统中这些消息的。按照钩子作用的范围不同,它们又可以分为局部钩子和全局钩子。局部钩子是针对某个线程的;而全局钩子则是作用于整个系统的基于消息的应用。全局钩子需要使用DLL文件,在DLL中实现相应的钩子函数。
??至于为什么全局钩子必须是DLL,简单思考就可以得到答案,因为我们需要对任何GUI进程进行挂钩,既然到用户进程只有DLL能做到。
??我们需要使用SetWindowsHookEx函数进行挂钩,如下是其函数原型:
HHOOK WINAPI SetWindowsHookEx( _In_ int idHook, _In_ HOOKPROC lpfn, _In_ HINSTANCE hMod, _In_ DWORD dwThreadId)

??第一个参数就是表示要安装的钩子程序的类型,第二个是处理函数,第三个是包含由lpfn参数指向的钩子过程的DLL句柄,最后一个参数是与钩子程序关联的线程标识符,如果此参数为0,则钩子过程与系统中所有线程相关联。
??在操作系统中安装全局钩子后,只要进程接收到可以发出钩子的消息,全局钩子的DLL文件就会由操作系统自动或强行地加载到该进程中。因此,设置全局钩子可以达到DLL注入的目的。创建一个全局钩子后,在对应事件发生的时候,系统就会把DLL加载到发生事件的进程中,这样,便实现了DLL注入。
??为了能够让DLL注入到所有的进程中,程序设置WH_GETMESSAGE消息的全局钩子。下面我们开始实现DLL
#include "pch.h"// 共享内存 #pragma data_seg("shared") HHOOK g_hHook = NULL; #pragma data_seg() #pragma comment(linker, "/SECTION:shared,RWS")#define EXPORT extern "C" __declspec(dllexport)HMODULE ghModule; // 钩子回调函数 LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam) { return ::CallNextHookEx(g_hHook, code, wParam, lParam); }EXPORT BOOL SetGlobalHook() { g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, ghModule, 0); if (NULL == g_hHook) { return FALSE; } return TRUE; }// 卸载钩子 EXPORT BOOL UnsetGlobalHook() { if (g_hHook) { ::UnhookWindowsHookEx(g_hHook); } return TRUE; }BOOL APIENTRY DllMain( HMODULE hModule, DWORDul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { ghModule = hModule; break; } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }

??上面代码实现了全局钩子的设置、钩子回调函数的实现以及全局钩子的卸载,这些操作都需要用到全局钩子的句柄作为参数。而全局钩子是以DLL形式加载到其他进程空间中的,而且进程都是独立的,所以任意修改其中一个内存里的数据是不会影响另一个进程的。那么,如何将钩子句柄传递给其他进程呢?为了解决这个问题,这里采用的方法是在DLL中创建共享内存。
??共享内存是指突破进程独立性,多个进程共享同一段内存。在DLL中创建共享内存,就是在DLL中创建一个变量,然后将DLL加载到多个进程空间,只要一个进程修改了该变量值,其他进程DLL中的这个值也会改变,就相当于多个进程共享一个内存。
??在上面的代码中,使用#pragma data_seg创建了一个名为shared的数据段,然后使用/section:shared,RWSshared数据段设置为可读、可写、可共享的共享数据段。
??下面我们实现加载全局钩子的程序:
#include #include using namespace std; #define DllPath L"E:\\VsProject\\C++\\DllTest\\x64\\Debug\\DllTest.dll"typedef BOOL(*SetGlobalHook)(); typedef BOOL (*UnsetGlobalHook)(); int main() { HMODULE lib = LoadLibrary(DllPath); if (lib) { SetGlobalHook sethook = (SetGlobalHook)GetProcAddress(lib, "SetGlobalHook"); UnsetGlobalHook unsethook = (UnsetGlobalHook)GetProcAddress(lib, "UnsetGlobalHook"); if (sethook&&unsethook) { if (sethook()) { cout << "已被 Hook ,按任意键取消 Hook ……" << endl; cin.get(); unsethook(); } } else { cout << "获取函数失败!!!" << endl; } } else { cout << "加载全局 Hook 失败!!!" << endl; } system("pause"); return 0; }

??然后我们加载钩子之后,启动新的记事本,就可以发现DLL被注入了。
输入法注入 ??IME输入法实际就是一个DLL文件,只不过后缀为IME罢了,需要导出必要的接口供系统加载输入法时调用。我们可以在此IME文件的DllMain函数的入口通过调用LoadLibrary函数来加载需要注入的DLL
??对于IME,必须导出如下函数:
ImeConversionList//将字符串/字符转换成目标字符串/字符 ImeConfigure//设置ime参数 ImeDestroy//退出当前使用的IME ImeEscape//应用软件访问输入法的接口函数 ImeInquire//启动并初始化当前ime输入法 ImeProcessKey//ime输入键盘事件管理函数 ImeSelect//启动当前的ime输入法 ImeSetActiveContext//设置当前的输入处于活动状态 ImeSetCompositionString//由应用程序设置输入法编码 ImeToAsciiEx//将输入的键盘事件转换为汉字编码事件 NotifyIME//ime事件管理函数 ImeRegisterWord//向输入法字典注册字符串 ImeUnregisterWord//删除被注册的字符串 ImeGetRegisterWordStyle ImeEnumRegisterWord

??其中最重要的就是ImeInquire函数,当切换到此输入法时此函数就会被调用启动并初始化输入法。参数lpIMEInfo用于输入对输入法初始化的内容结构,参数lpszUIClass为输入法的窗口类。lpszUIClass对应的窗口类必须已注册,我们应该在DllMain入口处注册此窗口类,我们来看一下函数原型:
BOOL WINAPI ImeInquire(LPIMEINFO lpIMEInfo,LPTSTR lpszUIClass,LPCTSTR lpszOption);

??由于实现起来还是比较复杂的,其原理就是用输入法弄个壳,安装好,被触发到然后执行目标代码,具体就不实现了。
下一篇 【羽夏逆向指引——注入】??羽夏逆向指引——符号

    推荐阅读