微信小程序前端代碼問題,一段微信小程序代碼
起初在研究對移動網(wǎng)絡(luò)傳輸進行功耗優(yōu)化,在一次意外的監(jiān)聽網(wǎng)絡(luò)傳輸包中截獲了微信小程序的請求包,借此來窺探當(dāng)下前端代碼安全。
0x01 小程序分析
小程序包結(jié)構(gòu)
SegmentNameLengthRemark
HeaderFirstMark1 byte0xBE 固定值
Edition4 bytes0 ->微信分發(fā)到客戶端 1 ->開發(fā)者上傳到微信后臺
IndexInfoLength4 bytes索引段的長度
BodyInfoLength4 bytes數(shù)據(jù)段的長度
LastMark1 byte0xED 固定值
FileCount4 bytes文件數(shù)量
IndexNameLength4 bytes文件名長度
NameNameLength bytes文件名,長度為NameLength
FileOffset4 bytes文件在數(shù)據(jù)段位置
FileSize4 bytes文件大小
LOOP......
DataFiles Package......
包結(jié)構(gòu)非常清晰,分為三個部分:
頭信息,包含一些包的標(biāo)識,版本定義等,包含了三個冗余字段 --- 索引段和數(shù)據(jù)段的長度應(yīng)該是用于做校驗,但實質(zhì)上沒有用(設(shè)計者覺得需要設(shè)計一些冗余字段來確保設(shè)計的完整性,防止解析的時候溢出,但實際工程實踐中并沒有起到相應(yīng)的作用),文件數(shù)量應(yīng)該是用于簡化包解析過程,實際上知道了索引段長度或數(shù)據(jù)段長度中任何一個皆可推算出文件數(shù)量。
索引段,包含文件的元信息 --- 文件名以及文件位置(通過FileOffset和FileSize定位數(shù)據(jù)段中的文件)。如果從精簡包的大小的角度來看,F(xiàn)ileOffset和FileSize只需一個存在即可,但是這樣解析包的難度就大大增加了,還是以工程實踐為主。
數(shù)據(jù)段,將所有的文件羅列在一起。
由此可見,數(shù)據(jù)完全沒有經(jīng)過壓縮或者加密,連包的簽名信息也沒有。這導(dǎo)致只能在制品流程上進行嚴(yán)格控制,例如在開發(fā)者上傳代碼過程中需要授信,必須經(jīng)過審查,也一定得由授信平臺進行代碼分發(fā)等。這些都無關(guān)風(fēng)月,畢竟App Store就是這種模式,但是......
如何拆解這種自定義文件格式呢?
對多個相同格式的文件進行對比,對大體結(jié)構(gòu)有宏觀的感覺,很容易發(fā)現(xiàn)一些固定的字段以及一些結(jié)構(gòu)的長度。對于像小程序這種有軟件本體的例子,還可以通過微量修改來觀察文件的變化來找到文件結(jié)構(gòu)和意義。
觀察特殊形式,首先英文的字符串是很明顯的,一般hex編輯器都自帶字符串化窗口,如果發(fā)現(xiàn)常見的字符串,就可以繼續(xù)去尋找字符串的邊界,字符串在二進制文件里有兩種儲存方式:一種是不記錄字符串的長度,讀取字符串到0x00位置,另一種一定在某一個地方儲存了這個字符串的長度,因此一旦得知了該字符串的內(nèi)容,搜索該長度字段即可獲取更多的信息。其次一些文件頭也非常顯眼,例如PNG、ZIP等通用標(biāo)準(zhǔn)文件格式都有固定的文件頭,在小程序的自定義格式中很容易發(fā)現(xiàn)一些png、jpg等資源的文件頭,因此可以定位數(shù)據(jù)區(qū)的位置。
對特定區(qū)域的二進制進行推理猜測,一般來說二進制文件里需要儲存大量的offset和size的數(shù)據(jù)作為數(shù)據(jù)段的索引。offset相當(dāng)于一個指針 - 索引文件在數(shù)據(jù)段的位置,工程實踐中,大部分儲存了offset的地方也會儲存size字段,畢竟在解析文件的時候會方便很多,也可以防止校驗數(shù)據(jù)出現(xiàn)指針越界。因此,一旦確認(rèn)了文件中的數(shù)據(jù)段,就可以通過它的位置(offset)和大?。╯ize)的實際數(shù)據(jù)進行搜索,逆向找到指向它的數(shù)據(jù)位置,并且繼續(xù)逆向直到解析完整的文件。另外,如果要考慮設(shè)計的完備性,需要在二進制文件中加入一些冗余字段進行校驗或者糾錯,例如CheckSum、CRC32、Alder32、MD5、ECC等,這些通過hex編輯器很容易計算并發(fā)現(xiàn)。小程序中FileCount的字段,這完全是為了工程實踐考慮的,在小程序中并沒有出現(xiàn)這類的計算值,這是可能是因為小程序為了簡單設(shè)計考慮,一旦發(fā)現(xiàn)包體被篡改或損壞就直接丟棄。
其實拆解小程序這種格式并不需要花費特別多的精力,因為其格式比較簡單,而且從下圖流程上來說,后綴為wx的二進制格式很可能與wxapkg格式是同源的。
從開發(fā)者工具的代碼中的 pack 很容易發(fā)現(xiàn)一些對wx格式封裝的痕跡,只不過其中 unpack 的代碼被隱去了。通過實際的分析發(fā)現(xiàn)(wxapkg文件可以通過截獲網(wǎng)絡(luò)包請求獲得或者在本地的微信appbrand目錄下可以發(fā)現(xiàn)),wxapkg格式就是將wx格式進行了轉(zhuǎn)化:Wxml -> Html、 Wxml -> JS、Wxss -> Css,其二進制格式跟后綴名為wx二進制格式完全一致。我寫了兩個版本的解析二進制包的代碼( Java版本傳送門, python版本傳送門),其實非常簡單,根據(jù)小程序包結(jié)構(gòu)一步一步解析,基本上沒啥難度。但如果要將Html -> Wxml, JS -> Wxml, Css -> Wxss進行還原,其中JS -> Wxml的過程中需要將if語句轉(zhuǎn)變成wx-if標(biāo)簽、for語句轉(zhuǎn)化成wx-for標(biāo)簽有點麻煩,需要對解析包后的 page-frame.html 中 JS 代碼進行修改,修改細節(jié)太多就不再詳說了,總之微信小程序的代碼沒有經(jīng)過額外的保護措施,比較容易進行還原。
(PS:暴露一下微信小程序未公開的API, openUrl - 在小程序中打開外部網(wǎng)頁; getGroupInfo - 獲得群的名稱,群內(nèi)成員的昵稱等數(shù)據(jù); getOpenidToken - 獲得用戶openid;這些權(quán)限微信應(yīng)該是沒有準(zhǔn)備開放的。每次在進入小程序時,客戶端都需要先去請求該小程序的元數(shù)據(jù),例如應(yīng)用名、版本號、一些權(quán)限列表、代碼包下載地址等描述信息,修改這些元數(shù)據(jù)可以獲得相應(yīng)的權(quán)限,小程序的關(guān)鍵信息完全由后臺控制進行配置,另外小程序的本地文件存儲采用HASH映射機制進行文件定位,文件存儲在外部存儲,本身通過自定義算法實現(xiàn)完整性校驗 - 首先,小程序最終存儲的文件名是:對稱加密(文件流內(nèi)容Alder32校驗和 | 原始文件名)生成的,最終文件名和文件內(nèi)容會通過自校驗判斷完整性;其次,本地緩存是通過HASH映射查找文件。所以即使能破解文件名和文件內(nèi)容,繞過文件自身簽名校驗,篡改為攻擊者的偽造文件,小程序APP也無法映射到該偽造文件進行使用。)
0x02 前端代碼安全
由上可見,微信并沒有在代碼安全上進行過多的考慮。這導(dǎo)致需要在應(yīng)用審核過程中花費比較多的功夫,不然作品太容易被復(fù)制竄改,以至于會失去渠道先機,這對流量是致命打擊。由于歷史原因,前端的代碼安全技術(shù)發(fā)展的比較緩慢,相比其他被編譯成二進制的應(yīng)用,前端這種純文本應(yīng)用,太容易被辨識與竄改。
對前端代碼進行保護的目的在于讓機器容易識別相關(guān)的指令,而使人難以理解代碼的邏輯,但往往在對前端代碼進行保護過程中,很難既兼顧指令效率又能使可讀性降低。因此,常常需要在現(xiàn)有的代碼中增加一些額外的驗證邏輯,例如一些增加無效的代碼進行混淆、采用守護代碼保護業(yè)務(wù)代碼不能在其他的域名下正常運行、增加一些防止調(diào)試跟蹤的斷點等,這些措施都是使得破解代碼時人工成本增加,從而增加代碼的安全性。
下面提供一些能夠增加前端代碼安全性的策略:
1. 精簡(minify)
這是最簡單且無害的方法,精簡代碼能減少代碼體積,從而減小數(shù)據(jù)傳輸?shù)呢摵?,同時也能降低代碼的可讀性。在小程序開發(fā)者工具中也提供該選項。對Java代碼進行精簡大致可以從以下幾個方面入手:
刪除注釋,刪除無意義或者多余的空白,刪除可以省略的符號
刪除一些沒有調(diào)用的代碼(Dead code),對函數(shù)進行精簡(三元運算符?:、字符串操作、對象函數(shù)、對象繼承、函數(shù)引用、無名函數(shù)、遞歸函數(shù))
將變量名進行簡化,將零散的變量聲明合并,縮短語句
......
常用的工具有很多: YUI Compressor 、 UglifyJS 、 Google Closure Compiler 、 JS Packer 、 JS Min ...
使用工具對代碼進行精簡時需要注意:1. 最好備份原始代碼,方便調(diào)試與后期修改。 2. 用于調(diào)試精簡代碼時保存的sourcemap,在線上應(yīng)該刪除。 3.編寫代碼的時候應(yīng)該嚴(yán)格按照規(guī)范,最好使用lint工具對代碼進行檢查,精簡代碼后導(dǎo)致代碼不可用時,調(diào)試非常困難。
這種簡單的方法雖然很實用,但是也很容易被還原出源代碼,使用一些代碼格式化工具可以補齊被刪除的空格、換行、符號等,例如 jsbeautifier 。另外2015年就有相關(guān)的研究,從大量的代碼中推測出被精簡的代碼,因為人寫代碼總有固定的范式,所寫的代碼相似性都非常的高,如果用統(tǒng)計方式就很容易反推源代碼,蘇黎世聯(lián)邦理工大學(xué)Martin Vechev教授領(lǐng)帶下開發(fā)的工具 JSNice 就是一款運用條件隨機場(Conditional Random Fields)機器學(xué)習(xí)和程序分析方法來還原Java代碼利器,利用大量的開源代碼,去學(xué)習(xí)命名和類型的規(guī)律。JSNice可以用于以下不同的方面:反精簡的Java代碼、對當(dāng)前的代碼提供更多的更有意義的變量名、自動化程序的注釋等。相關(guān)論文傳送門后臺代碼傳送門
2. 混淆(obfuscation)
混淆可以減低代碼的可讀性,防止被輕易追蹤出程序邏輯。常見的混淆方法有如下幾種:
通過編碼混淆代碼,這篇文章《Java常用混淆方法》里面介紹了很多不錯的編碼加密方法。但是這些方法有個明顯缺點,增加代碼體積,而且編碼加密都是可逆的。
將標(biāo)識符混淆和控制邏輯混淆(分離靜態(tài)資源、打亂控制流、增加無義的代碼等),例如 aaencode 和 jjencode 。
標(biāo)識符混淆的方法有多種,有些與編碼混淆代碼方法有些重疊,常用方法有哈希函數(shù)命名、標(biāo)識符交換和重載歸納等。哈希函數(shù)命名是簡單地將原來標(biāo)識符的字符串替換成該字符串的哈希值,這樣標(biāo)識符的字符串就與軟件代碼不相關(guān)了;標(biāo)識符交換是指先收集軟件代碼中所有的標(biāo)識符字符串,然后再隨機地分配給不同的標(biāo)識符,該方法不易被攻擊者察覺;重載歸納是指利用高級編程語言命名規(guī)則中的一些特點,例如在不同的命名空間中變量名可以相同,使代碼中不同的標(biāo)識符盡量使用相同的字符串,增加攻擊者對軟件源代碼的理解難度。
控制混淆是改變程序的執(zhí)行流程,從而打斷逆向分析人員的跟蹤思路,達到保護軟件的目的。一般采用的技術(shù)有插入指令、偽裝條件語句、斷點等。偽裝條件語句是當(dāng)程序順序執(zhí)行從A到B,混淆后在A和B之間加入條件判斷,使A執(zhí)行完后輸出TRUE或FALSE,但不論怎么輸出,B一定會執(zhí)行??刂苹煜捎帽容^多的還有模糊謂詞、內(nèi)嵌外聯(lián)、打破順序等方法。模糊謂詞是利用消息不對稱的原理,在加入模糊謂詞時其值對混淆者是已知的,而對反混淆者卻很難推知。所以加入后將干擾反匯編者對值的分析。模糊謂詞的使用一般是插入一些死的或不相關(guān)的代碼(bogus code),或者是插入在循環(huán)或分支語句中,打斷程序執(zhí)行流程。內(nèi)嵌(in-line)是將一小段程序嵌入到被調(diào)用的每一個程序點,外聯(lián)(out-line)是將沒有任何邏輯聯(lián)系的一段代碼抽象成一段可被多次調(diào)用的程序。打破順序是指打破程序的局部相關(guān)性。由于程序員往往傾向于把相關(guān)代碼放在一起,通過打破順序改變程序空間結(jié)構(gòu),將加大破解者的思維跳躍。
另外還有些混淆方式是專門針對于反混淆工具設(shè)計的,這就需要去仔細分析反混淆工具的原理,在一些特定的地方插入代碼使反混淆器進入死循環(huán)或者異常跳出。
一般來說,提供代碼精簡的工具都會提供一些混淆的方法,除此之外,比較知名的商業(yè)工具有 jasob 、 jscrambler ,一般越商業(yè)的越難被反混淆,然而這些高級的代碼混淆也常會被用于隱藏應(yīng)用中的惡意代碼。對惡意代碼進行混淆是為了躲避殺毒軟件的檢測,這些代碼在被混淆擴充后會難以被識別為惡意軟件。相應(yīng)的也有一些反混淆的工具出現(xiàn),例如上面提到的 JSNice 工具能夠?qū)煜拇a進行推理,另外反混淆工具 JSDetox 專門針對一些混淆方法做過專門的支持。反混淆一直是一項體力活,根據(jù)不同的混淆策略需要進行反推演算,這就是一場攻與防的游戲罷了。
3. 加密(encryption)
加密的關(guān)鍵思想在于將需要執(zhí)行的代碼進行一次編碼,在執(zhí)行的時候還原出瀏覽器可執(zhí)行的合法的腳本,在某個角度也可以認(rèn)為是一種混淆的形式,看上去和可執(zhí)行文件的加殼有點類似。Java提供了將字符串當(dāng)做代碼執(zhí)行(evaluate)的能力,可以通過 constructor 、 eval 、 、 setInterval 、 Worker 、 DOM event 等將字符串傳遞給JS引擎進行解析執(zhí)行,由于有些需要用到 eval 函數(shù),會導(dǎo)致代碼性能會減低。以Worker 執(zhí)行舉例:
var URL = window.URL || window.webkitURL; var Blob = window.Blob || window.webkitBlob; var blobURL = URL.createObjectURL( new Blob(['console.log("Hello World!")'], {type: 'application/java'})
); new Worker(blobURL);
URL.revokeObjectURL(blobURL);
有以下常見的幾種加密方法:
base64編碼,一種簡單的方法就是將代碼轉(zhuǎn)化成base64編碼,然后通過 atob 以及 eval 進行解碼然后運行,另外一種采用 base62編碼技術(shù)更為常見,其最明顯的特征是生成的代碼以(p,a,c,k,e,r)) 開頭。無論代碼如何進行變形,其最終都要調(diào)用一次 eval等函數(shù)。解密的方法不需要對其算法做任何分析,只需要簡單地找到這個最終的調(diào)用,改為 console.log 或者其他方式,將程序解碼后的結(jié)果按照字符串輸出即可。
(PS: 從算法上看, packer 是一種base64編碼字典壓縮策略,packer的base64編碼的壓縮率很高,精簡后代碼依然可以減少50%體積以上,因為帶有解壓器和字符表,越長的代碼理論上壓縮率更高,想要了解詳情可以看看這篇文章《Packer,你對我的JS做了什么!》 )
使用復(fù)雜化表達式,在Java中可以把原本簡單的字面量(Literal)、成員訪問(Member)、函數(shù)調(diào)用(Call)等代碼片段變得難以閱讀。例如這個方法僅用+!等符號就足以實現(xiàn)幾乎任意Java代碼。在 JS 代碼中可以找到許多這樣互逆的運算,通過使用隨機生成的方式將其組合使用,可以把簡單的表達式無限復(fù)雜化。
隱寫術(shù),將 JS 代碼隱藏到了特定的介質(zhì)當(dāng)中。如通過最低有效位(LSB)算法嵌入到圖片的 RGB 通道、隱藏在圖片 EXIF 元數(shù)據(jù)、隱藏在 HTML 空白字符、放到css文件中(利用content樣式能存放字符串的特性)等。比如一張圖片黑掉你:在圖片中嵌入惡意程序,這個正是使用了最低有效位平面算法,結(jié)合HTML5的canvas或者處理二進制數(shù)據(jù)的TypeArray,抽取出載體中隱藏的數(shù)據(jù)(如代碼)。隱寫的方式同樣需要解碼程序和動態(tài)執(zhí)行,所以破解的方式和前者相同,在瀏覽器上下文中劫持替換關(guān)鍵函數(shù)調(diào)用的行為,改為文本輸出即可得到載體中隱藏的代碼。
混合加密,單個方法容易被破解,但組合起來就不會那么容易了,破解成本也會指數(shù)增長,例如 jdists 采用組合加密和嵌套加密的方式。
這些加密的方式都很容易通過對源代碼進行詞法分析、語法分析進行還原,首先將代碼的字符串轉(zhuǎn)換為抽象語法樹(Abstract Syntax Tree, AST)的數(shù)據(jù)形式,然后從語法樹的根節(jié)點開始,使用深度優(yōu)先遍歷整棵樹的所有節(jié)點,根據(jù)節(jié)點上分析出來的指令逐個執(zhí)行,直到腳本結(jié)束返回結(jié)果。這種方法大多數(shù)用于對代碼進行優(yōu)化,例如最近Facebook開源了代碼優(yōu)化工具 Prepack,可以自動消除冗余代碼,降低打包體積和執(zhí)行時間,基本上就可以用來將這些加密的字符串進行還原,畢竟編碼這些字符串都是可以通過詞法語法推測出來的。
4. 編譯(compile)
Github上有一份清單記錄了所有Java擴展語言,這些語言都可以通過編譯器轉(zhuǎn)化為Java語言,這也是前端發(fā)展的一個趨勢,原來寫的html,css,Java已經(jīng)開始變成了一個“中間語言”,而且越來越多的團隊也有了自己的一套前端編譯系統(tǒng)。Java越來越像Web中的匯編語言,特別是近些年Node的普及,讓前端變得越來越復(fù)雜,大量前端框架的出現(xiàn),使得Java代碼可以通過手工編寫,也可以從另一種語言編譯而來,詳情參考幾年前Brendan Eich(Java之父)、Douglas Crockford(JSON之父),還有Mike Shaver(Mozilla技術(shù)副總裁)的郵件。通過編譯后的Java代碼越方便機器的理解,降低可讀性,在某一定角度上講,這也不愧為一種代碼保護措施。據(jù)說幾大科技巨頭正在醞釀給瀏覽器應(yīng)用設(shè)計一款通用的字節(jié)碼標(biāo)準(zhǔn)——WebAssembly ,一旦這個設(shè)想得以實現(xiàn),代碼保護將可以引入真正意義上的“加殼”或者虛擬機保護,對抗技術(shù)又將提升到一個新的臺階。目前在桌面端,使用 NW 框架可以Java應(yīng)用程序的源代碼可以被編譯為本地代碼,在運行時通過NW動態(tài)還原出源代碼,但是這種方法目前會比正常的JS代碼慢30%左右。
5. 防止被調(diào)試
對代碼進行破解分析無非分為靜態(tài)分析和動態(tài)分析,如果對代碼進行混淆加密等形式操作,那么靜態(tài)分析就很麻煩了,對代碼調(diào)試跟蹤分析可以對代碼整體邏輯有一個宏觀的把控。例如首先判斷瀏覽器是否開啟了開發(fā)者工具控制臺(目前最完美的解決方案傳送門),如果檢測出控制臺開啟則堵塞Java執(zhí)行或讓代碼異常跳出。另外Android 4.4及以上和iOS是支持webkit remote debug的,因此應(yīng)該在debug模式下,設(shè)置代碼可以被debug,release模式下,禁止debug以保護數(shù)據(jù)安全。
6. 前后端協(xié)作
首先得強調(diào)的事情是不要在前端放敏感數(shù)據(jù),前端容易破解,因此需要配合后端進行安全防護,例如微信小程序的登錄,必須利用授信的后端配合才能完成此項功能,另外在小程序的網(wǎng)絡(luò)請求中的referer是不可以設(shè)置的,其中 {appid} 為小程序的appid,通過驗證appid字段可以抵御一些直接的山寨,其次就是加快迭代速度更改代碼保護策略,這樣可以讓之前的分析失效,增加破解的成本。
以上就是對當(dāng)前前端代碼安全進行的探索。
更多關(guān)于微信小程序開發(fā)內(nèi)容,可以多關(guān)注hi小程序。
第二部分:如何開通一個小商店