作者:jiawen
鏈接:juejin.im/post/6890344584078721031# 背景
之前扒過(guò)飛書(shū)的源碼,從代碼設(shè)計(jì)架構(gòu)層面里里外外學(xué)習(xí)一把,飛書(shū)還是挺 “大方” 的,源碼在客戶(hù)端和網(wǎng)頁(yè)端都一覽無(wú)余,不過(guò)好像新版本已經(jīng)看不到了。相關(guān)的文章由于在內(nèi)網(wǎng)技術(shù)論壇發(fā)過(guò)了不便于再發(fā)出來(lái)(泄露內(nèi)部資料會(huì)被查水表的),因此這次周末抽時(shí)間換一個(gè)鳥(niǎo)窩來(lái)掏一掏。一不小心發(fā)現(xiàn)迅雷的客戶(hù)端竟然也是基于 Electron 開(kāi)發(fā)的,那代碼就好扒拉了。(先吐槽一下這新版本的某 lei 為什么要抄釘釘?shù)慕缑?,這些年某 lei 都不知道自己要干什么了,每個(gè)版本都招人嫌)。
# 拆解篇
一點(diǎn)背景知識(shí)說(shuō)明
基于前端技術(shù)棧 Electron 構(gòu)建的桌面應(yīng)用,本質(zhì)上都是加載本地前端資源文件,而這些文件通常是用 asar 格式(類(lèi)似 windows iso 鏡像)的方式進(jìn)行打包,然后運(yùn)行時(shí)再通過(guò)掛在到內(nèi)存實(shí)現(xiàn)前端資源文件 js/css/html/img 等文件的讀取。這么說(shuō) asar 想辦法掛載就可以隨意閱讀源碼了嗎?不是的。同時(shí) asar 會(huì)提供一套通過(guò)加密方式防止任意解壓,飛書(shū)就是這么做的,直接通過(guò) asar extract 的方式無(wú)法解包出來(lái)。但是由于 node 端和 rust 構(gòu)建的二進(jìn)制文件如果打包到 asar 會(huì)導(dǎo)致無(wú)法鏈接到這些二進(jìn)制文件,因此需要從 asar 中獨(dú)立出來(lái),因而導(dǎo)致有部分 js 文件仍然裸露在外面。不過(guò)即便沒(méi)有任何 js 是暴露的仍然是有辦法爆破的。啊,跑偏了,先不談飛書(shū),今天的主菜是迅雷。是在下想多了,不好意思,迅雷梅川酷子,都攤著在那呢,根本沒(méi)用 asar 打包 / 加密。開(kāi)撬
既然 js 都暴露了,也沒(méi)什么好繞的,直接植入代碼吧。我們都知道 Electron 是有 render 進(jìn)程和 Node 進(jìn)程的,接下來(lái)這一步需要猜猜看哪個(gè)文件是負(fù)責(zé) render 主進(jìn)程的?好吧不用猜,名字都非常人類(lèi)可讀,就 main-renderer(主窗口渲染進(jìn)程)。打開(kāi)找到 html 文件(js 也可以)插入如下這串。雙擊啟動(dòng),調(diào)試窗口出來(lái)了,可以大致看到整體頁(yè)面結(jié)構(gòu)了。然后看了一下,迅雷的懸浮小圓圈和主窗口,分別用一個(gè) BrowserWindow 來(lái)實(shí)現(xiàn)。有趣的是那個(gè)小圓圈窗口其實(shí)并不小,鼠標(biāo)懸停出來(lái)的那個(gè)浮窗也是它的一部分,為了讓小圓圈在屏幕的任何位置都可以看到懸浮窗,所以整個(gè)小圓圈的 BrowserWindow 是大約 4 倍的懸浮窗口大小。獨(dú)立窗口的檢視界面 - 窗口實(shí)際是 4 倍 浮窗大小,灰色部分全都是這個(gè) “小” 浮窗所使用的 BrowserWindow 區(qū)域。一點(diǎn)防御措施
從代碼來(lái)看,nodejs 進(jìn)程只有一個(gè)文件 main.js ,是 webpack 的構(gòu)建產(chǎn)物,看源碼這里的 BrowserWindow 的 webPreference 參數(shù)是把 devTools 禁用掉的,導(dǎo)致直接在命令行里敲 openDevTools 是不能檢視任意窗口的。當(dāng)然了,這里即便是混淆過(guò)了也沒(méi)關(guān)系,畢竟還是明文,把 1 改成 0,把它打開(kāi)就好(雙嘆號(hào) /true/1 啥都行,開(kāi)心就好)。不過(guò)由于迅雷的窗口實(shí)在是太多了,下載彈窗是獨(dú)立窗口,選擇文件夾是獨(dú)立窗口,各種廣告窗口也是,需要改的配置點(diǎn)很多,這里就不列了,總共有 10 個(gè)窗口,這個(gè)配置點(diǎn)按需打開(kāi)(批量替換也行,謹(jǐn)慎操作就行)。# 進(jìn)程結(jié)構(gòu)
呃…… 然后要干啥…… 好像也沒(méi)什么好看的了,代碼是混淆過(guò)的,也沒(méi)有 map 文件。而且前端部分的代碼也沒(méi)什么技術(shù)含量可以說(shuō)的,哪個(gè) web 頁(yè)面都那樣。那看看進(jìn)程分工吧。進(jìn)程樹(shù)
在進(jìn)程樹(shù)里可以看出來(lái),幾乎全部的進(jìn)程都是 Thunder.exe,可見(jiàn) Thunder.exe 作為進(jìn)程派發(fā)入口(類(lèi)似 server 的網(wǎng)關(guān),而并不直接是業(yè)務(wù)本身),用戶(hù)啟動(dòng)的時(shí)候傳參是 --StartType:DesktopIcon,隨后它喚起了兩組進(jìn)程,一組是 Electron main 進(jìn)程,main 進(jìn)程喚起相關(guān)的 renderer;然后是下載的 SDK 服務(wù) DownlaodSDKServer。那么迅雷的進(jìn)程關(guān)系差不多是清楚了:多個(gè) Electron 窗口,對(duì)應(yīng)一個(gè) DownloadSDK。通信方式
那么 Electron 的進(jìn)程(甭管 main-process 還是 renderer-process,統(tǒng)稱(chēng) electron 進(jìn)程) 和 DownloadSDK 是如何通信的呢?進(jìn)程間通信一般都是依靠 ipc 管道的形式來(lái)實(shí)現(xiàn)。不過(guò)迅雷似乎沒(méi)按套路來(lái),它的 DownloadSDK 是控制臺(tái)程序,意味著很有可能是通過(guò) stdio 的方式來(lái)進(jìn)行交互的(后續(xù)證明不是)。通過(guò)觀察進(jìn)程打開(kāi)的句柄,看到很詭異的一個(gè)現(xiàn)象:DownloadSDK 并沒(méi)有打開(kāi)任何 ipc 管道,反倒是前端進(jìn)程打開(kāi)了一個(gè)。前端的 ipc
而 Electron 打開(kāi)的這個(gè) handler 進(jìn)程名稱(chēng),查了一下,竟然全是 Electron 進(jìn)程使用的,而且是所有進(jìn)程。那么不妨做出一個(gè)大膽的推測(cè):前端多窗口之間是靠自建的 ipc 通道實(shí)現(xiàn)的,而 ipc 是 1 server 對(duì) N client 的方式,那么 server 很有可能就是在主窗口上的,也就是前文看到那個(gè)及其明顯的 main-renderer 進(jìn)程,通過(guò)控制臺(tái)查看,確實(shí)如此,nodejs 的 net 方式創(chuàng)建了一個(gè) server,并且將一個(gè)叫做 __xdasIPCServerInstance 的對(duì)象暴露在全局環(huán)境供前端 js 調(diào)用,也即 jsapi。而小窗口并不存在上述 server 實(shí)例,而相對(duì)應(yīng)的有一個(gè) client 實(shí)例。
和 DownloadSDK 的通訊方式
這樣看起來(lái)就很奇葩了,前端進(jìn)程之間是通過(guò)自建的 ipc 管道通信的,但是并沒(méi)有跟 DownloadSDK 有任何通信管道,難道它倆是心有靈犀無(wú)言自通?啊這…… 程序員是唯物主義的!那怎么查它到底是怎么跟前端進(jìn)程交互的呢?既然前端暴露了 server sdk instance,那意味著 DownLoadSDK 肯定是以一種 proxy 的方式暴露在這上面作為 jsapi 的。可以拿【創(chuàng)建一個(gè)下載任務(wù) api】來(lái)順藤摸瓜??戳酥鞔翱诘?server instance 一下果然有這個(gè)方法:createTask ,應(yīng)該就是前端用于創(chuàng)建下載任務(wù)用的 api。chrome 瀏覽器里查代碼不方便,轉(zhuǎn)戰(zhàn) vscode 看源碼,搜索 createTask 這個(gè)函數(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,?[??????????);????????});??????}
沒(méi)跑了,證實(shí)了我前面的猜想,這個(gè) __xdasIPCServerInstance 就是 download sdk 封裝到前端的 proxy。繼續(xù)查,這個(gè) fireTaskEvent 是怎么處理的,閱讀代碼過(guò)程繁瑣按下不提,就看這兩段代碼 (有刪減整理)。
//?片段一
(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 是通過(guò)一個(gè) ThunderHelper.node 的 nodejs addon 模塊來(lái)啟動(dòng)、通信的。我們知道,nodejs 可以通過(guò) ffi 等方式實(shí)現(xiàn)內(nèi)存共享,以達(dá)到兩個(gè)進(jìn)程不需要通過(guò) pipe/sock 等管道就達(dá)到通信的目的。而通過(guò)工具觀察 Thunder.exe 的喚起關(guān)系、句柄關(guān)系,兩者的關(guān)系就更加一目了然了:ELectron 前端進(jìn)程加載 DownloadSDK 進(jìn)程,并且通過(guò) \Sessions\5\BaseNamedObjects\xx@22123720|SendShareMemory 這種內(nèi)存通道來(lái)實(shí)現(xiàn)的通信,句柄一一對(duì)應(yīng)上了。
# 總結(jié)
扒拉了半天,扒完了有點(diǎn)空虛是怎么回事?如果你也有好的開(kāi)源項(xiàng)目,歡迎推薦!
微信號(hào)聯(lián)系:westbrook12000(ps:加好友請(qǐng)備注“開(kāi)源”)
回復(fù)?【小程序】獲取15套小程序源碼【學(xué)習(xí)+實(shí)戰(zhàn)+賺錢(qián)】回復(fù)?【關(guān)閉】學(xué)關(guān)閉微信朋友圈廣告回復(fù)?【實(shí)戰(zhàn)】獲取20套實(shí)戰(zhàn)源碼回復(fù)?【福利】獲取最新微信支付有獎(jiǎng)勵(lì)回復(fù)?【被刪】學(xué)查看你哪個(gè)好友刪除了你巧回復(fù)?【訪(fǎng)客】學(xué)微信查看朋友圈訪(fǎng)客記錄回復(fù)?【python】學(xué)微獲取全套0基礎(chǔ)Python知識(shí)手冊(cè)