基於某釘探索針對CEF框架的一些逆向思路

語言: CN / TW / HK

本文為看雪論壇精華文章

看雪論壇作者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_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。

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加固分析

球分享

球點贊

球在看

點擊“閲讀原文”,瞭解更多!