此系列是本人一个字一个字码出来的,包括示例和实验截图。可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
简述你如果是从中间插过来看的,请仔细阅读 羽夏逆向指引——序 ,方便学习本教程。
在安全领域,你或多或少听过注入这个名词,并了解高中注入手段:远程线程注入、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;
}
如下是实验效果图:
注意事项
- 注意程序的位数,64位程序注入64位的
DLL
,32位注入32位的。 - 如果注入高权限的程序,请具有相应的权限。
- 如果注入系统服务进程的话,需要通过使用未导出的函数
ZwCreateThreadEx
,在ntdll
里面,需要手动获取。由于会话隔离机制,你无法使用弹窗的形式验证注入成功。
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;
}
效果图如下:
注意事项
- 注意程序的位数,64位程序注入64位的
DLL
,32位注入32位的。 - 里面的相关细节请学习我在文中提到的教程,这些东西并不是一言两语就能说明白的。
在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,RWS
把shared
数据段设置为可读、可写、可共享的共享数据段。
下面我们实现加载全局钩子的程序:
#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 国际许可协议 进行许可