我擦!迅雷的代碼結構竟然被扒了精光~

背景

之前扒過飛書的源碼,從代碼設計架構層面裏裏外外學習一把,飛書還是挺“大方”的,源碼在客戶端和網頁端都一覽無餘,不過好像新版本已經看不到了。相關的文章由於在內網技術論壇發過了不便於再發出來(泄露內部資料會被查水錶的),因此這次週末抽時間換一個鳥窩來掏一掏。一不小心發現迅雷的客戶端竟然也是基於 Electron 開發的,那代碼就好扒拉了。(先吐槽一下這新版本的某lei爲什麼要抄釘釘的界面,這些年某lei都不知道自己要幹什麼了,每個版本都招人嫌)。

# 拆解篇

一點背景知識說明

基於前端技術棧 Electron 構建的桌面應用,本質上都是加載本地前端資源文件,而這些文件通常是用 asar 格式(類似 windows iso 鏡像)的方式進行打包,然後運行時再通過掛在到內存實現前端資源文件 js/css/html/img 等文件的讀取。這麼說 asar 想辦法掛載就可以隨意閱讀源碼了嗎?不是的。同時 asar 會提供一套通過加密方式防止任意解壓,飛書就是這麼做的,直接通過 asar extract 的方式無法解包出來。但是由於 node 端和 rust 構建的二進制文件如果打包到 asar 會導致無法鏈接到這些二進制文件,因此需要從 asar 中獨立出來,因而導致有部分 js 文件仍然裸露在外面。不過即便沒有任何 js 是暴露的仍然是有辦法爆破的。啊,跑偏了,先不談飛書,今天的主菜是迅雷。**那迅雷的前端資源文件是怎麼管理的呢?

**

是在下想多了,不好意思,迅雷梅川酷子,都攤着在那呢,根本沒用 asar 打包/加密。

開撬

既然 js 都暴露了,也沒什麼好繞的,直接植入代碼吧。我們都知道 Electron 是有 render 進程和 Node 進程的,接下來這一步需要猜猜看哪個文件是負責 render 主進程的?好吧不用猜,名字都非常人類可讀,就 main-renderer(主窗口渲染進程)。打開找到 html 文件(js也可以)插入如下這串。

雙擊啓動,調試窗口出來了,可以大致看到整體頁面結構了。

然後看了一下,迅雷的懸浮小圓圈和主窗口,分別用一個 BrowserWindow 來實現。有趣的是那個小圓圈窗口其實並不小,鼠標懸停出來的那個浮窗也是它的一部分,爲了讓小圓圈在屏幕的任何位置都可以看到懸浮窗,所以整個小圓圈的 BrowserWindow 是大約 4 倍的懸浮窗口大小。

獨立窗口的檢視界面 - 窗口實際是 4倍 浮窗大小,灰色部分全都是這個“小”浮窗所使用的 BrowserWindow區域。

一點防禦措施

從代碼來看,nodejs 進程只有一個文件 main.js ,是 webpack 的構建產物,看源碼這裏的 BrowserWindow 的 webPreference 參數是把 devTools 禁用掉的,導致直接在命令行裏敲 openDevTools 是不能檢視任意窗口的。

當然了,這裏即便是混淆過了也沒關係,畢竟還是明文,把 1 改成 0,把它打開就好(雙歎號/true/1啥都行,開心就好)。不過由於迅雷的窗口實在是太多了,下載彈窗是獨立窗口,選擇文件夾是獨立窗口,各種廣告窗口也是,需要改的配置點很多,這裏就不列了,總共有 10 個窗口,這個配置點按需打開(批量替換也行,謹慎操作就行)。

# 進程結構

呃……然後要幹啥……好像也沒什麼好看的了,代碼是混淆過的,也沒有 map 文件。而且前端部分的代碼也沒什麼技術含量可以說的,哪個 web 頁面都那樣。那看看進程分工吧。

進程樹

在進程樹裏可以看出來,幾乎全部的進程都是 Thunder.exe,可見 Thunder.exe 作爲進程派發入口(類似 server 的網關,而並不直接是業務本身),用戶啓動的時候傳參是 --StartType:DesktopIcon,隨後它喚起了兩組進程,一組是 Electron main 進程,main 進程喚起相關的 renderer;然後是下載的 SDK 服務 DownlaodSDKServer。那麼迅雷的進程關係差不多是清楚了:多個 Electron 窗口,對應一個 DownloadSDK。

通信方式

那麼 Electron 的進程(甭管 main-process 還是 renderer-process,統稱 electron進程) 和 DownloadSDK 是如何通信的呢?進程間通信一般都是依靠 ipc 管道的形式來實現。不過迅雷似乎沒按套路來,它的 DownloadSDK 是控制檯程序,意味着很有可能是通過 stdio 的方式來進行交互的(後續證明不是)。通過觀察進程打開的句柄,看到很詭異的一個現象:DownloadSDK 並沒有打開任何 ipc 管道,反倒是前端進程打開了一個。

前端的 ipc

而 Electron 打開的這個 handler 進程名稱,查了一下,竟然全是 Electron 進程使用的,而且是所有進程。

那麼不妨做出一個大膽的推測:前端多窗口之間是靠自建的 ipc 通道實現的,而 ipc 是 1 server 對 N client 的方式,那麼 server 很有可能就是在主窗口上的,也就是前文看到那個及其明顯的 main-renderer 進程,通過控制檯查看,確實如此,nodejs 的 net 方式創建了一個 server,並且將一個叫做 __xdasIPCServerInstance 的對象暴露在全局環境供前端 js 調用,也即 jsapi。

而小窗口並不存在上述 server 實例,而相對應的有一個 client 實例。

和 DownloadSDK 的通訊方式

