大神嘗試扒迅雷的代碼,竟然被扒了個精光!
回復(fù)架構(gòu)師獲取資源
大家好,我是你們的朋友架構(gòu)君,一個會寫代碼吟詩的架構(gòu)師。
'javajgs.com';

背景
之前扒過飛書的源碼,從代碼設(shè)計架構(gòu)層面里里外外學(xué)習(xí)一把,飛書還是挺 “大方” 的,源碼在客戶端和網(wǎng)頁端都一覽無余,不過好像新版本已經(jīng)看不到了。相關(guān)的文章由于在內(nèi)網(wǎng)技術(shù)論壇發(fā)過了不便于再發(fā)出來(泄露內(nèi)部資料會被查水表的),因此這次周末抽時間換一個鳥窩來掏一掏。
一不小心發(fā)現(xiàn)迅雷的客戶端竟然也是基于 Electron 開發(fā)的,那代碼就好扒拉了。(先吐槽一下這新版本的某 lei 為什么要抄釘釘?shù)慕缑妫@些年某 lei 都不知道自己要干什么了,每個版本都招人嫌)。

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

是在下想多了,不好意思,迅雷梅川酷子,都攤著在那呢,根本沒用 asar 打包 / 加密。
2、開撬
既然 js 都暴露了,也沒什么好繞的,直接植入代碼吧。我們都知道 Electron 是有 render 進(jìn)程和 Node 進(jìn)程的,接下來這一步需要猜猜看哪個文件是負(fù)責(zé) render 主進(jìn)程的?
好吧不用猜,名字都非常人類可讀,就 main-renderer(主窗口渲染進(jìn)程)。打開找到 html 文件(js 也可以)插入如下這串。

雙擊啟動,調(diào)試窗口出來了,可以大致看到整體頁面結(jié)構(gòu)了。

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

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

3、一點防御措施
從代碼來看,nodejs 進(jìn)程只有一個文件 main.js ,是 webpack 的構(gòu)建產(chǎn)物,看源碼這里的 BrowserWindow 的 webPreference 參數(shù)是把 devTools 禁用掉的,導(dǎo)致直接在命令行里敲 openDevTools 是不能檢視任意窗口的。

當(dāng)然了,這里即便是混淆過了也沒關(guān)系,畢竟還是明文,把 1 改成 0,把它打開就好(雙嘆號 /true/1 啥都行,開心就好)。不過由于迅雷的窗口實在是太多了,下載彈窗是獨立窗口,選擇文件夾是獨立窗口,各種廣告窗口也是,需要改的配置點很多,這里就不列了,總共有 10 個窗口,這個配置點按需打開(批量替換也行,謹(jǐn)慎操作就行)。
進(jìn)程結(jié)構(gòu)
呃…… 然后要干啥…… 好像也沒什么好看的了,代碼是混淆過的,也沒有 map 文件。而且前端部分的代碼也沒什么技術(shù)含量可以說的,哪個 web 頁面都那樣。那看看進(jìn)程分工吧。
1、進(jìn)程樹
在進(jìn)程樹里可以看出來,幾乎全部的進(jìn)程都是 Thunder.exe,可見 Thunder.exe 作為進(jìn)程派發(fā)入口(類似 server 的網(wǎng)關(guān),而并不直接是業(yè)務(wù)本身),用戶啟動的時候傳參是 --StartType:DesktopIcon,隨后它喚起了兩組進(jìn)程,一組是 Electron main 進(jìn)程,main 進(jìn)程喚起相關(guān)的 renderer;然后是下載的 SDK 服務(wù) DownlaodSDKServer。
那么迅雷的進(jìn)程關(guān)系差不多是清楚了:多個 Electron 窗口,對應(yīng)一個 DownloadSDK。

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

3、前端的 ipc
而 Electron 打開的這個 handler 進(jìn)程名稱,查了一下,竟然全是 Electron 進(jìn)程使用的,而且是所有進(jìn)程。

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

而小窗口并不存在上述 server 實例,而相對應(yīng)的有一個 client 實例。

