Discuz! Board

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 1642|回复: 0
打印 上一主题 下一主题

[原创]基于钉钉探索针对CEF框架的一些逆向思路

[复制链接]

1272

主题

2067

帖子

7960

积分

认证用户组

Rank: 5Rank: 5

积分
7960
跳转到指定楼层
楼主
发表于 2022-10-5 12:46:16 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
https://bbs.pediy.com/thread-274198.htm
前言
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文档呢?反正我是没找到。不过虽然没有文档,还是有源码和大量注释的。

这个函数的定义是这样的
1
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可以从本地或者网络加载资源,一般来说桌面应用程序会将大部分需要用到的文件缓存在本地。
所以第一步就是需要找到资源文件的位置,这个不同的软件可能使用的资源文件的名称不太一样,存放的位置也不太一样。比如钉钉是放在安装目录下的,但是网易云音乐就没有放在安装目录下。
从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的参数是内存地址和大小,即内存中的文件数据。
这两个函数的声明和相关的结构体如下:
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 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文件读取数据

函数声明和相关结构体声明
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
///
// 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成员
1
2
3
4
5
6
///
// 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函数实现在内存中修改。

不过内存替换我也没有去尝试,这里只提供一种思路。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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声明
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 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)。
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 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应用程序的一点浅见 的评论区风铃i大佬的评论,我做了一些修改)
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;
}


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
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层做防撤回。
  • 准备两个钉钉号,其中A给B发消息
  • B收到消息之后,给页面元素下一个子树修改断点
  • 断点设置好之后,A撤回消息
  • 断点命中,观察栈锁定关键点
设置好断点

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

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

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

这里调试的时候还会遇到一个问题--Js文件太大,调试窗口格式化代码的时候卡死了。
解决方法很简单,我们把在web_content.pak中找到代码文件把该文件先格式化了,不用调试的时候去格式化,这样调试就不会因为格式化的原因卡死了。
总结
CEF框架是一个开源的框架,而且钉钉也没有加入诸如反调试之内的对抗手段,研究起来比较容易,遇到的一些问题基本都解决了。最大的坑就在于库的版本问题,但是通过调试也能发现端倪。
最后可以思考一些防御的手段,比如:
  • 在加载文件的时候校验文件是否被修改,如果被修改则不加载。
  • 在libcef库的代码中将调试功能相关代码删除,防止开启调试窗口。
  • 或者在Js代码中加反调试,增加调试难度,等等等......
可以进行的相关研究还有很多,无聊的时候玩玩也挺好,毕竟CEF框架的使用还是挺普遍的。
参考资料
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|firemail ( 粤ICP备15085507号-1 )

GMT+8, 2024-11-25 08:29 , Processed in 0.073676 second(s), 19 queries .

Powered by Discuz! X3

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表