什么是注入
定义:注入技术是指将外部代码(通常是DLL)或数据强制加载到目标进程的地址空间并执行的技术手段 。
简单来说,就是当前进程强制把一段代码 或者 是一些数据 加载到 目标进程 的地址空间 ,以此来控制目标进程的行为,达到当前进程的某种目的的一种技术手段。
那么,从上面定义中我们可以总结出下面几个问题:
- 为什么需要控制另一个进程?换句话说,为什么我们需要注入呢?
- 如何控制另一个进程执行自己设定好的代码?换句话说,如何注入?
带着上面两个问题,下面对windows 注入进行一个基本的了解和学习。
为什么需要注入?
听起来,注入貌似非常敏感和危险。那为什么windows 允许这一机制存在呢?
允许注入,本质上是因为操作系统需要为软件提供一种 "合法地打破进程隔离墙" 的通道。这不是疏忽,而是有意设计。核心原因有三点:
- 实现"无缝集成"的用户体验
用户期望软件之间能深度协作。比如:
右键菜单快速打开压缩文件工能,翻译软件可以直接翻译当前程序的语言等
如果没有注入,这些功能要么无法实现,要么体验会变得极其笨拙(比如想要压缩一个文件还要先找到zip 这个程序)。 - 提供系统级的扩展能力
思考一下: 你在浏览器里打字、在Word里打字、在游戏里打字,为什么都能呼出同一个搜狗输入法或微软拼音?
你打开浏览器,准备在搜索框打字。
系统告诉输入法:"当前焦点是浏览器,它要输入了"。
输入法框架(一个系统服务)会将输入法的核心组件(一个DLL)加载(注入)到浏览器的进程空间。
于是,你在浏览器里按下的每一个字母,都会先被浏览器进程内部的这个输入法组件接收到,转换成汉字候选框,再画在浏览器的窗口上。
你切换到Word,同样的过程再来一遍,输入法组件被注入到Word的进程里。
如果没有这种"注入"机制, 每个软件都必须自己内置一套输入法处理逻辑,或者你只能使用操作系统自带的那一种,第三方输入法(搜狗、QQ拼音等)根本不可能存在。整个中文输入的生态就没了 - 满足底层开发和维护的刚性需求
以我们开发人员最常用的工具调试器为例,为什么调试器(debugger)可以调试我们的程序?为什么打一个断点,进程就"停止"了?很显然,调试器控制了被调试的进程,无论是数据还是行为。
从操作系统视角看,杀毒软件和安全监控工具,与调试器是同一类东西------它们都是"需要深入监视和控制其他程序行为的系统级程序"。
一个没有注入机制的系统,杀毒软件将只能扫描静态文件,无法实时监控内存中的病毒行为。
性能剖析、热修复补丁、无障碍辅助工具等大量底层工具都将失效。
如何注入
- 远程线程注入(最直接)
远程线程注入是最直接、最经典的方法。其思路是"在目标进程的内部,创建一个完全听命于我的新线程"。这好比目标进程是一家公司,你无法从外部强行闯入。于是,你利用系统提供的"管理权",在这家公司内部直接"招聘"一个属于你的新员工(创建远程线程),并给他一份"立即加载并执行指定代码"的入职任务(调用LoadLibrary)。这个方法的优势是直接、通用,但由于其行为特征非常明显(创建远程线程),极易被现代安全软件检测和拦截。
观察下面代码示例
cpp
// 1. 打开目标进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
// 2. 在目标进程中分配内存
LPVOID pRemoteMemory = VirtualAllocEx(hProcess, NULL, dllPathLength,
MEM_COMMIT, PAGE_READWRITE);
// 3. 写入DLL路径到目标进程
WriteProcessMemory(hProcess, pRemoteMemory, dllPath, dllPathLength, NULL);
// 4. 获取LoadLibraryA函数地址
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
LPTHREAD_START_ROUTINE loadLibAddr = (LPTHREAD_START_ROUINE)GetProcAddress(hKernel32, "LoadLibraryA");
// 5. 创建远程线程执行LoadLibraryA
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, loadLibAddr, pRemoteMemory, 0, NULL);
// 6. 等待线程执行完毕
WaitForSingleObject(hThread, INFINITE);
- DLL劫持(最隐蔽)
DLL劫持则是一种更隐蔽的"欺骗"方法。**它不主动强行进入,而是"伪装成目标进程正在寻找的伙伴,让它自己开门请我进去"。许多程序启动时会按固定顺序在一些目录中寻找必需的DLL文件。**攻击者可以将恶意DLL命名为与某个合法DLL相同的名称,并将其放置在更优先被搜索的路径下(例如程序同级目录)。当目标进程启动时,它会"误认"这个恶意DLL为所需组件,并主动将其加载到自己的内存空间中。这种方法的成功依赖于程序不严谨的DLL加载逻辑,隐蔽性高,但并非对每个目标程序都有效。
观察下面代码:
cpp
// 劫持逻辑很简单,主要是文件操作
// 1. 将你的malicious.dll重命名为target.exe需要加载的xxx.dll
// 2. 将原版xxx.dll重命名为xxx_orig.dll
// 3. 在你的malicious.dll中,转发调用到原版DLL
// malicious.cpp (你的恶意DLL)
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
if (fdwReason == DLL_PROCESS_ATTACH) {
// 恶意代码在这里执行
MessageBoxA(NULL, "成功劫持!", "提示", MB_OK);
}
return TRUE;
}
// 导出函数要转发到原版DLL
#pragma comment(linker, "/export:SomeFunction=xxx_orig.SomeFunction")
#pragma comment(linker, "/export:AnotherFunc=xxx_orig.AnotherFunc")
- APC注入(最巧妙)
APC注入是一种更为"巧妙"的方法,它不创建新线程,而是"将任务塞进目标进程现有线程的待办事项队列"。每个线程都有一个异步过程调用队列。攻击者可以向目标进程的某个线程的APC队列中插入一个请求,这个请求通常是指向恶意代码的回调函数。当该线程进入"可警报状态"(例如调用SleepEx等函数)时,它便会自动执行队列中的这个任务。这种方法不需要创建新线程,行为相对隐蔽,但它依赖目标线程进入特定的、可被触发的状态,因此成功率不如前两种方法稳定。
观察下面代码示例
cpp
// 1. 打开目标进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
// 2. 枚举目标进程的线程
HANDLE hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te32 = { sizeof(THREADENTRY32) };
Thread32First(hThreadSnap, &te32);
// 3. 找到目标进程的线程
do {
if (te32.th32OwnerProcessID == targetPid) {
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
// 4. 分配内存并写入shellcode
LPVOID pRemoteMem = VirtualAllocEx(hProcess, NULL, shellcodeSize,
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, pRemoteMem, shellcode, shellcodeSize, NULL);
// 5. 向线程APC队列插入任务
QueueUserAPC((PAPCFUNC)pRemoteMem, hThread, 0);
CloseHandle(hThread);
}
} while (Thread32Next(hThreadSnap, &te32));
BHO 注册机制
BHO(Browser Helper Object,浏览器帮助对象) 是微软提供的一种官方扩展机制,允许开发者将自己的 COM 组件注入到 Internet Explorer 和 Windows Explorer(资源管理器) 中。
与前面介绍的几种注入方式不同,BHO 是一种被动式的"合法注入":
- 不需要主动攻击目标进程
- 不需要绕过安全检测
- 由操作系统主动将你的 DLL 加载到 Explorer 进程中
这就像是你在政府部门正式登记注册了一家公司,然后系统会自动把你的代表派驻到相关机构中工作。
4.2 BHO 的工作原理
┌─────────────────────────────────────────────────────────────────┐
│ BHO 加载流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Explorer.exe 启动 │
│ │ │
│ ▼ │
│ 2. 系统读取注册表 BHO 键值 │
│ HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ │
│ Explorer\Browser Helper Objects\{Your-CLSID} │
│ │ │
│ ▼ │
│ 3. 通过 CLSID 查找 COM 组件注册信息 │
│ HKCR\CLSID\{Your-CLSID}\InprocServer32 │
│ │ │
│ ▼ │
│ 4. 系统调用 DllGetClassObject 获取类工厂 │
│ │ │
│ ▼ │
│ 5. 通过类工厂创建 BHO 对象实例 │
│ │ │
│ ▼ │
│ 6. 调用 IObjectWithSite::SetSite(IWebBrowser2*) │
│ ┌─────────────────────────────────────────┐ │
│ │ 此时你的代码已经运行在 Explorer 进程中! │ │
│ │ 可以: │ │
│ │ • 订阅导航事件 │ │
│ │ • 监控文件夹切换 │ │
│ │ • 拦截用户操作 │ │
│ │ • 修改 Shell 行为 │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3 注册表结构详解
BHO 的注册涉及两个关键的注册表位置:
4.3.1 BHO 注册位置(告诉系统"我是一个 BHO")
registry
; 64位系统上的64位BHO
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\{Your-CLSID-Here}
(默认) REG_SZ (可选的描述)
; 64位系统上的32位BHO(WOW64)
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\{Your-CLSID-Here}
4.3.2 COM 组件注册(告诉系统"如何加载我")
registry
; COM 类注册
HKEY_CLASSES_ROOT\CLSID\{Your-CLSID-Here}
(默认) REG_SZ YourClassName
; 指定 DLL 路径和线程模型
HKEY_CLASSES_ROOT\CLSID\{Your-CLSID-Here}\InprocServer32
(默认) REG_SZ C:\Path\To\Your.dll
ThreadingModel REG_SZ Apartment
; 类型库(可选)
HKEY_CLASSES_ROOT\CLSID\{Your-CLSID-Here}\TypeLib
(默认) REG_SZ {Your-TypeLib-GUID}
4.4 代码实现
4.4.1 BHO 类定义
cpp
// BHO 需要实现两个关键接口
class ATL_NO_VTABLE CMyExplorerBHO :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMyExplorerBHO, &CLSID_MyExplorerBHO>,
public IObjectWithSiteImpl<CMyExplorerBHO>, // 关键接口1:接收Site
public IDispatchImpl<IMyExplorerBHO>,
public IDispEventImpl<1, CMyExplorerBHO, &DIID_DWebBrowserEvents2> // 关键接口2:事件接收
{
public:
CMyExplorerBHO() : m_dwCookie(0) {}
DECLARE_REGISTRY_RESOURCEID(IDR_MYEXPLORERBHO)
DECLARE_NOT_AGGREGATABLE(CMyExplorerBHO)
BEGIN_COM_MAP(CMyExplorerBHO)
COM_INTERFACE_ENTRY(IMyExplorerBHO)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IObjectWithSite)
END_COM_MAP()
// 事件映射
BEGIN_SINK_MAP(CMyExplorerBHO)
SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2, DISPID_NAVIGATECOMPLETE2, OnNavigateComplete2)
SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2, DISPID_BEFORENAVIGATE2, OnBeforeNavigate2)
END_SINK_MAP()
private:
CComPtr<IWebBrowser2> m_spWebBrowser2; // Explorer 的 IWebBrowser2 接口
DWORD m_dwCookie; // 事件连接 Cookie
public:
// 核心方法:系统调用此方法将 Explorer 接口传递给我们
STDMETHOD(SetSite)(IUnknown* pUnkSite);
// 事件处理
void STDMETHODCALLTYPE OnNavigateComplete2(IDispatch* pDisp, VARIANT* URL);
void STDMETHODCALLTYPE OnBeforeNavigate2(IDispatch* pDisp, VARIANT* URL,
VARIANT* Flags, VARIANT* TargetFrameName, VARIANT* PostData,
VARIANT* Headers, VARIANT_BOOL* Cancel);
};
4.4.2 SetSite 实现(BHO 的入口点)
cpp
STDMETHODIMP CMyExplorerBHO::SetSite(IUnknown* pUnkSite)
{
// 先断开之前的连接(如果有)
if (m_spWebBrowser2)
{
DispEventUnadvise(m_spWebBrowser2);
m_spWebBrowser2.Release();
}
// 保存新的 Site
if (pUnkSite != nullptr)
{
// 获取 IWebBrowser2 接口 - 这是与 Explorer 交互的核心接口
HRESULT hr = pUnkSite->QueryInterface(IID_IWebBrowser2, (void**)&m_spWebBrowser2);
if (SUCCEEDED(hr) && m_spWebBrowser2)
{
// 订阅导航事件
hr = DispEventAdvise(m_spWebBrowser2);
if (SUCCEEDED(hr))
{
// 此时,我们的代码已经成功"注入"到 Explorer 进程中
// 可以开始监控和响应用户的文件浏览行为了
OutputDebugString(L"[MyBHO] 成功连接到 Explorer!\n");
}
}
}
return IObjectWithSiteImpl<CMyExplorerBHO>::SetSite(pUnkSite);
}
4.4.3 事件处理示例
cpp
// 导航完成事件 - 用户进入了一个新文件夹
void STDMETHODCALLTYPE CMyExplorerBHO::OnNavigateComplete2(IDispatch* pDisp, VARIANT* URL)
{
if (URL && URL->vt == VT_BSTR)
{
CString strPath(URL->bstrVal);
// 检查是否是"此电脑"
if (strPath.Find(L"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}") != -1)
{
OutputDebugString(L"[MyBHO] 用户打开了「此电脑」!\n");
// 在这里可以执行自定义逻辑,比如弹出提示、收集信息等
}
// 检查是否是特定文件夹
if (strPath.Find(L"C:\\Windows\\System32") != -1)
{
OutputDebugString(L"[MyBHO] 用户正在浏览系统目录!\n");
}
}
}
// 导航前事件 - 可以拦截或修改导航行为
void STDMETHODCALLTYPE CMyExplorerBHO::OnBeforeNavigate2(
IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,
VARIANT* TargetFrameName, VARIANT* PostData,
VARIANT* Headers, VARIANT_BOOL* Cancel)
{
if (URL && URL->vt == VT_BSTR)
{
CString strPath(URL->bstrVal);
// 示例:阻止访问某些路径
if (strPath.Find(L"Forbidden") != -1)
{
*Cancel = VARIANT_TRUE; // 取消导航
MessageBox(NULL, L"访问被阻止!", L"提示", MB_OK | MB_ICONWARNING);
}
}
}
4.5 注册与卸载
4.5.1 DLL 自注册函数
cpp
// DllRegisterServer - 注册 COM 组件和 BHO
STDAPI DllRegisterServer(void)
{
HRESULT hr = _AtlModule.DllRegisterServer();
if (FAILED(hr))
return hr;
// 注册为 BHO
HKEY hKey;
CString strBHOKey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Browser Helper Objects\\";
strBHOKey += L"{Your-CLSID-Here}";
if (RegCreateKeyEx(HKEY_LOCAL_MACHINE, strBHOKey, 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS)
{
RegCloseKey(hKey);
}
return S_OK;
}
// DllUnregisterServer - 卸载
STDAPI DllUnregisterServer(void)
{
// 删除 BHO 注册
CString strBHOKey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Browser Helper Objects\\";
strBHOKey += L"{Your-CLSID-Here}";
RegDeleteKey(HKEY_LOCAL_MACHINE, strBHOKey);
return _AtlModule.DllUnregisterServer();
}
4.5.2 手动注册(使用 regsvr32)
batch
:: 注册 BHO
regsvr32 /s "C:\Path\To\YourBHO.dll"
:: 卸载 BHO
regsvr32 /u /s "C:\Path\To\YourBHO.dll"
:: 注册后需要重启 Explorer 才能生效
taskkill /f /im explorer.exe
start explorer.exe
4.6 BHO 的优势
| 特性 | BHO | 远程线程注入 | DLL劫持 |
|---|---|---|---|
| 合法性 | 官方支持的机制 | 可能被视为恶意行为 | 通常被视为攻击手段 |
| 稳定性 | 由系统管理生命周期 | 需自行管理 | 依赖目标程序 |
| 隐蔽性 | 完全合法,不触发告警 | 极易被检测 | 可能被检测 |
| 维护成本 | 低,遵循 COM 规范即可 | 高,需处理各种边界情况 | 中等 |
| 适用场景 | Shell 扩展、安全软件 | 通用注入 | 特定目标攻击 |
4.7 实际应用场景
- 安全软件:监控用户的文件浏览行为,实时扫描打开的文件夹
- Shell 扩展:在"此电脑"中添加自定义图标或功能入口
- 企业管理:监控员工访问的网络路径,审计文件操作
- 功能增强:为资源管理器添加标签页、快捷操作等功能
五、总结
本文介绍了 Windows 下的几种主流注入技术:
- 远程线程注入:直接但容易被检测
- DLL 劫持:隐蔽但依赖目标程序的漏洞
- APC 注入:巧妙但成功率不稳定
- BHO 注册:官方支持、稳定可靠、最适合 Shell 扩展场景
对于需要与 Windows Explorer 集成的应用(如安全软件、Shell 扩展),BHO 是最推荐的方案。它不仅是微软官方支持的机制,而且具有最高的稳定性和兼容性。