基于某钉探索针对CEF框架的一些逆向思路
2022-9-3 18:1:26 Author: 看雪学苑(查看原文) 阅读量:13 收藏


本文为看雪论坛精华文章

看雪论坛作者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框架的区域主要在聊天框显示区域。

下面主要介绍三个方面的内容:

  1. CEF框架部分API和数据结构的介绍;

  2. web_content.pak文件解密;

  3. 在某钉中开启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文件了。想让修改生效,有两种方式:

  1. 直接修改文件,然后重新加密替换原来的资源文件

  2. 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 应用程序的入口点。#include "pch.h"#include "detours/detours.h"#include "include/capi/cef_browser_capi.h"#include "include/internal/cef_types_win.h"#include "include/capi/cef_client_capi.h"#include "include/internal/cef_win.h"#include <Windows.h>  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_browserBOOL 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。

cef_browser_host->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层做防撤回。

  1. 准备两个号,其中A给B发消息

  2. B收到消息之后,给页面元素下一个子树修改断点

  3. 断点设置好之后,A撤回消息

  4. 断点命中,观察栈锁定关键点

设置好断点

撤回时断点命中,调用链出来了。阅读代码看看什么地方修改比较合适。


找了一圈,发现最顶层的调用处做消息过滤比较合适。

修改代码如下,成功防撤回。

这里调试的时候还会遇到一个问题--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

*本文由看雪论坛 Learn Life 原创,转载请注明来自看雪社区

# 往期推荐

1.四级分页下的页表自映射与基址随机化原理介绍

2.Android 10属性系统原理,检测与定制源码反检测

3.WhatsApp私信协议实现记录

4.Android4.4和8.0 DexClassLoader加载流程分析之寻找脱壳点

5.实战DLL注入

6.某车联网APP加固分析

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458467650&idx=1&sn=480efb5a7ad648fb455d5ea65f327dce&chksm=b18e0dc886f984de654bb6da544fde9c19dca0393fcf76bfcff14c3d3a472930041d032461db#rd
如有侵权请联系:admin#unsafe.sh