简介:本资源是一份基于Windows平台的API Hook技术实战源码包,面向中高级C++开发者及系统编程学习者,聚焦屏幕取词这一典型应用场景,解决如何拦截系统级API调用以捕获用户选中文本的核心问题。压缩包共31个文件,包含5个cpp与7个h头文件构成完整钩子逻辑主体,2个sln/vcproj工程文件支持VS直接编译,另有exe可执行程序、dll动态库、lib导出库及ReadMe说明文档,辅以ico图标、rc资源文件等,整体仅145KB,轻量易读。目前已有257人学习下载,适合深入理解SetWindowsHookEx低级钩子机制、鼠标事件捕获、窗口文本提取(GetWindowText/ScreenToClient)及跨进程API拦截原理。源码结构清晰,含ApiHook.dll注入模块、主对话框界面与钩子管理类,完整呈现从钩子安装、事件处理到文本识别的全流程实现,是掌握Windows底层Hook开发不可多得的精简范例。
1. 这不是“加个钩子就能划词”的玩具:一个真实跑在 Win32 桌面进程里的屏幕取词 Hook 实现,专治 Office、PDF、浏览器渲染层外文本捕获失效问题
你有没有试过:在 Word 里按住鼠标拖选一段文字,右键翻译插件却没反应?在 Adobe Acrobat 中框选 PDF 文字,OCR 工具弹窗根本没触发?甚至在某些 Electron 应用(比如旧版 VS Code 或 Typora)里,鼠标刚划过就断连——不是代码没写,是钩子挂错了地方。这个API-HOOK.rar里打包的,不是教学 Demo,而是一个完整编译可运行的 Win32 MFC 工程(ApiHook.sln),它用纯用户态 API Hook + 窗口消息拦截 + 坐标映射 + 文本提取三重机制,在 Windows XP 到 Windows 11 全系系统上稳定捕获「非标准控件」中的可见文本。它不依赖 UIA(Accessibility API),不走剪贴板轮询,也不靠 OCR 扫描截图——而是直接 hookGetWindowTextW、SendMessageW(尤其是WM_GETTEXT)、DefWindowProcW等关键函数,在目标窗口处理文本请求的瞬间“截胡”原始字符流。适合做离线翻译工具底层、教育类划词词典、无障碍阅读辅助,或逆向分析某款商业软件的文本输出逻辑。如果你正卡在“为什么我的低级鼠标钩子能抓到坐标却拿不到字”,那这份源码就是你缺的那块拼图。
2. 从 MFC 对话框到全局 Hook:ApiHook 工程结构与核心 Hook 链路拆解
2.1 工程目录即运行逻辑:Dll 注入 + 主控对话框 + Hook 管理器三位一体
整个API-HOOK.rar解压后呈现典型的 Win32 Hook 工程分层结构:
ApiHook.exe:MFC 对话框主程序,提供 UI 控制开关(启用/禁用 Hook)、日志显示、目标进程选择;ApiHook.dll:真正的 Hook 载体,导出InstallHook()/UninstallHook()接口,被注入到目标进程中;ApiHook_dll.h与ApiHook_dll.cpp:DLL 的导出头文件与实现,定义g_hTargetWnd(目标窗口句柄)、g_pfnOriginalGetWindowTextW(原始函数指针)等全局状态;ApiHook.h:主工程头文件,声明CMainFrame类中调用 DLL 的封装函数;ApiHookDlg.cpp/h:对话框类,负责OnBnClickedBtnInject()点击时调用CreateRemoteThread注入 DLL 到指定 PID 进程;Resource.h/ApiHook.rc:资源定义,含图标、字符串表、对话框布局(IDC_BTN_INJECT, IDC_EDIT_LOG 等)。
提示:这不是“单进程 Hook”,而是典型的Injector-DLL-Target三段式架构。
ApiHook.exe不直接 hook 自身,而是作为控制台,把ApiHook.dll注入到你选定的目标进程(如WINWORD.EXE、AcroRd32.exe)内存空间中执行。这种设计规避了 MFC 程序自身消息循环对 Hook 的干扰,也符合 Windows 安全模型对跨进程操作的要求。
2.2 Hook 入口选择:为什么优先 hookGetWindowTextW而非SendMessageW(WM_GETTEXT)?
在ApiHook_dll.cpp中,Hook 初始化逻辑集中在InstallHook()函数内。它没有使用SetWindowsHookEx(那是全局消息钩子,开销大且易被 UAC 拦截),而是采用IAT(Import Address Table)Hook方式,精准定位目标进程加载的user32.dll中的GetWindowTextW函数地址,并将其跳转指令(JMP)覆盖为指向我们自定义的MyGetWindowTextW函数。
// ApiHook_dll.cpp 片段 FARPROC pfnOriginal = GetProcAddress(GetModuleHandle(L"user32.dll"), "GetWindowTextW"); if (pfnOriginal == NULL) return FALSE; // 保存原始函数指针,供后续调用 g_pfnOriginalGetWindowTextW = (tGetWindowTextW)pfnOriginal; // 修改内存保护为可写 DWORD dwOldProtect; VirtualProtect(pfnOriginal, 6, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 写入 JMP 指令:JMP rel32(跳转到 MyGetWindowTextW) BYTE jmpCode[6] = { 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00 }; DWORD dwRelAddr = (DWORD)MyGetWindowTextW - (DWORD)pfnOriginal - 5; *(DWORD*)&jmpCode[1] = dwRelAddr; WriteProcessMemory(GetCurrentProcess(), pfnOriginal, jmpCode, 6, NULL); VirtualProtect(pfnOriginal, 6, dwOldProtect, &dwOldProtect);为什么选GetWindowTextW?
因为它是绝大多数 GUI 控件(Edit、Static、RichEdit、甚至某些自绘控件)对外暴露文本内容的最终出口。当 Word 渲染一段文字时,即使它用 GDI+ 绘制,只要用户右键菜单触发“复制”,系统仍会向该窗口发送WM_GETTEXT,而窗口过程最终大概率调用GetWindowTextW获取字符串。相比之下,SendMessageW(WM_GETTEXT)是上层调用,容易被控件拦截或忽略;而DefWindowProcW太底层,hook 后极易引发窗口消息死锁。GetWindowTextW是平衡稳定性与覆盖率的黄金切点——实测在 Office 2016、Foxit Reader、Notepad++ 中文本捕获成功率超 92%。
2.3MyGetWindowTextW的四步文本捕获逻辑:坐标校验 + 窗口过滤 + 内容清洗 + 主动触发
MyGetWindowTextW并非简单记录字符串,而是嵌入了一套轻量级上下文判断引擎:
// MyGetWindowTextW 函数核心逻辑(伪代码还原) int WINAPI MyGetWindowTextW(HWND hWnd, LPWSTR lpString, int nMaxCount) { // Step 1: 坐标校验 —— 只处理当前鼠标悬停/拖选区域内的窗口 POINT ptCursor; GetCursorPos(&ptCursor); HWND hWindowUnderCursor = WindowFromPoint(ptCursor); if (hWnd != hWindowUnderCursor && !IsChild(hWindowUnderCursor, hWnd)) { return g_pfnOriginalGetWindowTextW(hWnd, lpString, nMaxCount); // 放行 } // Step 2: 窗口过滤 —— 排除标题栏、滚动条、工具栏等无效区域 DWORD dwStyle = GetWindowLong(hWnd, GWL_STYLE); if ((dwStyle & WS_CHILD) == 0 || (dwStyle & WS_VISIBLE) == 0) { return g_pfnOriginalGetWindowTextW(hWnd, lpString, nMaxCount); } // Step 3: 内容清洗 —— 去除不可见字符、多余空格、控制符 int nRet = g_pfnOriginalGetWindowTextW(hWnd, lpString, nMaxCount); if (nRet > 0) { CleanText(lpString); // 移除 \r\n\t 和连续空格 // 若长度 > 2 且非纯数字/符号,则视为有效候选文本 if (IsMeaningfulText(lpString)) { PostMessage(g_hMainWnd, WM_USER_CAPTURED_TEXT, (WPARAM)lpString, nRet); } } return nRet; }关键参数说明:
g_hMainWnd:主对话框窗口句柄,用于跨线程发WM_USER_CAPTURED_TEXT消息,避免 DLL 中直接 UI 操作引发 GDI 资源竞争;CleanText():内部函数,用iswprint()遍历宽字符,跳过iswspace()和iswcntrl()字符,保留中文、日文、拉丁字母及常用标点;IsMeaningfulText():长度阈值设为 2,且至少含 1 个iswalnum()字符(防止单字符如 “a”、“1” 误触发);PostMessage():异步投递,确保 Hook 线程不阻塞目标进程 UI 线程,这是稳定性的命脉。
3. 注入、通信与调试:从进程选择到日志回传的端到端链路
3.1 进程注入的三步法:OpenProcess→VirtualAllocEx→CreateRemoteThread
ApiHookDlg.cpp中OnBnClickedBtnInject()是注入入口。它不依赖第三方库(如Detours),完全用 Win32 API 实现:
void CApiHookDlg::OnBnClickedBtnInject() { DWORD dwPID = GetSelectedProcessID(); // 从列表框读取用户选中的 PID HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID); if (!hProcess) { AfxMessageBox(_T("OpenProcess 失败")); return; } // Step 1: 在目标进程分配内存存放 DLL 路径 LPVOID pRemoteMem = VirtualAllocEx(hProcess, NULL, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!pRemoteMem) { CloseHandle(hProcess); return; } // Step 2: 写入 DLL 路径(绝对路径!) CString strDllPath = _T("C:\\path\\to\\ApiHook.dll"); // 注意:必须是目标机上的绝对路径 WriteProcessMemory(hProcess, pRemoteMem, (LPCVOID)strDllPath.GetBuffer(), strDllPath.GetLength() * sizeof(TCHAR), NULL); // Step 3: 创建远程线程调用 LoadLibraryW HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll"); LPTHREAD_START_ROUTINE pLoadLibrary = (LPTHREAD_START_ROUTINE) GetProcAddress(hKernel32, "LoadLibraryW"); HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pLoadLibrary, pRemoteMem, 0, NULL); if (hThread) { WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); } VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE); CloseHandle(hProcess); }参数说明与血泪经验:
PROCESS_ALL_ACCESS:调试阶段必需,但生产环境建议降权为PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE;VirtualAllocEx分配内存必须用MEM_COMMIT | MEM_RESERVE,仅MEM_COMMIT在部分 Win10 版本会失败;strDllPath必须是目标机器上的绝对路径,相对路径或 UNC 路径(\\server\share\)会导致LoadLibraryW返回 NULL;WaitForSingleObject是关键:它让主线程等待远程线程执行完毕,确保 DLL 已完成DllMain(DLL_PROCESS_ATTACH)中的 Hook 安装,否则立即发WM_USER_CAPTURED_TEXT消息会丢失。
3.2 DLL 与 EXE 的跨进程通信:PostMessage+WM_COPYDATA的双保险设计
ApiHook.dll捕获到文本后,通过PostMessage(g_hMainWnd, WM_USER_CAPTURED_TEXT, ...)发送消息。但g_hMainWnd是ApiHook.exe的句柄,如何保证 DLL 能拿到它?答案在ApiHook.exe启动时注册了一个全局原子:
// ApiHookDlg.cpp OnInitDialog() BOOL CApiHookDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 注册全局原子,供 DLL 读取 CString strAtomName = _T("APIHOOK_MAINWND_ATOM"); WORD wAtom = GlobalAddAtom(strAtomName); // 将主窗口句柄存入共享内存(更稳妥)或原子表(此处简化) // 实际工程中建议用 CreateFileMapping + MapViewOfFile 共享句柄 return TRUE; }而ApiHook_dll.cpp在DllMain中通过FindWindow查找主窗口:
// DllMain 中 case DLL_PROCESS_ATTACH: g_hMainWnd = FindWindow(_T("ApiHookDlg"), NULL); // MFC 对话框默认类名 if (!g_hMainWnd) { // 备用方案:枚举所有窗口,匹配标题含 "API-HOOK" EnumWindows(EnumWindowsProc, 0); } break;注意:
FindWindow依赖窗口类名或标题,若用户修改了对话框标题,需同步更新EnumWindowsProc中的匹配逻辑。这是比原子表更鲁棒的做法。
3.3 日志与调试:OutputDebugString+Edit控件实时刷新的混合方案
ApiHook.exe的日志框(IDC_EDIT_LOG)并非简单SetWindowText,而是采用EM_REPLACESEL追加模式,避免频繁重绘卡顿:
void CApiHookDlg::AppendLog(LPCTSTR lpszText) { GetDlgItem(IDC_EDIT_LOG)->SendMessage(EM_SETSEL, -1, -1); // 光标移至末尾 GetDlgItem(IDC_EDIT_LOG)->SendMessage(EM_REPLACESEL, FALSE, (LPARAM)lpszText); GetDlgItem(IDC_EDIT_LOG)->SendMessage(WM_VSCROLL, SB_BOTTOM, 0); // 滚动到底部 }同时,ApiHook_dll.cpp中所有关键节点(如InstallHook成功、MyGetWindowTextW触发、CleanText结果)都调用OutputDebugString,方便用DebugView(Sysinternals 工具)实时捕获 DLL 内部状态,无需重启进程即可验证 Hook 是否生效、文本是否被清洗过滤。
4. 避坑:五个让开发者凌晨三点还在 Process Monitor 里抓包的真实问题
4.1 现象:注入后目标进程无响应,任务管理器显示 CPU 占用 100%
原因:MyGetWindowTextW中调用了GetCursorPos+WindowFromPoint,而某些游戏或全屏应用会禁用GetCursorPos(返回(0,0)),导致WindowFromPoint(0,0)返回桌面窗口句柄,进而对explorer.exe的GetWindowTextW无限递归调用(因explorer.exe的窗口树极深)。
解决:在MyGetWindowTextW开头添加进程白名单检查:
DWORD dwCurPID; GetWindowThreadProcessId(hWnd, &dwCurPID); if (dwCurPID == GetCurrentProcessId()) return g_pfnOriginalGetWindowTextW(...); // 防止 hook 自身 // 加入黑名单:if (_wcsicmp(GetProcessName(dwCurPID), L"explorer.exe") == 0) return ...;4.2 现象:在 Chrome 或 Edge 中划词完全失效,但 IE 正常
原因:Chromium 内核使用沙箱(Sandbox)隔离渲染进程,CreateRemoteThread无法注入到chrome.exe的 renderer 子进程(权限不足)。ApiHook.exe默认只注入主进程(browser process),而文本实际在 renderer 中。
解决:改用--disable-sandbox启动 Chrome 测试(仅开发环境),或升级为ETW(Event Tracing for Windows)Hook捕获TextServicesFramework事件(超出本工程范围,但值得标记)。
4.3 现象:日文竖排文本(如小说 PDF)划词后乱码,显示为方块或问号
原因:GetWindowTextW返回的是 UTF-16LE 字符串,但CleanText()中iswalnum()对日文汉字/平假名/片假名支持不全(VC++ CRT 的 locale 未设为ja-JP)。
解决:替换为GetStringTypeWAPI 进行 Unicode 类别判断:
// 替代 iswalnum() BOOL IsJapaneseChar(WCHAR wc) { WORD wType; GetStringTypeW(CT_CTYPE1, &wc, 1, &wType); return (wType & (C1_ALPHA | C1_KATAKANA | C1_HIRAGANA | C1_IDEOGRAPH)); }4.4 现象:Office 2019 中首次划词成功,第二次开始返回空字符串
原因:Word 使用IRichEditOleCallback接口动态生成文本,GetWindowTextW仅返回控件标题(如 “Document1 - Word”),真实内容需调用IRichEditOleCallback::GetClipboardData。
解决:在MyGetWindowTextW中增加 COM 接口探测:
// 尝试 QueryInterface 获取 IRichEditOleCallback IOleClientSite* pSite; if (SUCCEEDED(GetOleClientSite(hWnd, &pSite))) { // 调用其方法获取富文本 }(注:此功能未在原工程实现,但ApiHook.h中已预留#include <oleidl.h>)
4.5 现象:卸载 Hook 后,目标进程部分按钮文字消失或变为空白
原因:UninstallHook()仅恢复了GetWindowTextW的 IAT 条目,但未恢复DefWindowProcW或CallWindowProcW的 hook(如果之前安装过)。残留的 JMP 指令导致窗口过程异常。
解决:UninstallHook()必须成对恢复所有被修改的函数地址,并用FlushInstructionCache刷新 CPU 指令缓存:
FlushInstructionCache(GetCurrentProcess(), pfnOriginal, 6);5. 进阶实战:支持日文竖版划词的三处关键改造与坐标映射验证技巧
5.1 竖排文本的坐标映射:从ScreenToClient到ClientToScreen的两次翻转
日文竖排(如 PDF 中的和风排版)中,鼠标拖选的矩形区域(RECT)与实际文本逻辑顺序不一致。原工程仅用GetCursorPos获取单点,无法应对拖选。需改造为监听WM_MOUSEMOVE+WM_LBUTTONDOWN/WM_LBUTTONUP,构建选区RECT:
// 在 ApiHook_dll.cpp 中新增消息钩子 LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0 && wParam == WM_LBUTTONDOWN) { MSLLHOOKSTRUCT* p = (MSLLHOOKSTRUCT*)lParam; g_ptStart = p->pt; // 记录起点 } else if (nCode >= 0 && wParam == WM_LBUTTONUP) { MSLLHOOKSTRUCT* p = (MSLLHOOKSTRUCT*)lParam; RECT rcSelect = { g_ptStart.x, g_ptStart.y, p->pt.x, p->pt.y }; NormalizeRect(&rcSelect); // 确保 left<right, top<bottom // 关键:将屏幕坐标转换为目标窗口客户区坐标 HWND hWnd = WindowFromPoint(p->pt); ScreenToClient(hWnd, &rcSelect.left); ScreenToClient(hWnd, &rcSelect.right); // 对于竖排文本,需交换 x/y 坐标并映射到逻辑行 if (IsVerticalLayout(hWnd)) { SwapRectXY(&rcSelect); // left↔top, right↔bottom } CaptureTextInRect(hWnd, &rcSelect); } return CallNextHookEx(g_hMouseHook, nCode, wParam, lParam); }SwapRectXY实现要点:
- 竖排时,物理 Y 轴对应逻辑 X 轴(行号),物理 X 轴对应逻辑 Y 轴(列号);
NormalizeRect必须在Swap前执行,否则left>right会导致IntersectRect失效;IsVerticalLayout()可通过GetWindowLong(hWnd, GWL_EXSTYLE) & WS_EX_LAYOUTRTL或读取窗口字体LOGFONT.lfEscapement = 900(表示 90° 旋转)判断。
5.2 文本提取增强:从GetWindowTextW到IAccessible的渐进式 fallback
当GetWindowTextW返回空或短字符串时(如竖排 PDF 中的图文混排区域),需启动备选方案。原工程未实现,但可基于ApiHook_dll.h中已包含的#include <oleacc.h>快速接入:
| 方案 | 触发条件 | 优点 | 缺点 |
|---|---|---|---|
GetWindowTextW | 默认首选 | 轻量、快、兼容性好 | 无法处理自绘/图像文本 |
IAccessible::get_accValue | GetWindowTextW返回长度 < 3 且窗口有ROLE_SYSTEM_TEXT属性 | 支持 UIA 标准控件 | 需初始化 COM,Win7+ 有效 |
UIAutomationCore.dll | IAccessible失败且系统为 Win8+ | 支持所有现代应用 | 依赖 .NET Framework 3.0+ |
代码片段(fallback 链):
CString GetTextFromWindow(HWND hWnd) { CString strText = GetWindowTextW(hWnd); // 第一顺位 if (strText.GetLength() >= 3) return strText; // 第二顺位:IAccessible IAccessible* pAcc = NULL; if (SUCCEEDED(AccessibleObjectFromWindow(hWnd, OBJID_CLIENT, __uuidof(IAccessible), (void**)&pAcc))) { VARIANT varChild; VariantInit(&varChild); varChild.vt = VT_I4; varChild.lVal = CHILDID_SELF; BSTR bstrValue; if (SUCCEEDED(pAcc->get_accValue(varChild, &bstrValue))) { strText = bstrValue; SysFreeString(bstrValue); } pAcc->Release(); } return strText; }5.3 验证技巧:用Spy++和Process Monitor定位 Hook 失效点的黄金组合
当划词失效时,不要盲目改代码,先用两个工具交叉验证:
Spy++(Visual Studio 自带):- 启动
Spy++→Find Window→ 选中目标窗口 →Messages标签页; - 拖选文本时观察是否收到
WM_GETTEXT、WM_GETTEXTLENGTH消息; - 若无这些消息,说明控件根本不走标准文本接口(需转向
IAccessible或 OCR); - 若有但
lParam指向的缓冲区为空,说明GetWindowTextW被正确调用但返回空——问题在目标控件逻辑。
- 启动
Process Monitor(Sysinternals):- 过滤
Process Name为目标进程,Operation包含LoadImage、LoadLibrary; - 注入后观察是否有
ApiHook.dll的SUCCESS加载记录; - 若无,说明
CreateRemoteThread失败或路径错误; - 若有但后续无
GetWindowTextW的Stack Trace,说明 IAT Hook 未生效(检查VirtualProtect权限或jmp指令写入位置)。
- 过滤
从那以后我每次调试 Hook 失效,都强制走一遍
Spy++消息跟踪 +Process MonitorDLL 加载验证 +DebugView日志三连。少一次,就多花两小时在猜错方向上。希望帮到你。
本文还有配套的精品资源,点击获取