基於某釘探索針對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會使我們事半功倍。

框架下載

根據官方庫的指引,我們前往 http://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 桌面軟體開發實戰( http://juejin.cn/book/7075387142121193502

  • 知乎專欄- CEF( http://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應用程式的一點淺見 http://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框架的使用還是挺普遍的。

參考資料

框架原始碼

http://bitbucket.org/chromiumembedded/cef/src/master/

知乎專欄-CEF

http://www.zhihu.com/column/c_1333096419650269184

CEF 桌面軟體開發實戰

http://juejin.cn/book/7075387142121193502

將js程式碼注入到第三方CEF應用程式的一點淺見

http://bbs.pediy.com/thread-268570.htm

看雪ID:Learn Life

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

球分享

球點贊

球在看

點選“閱讀原文”,瞭解更多!