是 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会使我们事半功倍。
根据官方库的指引,我们前往下载框架。
官方实现了C语言版本的CEF框架以及C++版本的CEF框架,其中C++版本的框架是基于C语言版本的二次封装。而我们需要的libcef.dll就是C版本的框架。
 
在此处下载的文件包含了已经编译好的libcef.dll,无需我们从源码编译libcef库。
实质上从源码编译libcef库并不容易,因为其中涉及到编译chromium,我猜这也是为什么官方会提供各种平台各种版本的库的原因吧。
 
在下载时我们需要先了解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在下载界面搜索也没有发现相同的版本。后面的版本信息显示不全,得想个办法解决一下子,争取下载一个最接近的版本。其实这里有一个大坑,后面会提到。
其实文件属性的信息是存在于PE中的资源节中的,使用Windows系统提供的API或者自己解析都可以拿到相关信息。不过我是本着能不写代码就不写代码的懒人思想的。
一般这种库或者框架的动态库中都会提供函数查询版本信息,所以我浏览了一下libcef的导出函数
在libcef的导出函数中我发现了cef_version_info这个函数,看名字就知道干什么用的了。
该说不说,官方提供了C++版本的文档,为什么不提供一个libcef的api文档呢?反正我是没找到。不过虽然没有文档,还是有源码和大量注释的。
 
这个函数的定义是这样的
| 
      1
      | intcef_version_info(intentry); | 
我们再结合下面的信息
从反汇编很明显的看出来这是一个数组下标寻址
 
从源码得知不同的参数获取不同的信息,那么完整的版本信息存在于一个32字节的数组中
 
在内存窗口转到数组内存
 
我们缺少的是最后Chromium的版本信息,那么就是最后四个int。那么简单的拼接,得到5B.0.1178.A4 转成10进制 91.0.4472.164。
搜索发现只有一个版本满足要求,那么就用这个好了,下载Standard Distribution,这个里面的文件是完整的,包含了框架代码和示例代码。
 
后面突然想起使用解析PE的格式的一些工具,也能很方便的查看资源信息。
我用CFF试了一下
 
将下载后的文件解压,使用cmake生成vs工程。然后使用vs编译。
这个时候编译成功了,当然可能会在编译的时候遇到一些错误或者警告,按照提示解决即可。
那么环境准备好了,我们需要去学习一些CEF框架的基础知识了,直接看示例代码或者直接看框架源码都不是那么容易的,可以先在网上找前辈取点经。
掘金小册-
知乎专栏-
最终的目标是实现钉钉聊天窗口的防撤回功能,基于这个目标,一步步的解决一些遇到的问题。
 CEF可以从本地或者网络加载资源,一般来说桌面应用程序会将大部分需要用到的文件缓存在本地。
 所以第一步就是需要找到资源文件的位置,这个不同的软件可能使用的资源文件的名称不太一样,存放的位置也不太一样。比如钉钉是放在安装目录下的,但是网易云音乐就没有放在安装目录下。
 在钉钉登录页面附加DingTalk.exe
 
选择没有命令行参数的附加
 
选择这两个函数下断点cef_stream_reader_create_for_datacef_stream_reader_create_for_file
这两个函数是CEF提供的两个操作文件数据的函数,返回值都是cef_stream_reader_t结构体。
区别在于cef_stream_reader_create_for_file的参数是文件路径cef_stream_reader_create_for_data的参数是内存地址和大小,即内存中的文件数据。
这两个函数的声明和相关的结构体如下:
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      
      21
      
      22
      
      23
      
      24
      
      25
      
      26
      
      27
      
      28
      
      29
      
      30
      
      31
      
      32
      
      33
      
      34
      
      35
      
      36
      
      37
      
      38
      
      39
      
      40
      
      41
      
      42
      
      43
      
      44
      
      45
      
      46
      
      47
      
      48
      
      49
      
      50
      
      51
      
      52
      
      53
      
      54
      
      55
      
      56
      
      57
      | /////Structure used to read data froma stream. The functions of this structure//may be called on anythread.///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 anyone of SEEK_CUR,  //SEEK_END orSEEK_SET. Returns zero on success andnon-zero on failure.  ///  int(CEF_CALLBACK*seek)(struct _cef_stream_reader_t*self,                          int64 offset,                          intwhence);  ///  //Return the current offset position.  ///  int64(CEF_CALLBACK*tell)(struct _cef_stream_reader_t*self);  ///  //Return non-zero ifat end of file.  ///  int(CEF_CALLBACK*eof)(struct _cef_stream_reader_t*self);  ///  //Returns true (1) ifthis reader performs work like accessing the file  //system which may block. Used as a hint fordetermining 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 objectfroma 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 objectfromdata.///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函数,那我们就不能从参数直接得到路径了。这个时候需要配合下面的方法使用。
