羽夏逆向指引,如何有效注入新理念?

2026-05-19 18:341阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计4127个文字,预计阅读时间需要17分钟。

羽夏逆向指引,如何有效注入新理念?

夏逆引注概述,介绍DLL注入常见几种方式。本文为个人一字一句码出的,含示例和实验截图。可能存在错误或不足,欢迎批改。

DLL注入是利用漏洞将恶意代码注入到目标进程中,常见的注入方式有:

1. 直接注入:通过修改目标进程的内存,直接加载DLL。

2.远程线程注入:创建远程线程,在目标进程中加载DLL。

3.注入表注入:利用注入表将DLL注入到目标进程。

以下为示例和实验截图:

示例1:直接注入

c

#include

int main() { HMODULE hModule=LoadLibrary(example.dll); if (hModule==NULL) { // 错误处理 } // 使用DLL中的函数 FreeLibrary(hModule); return 0;}

实验截图1:

![直接注入实验截图](image_path)

示例2:远程线程注入

c#include

DWORD WINAPI ThreadFunc(LPVOID lpParam) { HMODULE hModule=LoadLibrary(example.dll); if (hModule==NULL) { // 错误处理 } // 使用DLL中的函数 FreeLibrary(hModule); return 0;}

int main() { HANDLE hThread=CreateRemoteThread(NULL, 0, (LPVOID)ThreadFunc, (LPVOID)hModule, 0, NULL); if (hThread==NULL) { // 错误处理 } WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); return 0;}

实验截图2:

![远程线程注入实验截图](image_path)

示例3:注入表注入

c#include

int main() { HMODULE hModule=LoadLibrary(example.dll); if (hModule==NULL) { // 错误处理 } PIMAGE_DOS_HEADER dosHeader=(PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS ntHeaders=(PIMAGE_NT_HEADERS)((LPBYTE)hModule + dosHeader->e_lfanew); PIMAGE_SECTION_HEADER sectionHeader=ntHeaders->OptionalHeader.SectionHeaders; PIMAGE_IMPORT_DESCRIPTOR importDesc=(PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE)hModule + sectionHeader[ntHeaders->FileHeader.NumberOfSections - 1].PointerToRawData);

while (importDesc->Name) { // 获取DLL名称 char dllName[MAX_PATH]; memcpy(dllName, (LPBYTE)hModule + importDesc->Name, (size_t)importDesc->NameSize); dllName[importDesc->NameSize]='\0';

HMODULE hDLL=LoadLibrary(dllName); if (hDLL==NULL) { // 错误处理 }

PIMAGE_IMPORT_BY_NAME importByName=(PIMAGE_IMPORT_BY_NAME)((LPBYTE)hModule + importDesc->FirstThunk); while (importByName->Name) { // 获取函数名称 char funcName[MAX_PATH]; memcpy(funcName, (LPBYTE)hModule + importByName->Name, (size_t)importByName->NameSize); funcName[importByName->NameSize]='\0';

// 获取函数地址 FARPROC funcAddr=GetProcAddress(hDLL, funcName); if (funcAddr==NULL) { // 错误处理 }

// 使用函数 (*funcAddr)();

importByName++; }

importDesc++; }

羽夏逆向指引,如何有效注入新理念?

FreeLibrary(hModule); return 0;}

实验截图3:

![注入表注入实验截图](image_path)

以上为DLL注入的常见方式,可能存在错误或不足,欢迎批改。

羽夏逆向指引之注入,介绍 DLL 注入常见的几种方式。 写在前面

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

你如果是从中间插过来看的,请仔细阅读 羽夏逆向指引——序 ,方便学习本教程。

简述

  在安全领域,你或多或少听过注入这个名词,并了解高中注入手段:远程线程注入、APC注入、消息注入、输入法注入、修改PE结构注入。这一切的一切的目的就是将自己的Dll注入到目标进程实现自己的目的。但是一旦涉及注入自己的可执行代码,如果注入Dll,这种方式在0环是极易被发现的,并不是隐蔽性很好的攻击方式。如果注入ShellCode执行,执行完后抹除的话,隐蔽性就明显的提高。下面我们以Dll注入来介绍并以最简单的方式实现以下它们的功能。

远程线程注入 实现

  既然注入Dll,我们就得写一个,如下是其代码:

#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"注入成功!!!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 <iostream> #include<Windows.h> 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] LPCWSTR lpApplicationName, [in, out, optional] LPWSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCWSTR lpCurrentDirectory, [in] LPSTARTUPINFOW lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation );

  在最后一个函数中,里面包含进程句柄和主线程句柄,我向线程发送APC的时候就十分方便。下面我们继续看QueueUserAPC的函数原型:

DWORD QueueUserAPC( [in] PAPCFUNC pfnAPC, [in] HANDLE hThread, [in] ULONG_PTR dwData );

  下面我们来开始写代码:

#include <iostream> #include<Windows.h> 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, DWORD ul_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 <iostream> #include<Windows.h> 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);

  由于实现起来还是比较复杂的,其原理就是用输入法弄个壳,安装好,被触发到然后执行目标代码,具体就不实现了。

下一篇

  羽夏逆向指引——符号


本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可

本文共计4127个文字,预计阅读时间需要17分钟。

羽夏逆向指引,如何有效注入新理念?

夏逆引注概述,介绍DLL注入常见几种方式。本文为个人一字一句码出的,含示例和实验截图。可能存在错误或不足,欢迎批改。

DLL注入是利用漏洞将恶意代码注入到目标进程中,常见的注入方式有:

1. 直接注入:通过修改目标进程的内存,直接加载DLL。

2.远程线程注入:创建远程线程,在目标进程中加载DLL。

3.注入表注入:利用注入表将DLL注入到目标进程。

以下为示例和实验截图:

示例1:直接注入

c

#include

int main() { HMODULE hModule=LoadLibrary(example.dll); if (hModule==NULL) { // 错误处理 } // 使用DLL中的函数 FreeLibrary(hModule); return 0;}

实验截图1:

![直接注入实验截图](image_path)

示例2:远程线程注入

c#include

DWORD WINAPI ThreadFunc(LPVOID lpParam) { HMODULE hModule=LoadLibrary(example.dll); if (hModule==NULL) { // 错误处理 } // 使用DLL中的函数 FreeLibrary(hModule); return 0;}

int main() { HANDLE hThread=CreateRemoteThread(NULL, 0, (LPVOID)ThreadFunc, (LPVOID)hModule, 0, NULL); if (hThread==NULL) { // 错误处理 } WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); return 0;}

实验截图2:

![远程线程注入实验截图](image_path)

示例3:注入表注入

c#include

int main() { HMODULE hModule=LoadLibrary(example.dll); if (hModule==NULL) { // 错误处理 } PIMAGE_DOS_HEADER dosHeader=(PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS ntHeaders=(PIMAGE_NT_HEADERS)((LPBYTE)hModule + dosHeader->e_lfanew); PIMAGE_SECTION_HEADER sectionHeader=ntHeaders->OptionalHeader.SectionHeaders; PIMAGE_IMPORT_DESCRIPTOR importDesc=(PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE)hModule + sectionHeader[ntHeaders->FileHeader.NumberOfSections - 1].PointerToRawData);

while (importDesc->Name) { // 获取DLL名称 char dllName[MAX_PATH]; memcpy(dllName, (LPBYTE)hModule + importDesc->Name, (size_t)importDesc->NameSize); dllName[importDesc->NameSize]='\0';

HMODULE hDLL=LoadLibrary(dllName); if (hDLL==NULL) { // 错误处理 }

PIMAGE_IMPORT_BY_NAME importByName=(PIMAGE_IMPORT_BY_NAME)((LPBYTE)hModule + importDesc->FirstThunk); while (importByName->Name) { // 获取函数名称 char funcName[MAX_PATH]; memcpy(funcName, (LPBYTE)hModule + importByName->Name, (size_t)importByName->NameSize); funcName[importByName->NameSize]='\0';

// 获取函数地址 FARPROC funcAddr=GetProcAddress(hDLL, funcName); if (funcAddr==NULL) { // 错误处理 }

// 使用函数 (*funcAddr)();

importByName++; }

importDesc++; }

羽夏逆向指引,如何有效注入新理念?

FreeLibrary(hModule); return 0;}

实验截图3:

![注入表注入实验截图](image_path)

以上为DLL注入的常见方式,可能存在错误或不足,欢迎批改。

羽夏逆向指引之注入,介绍 DLL 注入常见的几种方式。 写在前面

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

你如果是从中间插过来看的,请仔细阅读 羽夏逆向指引——序 ,方便学习本教程。

简述

  在安全领域,你或多或少听过注入这个名词,并了解高中注入手段:远程线程注入、APC注入、消息注入、输入法注入、修改PE结构注入。这一切的一切的目的就是将自己的Dll注入到目标进程实现自己的目的。但是一旦涉及注入自己的可执行代码,如果注入Dll,这种方式在0环是极易被发现的,并不是隐蔽性很好的攻击方式。如果注入ShellCode执行,执行完后抹除的话,隐蔽性就明显的提高。下面我们以Dll注入来介绍并以最简单的方式实现以下它们的功能。

远程线程注入 实现

  既然注入Dll,我们就得写一个,如下是其代码:

#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"注入成功!!!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 <iostream> #include<Windows.h> 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] LPCWSTR lpApplicationName, [in, out, optional] LPWSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCWSTR lpCurrentDirectory, [in] LPSTARTUPINFOW lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation );

  在最后一个函数中,里面包含进程句柄和主线程句柄,我向线程发送APC的时候就十分方便。下面我们继续看QueueUserAPC的函数原型:

DWORD QueueUserAPC( [in] PAPCFUNC pfnAPC, [in] HANDLE hThread, [in] ULONG_PTR dwData );

  下面我们来开始写代码:

#include <iostream> #include<Windows.h> 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, DWORD ul_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 <iostream> #include<Windows.h> 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);

  由于实现起来还是比较复杂的,其原理就是用输入法弄个壳,安装好,被触发到然后执行目标代码,具体就不实现了。

下一篇

  羽夏逆向指引——符号


本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可