本文为看雪论坛精华文章
看雪论坛作者ID:Learn Life
一
前言
CEF 是 Chromium Embedded Framework 的简写,这是一个把 Chromium 嵌入其他应用的开源框架。
现在市面上有许多桌面软件都使用了CEF框架,比如我们经常使用的某钉、某云音乐等等。
我本意是突破某钉的一些功能限制,结果发现某钉使用了CEF框架,故开始对CEF框架做了一些浮于表面的探索。由于个人能力有限,如果文章中有什么错误之处,还望大家多多指教。
二
初探
在开始正式开始之前,有必要先观察一下某钉的安装目录,看看里面有哪些我们感兴趣的文件。
我电脑上的某钉版本是6.5.30-Release.7289101。
通过查看运行中的DingTalk.exe进程的映射文件锁定你电脑上目前运行的某钉的目录(这个地方会发现有多个同名进程,我们随便选择一个)。
有朋友可能要问为什么要通过这种方式确定目录,这其实是因为某钉的安装目录下面一般都会存在两个版本的文件,一个是当前版本另外一个则是上一个版本。据我观察这两个目录下的文件结构基本一致。
我电脑上的某钉目前就使用的是current目录。
打开current目录可以发现许多的资源文件和依赖库文件,其中对于本文来说最重要的文件是libcef.dll和web_content.pak。libcef.dll是CEF框架的支持库,web_content.pak则是某钉缓存在本地的html、js、css文件。
web_content.pak本质是一个zip压缩文件,我们可以通过解压软件查看里面的内容。
那么可以知道这个压缩文件是被加密了,解压的时候会让输入密码,后面会提到怎么获取密码。通过观察文件的名字也大致可以猜出这些文件的作用。
某钉中使用CEF框架的区域主要在聊天框显示区域。
下面主要介绍三个方面的内容:
-
CEF框架部分API和数据结构的介绍;
-
web_content.pak文件解密;
-
在某钉中开启CEF框架内置的调试窗口。
另外提一嘴,在某钉的安装目录下面我们还可以发现有cef_LICENSE.txt“duilib_license.txt等license声明,通过这些声明我们也可以获得一些信息,比如钉钉还使用了duilib界面库。
三
环境准备
既然某钉使用了CEF框架,那么学会简单的使用CEF框架,了解相关的API会使我们事半功倍。
框架下载
根据官方库的指引,我们前往https://cef-builds.spotifycdn.com/index.html下载框架。
官方实现了C语言版本的CEF框架以及C++版本的CEF框架,其中C++版本的框架是基于C语言版本的二次封装。而我们需要的libcef.dll就是C版本的框架。
在此处下载的文件包含了已经编译好的libcef.dll,无需我们从源码编译libcef库。
实质上从源码编译libcef库并不容易,因为其中涉及到编译chromium,我猜这也是为什么官方会提供各种平台各种版本的库的原因吧。
CEF版本编号格式
在下载时我们需要先了解CEF的版本编号格式。
格式解释如下:
以cef_binary_104.4.25+gd80d467+chromium-104.0.5112.102_windows32.tar.bz2为例,其中
104.4.25和104.0.5112.102是CEF和Chromium的版本信息,gd80d467是git commit的hash。
我们可以先看看某钉使用的libcef.dll是什么版本。
这里发现一个很坑的点,就是Windows的文件属性显示不全,而且还不能拖开,也不能复制。
不过根据已经显示出来的内容,可以发现某钉使用的libcef.dll明显不是在官方提供的页面下载的。版本约定和官方的太不一样,git commit是8位的,官方库可是只有7位。
g2e1fb6b,我尝试使用g2e1fb6、2e1fb6b等hash在commit列表中搜索也没有发现,只能猜测某钉使用的libcef.dll是自己从源码编译的,而且可能对源码做了一些修改吧。
同时我使用91.0.0在下载界面搜索也没有发现相同的版本。后面的版本信息显示不全,得想个办法解决一下子,争取下载一个最接近的版本。其实这里有一个大坑,后面会提到。
获取某钉libcef版本信息
其实文件属性的信息是存在于PE中的资源节中的,使用Windows系统提供的API或者自己解析都可以拿到相关信息。不过我是本着能不写代码就不写代码的懒人思想的。
一般这种库或者框架的动态库中都会提供函数查询版本信息,所以我浏览了一下libcef的导出函数。
在libcef的导出函数中我发现了cef_version_info这个函数,看名字就知道干什么用的了。
该说不说,官方提供了C++版本的文档,为什么不提供一个libcef的api文档呢?反正我是没找到。不过虽然没有文档,还是有源码和大量注释的。
这个函数的定义是这样的:
int cef_version_info(int entry);
我们再结合下面的信息。
从反汇编很明显的看出来这是一个数组下标寻址。
从源码得知不同的参数获取不同的信息,那么完整的版本信息存在于一个32字节的数组中。
在内存窗口转到数组内存。
我们缺少的是最后Chromium的版本信息,那么就是最后四个int。那么简单的拼接,得到
5B.0.1178.A4 转成10进制 91.0.4472.164。
搜索发现只有一个版本满足要求,那么就用这个好了,下载Standard Distribution,这个里面的文件是完整的,包含了框架代码和示例代码。
后面突然想起使用解析PE的格式的一些工具,也能很方便的查看资源信息。
我用CFF试了一下。
一些学习资料
将下载后的文件解压,使用cmake生成vs工程。然后使用vs编译。
这个时候编译成功了,当然可能会在编译的时候遇到一些错误或者警告,按照提示解决即可。
那么环境准备好了,我们需要去学习一些CEF框架的基础知识了,直接看示例代码或者直接看框架源码都不是那么容易的,可以先在网上找前辈取点经。
-
掘金小册-CEF 桌面软件开发实战(https://juejin.cn/book/7075387142121193502)
-
知乎专栏-CEF(https://www.zhihu.com/column/c_1333096419650269184)
四
基于某钉的实战
最终的目标是实现某钉聊天窗口的防撤回功能,基于这个目标,一步步的解决一些遇到的问题。
定位资源文件
CEF可以从本地或者网络加载资源,一般来说桌面应用程序会将大部分需要用到的文件缓存在本地。
所以第一步就是需要找到资源文件的位置,这个不同的软件可能使用的资源文件的名称不太一样,存放的位置也不太一样。比如某钉是放在安装目录下的,但是网易云音乐就没有放在安装目录下。
从CEF框架API入手
在某钉登录页面附加DingTalk.exe。
选择没有命令行参数的附加。
选择这两个函数下断点
cef_stream_reader_create_for_data
cef_stream_reader_create_for_file
这两个函数是CEF提供的两个操作文件数据的函数,返回值都是cef_stream_reader_t结构体。
区别在于cef_stream_reader_create_for_file的参数是文件路径
cef_stream_reader_create_for_data的参数是内存地址和大小,即内存中的文件数据。
这两个函数的声明和相关的结构体如下:
///
// Structure used to read data from a stream. The functions of this structure
// may be called on any thread.
///
typedef struct _cef_stream_reader_t {
///
// Base structure.
///
cef_base_ref_counted_t base;
///
// Read raw binary data.
///
size_t(CEF_CALLBACK* read)(struct _cef_stream_reader_t* self,
void* ptr,
size_t size,
size_t n);
///
// Seek to the specified offset position. |whence| may be any one of SEEK_CUR,
// SEEK_END or SEEK_SET. Returns zero on success and non-zero on failure.
///
int(CEF_CALLBACK* seek)(struct _cef_stream_reader_t* self,
int64 offset,
int whence);
///
// Return the current offset position.
///
int64(CEF_CALLBACK* tell)(struct _cef_stream_reader_t* self);
///
// Return non-zero if at end of file.
///
int(CEF_CALLBACK* eof)(struct _cef_stream_reader_t* self);
///
// Returns true (1) if this reader performs work like accessing the file
// system which may block. Used as a hint for determining the thread to access
// the reader from.
///
int(CEF_CALLBACK* may_block)(struct _cef_stream_reader_t* self);
} cef_stream_reader_t;
///
// Create a new cef_stream_reader_t object from a file.
///
CEF_EXPORT cef_stream_reader_t* cef_stream_reader_create_for_file(
const cef_string_t* fileName);
///
// Create a new cef_stream_reader_t object from data.
///
CEF_EXPORT cef_stream_reader_t* cef_stream_reader_create_for_data(
void* data,
size_t size);
断点下好之后,直接登录。
某钉中没有使用cef_stream_reader_create_for_data函数,使用的是cef_stream_reader_create_for_file。
命中断点,观察参数
/local_res/common_res.pak
/web_content.pak
/local_res/common_res.pak文件中的内容
/web_content.pak文件中的内容
到这就已经确定了资源文件的路径了。
不过需要注意的一点是,如果程序使用了cef_stream_reader_create_for_data函数,那我们就不能从参数直接得到路径了。这个时候需要配合下面的方法使用。
从Windows API入手
直接在kernel32.dll.CreateFileW/A和kernel32.dll.ReadFileW/A下断点,观察函数的参数,如果觉得这样比较废手的话,可以使用行为监控软件比如微软的ProcessMonitor,设置好过滤选项之后监控程序的文件操作。
解密资源文件
如果资源文件被加密了,怎么解密文件。
思路其实很简单,程序运行时肯定会在某个时机解密数据,我们在相关API处下断点,逆向分析即可得到密码。
某钉的资源文件是zip压缩加密,得到密码的方式有两个方向。
从CEF框架API入手
cef_zip_directory 写数据到zip文件
cef_zip_reader_create从zip文件读取数据
函数声明和相关结构体声明:
///
// All ref-counted framework structures must include this structure first.
///
typedef struct _cef_base_ref_counted_t {
///
// Size of the data structure.
///
size_t size;
///
// Called to increment the reference count for the object. Should be called
// for every new copy of a pointer to a given object.
///
void(CEF_CALLBACK* add_ref)(struct _cef_base_ref_counted_t* self);
///
// Called to decrement the reference count for the object. If the reference
// count falls to 0 the object should self-delete. Returns true (1) if the
// resulting reference count is 0.
///
int(CEF_CALLBACK* release)(struct _cef_base_ref_counted_t* self);
///
// Returns true (1) if the current reference count is 1.
///
int(CEF_CALLBACK* has_one_ref)(struct _cef_base_ref_counted_t* self);
///
// Returns true (1) if the current reference count is at least 1.
///
int(CEF_CALLBACK* has_at_least_one_ref)(struct _cef_base_ref_counted_t* self);
} cef_base_ref_counted_t;
///
// Structure that supports the reading of zip archives via the zlib unzip API.
// The functions of this structure should only be called on the thread that
// creates the object.
///
typedef struct _cef_zip_reader_t {
///
// Base structure.
///
cef_base_ref_counted_t base;
///
// Moves the cursor to the first file in the archive. Returns true (1) if the
// cursor position was set successfully.
///
int(CEF_CALLBACK* move_to_first_file)(struct _cef_zip_reader_t* self);
///
// Moves the cursor to the next file in the archive. Returns true (1) if the
// cursor position was set successfully.
///
int(CEF_CALLBACK* move_to_next_file)(struct _cef_zip_reader_t* self);
///
// Moves the cursor to the specified file in the archive. If |caseSensitive|
// is true (1) then the search will be case sensitive. Returns true (1) if the
// cursor position was set successfully.
///
int(CEF_CALLBACK* move_to_file)(struct _cef_zip_reader_t* self,
const cef_string_t* fileName,
int caseSensitive);
///
// Closes the archive. This should be called directly to ensure that cleanup
// occurs on the correct thread.
///
int(CEF_CALLBACK* close)(struct _cef_zip_reader_t* self);
// The below functions act on the file at the current cursor position.
///
// Returns the name of the file.
///
// The resulting string must be freed by calling cef_string_userfree_free().
cef_string_userfree_t(CEF_CALLBACK* get_file_name)(
struct _cef_zip_reader_t* self);
///
// Returns the uncompressed size of the file.
///
int64(CEF_CALLBACK* get_file_size)(struct _cef_zip_reader_t* self);
///
// Returns the last modified timestamp for the file.
///
cef_basetime_t(CEF_CALLBACK* get_file_last_modified)(
struct _cef_zip_reader_t* self);
///
// Opens the file for reading of uncompressed data. A read password may
// optionally be specified.
///
int(CEF_CALLBACK* open_file)(struct _cef_zip_reader_t* self,
const cef_string_t* password);
///
// Closes the file.
///
int(CEF_CALLBACK* close_file)(struct _cef_zip_reader_t* self);
///
// Read uncompressed file contents into the specified buffer. Returns < 0 if
// an error occurred, 0 if at the end of file, or the number of bytes read.
///
int(CEF_CALLBACK* read_file)(struct _cef_zip_reader_t* self,
void* buffer,
size_t bufferSize);
///
// Returns the current offset in the uncompressed file contents.
///
int64(CEF_CALLBACK* tell)(struct _cef_zip_reader_t* self);
///
// Returns true (1) if at end of the file contents.
///
int(CEF_CALLBACK* eof)(struct _cef_zip_reader_t* self);
} cef_zip_reader_t;
///
// Writes the contents of |src_dir| into a zip archive at |dest_file|. If
// |include_hidden_files| is true (1) files starting with "." will be included.
// Returns true (1) on success. Calling this function on the browser process UI
// or IO threads is not allowed.
///
CEF_EXPORT int cef_zip_directory(const cef_string_t* src_dir,
const cef_string_t* dest_file,
int include_hidden_files);
///
// Create a new cef_zip_reader_t object. The returned object's functions can
// only be called from the thread that created the object.
///
CEF_EXPORT cef_zip_reader_t* cef_zip_reader_create(
struct _cef_stream_reader_t* stream);
需要特别关注的是cef_zip_reader_t中的open_file成员。
///
// Opens the file for reading of uncompressed data. A read password may
// optionally be specified.
///
int(CEF_CALLBACK* open_file)(struct _cef_zip_reader_t* self,
const cef_string_t* password);
参数中带有password,那我们在这个函数下断点就可以得到密码了。
具体步骤如下:
在某钉登录页面附加程序,cef_stream_reader_create_for_file函数下断点。
登录某钉,在函数cef_stream_reader_create_for_file参数是web_content.pak路径的时候记住返回值,并给cef_zip_reader_create下断点,程序继续运行。
cef_zip_reader_create断点名命中,检查参数是否是上面记住的返回值。
如果没问题断到则先让程序回到返回处,得到cef_zip_reader_t*返回值0x25CF2940。
在内存中按地址查看0x25CF2940。
根据open_file在结构体中的偏移我们直接就可以找到函数地址,我直接数了一下偏移是0x30,下标第12项,直接下断点,运行程序等待断点命中。
然后断点确实命中了,第二个参数就是密码。这里就不截图了,感兴趣的可以自己去试一下。
从Windows API入手
如果程序没有使用CEF框架提供的函数解密,那么上面说的方法就不行了。这种时候只能使用老办法,在CreateFileA/W和ReadFileA/W下断点,调试程序。
用这种方式也能得到密码,好奇的同学可以去试一下,可以在栈中发现密码。
最后提一嘴,这个密码某钉是怎么计算出来的。我只能说这个算法是MD5,可以利用IDA分析安装目录下的MainFrame.dll结合算法识别插件。不过我没有逆,有大哥逆过,感谢大哥,手动at大哥0xC5。
修改CEF框架加载的资源
可以解密资源之后,我们就可以分析Js文件了。想让修改生效,有两种方式:
-
直接修改文件,然后重新加密替换原来的资源文件
-
hook CEF框架的相关函数在内存中实现修改
直接替换文件非常简单,但是有个问题。这个方式不太稳定,据我观察某钉会不定期的更新资源文件(这个更新不是指某钉的升级),更新之后还得重新替换。
第二种方式的话,其实也不难。我们可以hook cef_zip_reader_t结构体中的read_file函数,并配合get_file_name函数实现在内存中修改。
不过内存替换我也没有去尝试,这里只提供一种思路。
int CEF_CALLBACK hook_read_file(
struct _cef_zip_reader_t* self,
void* buffer,
size_t bufferSize) {
// 调用原始的read_file
int result = old_read_file(self, buffer, bufferSize);
// 获取文件名
cef_string_userfree_t ptr_file_name = get_file_name(self);
// 对比文件名
if (strcmp(ptr_file_name->str, "xxxx") == 0) {
// 如果文件名满足要求,则可以考虑遍历buffer修改关键点
}
}
开启DevTools
改代码不是什么难事,难的是找到关键点。如果能开启Chromium本身的动态调试功能,那对于分析人员来说简直是如虎添翼。
在 cef_browser_host_t结构体中有一个show_dev_tools成员,可以用来开启调试窗口。
cef_browser_host_t对象可以通过cef_browser_t的get_host拿到。
get_host “show_dev_tools声明:
///
// Returns the browser host object. This function can only be called in the
// browser process.
///
struct _cef_browser_host_t* CEF_CALLBACK get_host(
struct _cef_browser_t* self);
///
// Open developer tools (DevTools) in its own browser. The DevTools browser
// will remain associated with this browser. If the DevTools browser is
// already open then it will be focused, in which case the |windowInfo|,
// |client| and |settings| parameters will be ignored. If |inspect_element_at|
// is non-NULL then the element at the specified (x,y) location will be
// inspected. The |windowInfo| parameter will be ignored if this browser is
// wrapped in a cef_browser_view_t.
///
void CEF_CALLBACK show_dev_tools(
struct _cef_browser_host_t* self,
const struct _cef_window_info_t* windowInfo,
struct _cef_client_t* client,
const struct _cef_browser_settings_t* settings,
const cef_point_t* inspect_element_at);
cef_browser_t声明,cef_browser_host_t声明比较大,就不放上来了,可以自己去看头文件(include/capi/cef_browser_capi.h)。
///
// Structure used to represent a browser window. When used in the browser
// process the functions of this structure may be called on any thread unless
// otherwise indicated in the comments. When used in the render process the
// functions of this structure may only be called on the main thread.
///
typedef struct _cef_browser_t {
///
// Base structure.
///
cef_base_ref_counted_t base;
///
// Returns the browser host object. This function can only be called in the
// browser process.
///
struct _cef_browser_host_t*(CEF_CALLBACK* get_host)(
struct _cef_browser_t* self);
///
// Returns true (1) if the browser can navigate backwards.
///
int(CEF_CALLBACK* can_go_back)(struct _cef_browser_t* self);
///
// Navigate backwards.
///
void(CEF_CALLBACK* go_back)(struct _cef_browser_t* self);
///
// Returns true (1) if the browser can navigate forwards.
///
int(CEF_CALLBACK* can_go_forward)(struct _cef_browser_t* self);
///
// Navigate forwards.
///
void(CEF_CALLBACK* go_forward)(struct _cef_browser_t* self);
///
// Returns true (1) if the browser is currently loading.
///
int(CEF_CALLBACK* is_loading)(struct _cef_browser_t* self);
///
// Reload the current page.
///
void(CEF_CALLBACK* reload)(struct _cef_browser_t* self);
///
// Reload the current page ignoring any cached data.
///
void(CEF_CALLBACK* reload_ignore_cache)(struct _cef_browser_t* self);
///
// Stop loading the page.
///
void(CEF_CALLBACK* stop_load)(struct _cef_browser_t* self);
///
// Returns the globally unique identifier for this browser. This value is also
// used as the tabId for extension APIs.
///
int(CEF_CALLBACK* get_identifier)(struct _cef_browser_t* self);
///
// Returns true (1) if this object is pointing to the same handle as |that|
// object.
///
int(CEF_CALLBACK* is_same)(struct _cef_browser_t* self,
struct _cef_browser_t* that);
///
// Returns true (1) if the window is a popup window.
///
int(CEF_CALLBACK* is_popup)(struct _cef_browser_t* self);
///
// Returns true (1) if a document has been loaded in the browser.
///
int(CEF_CALLBACK* has_document)(struct _cef_browser_t* self);
///
// Returns the main (top-level) frame for the browser window. In the browser
// process this will return a valid object until after
// cef_life_span_handler_t::OnBeforeClose is called. In the renderer process
// this will return NULL if the main frame is hosted in a different renderer
// process (e.g. for cross-origin sub-frames).
///
struct _cef_frame_t*(CEF_CALLBACK* get_main_frame)(
struct _cef_browser_t* self);
///
// Returns the focused frame for the browser window.
///
struct _cef_frame_t*(CEF_CALLBACK* get_focused_frame)(
struct _cef_browser_t* self);
///
// Returns the frame with the specified identifier, or NULL if not found.
///
struct _cef_frame_t*(CEF_CALLBACK* get_frame_byident)(
struct _cef_browser_t* self,
int64 identifier);
///
// Returns the frame with the specified name, or NULL if not found.
///
struct _cef_frame_t*(CEF_CALLBACK* get_frame)(struct _cef_browser_t* self,
const cef_string_t* name);
///
// Returns the number of frames that currently exist.
///
size_t(CEF_CALLBACK* get_frame_count)(struct _cef_browser_t* self);
///
// Returns the identifiers of all existing frames.
///
void(CEF_CALLBACK* get_frame_identifiers)(struct _cef_browser_t* self,
size_t* identifiersCount,
int64* identifiers);
///
// Returns the names of all existing frames.
///
void(CEF_CALLBACK* get_frame_names)(struct _cef_browser_t* self,
cef_string_list_t names);
} cef_browser_t;
我们通过注入DLL,HOOK CEF的事件处理回调函数,使用回调函数的struct _cef_browser_t* browser参数,从而调用到show_dev_tools。
以按键事件为例
(代码来自
将js代码注入到第三方CEF应用程序的一点浅见https://bbs.pediy.com/thread-268570.htm
的评论区风铃i大佬的评论,我做了一些修改)
// dllmain.cpp : 定义 DLL 应用程序的入口点。
PVOID g_cef_browser_host_create_browser = nullptr;
PVOID g_cef_get_keyboard_handler = NULL;
PVOID g_cef_on_key_event = NULL;
void SetAsPopup(cef_window_info_t* window_info) {
window_info->style =
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_VISIBLE;
window_info->parent_window = NULL;
window_info->x = CW_USEDEFAULT;
window_info->y = CW_USEDEFAULT;
window_info->width = CW_USEDEFAULT;
window_info->height = CW_USEDEFAULT;
}
int CEF_CALLBACK hook_cef_on_key_event(
struct _cef_keyboard_handler_t* self,
struct _cef_browser_t* browser,
const struct _cef_key_event_t* event,
cef_event_handle_t os_event) {
OutputDebugStringA("[detours] hook_cef_on_key_event n");
auto cef_browser_host = browser->get_host(browser);
// 键盘按下且是F12
if (event->type == KEYEVENT_RAWKEYDOWN && event->windows_key_code == 123) {
cef_window_info_t windowInfo{};
cef_browser_settings_t settings{};
cef_point_t point{};
SetAsPopup(&windowInfo);
OutputDebugStringA("[detours] show_dev_tools n");
// 开启调试窗口
cef_browser_host->show_dev_tools
(cef_browser_host, &windowInfo, 0, &settings, &point);
}
return reinterpret_cast<decltype(&hook_cef_on_key_event)>
(g_cef_on_key_event)(self, browser, event, os_event);
}
struct _cef_keyboard_handler_t* CEF_CALLBACK hook_cef_get_keyboard_handler(
struct _cef_client_t* self) {
OutputDebugStringA("[detours] hook_cef_get_keyboard_handler n");
// 调用原始的修改get_keyboard_handler函数
auto keyboard_handler = reinterpret_cast<decltype(&hook_cef_get_keyboard_handler)>
(g_cef_get_keyboard_handler)(self);
if (keyboard_handler) {
// 记录原始的按键事件回调函数
g_cef_on_key_event = keyboard_handler->on_key_event;
// 修改返回值中的按键事件回调函数
keyboard_handler->on_key_event = hook_cef_on_key_event;
}
return keyboard_handler;
}
int hook_cef_browser_host_create_browser(
const cef_window_info_t* windowInfo,
struct _cef_client_t* client,
const cef_string_t* url,
const struct _cef_browser_settings_t* settings,
struct _cef_dictionary_value_t* extra_info,
struct _cef_request_context_t* request_context) {
OutputDebugStringA("[detours] hook_cef_browser_host_create_browser n");
// 记录原始的get_keyboard_handler
g_cef_get_keyboard_handler = client->get_keyboard_handler;
// 修改get_keyboard_handler
client->get_keyboard_handler = hook_cef_get_keyboard_handler;
return reinterpret_cast<decltype(&hook_cef_browser_host_create_browser)>
(g_cef_browser_host_create_browser)(
windowInfo, client, url, settings, extra_info, request_context);
}
// Hook cef_browser_host_create_browser
BOOL APIENTRY InstallHook()
{
OutputDebugStringA("[detours] InstallHook n");
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
g_cef_browser_host_create_browser =
DetourFindFunction("libcef.dll", "cef_browser_host_create_browser");
DetourAttach(&g_cef_browser_host_create_browser,
hook_cef_browser_host_create_browser);
LONG ret = DetourTransactionCommit();
return ret == NO_ERROR;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
InstallHook();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
这个有个需要注意的点,非常重要(还记得我上面说的大坑嘛)。我使用的库的版本和某钉的不一致,那么上面代码中使用的结构体声明可能在不同版本会有不同。这意味着我们编译出来的DLL中结构体的偏移和某钉中也可能不一致。
注意上面的第43行代码,调用show_dev_tools。
show_dev_tools
(cef_browser_host, &windowInfo, 0, &settings, &point);
在我实际测试中,show_dev_tools的偏移和某钉中就不一致。当时也是找了很久原因,一开始也没往这方面想,还以为是参数没传对,或者有什么对抗存在。最后在调试的时候和官方例子做了对比,才发现调用的函数都不是show_dev_tools!
所以我最后改了一下43行的代码,show_dev_tools偏移差了4个字节,用close_dev_tools刚好对上。
reinterpret_cast<decltype(cef_browser_host->show_dev_tools)>
(cef_browser_host->close_dev_tools)
(cef_browser_host, &windowInfo, 0, &settings, &point);
在聊天框中F12,最后终于是开启成功。
最后还要说一点就是DLL注入的时机,我选择的是程序在登录框界面的时候。这个时候libcef.dll已经加载,cef_browser_host_create_browser函数也没被调用。
聊天框防撤回功能
刀已经准备好了,可以试试刀锋了。
首先考虑消息撤回的时候大概发生了什么。
用户A点击撤回->触发Js点击事件->向服务器发送网络请求->服务器处理请求,向各个客户端发送消息。
用户B收到撤回的请求->Js处理请求,最后修改页面元素。
向服务器发送请求这里有两种可能,一种是直接在Js中发送请求,另一种是Js代码和C++代码通信C++来发这个请求。某钉使用的是后者,因为在撤回的时候调试窗口的Network页面没有发现有网络请求。
所以防撤回的实现点有很多种,我这里主要尝试在Js层做防撤回。
-
准备两个号,其中A给B发消息
-
B收到消息之后,给页面元素下一个子树修改断点
-
断点设置好之后,A撤回消息
-
断点命中,观察栈锁定关键点
设置好断点
撤回时断点命中,调用链出来了。阅读代码看看什么地方修改比较合适。
找了一圈,发现最顶层的调用处做消息过滤比较合适。
修改代码如下,成功防撤回。
这里调试的时候还会遇到一个问题–Js文件太大,调试窗口格式化代码的时候卡死了。
解决方法很简单,我们把在web_content.pak中找到代码文件把该文件先格式化了,不用调试的时候去格式化,这样调试就不会因为格式化的原因卡死了。
五
总结
CEF框架是一个开源的框架,而且某钉也没有加入诸如反调试之内的对抗手段,研究起来比较容易,遇到的一些问题基本都解决了。最大的坑就在于库的版本问题,但是通过调试也能发现端倪。
最后可以思考一些防御的手段,比如:
-
在加载文件的时候校验文件是否被修改,如果被修改则不加载。
-
在libcef库的代码中将调试功能相关代码删除,防止开启调试窗口。
-
或者在Js代码中加反调试,增加调试难度,等等等……
可以进行的相关研究还有很多,无聊的时候玩玩也挺好,毕竟CEF框架的使用还是挺普遍的。
参考资料
框架源码
(https://bitbucket.org/chromiumembedded/cef/src/master/)
知乎专栏-CEF
(https://www.zhihu.com/column/c_1333096419650269184)
CEF 桌面软件开发实战
(https://juejin.cn/book/7075387142121193502)
将js代码注入到第三方CEF应用程序的一点浅见
(https://bbs.pediy.com/thread-268570.htm)
看雪ID:Learn Life
https://bbs.pediy.com/user-home-861753.htm
# 往期推荐
4.Android4.4和8.0 DexClassLoader加载流程分析之寻找脱壳点
5.实战DLL注入
球分享
球点赞
球在看
点击“阅读原文”,了解更多!
原文始发于微信公众号(看雪学苑):基于某钉探索针对CEF框架的一些逆向思路