這樣看起來就很奇葩了,前端進程之間是通過自建的 ipc 管道通信的,但是並沒有跟 DownloadSDK 有任何通信管道,難道它倆是心有靈犀無言自通?啊這……程序員是唯物主義的!那怎麼查它到底是怎麼跟前端進程交互的呢?既然前端暴露了 server sdk instance,那意味着 DownLoadSDK 肯定是以一種 proxy 的方式暴露在這上面作爲 jsapi 的。可以拿【創建一個下載任務api】來順藤摸瓜。看了主窗口的 server instance 一下果然有這個方法:createTask ,應該就是前端用於創建下載任務用的 api。

chrome 瀏覽器裏查代碼不方便,轉戰 vscode 看源碼,搜索 createTask 這個函數的聲明位置,看到這一段(篇幅控制,此處刪減了部分代碼)。

createTask(e, t) 
{return n(this, void 0, void 0, function* () {          
.....          }
switch (e) {
   case h.DownloadKernel.TaskType.P2sp:         
...case h.DownloadKernel.TaskType.Bt:             
...case h.DownloadKernel.TaskType.Emule:             
...case h.DownloadKernel.TaskType.Group:              
...case h.DownloadKernel.TaskType.Magnet:             
...default:              
  i = !1;}
  return(            
... _.fireTaskEvent(h.DownloadKernel.TaskEventType.TaskCreated, [          );        });      }</pre>

沒跑了,證實了我前面的猜想,這個 __xdasIPCServerInstance 就是 download sdk 封裝到前端的 proxy。

繼續查,這個 fireTaskEvent 是怎麼處理的,閱讀代碼過程繁瑣按下不提,就看這兩段代碼(有刪減整理)。

// 片段一
(e.getDownloadSdkVersion = function () {
let e = a.join(__rootDir, "../bin/SDK/DownloadSDKServer.exe");
return v.getFileVersion(e);
}),
// 片段二
y = l.default(o.join(__rootDir, "../bin/ThunderHelper.node"));
let F = "/ssdkver " + u.DownloadKernelManager.getDownloadSdkVersion();
B.push(F)
y.shellExecute(0, "open", o, B, H, "SW_SHOW");

很顯然,DownloadSDK 是通過一個 ThunderHelper.node 的 nodejs addon 模塊來啓動、通信的。我們知道, nodejs 可以通過 ffi 等方式實現內存共享,以達到兩個進程不需要通過 pipe/sock 等管道就達到通信的目的。而通過工具觀察 Thunder.exe 的喚起關係、句柄關係,兩者的關係就更加一目瞭然了:ELectron 前端進程加載 DownloadSDK 進程,並且通過 \Sessions\5\BaseNamedObjects\xx@22123720|SendShareMemory 這種內存通道來實現的通信,句柄一一對應上了。

# 總結

扒拉了半天,扒完了有點空虛是怎麼回事?

  • 迅雷的代碼架構關係是輕 node 而重前端,把所有的 node 加載、進程管理、多窗口通信都放在前端進程的主窗口進程裏。關於這個做法,我尊重而不認同。前端進程不應該做太重的底層交互,尤其是 js 這種單線程語言,天然的就運行效率低,而且主窗口使用這麼頻繁就不怕卡住嗎

  • Electron 天然就有 ipc 通信能力,完全可以在 node 端做一個消息網關,達成每個窗口通信的能力,完全不需要自建一個 ipc server-client 體系。可能這也是一開始就把大量工作放在前端(主窗口)了導致後期的程序設計受限。說不定是個歷史包袱

  • 用一個 node addon 的方式來跟 DownloadSDK 來通信,這點是可以點個讚的,雖然是業界標準(飛書是通過rust,基本原理類似),但是我目前所負責的業務並沒有做到這樣,所以在慚愧的同時也給它點個贊

  • 迅雷使用的 Electron 版本是 9.2.1,vscode 也是這個版本,好神奇!非常好奇爲何業界都用這個版本,事實上 electron 9.x 最新版本已經更新到 9.3.3 了(2020年10月28日)這個 9.2.1 有什麼魔力讓業界都用它嗎

  • 這裏說明一下,Electron 從 6.0 開始就不支持 windows7(非sp1) 及以下的版本了。

  • 我在 win7 系統上用迅雷安裝器安裝迅雷最新版本,發現 electron 用的是 1.8.6 版本

  • Electron 的主入口是處理過了的,通過 Thunder.exe 程序幹了很多除了啓動前端以外的事情,這個定製還是挺棒的,因爲這樣就可以把各種進程模塊管理起來,不會出現多個獨立進程。就我所看到的不少 Electron 應用其實都沒有定製過。

  • 以上是純粹技術挖掘,沒有破壞到迅雷的核心機密,僅做學習交流使用哈~

關於我們

我們部門有若干桌面端應用,用戶基數大,挑戰大,招聘有 electron 經驗或者前端技術專家。經常會做一些好玩的桌面端實踐,也會深入挖掘和學習一些 app 的程序設計。如果有興趣的話歡迎聯繫我,簡歷請狠砸我!私信我獲取聯繫方式哦!都可以聊一下,說不定就成爲同事了,沒有你想象的那麼難!
還記得開頭說的扒了飛書的源碼的事情嗎?想看嗎?想就投簡歷吧哈哈哈哈

聲明轉載自掘金jiawen的文章,侵刪

作者:jiawen
鏈接:juejin.im/post/6890344584078721031
來源:掘金

最後

喜歡文章的小夥伴可以點個贊哦~,最後,照舊安利一波我們的公衆號:「終端研發部」,目前每天都會推薦一篇優質的技術相關的文章,主要分享java相關的技術與面試技巧, 學習java不迷路。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章