直接在kernel32.dll.CreateFileW/A和kernel32.dll.ReadFileW/A下断点,观察函数的参数,如果觉得这样比较废手的话,可以使用行为监控软件比如微软的ProcessMonitor,设置好过滤选项之后监控程序的文件操作。
如果资源文件被加密了,怎么解密文件。
思路其实很简单,程序运行时肯定会在某个时机解密数据,我们在相关API处下断点,逆向分析即可得到密码。
钉钉的资源文件是zip压缩加密,得到密码的方式有两个方向。
cef_zip_directory 写数据到zip文件cef_zip_reader_create从zip文件读取数据
函数声明和相关结构体声明
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      
      21
      
      22
      
      23
      
      24
      
      25
      
      26
      
      27
      
      28
      
      29
      
      30
      
      31
      
      32
      
      33
      
      34
      
      35
      
      36
      
      37
      
      38
      
      39
      
      40
      
      41
      
      42
      
      43
      
      44
      
      45
      
      46
      
      47
      
      48
      
      49
      
      50
      
      51
      
      52
      
      53
      
      54
      
      55
      
      56
      
      57
      
      58
      
      59
      
      60
      
      61
      
      62
      
      63
      
      64
      
      65
      
      66
      
      67
      
      68
      
      69
      
      70
      
      71
      
      72
      
      73
      
      74
      
      75
      
      76
      
      77
      
      78
      
      79
      
      80
      
      81
      
      82
      
      83
      
      84
      
      85
      
      86
      
      87
      
      88
      
      89
      
      90
      
      91
      
      92
      
      93
      
      94
      
      95
      
      96
      
      97
      
      98
      
      99
      
      100
      
      101
      
      102
      
      103
      
      104
      
      105
      
      106
      
      107
      
      108
      
      109
      
      110
      
      111
      
      112
      
      113
      
      114
      
      115
      
      116
      
      117
      
      118
      
      119
      
      120
      
      121
      
      122
      
      123
      
      124
      
      125
      
      126
      
      127
      
      128
      
      129
      
      130
      
      131
      
      132
      
      133
      
      134
      
      135
      
      136
      
      137
      
      138
      
      139
      
      140
      | /////Allref-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 forthe object. Should be called  //forevery 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 forthe object. If the reference  //count falls to 0the objectshould self-delete. Returns true (1) ifthe  //resulting reference count is0.  ///  int(CEF_CALLBACK*release)(struct _cef_base_ref_counted_t*self);  ///  //Returns true (1) ifthe current reference count is1.  ///  int(CEF_CALLBACK*has_one_ref)(struct _cef_base_ref_counted_t*self);  ///  //Returns true (1) ifthe current reference count isat 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 ziparchives 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 fileinthe archive. Returns true (1) ifthe  //cursor position was setsuccessfully.  ///  int(CEF_CALLBACK*move_to_first_file)(struct _cef_zip_reader_t*self);  ///  //Moves the cursor to the nextfileinthe archive. Returns true (1) ifthe  //cursor position was setsuccessfully.  ///  int(CEF_CALLBACK*move_to_next_file)(struct _cef_zip_reader_t*self);  ///  //Moves the cursor to the specified fileinthe archive. If |caseSensitive|  //istrue (1) then the search will be case sensitive. Returns true (1) ifthe  //cursor position was setsuccessfully.  ///  int(CEF_CALLBACK*move_to_file)(struct _cef_zip_reader_t*self,                                  const cef_string_t*fileName,                                  intcaseSensitive);  ///  //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 fileat 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 forthe file.  ///  cef_basetime_t(CEF_CALLBACK*get_file_last_modified)(      struct _cef_zip_reader_t*self);  ///  //Opens the fileforreading 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 filecontents into the specified buffer. Returns < 0if  //an error occurred, 0ifat the end of file, orthe number of bytes read.  ///  int(CEF_CALLBACK*read_file)(struct _cef_zip_reader_t*self,                               void*buffer,                               size_t bufferSize);  ///  //Returns the current offset inthe uncompressed filecontents.  ///  int64(CEF_CALLBACK*tell)(struct _cef_zip_reader_t*self);  ///  //Returns true (1) ifat end of the filecontents.  ///  int(CEF_CALLBACK*eof)(struct _cef_zip_reader_t*self);} cef_zip_reader_t;/////Writes the contents of |src_dir| into a ziparchive at |dest_file|. If//|include_hidden_files| istrue (1) files starting with "."will be included.//Returns true (1) on success.  Calling this function on the browser process UI//orIO threads isnotallowed.///CEF_EXPORT intcef_zip_directory(const cef_string_t*src_dir,                                 const cef_string_t*dest_file,                                 intinclude_hidden_files);/////Create a new cef_zip_reader_t object. The returned object's functions can//only be called fromthe 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成员
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      | /////Opens the fileforreading 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); | 
具体步骤如下
在钉钉登录页面附加程序,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项,直接下断点,运行程序等待断点命中。
然后断点确实命中了,第二个参数就是密码。这里就不截图了,感兴趣的可以自己去试一下。
如果程序没有使用CEF框架提供的函数解密,那么上面说的方法就不行了。这种时候只能使用老办法,在CreateFileA/W和ReadFileA/W下断点,调试程序。
用这种方式也能得到密码,好奇的同学可以去试一下,可以在栈中发现密码。
最后提一嘴,这个密码钉钉是怎么计算出来的。我只能说这个算法是MD5,可以利用IDA分析安装目录下的MainFrame.dll结合算法识别插件。不过我没有逆,有大哥逆过,感谢大哥,手动at大哥。
可以解密资源之后,我们就可以分析Js文件了。想让修改生效,有两种方式
直接替换文件非常简单,但是有个问题。这个方式不太稳定,据我观察钉钉会不定期的更新资源文件(这个更新不是指钉钉的升级),更新之后还得重新替换。
第二种方式的话,其实也不难。我们可以hook cef_zip_reader_t结构体中的read_file函数,并配合get_file_name函数实现在内存中修改。
不过内存替换我也没有去尝试,这里只提供一种思路。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      | intCEF_CALLBACK hook_read_file(    struct _cef_zip_reader_t*self,    void*buffer,    size_t bufferSize) {    //调用原始的read_file    intresult =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修改关键点    }} | 
改代码不是什么难事,难的是找到关键点。如果能开启Chromium本身的动态调试功能,那对于分析人员来说简直是如虎添翼。
在 cef_browser_host_t结构体中有一个show_dev_tools成员,可以用来开启调试窗口。cef_browser_host_t对象可以通过cef_browser_t的get_host拿到。
get_host ``show_dev_tools声明
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      
      21
      
      22
      | /////Returns the browser host object. This function can only be called inthe//browser process.///struct _cef_browser_host_t*CEF_CALLBACK get_host(      struct _cef_browser_t*self);/////Opendeveloper tools (DevTools) inits own browser. The DevTools browser//will remain associated with this browser. If the DevTools browser is//already openthen it will be focused, inwhich case the |windowInfo|,//|client| and|settings| parameters will be ignored. If |inspect_element_at|//isnon-NULL then the element at the specified (x,y) location will be//inspected. The |windowInfo| parameter will be ignored ifthis browser is//wrapped ina 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)。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      
      21
      
      22
      
      23
      
      24
      
      25
      
      26
      
      27
      
      28
      
      29
      
      30
      
      31
      
      32
      
      33
      
      34
      
      35
      
      36
      
      37
      
      38
      
      39
      
      40
      
      41
      
      42
      
      43
      
      44
      
      45
      
      46
      
      47
      
      48
      
      49
      
      50
      
      51
      
      52
      
      53
      
      54
      
      55
      
      56
      
      57
      
      58
      
      59
      
      60
      
      61
      
      62
      
      63
      
      64
      
      65
      
      66
      
      67
      
      68
      
      69
      
      70
      
      71
      
      72
      
      73
      
      74
      
      75
      
      76
      
      77
      
      78
      
      79
      
      80
      
      81
      
      82
      
      83
      
      84
      
      85
      
      86
      
      87
      
      88
      
      89
      
      90
      
      91
      
      92
      
      93
      
      94
      
      95
      
      96
      
      97
      
      98
      
      99
      
      100
      
      101
      
      102
      
      103
      
      104
      
      105
      
      106
      
      107
      
      108
      
      109
      
      110
      
      111
      
      112
      
      113
      
      114
      
      115
      
      116
      
      117
      
      118
      
      119
      
      120
      
      121
      
      122
      
      123
      
      124
      
      125
      
      126
      
      127
      
      128
      
      129
      | /////Structure used to represent a browser window. When used inthe browser//process the functions of this structure may be called on anythread unless//otherwise indicated inthe comments. When used inthe 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 inthe  //browser process.  ///  struct _cef_browser_host_t*(CEF_CALLBACK*get_host)(      struct _cef_browser_t*self);  ///  //Returns true (1) ifthe 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) ifthe 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) ifthe browser iscurrently loading.  ///  int(CEF_CALLBACK*is_loading)(struct _cef_browser_t*self);  ///  //Reloadthe current page.  ///  void(CEF_CALLBACK*reload)(struct _cef_browser_t*self);  ///  //Reloadthe current page ignoring anycached 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 forthis browser. This value isalso  //used as the tabId forextension APIs.  ///  int(CEF_CALLBACK*get_identifier)(struct _cef_browser_t*self);  ///  //Returns true (1) ifthis objectispointing 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) ifthe window isa popup window.  ///  int(CEF_CALLBACK*is_popup)(struct _cef_browser_t*self);  ///  //Returns true (1) ifa document has been loaded inthe browser.  ///  int(CEF_CALLBACK*has_document)(struct _cef_browser_t*self);  ///  //Returns the main (top-level) frame forthe browser window. In the browser  //process this will returna valid objectuntil after  //cef_life_span_handler_t::OnBeforeClose iscalled. In the renderer process  //this will returnNULL ifthe main frame ishosted ina different renderer  //process (e.g. forcross-origin sub-frames).  ///  struct _cef_frame_t*(CEF_CALLBACK*get_main_frame)(      struct _cef_browser_t*self);  ///  //Returns the focused frame forthe browser window.  ///  struct _cef_frame_t*(CEF_CALLBACK*get_focused_frame)(      struct _cef_browser_t*self);  ///  //Returns the frame with the specified identifier, orNULL ifnotfound.  ///  struct _cef_frame_t*(CEF_CALLBACK*get_frame_byident)(      struct _cef_browser_t*self,      int64 identifier);  ///  //Returns the frame with the specified name, orNULL ifnotfound.  ///  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 allexisting frames.  ///  void(CEF_CALLBACK*get_frame_identifiers)(struct _cef_browser_t*self,                                            size_t*identifiersCount,                                            int64*identifiers);  ///  //Returns the names of allexisting 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。
以按键事件为例
(代码来自 的评论区大佬的评论,我做了一些修改)
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      
      21
      
      22
      
      23
      
      24
      
      25
      
      26
      
      27
      
      28
      
      29
      
      30
      
      31
      
      32
      
      33
      
      34
      
      35
      
      36
      
      37
      
      38
      
      39
      
      40
      
      41
      
      42
      
      43
      
      44
      
      45
      
      46
      
      47
      
      48
      
      49
      
      50
      
      51
      
      52
      
      53
      
      54
      
      55
      
      56
      
      57
      
      58
      
      59
      
      60
      
      61
      
      62
      
      63
      
      64
      
      65
      
      66
      
      67
      
      68
      
      69
      
      70
      
      71
      
      72
      
      73
      
      74
      
      75
      
      76
      
      77
      
      78
      
      79
      
      80
      
      81
      
      82
      
      83
      
      84
      
      85
      
      86
      
      87
      
      88
      
      89
      
      90
      
      91
      
      92
      
      93
      
      94
      
      95
      
      96
      
      97
      
      98
      
      99
      
      100
      
      101
      
      102
      
      103
      
      104
      
      105
      
      106
      
      107
      
      108
      
      109
      
      110
      
      111
      
      112
      
      113
      
      114
      
      115
      
      116
      
      117
      
      118
      
      119
      
      120
      
      121
      
      122
      
      123
      
      124
      
      125
      
      126
      
      127
      
      128
      
      129
      | //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;}intCEF_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);    }    returnreinterpret_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;    }    returnkeyboard_handler;}inthook_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;    returnreinterpret_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_browserBOOLAPIENTRY 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);    LONGret =DetourTransactionCommit();    returnret ==NO_ERROR;}BOOLAPIENTRY 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;    }    returnTRUE;} | 
这个有个需要注意的点,非常重要(还记得我上面说的大坑嘛)。我使用的库的版本和钉钉的不一致,那么上面代码中使用的结构体声明可能在不同版本会有不同。这意味着我们编译出来的DLL中结构体的偏移和钉钉中也可能不一致。
注意上面的第43行代码,调用show_dev_tools
| 
      1
      
      2
      | 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刚好对上。
| 
      1
      
      2
      
      3
      | 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层做防撤回。
设置好断点
 
撤回时断点命中,调用链出来了。阅读代码看看什么地方修改比较合适。
 
找了一圈,发现最顶层的调用处做消息过滤比较合适
 
修改代码如下,成功防撤回
 
这里调试的时候还会遇到一个问题--Js文件太大,调试窗口格式化代码的时候卡死了。
解决方法很简单,我们把在web_content.pak中找到代码文件把该文件先格式化了,不用调试的时候去格式化,这样调试就不会因为格式化的原因卡死了。
CEF框架是一个开源的框架,而且钉钉也没有加入诸如反调试之内的对抗手段,研究起来比较容易,遇到的一些问题基本都解决了。最大的坑就在于库的版本问题,但是通过调试也能发现端倪。
最后可以思考一些防御的手段,比如:
可以进行的相关研究还有很多,无聊的时候玩玩也挺好,毕竟CEF框架的使用还是挺普遍的。
更多【基于钉钉探索针对CEF框架的一些逆向思路】相关视频教程:www.yxfzedu.com