先放代码
SJYssr/cef_cx_copy_tool(https://github.com/SJYssr/cef_cx_copy_tool)
可以边看帖子边看代码。欢迎Star,Fork,Follow!
背景
随着 Web 技术的飞速发展,越来越多的桌面应用程序开始将 Web 内容嵌入到自身的界面中。为了解决这个问题,Chromium Embedded Framework(CEF)应运而生。CEF 是一个开源项目,它提供了一个框架,使得开发者可以将 Chromium 浏览器引擎嵌入到自己的应用程序中,从而轻松地在桌面应用中显示 Web 页面。通过 CEF,开发者可以在应用中利用浏览器的强大功能,同时保持对应用界面的完全控制。
然而,大部分发布版的软件都禁用了开发者工具,对于逆向调试来说会很棘手。有时也可能会遇到需要修改或监控 CEF 内部功能的需求,这时钩子技术便显得尤为重要。钩子技术可以让我们打开禁用的开发者工具,拦截并修改函数的执行流程,通常用于调试、监控或实现一些定制功能。
CEF
CEF 的发展源自于 Chromium 项目,Chromium 是 Google 推出的开源浏览器项目,也是 Google Chrome 浏览器的基础。Chromium 的设计初衷是提供一个快速、稳定、安全的浏览器框架,同时支持广泛的平台。
随着越来越多的开发者希望将 Chromium 浏览器集成到他们的桌面应用中,CEF 项目应运而生。CEF 本质上是 Chromium 的一个封装,它为开发者提供了 API,使得开发者可以轻松将 Web 内容嵌入到应用程序中。
CEF的关键特点是:
- 支持多平台,Windows、Mac、Linux 都可以使用。
- 提供了强大的 Web 技术支持,包括 HTML5、CSS3、JavaScript 和 WebGL 等。
- 支持多进程架构,通过 Renderer 和 Browser 进程的隔离,提高应用的稳定性和安全性。
vcpkg
vcpkg 是一个开源的 C++ 包管理工具,由 Microsoft 开发。它最初发布于 2016 年,旨在简化 C++ 项目的依赖管理,并提供跨平台的包管理功能。通过 vcpkg,开发者可以轻松地安装和管理第三方库,避免手动配置和编译依赖项的繁琐工作。
vcpkg 的发展历程大致如下:
- 初期阶段:vcpkg 的目标是解决 Windows 平台上 C++ 项目的依赖管理问题。最初,它主要针对 Windows 提供支持,但很快就扩展到其他平台(如 Linux 和 macOS)。
- 跨平台支持:随着 CMake 和 vcpkg 的深度集成,vcpkg 逐渐成为跨平台 C++ 开发的标准工具之一,支持包括 Linux、macOS、Android、iOS 等在内的多个平台。
- 集成与发展:如今,vcpkg 已经成为一个广泛应用的工具,尤其在开源 C++ 项目中。它通过与 CMake 的深度集成,帮助开发者自动下载、编译和管理第三方依赖库。
vcpkg 解决了 C++ 开发中的一个重要问题,即跨平台和多平台的第三方库管理,它简化了依赖项的安装和配置,大大提高了开发效率。vcpkg 和 CMake 有良好集成,在加载 CMake 项目的时候会自动安装项目依赖,而不需要手动执行 vcpkg 命令。
Detours
Detours 是一个微软开源的库,最初由 Microsoft Research 开发,用于拦截 Windows API 函数调用。其核心功能是通过修改目标函数的地址(也就是通过修改函数指针),使得开发者能够在不修改目标代码的情况下,插入自己的代码逻辑。这种技术通常用于调试、监控、性能分析、或者实现自定义的行为。
Detours 采用了“函数钩子”技术,允许开发者将自己的代码插入到其他程序的函数执行流程中。它的应用范围非常广泛,常见的场景包括:
- 监控:拦截某些系统调用或应用程序函数,进行数据采集和监控。
- 调试:拦截并修改程序执行流,用于程序调试。
- 性能分析:记录函数调用的频率、参数和执行时间等信息。
Detours 是一个非常强大的工具,特别适合 Windows 平台的应用程序开发。
CMake
由于 Visual Studio 的一些历史遗留原因,导致 .sln 和 .vcxproj 文件的可读性很差,没有办法进行手写。不过,随着 2019 版本发布,VS 内置了对 CMake 的支持,而 CMakeLists.txt 手写还是很舒服的。因此,本项目使用了 CMake 的构建方式,而不是传统的 VS 构建方式。CMake 是一个开源的跨平台构建系统,最早由 Kitware 公司于 2000 年开发。CMake 使得开发者可以通过一个统一的脚本配置文件来管理多平台的编译过程,而不需要针对不同的编译器和平台编写复杂的 Makefile 文件。
CMake 的发展经历了以下几个阶段:
- 早期阶段:CMake 最初的设计是为了支持跨平台的 C++ 开发,特别是在 UNIX 和 Windows 平台之间。
- 广泛应用:随着 CMake 的功能不断完善,它逐渐成为开源软件项目的首选构建系统,特别是在 C++ 社区中。
- 现代阶段:如今,CMake 已经不仅仅支持 C++,它支持的语言已经扩展到 Python、Fortran、CUDA 等。CMake 的跨平台支持(包括 Windows、Linux、macOS 等)使其成为工业界和学术界广泛使用的工具。
CMake 的优势在于:
- 跨平台支持:开发者只需要编写一次 CMake 脚本,就能在不同的平台上生成相应的构建文件(如 Makefile、Visual Studio 项目文件等)。
- 易于集成第三方库:CMake 能够方便地处理第三方库的集成和依赖关系。
- 灵活性:CMake 提供了丰富的命令和宏,能够应对复杂的构建需求。
逆向过程
以某星学习通为例,它是大学生使用频率较高的学习软件。随着软件版本更新,客户端加入了防作弊机制,禁用了开发者工具,导致一些脚本(如搜题插件)不再可用。为了重新启用开发者工具或自定义功能,需要通过钩子技术进行逆向操作。
其实这个项目有很长时间,很早之前就打算分享的,但是因为懒(自己要用),一直没有公开。不过现在用不到了,所以拿出来分享一下。
Cef 的逆向在一些文章中其实都有说,像基于钉钉探索针对CEF框架的一些逆向思路、将js代码注入到第三方CEF应用程序的一点浅见等等都有说。但是考试客户端和普通的应用程序有很大不同:
- 时间:启动前有可能有很长的时间来操作,但是一旦启动客户端后实际操作的时间很有限(毕竟**又不瞎)。
- 兼容性:很难确定版本,也很难找齐所有的版本(不能保证版本升没升级)。
因此,需要一个通用所有版本的解决方案,直接修改主程序的方案被 PASS 掉了,剩下的要么是 loader 远程注入,要么是 DLL 劫持。不过远程注入不一定能兼容所有版本,因此尝试使用 DLL 劫持实现。为了方便起见,这里使用 Detours 来挂钩 CEF 中的事件和 Windows API 函数。
确定版本
首先是要确定 cef 的版本。由于 cef 随着 chromium 的升级,有一些函数的基址也在发生变化。不过好在实际操作中,客户端的内核版本不会随本体的升级而升级(估计也是为兼容性考虑的,毕竟 32 位的 WinXP 也要用)。
使用horsicq/Detect-It-Easy加载 libcef.dll,可以看到这个模块兼容 Windows XP 32 位的系统。
切到版本页,可以发现版本是 75.1.4+g4210896+chromium-75.0.3770.100。在chromiumembedded/cef:4210896 可以很快找到对应提交。这也是一个好消息,起码不是经过魔改的源代码。Chrome 的 49 版本是最后一个兼容 Windows XP 的版本了,而这边兼容到了 75 版本,估计也是做了不少努力。
加载 CXExam.exe 主程序,看一下导出表,可以发现加载了不少系统 DLL。而这些 DLL 都可以用于劫持。为了方便起见,这里直接选了 WINMM.dll 作为劫持对象。
钩取函数实现
劫持 DLL 有一些的工具可以辅助生成代码。这里使用strivexjun/AheadLib-x86-x64直接生成 WINMM.dll 的导出表结构。
生成代码如下
//
// created by AheadLib
// github:https://github.com/strivexjun/AheadLib-x86-x64
//
#include "pch.h"
#include <Shlwapi.h>
#include <string>
#pragma comment( lib, "Shlwapi.lib")
#pragma comment(linker, "/EXPORT:Noname2=_AheadLib_Unnamed2,@2,NONAME")
#pragma comment(linker, "/EXPORT:mciExecute=_AheadLib_mciExecute,@3")
// 省略
FARPROC pfnAheadLib_Unnamed2;
FARPROC pfnAheadLib_mciExecute;
// 省略
static
HMODULE g_OldModule = NULL;
VOID WINAPI Free()
{
if (g_OldModule)
{
FreeLibrary(g_OldModule);
}
}
BOOL WINAPI Load()
{
TCHAR tzPath[MAX_PATH];
TCHAR tzTemp[MAX_PATH * 2];
//
// 这里是否从系统目录或当前目录加载原始DLL
//
//GetModuleFileName(NULL,tzPath,MAX_PATH); //获取本目录下的
//PathRemoveFileSpec(tzPath);
GetSystemDirectory(tzPath, MAX_PATH); //默认获取系统目录的
lstrcat(tzPath, TEXT("\\winmm.dll"));
g_OldModule = LoadLibrary(tzPath);
if (g_OldModule == NULL)
{
wsprintf(tzTemp, TEXT("无法找到模块 %s,程序无法正常运行"), tzPath);
MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP);
}
return (g_OldModule != NULL);
}
FARPROC WINAPI GetAddress(PCSTR pszProcName)
{
FARPROC fpAddress;
CHAR szProcName[64];
TCHAR tzTemp[MAX_PATH];
fpAddress = GetProcAddress(g_OldModule, pszProcName);
if (fpAddress == NULL)
{
if (HIWORD(pszProcName) == 0)
{
wsprintfA(szProcName, "#%s", pszProcName);
pszProcName = szProcName;
}
wsprintf(tzTemp, TEXT("无法找到函数 %hs,程序无法正常运行"), pszProcName);
MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP);
ExitProcess(UINT(-2));
}
return fpAddress;
}
BOOL WINAPI Init()
{
pfnAheadLib_Unnamed2 = GetAddress(MAKEINTRESOURCEA(2));
pfnAheadLib_mciExecute = GetAddress("mciExecute");
// 省略
return TRUE;
}
extern DWORD WINAPI ThreadProc(LPVOID lpThreadParameter);
BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
(void)pvReserved;
if (dwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);
if (Load() && Init())
{
TCHAR szCurName[MAX_PATH];
GetModuleFileName(NULL, szCurName, MAX_PATH);
PathStripPath(szCurName);
//是否判断宿主进程名
if (endsWith(szCurName, TEXT(".exe")))
{
//启动补丁线程或者其他操作
HANDLE hThread = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
if (hThread)
{
CloseHandle(hThread);
}
}
}
}
else if (dwReason == DLL_PROCESS_DETACH)
{
Free();
}
return TRUE;
}
EXTERN_C __declspec(naked) void __cdecl AheadLib_Unnamed2(void)
{
__asm jmp pfnAheadLib_Unnamed2;
}
EXTERN_C __declspec(naked) void __cdecl AheadLib_mciExecute(void)
{
__asm jmp pfnAheadLib_mciExecute;
}
// 省略
可以看到导出表的结构已经被完整生成出来了,只需要实现 ThreadProc 补丁线程就可以实现钩子了。
编写钩子函数
在钩子中需要完成以下功能:
- 开发者工具快捷唤醒 - Alt键快速打开调试工具
- 网页限制解除 - 解除文本选择/复制限制
- 自动化特征伪装 - 隐藏WebDriver属性
- 界面保护机制 - 防止录屏黑屏问题
- 扩展脚本支持 - 篡改猴脚本注入基础
模块结构
核心组件概览
// 函数指针容器
PVOID g_cef_browser_host_create_browser = nullptr; // CEF浏览器创建函数
PVOID g_set_window_display_affinity = nullptr; // 窗口显示属性API
// 字符串处理函数
cef_string_from_ptr_t func_cef_string_from_ptr = nullptr;
// Hook处理函数
BOOL WINAPI hook_set_window_display_affinity(...);
void SetAsPopup(...);
int CEF_CALLBACK hook_cef_on_key_event(...);
// ...其他hook函数
功能实现解析
- 开发者工具快捷唤醒
int CEF_CALLBACK hook_cef_on_key_event(...)
{
if (event->type == KEYEVENT_RAWKEYDOWN &&
event->windows_key_code == 18) // VK_MENU
{
cef_browser_host->show_dev_tools(...);
}
//...
}
实现原理:
- 通过Hook键盘事件处理函数
- 检测Alt键按下事件(键码18)
- 调用CEF原生show_dev_tools方法
- 使用SetAsPopup设置开发者工具窗口属性
- 网页限制解除
const style = document.createElement('style');
style.textContent = `* { user-select: text !important; }`;
document.head.appendChild(style);
document.body.contentEditable = true;
document.designMode = 'on';
// 事件拦截
document.addEventListener('contextmenu', e => e.stopPropagation(), true);
技术要点
- 动态注入CSS样式覆盖限制
- 开启文档编辑模式
- 拦截上下文菜单事件
- 定时循环强化样式应用
- 自动化特征伪装
Object.defineProperty(navigator, 'webdriver', {
get: () => false
});
作用:
- 修改navigator.webdriver属性
- 绕过网站自动化检测
- 防止反爬虫机制识别
- 界面保护机制
BOOL WINAPI hook_set_window_display_affinity(...)
{
if (dwAffinity == WDA_NONE) return TRUE;
return OriginalFunc(hWnd, WDA_NONE); // 强制设置无限制
}
背景知识:
- WDA_MONITOR: 仅允许在显示器显示
- WDA_NONE: 默认无限制
- 解决录屏黑屏/花屏问题
- 扩展脚本支持
string_t tampermonkey_script = TEXT(R"TAMPERMONKEY(
// 用户脚本占位区
)TAMPERMONKEY");
frame->execute_java_script(frame, &tm_eval, &url, 0);
扩展能力:
- 提供篡改猴脚本注入入口
- 支持用户自定义脚本扩展
- 注释控制功能开关
Hook技术实现
Detours初始化流程
BOOL APIENTRY InstallHook()
{
DetourTransactionBegin();
DetourUpdateThread(...);
// 获取CEF函数地址
g_cef_browser_host_create_browser =
DetourFindFunction("libcef.dll", "...");
// 安装系统API Hook
g_set_window_display_affinity =
DetourFindFunction("user32.dll", "...");
DetourAttach(...);
// 处理字符串转换
#ifdef UNICODE
func_cef_string_from_ptr = cef_string_wide_to_utf16;
#else
func_cef_string_from_ptr = cef_string_ascii_to_utf16;
#endif
return DetourTransactionCommit();
}
多级Hook链
技术总结
核心价值点
- 深度CEF定制 - 通过多级Hook实现浏览器核心功能修改
- 跨版本兼容 - 使用动态寻址而非硬编码偏移
- 模块化设计 - 各功能组件低耦合高内聚
要点
- Detours库的使用规范
- CEF框架的消息机制
- 多线程Hook的安全控制
- 浏览器安全模型突破方法
// 典型调用时序示例
ThreadProc启动
→ InstallHook()
→ Hook CEF创建函数
→ Hook系统API
→ JavaScript注入
→ 解除网页限制
→ 伪装浏览器特征
→ 输入监控
→ Alt键捕获
→ 开发者工具唤醒
项目组织
本项目是在 Windows 环境下进行构建,因此使用被誉为“宇宙最强 IDE”的 Visual Studio 最为合适。然而,Visual Studio 的项目管理较为复杂,手动编辑 .sln 和 .vcxproj 文件并不现实。每次修改参数时都需要通过图形界面进行调整,不仅效率低下,而且有时很容易忘记具体改动的内容。幸运的是,从 Visual Studio 2019 开始,官方引入了对 CMake 的支持。使用 CMake 进行配置,不仅比直接操作 Visual Studio 的项目文件更方便,还能够跨版本使用,避免了 VS 项目文件与特定版本绑定的问题。
本案例依赖于 CEF 项目,因此需要将 CEF 作为引用文件夹引入。需要注意的是,虽然 CEF 编译依赖于 Chromium,但实际上在本案例中只依赖了一个 Chromium 的头文件,并不需要克隆整个 Chromium 仓库。
cmake_minimum_required (VERSION 3.10)
project(CefHook)
add_definitions(-DUNICODE -D_UNICODE)
include_directories(${CMAKE_SOURCE_DIR}/cef)
include_directories(${CMAKE_SOURCE_DIR}/chromium)
add_subdirectory(src)
在 src 目录下包含了本项目的所有源代码。我们需要将当前目录下的所有 C 和 C++ 文件添加到编译列表中,并设置链接器参数,同时引入 Detours 库。以下是相关的 CMake 配置:
find_path(DETOURS_INCLUDE_DIRS "detours/detours.h")
find_library(DETOURS_LIBRARY detours REQUIRED)
file(GLOB SOURCES "*.cpp" "*.c")
add_library(CefHook SHARED ${SOURCES})
set_target_properties(CefHook PROPERTIES
LINKER_LANGUAGE C
OUTPUT_NAME "winmm"
PREFIX ""
)
target_include_directories(CefHook PRIVATE ${DETOURS_INCLUDE_DIRS})
target_link_libraries(CefHook PRIVATE ${DETOURS_LIBRARY})
在这段代码中,首先查找当前目录下所有的 C 和 C++ 源代码文件,并添加到 SOURCES 中。接着,设置输出名称为 winmm.dll。最后,将 Detours 库的头文件目录和库文件链接到项目中。
完成上述配置后,可以选择两种方式来构建项目:
- 使用 Visual Studio 打开项目文件夹:Visual Studio 会自动识别 CMake 配置并进行构建。
- 使用命令行构建:可以直接通过命令行使用 CMake 进行构建。
需要注意的是,若在 Visual Studio 中打开项目,通常会自动绑定 vcpkg。但如果是在命令行中构建,则需要手动指定 vcpkg 的路径。
编译完成后,将会得到一个名为 winmm.dll 的动态链接库文件。将该文件复制到客户端应用程序的根目录下即可。
总结
本案例使用了 Detours 来实现 CEF 应用中的函数钩取,最后的 DLL 尺寸小于 50KB,非常精简。通过钩取 CEF 和 Windows API 中的关键事件,可以灵活地控制应用的行为,进行性能分析、调试,或实现自定义功能。
本案例中的所有代码均开源在 SJYssr/cef_cx_copy_tool(https://github.com/SJYssr/cef_cx_copy_tool),欢迎 Star、Fork、Follow!
相关客户端可以在网上搜索到,这里不再提供。此外,本文仅用于技术研究和学习,任何实际应用请遵守相关法律法规和使用条款。