4、和 DownloadSDK 的通訊方式
這樣看起來就很奇葩了,前端進(jìn)程之間是通過自建的 ipc 管道通信的,但是并沒有跟 DownloadSDK 有任何通信管道,難道它倆是心有靈犀無言自通?啊這…… 程序員是唯物主義的!微信搜索公眾號:前端技術(shù)編程,回復(fù):前端 領(lǐng)取資料 。
那怎么查它到底是怎么跟前端進(jìn)程交互的呢?既然前端暴露了 server sdk instance,那意味著 DownLoadSDK 肯定是以一種 proxy 的方式暴露在這上面作為 jsapi 的。可以拿【創(chuàng)建一個下載任務(wù) api】來順藤摸瓜。看了主窗口的 server instance 一下果然有這個方法:createTask ,應(yīng)該就是前端用于創(chuàng)建下載任務(wù)用的 api。

chrome 瀏覽器里查代碼不方便,轉(zhuǎn)戰(zhàn) vscode 看源碼,搜索 createTask 這個函數(shù)的聲明位置,看到這一段(篇幅控制,此處刪減了部分代碼)。
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, [ ); }); }
沒跑了,證實了我前面的猜想,這個 __xdasIPCServerInstance 就是 download sdk 封裝到前端的 proxy。
繼續(xù)查,這個 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 等方式實現(xiàn)內(nèi)存共享,以達(dá)到兩個進(jìn)程不需要通過 pipe/sock 等管道就達(dá)到通信的目的。而通過工具觀察 Thunder.exe 的喚起關(guān)系、句柄關(guān)系,兩者的關(guān)系就更加一目了然了:ELectron 前端進(jìn)程加載 DownloadSDK 進(jìn)程,并且通過 \Sessions\5\BaseNamedObjects\xx@22123720|SendShareMemory 這種內(nèi)存通道來實現(xiàn)的通信,句柄一一對應(yīng)上了。

總結(jié)
扒拉了半天,扒完了有點空虛是怎么回事?
迅雷的代碼架構(gòu)關(guān)系是輕 node 而重前端,把所有的 node 加載、進(jìn)程管理、多窗口通信都放在前端進(jìn)程的主窗口進(jìn)程里。關(guān)于這個做法,我尊重而不認(rèn)同。前端進(jìn)程不應(yīng)該做太重的底層交互,尤其是 js 這種單線程語言,天然的就運行效率低,而且主窗口使用這么頻繁就不怕卡住嗎
Electron 天然就有 ipc 通信能力,完全可以在 node 端做一個消息網(wǎng)關(guān),達(dá)成每個窗口通信的能力,完全不需要自建一個 ipc server-client 體系。可能這也是一開始就把大量工作放在前端 (主窗口) 了導(dǎo)致后期的程序設(shè)計受限。說不定是個歷史包袱
用一個 node addon 的方式來跟 DownloadSDK 來通信,這點是可以點個贊的,雖然是業(yè)界標(biāo)準(zhǔn)(飛書是通過 rust,基本原理類似),但是我目前所負(fù)責(zé)的業(yè)務(wù)并沒有做到這樣,所以在慚愧的同時也給它點個贊
迅雷使用的 Electron 版本是 9.2.1,vscode 也是這個版本,好神奇!非常好奇為何業(yè)界都用這個版本,事實上 electron 9.x 最新版本已經(jīng)更新到 9.3.3 了(2020 年 10 月 28 日)這個 9.2.1 有什么魔力讓業(yè)界都用它嗎
這里說明一下,Electron 從 6.0 開始就不支持 windows7 (非 sp1) 及以下的版本了。
我在 win7 系統(tǒng)上用迅雷安裝器安裝迅雷最新版本,發(fā)現(xiàn) electron 用的是 1.8.6 版本
Electron 的主入口是處理過了的,通過 Thunder.exe 程序干了很多除了啟動前端以外的事情,這個定制還是挺棒的,因為這樣就可以把各種進(jìn)程模塊管理起來,不會出現(xiàn)多個獨立進(jìn)程。就我所看到的不少 Electron 應(yīng)用其實都沒有定制過。
以上是純粹技術(shù)挖掘,沒有破壞到迅雷的核心機(jī)密,僅做學(xué)習(xí)交流使用哈~
文章來源:https://juejin.im/post/6890344584078721031
這些年小編給你分享過的干貨
2.優(yōu)質(zhì)ERP系統(tǒng)帶進(jìn)銷存財務(wù)生產(chǎn)功能(附源碼)
3.優(yōu)質(zhì)SpringBoot帶工作流管理項目(附源碼)
5.SBoot+Vue外賣系統(tǒng)前后端都有(附源碼)

轉(zhuǎn)發(fā)在看就是最大的支持??
