前言

  由於移動互聯網的普及,移動端應用已經取代傳統的PC與Web端,覆蓋了幾乎所有的互聯網業務。與此同時,行業內同類產品的競爭、黑色產業的視角也逐漸轉移到了移動端的業務上,直接導致近幾年移動端安全攻防愈演愈烈,在此基礎上衍生出的攻擊手段自然也是層出不窮。攻擊方的技術方案在不斷的攻防對抗中愈發精巧,攻擊行爲的隱蔽性的也在不斷提高。

  移動端應用安全是整個複雜的移動安全生態中的重要層級,也是與企業業務安全性關聯最緊密的層級。雖然系統級的安全漏洞可以造成防範的影響與重大危害,但是一方面由於系統漏洞帶來的威脅發生幾率較小,另一方面在此類漏洞實踐發生時,企業往往能做的也只是佈置臨時的應急策略保障對應的業務不受侵害。所以,系統層級的安全問題我們暫且不論,將目光聚焦於互聯網企業最應該關注的移動應用與業務安全。

  在如今的移動生態環境中,筆者所理解對業務影響較大的安全問題主要分爲三類:

  產品被逆向分析後所衍生的薅羊毛、數據爬蟲與外掛等

  前後端接口鑑權不嚴格造成的數據泄露

  由組件權限、輸入邊界模糊造成的或大或小的安全隱患

  筆者對以上的問題結合自身安全從業經驗進行了一些思考與探索,試圖將如何保障移動業務安全這個複雜的問題逐步拆解。在過去的一年中進行了一些具體性的方案實踐,在此分享一些在探索過程中不成熟的思考與體會。

  正文

  在互聯網企業中移動端業務相比較傳統IT公司的佔比大大增加,業務也更加複雜。爲了能夠及時、全面的的保障不同移動端業務的安全性,筆者將從三個方向入手,分析移動端安全威脅產生的原因與應對方案。

  

  在利用技術孵化產品對抗風險之外,往往容易被技術出身的我們忽略的一點是規範化的移動端安全要求以及科普性的移動端安全培訓。目前除少數公司如Google、Amazon外,絕大部分公司安全風險的內外檢出比例其實都不太樂觀。在這其中也可以分爲兩類公司,其一是產品在發佈時對風險缺乏感知能力、另一種則是發佈時已知漏洞已經進行了修復。雖然產品在發佈時已知漏洞已經修復,但隨着時間推移在線上運行的版本扔會暴露出新的問題,如果對此類問題無法感知終將會成爲威脅業務主體的安全風險。

  安全開發生命週期(Security Development Lifecycle),也就是我們俗稱的SDL可以使產品在生命週期內的設計、代碼和文檔中與安全相關的漏洞減到最少,儘可能的在產品版本回歸前儘早地清除漏洞。科普性的宣導、規範性的制度這些在日常工作中其實對業務研發人員的約束力及其有限,它僅能作爲一種規範去指導與約束研發人員的日常代碼迭代,但人員的不可靠性告訴我們必須有相應的監管手段,確保制度的落地執行,將風險儘可能多的轉化爲可控風險,所以我們需要一種“關門打狗”的能力,否則一切制度皆是空談。

  

  那“關門打狗”的審計節點放入哪裏更合適呢?這是個值得思考的問題。如果的產品是一款白盒審計的掃描器,那麼在Git提交處看起來更合理,但筆者假設一種極端情況:如果公司內部並未統一編碼規範、業務代碼混亂不堪,那麼將審計節點設置在此還是否真的合適。

  我更傾向於審計編譯後的產物,黑盒審計也會有多個位置可以安置,哪怕是確定設置在CI平臺(Continuous integration 持續集成)上具體應用時也需要思考初期的審計顆粒度是以APP還是SDK組件爲單位。在運營思維下思考作爲安全方服務於業務,首先要杜絕的情況就是對業務正常發展造成過度干擾,而上線審計節點後根據自身設定的SLA能否在預期之內不對業務造成困擾的情況下對整個APP進行審計,我覺得是不能的。按照金絲雀模式,我推薦將顆粒度設置到SDK組件層面,這樣做的好處有幾點:首先不會對業務側造成過度干擾,其次在一定規模以上的公司中模塊化開發已成常態化,並且可以預計算式對庫存未審計完成全量清理後,再行將CI平臺的審計節點開啓阻斷。

  自動化安全審計

  對於移動端APP的安全審計完全通過堆人力人工進行審計的方式在我看來是不划算的。對於業務大版本更新時人工審計和Review自動化審計結果是有必要的,但對於絕大部分情況下日常的版本迭代依靠人工進行審計的意義其實並不大,所以針對這種情況自動化監測也就呼之欲出了。快速搭建一個自動化審計平臺的話至少需要五個部分:

  

  Android 平臺的應用我們對dex文件格式中的相關struct進行一些解析,並通過一定的規則進行匹配進而完成部分的審計工作。目前對於靜態分析安卓程序有兩種不同的方式,一類是通過java僞代碼進行自動化審計,另外一種則是通過smali彙編進行自動化審計,這兩種優缺點都很明顯:

  第一種通過一些工具例如dex2jar此類的工具(當然也可以自研,實現難度也不是很大)將dex文件反編譯爲java僞代碼,然後利用設定好的規則通過正則匹配的方式進行“僞白盒審計”得到存在安全風險的name.java文件,並且規則建立相對簡單。聽起來這似乎是一種優雅的實現方式但坑點在於有些函數沒有辦法被dex2jar還原爲java僞代碼,所以這樣的方式終究是會存在一些“漏網之魚”而且單個job的工作時間會因爲轉化java僞代碼而大大延長。

  第二種是通過分析smali彙編,這種方式可以規避掉這樣的問題,通過dex2smali類型的工具將dex文件反編譯爲smali彙編。整體的實現思路和設計一款反彙編引擎類似,在實現規則匹配或者其他功能前首先要完成一個最基本的功能那就是界定函數邊界,值得高興的是我們並不需要像設計反彙編引擎時界定函數邊界那樣麻煩,通過.method和.end method很容易的可以區分出函數代碼塊,然後同樣是通過規則匹配的方式進行“僞黑盒審計”得到存在安全風險的name.smali以及行號,這種檢測的方法相對前種方法顆粒度更細一些,可以快速定位到具體類下的具體方法,並且單個job的工作時間會因爲不需要轉化java僞代碼而大大縮短,但相對來說規則的建立也相對複雜的多。

  iOS 平臺的應用並不能像Android一樣通過中間碼轉化成僞代碼的形式進行審計,不過幸好目前市面上大部分iOS項目的開發語言還是基於objective-c,因其Runtime的實現機制在macho文件格式中類名、方法名、屬性名得以保存,利用正則匹配的方式大部分情況下通過關鍵字符串可以檢測例如不安全的函數、框架等。

  如果單單依靠ipa包中的macho文件和其他文件提取信息來說其實還不能夠滿足靜態審計的需求,有相當一部分的匹配工作是基於ARM彙編的取點特徵碼進行判斷,如果想在這部分做的相對深入些、不侷限於特徵碼匹配的話,其實可以引入一套反彙編引擎來實現更多有趣並且實用的功能是個不錯的選擇。在反彙編引擎的選擇上,推薦使用Capstone這套引擎,這套引擎是從LLVM項目的組件中移植拆分了部分出來,支持的指令集種類是目前已知的反彙編引擎中最爲全面的,可惜的是因爲從LLVM移植出來的原因(項目開發語言從C++移植C語言)項目十分“臃腫”,內存消耗會比一般的引擎高出許多,這也算是美中不足的瑕疵。

  動態審計這裏其實沒有什麼太多可說的地方,普遍的來說可以分爲幾大類:輸出信息、數據存儲、網絡請求、敏感數據使用還有iOS存在後臺快照等等,大家的方式都是Hook鉤住一些大同小異的點比如說通信收發包的API、一些開源框架從這些方面進行入手。

  這裏面可能值得一聊的有兩點,首先在於動態審計得到的數據可以二次利用到一些別的方向,例如網絡請求的request、response可以對接給Web掃描器同步進行掃描一些後端的問題;另外要提的一點就是如果想把自動化審計做好要考慮的點其實不是如何從廣度出發去增加檢測範圍,而是如何增加自動化的深度、可以調用到更多的業務代碼邏輯路徑,普遍的做法都是從UI控件的遍歷入手但這個方法和Hook一樣都存在一個無法規避的問題“埋點越深要處理的消息、事件就越多”,直觀反饋出來就是一次job的週期很慢,目前來說也只能與可用性之間取除一個能接受的中間值而已;最後數據可視化與積累這很重要,這可以有效的縮短應急響應時間。

  代碼層加固

  利用自動化的方式去將常見的風險規避在產品上線前,但安全審計後仍然是會存在一些隱藏很深的安全問題。這部分隱患因爲已經隨着版本上線在用戶設備上運作,我們能做的就是增加利用安全隱患或漏洞的成本,在APP上線後將直接面臨着多方面的風險,除了前面提到的發佈生命週期中未發現的問題外還會面臨着被第三方分析、劫持、利用的風險,如何保護APP的代碼也就成了問題的關鍵。

  

  對於Android加固的衍變大概可以分爲上圖所述的四代,當前其中有一些小插曲比如第一代從最初的解密後落地加載衍變成不落地動態加載、在移動端虛擬化保護在成型前的一二代加固混合加密,這些相對來說更細緻的細節並沒有在上圖中所展示。

  從攻擊的角度來說,第一代加固將Dex文件整體進行加密通過自實現DexClassLoader的方式來進行加載,後來各家廠商進行了升級不將Dex文件解密後落地加密,直接在內存中解密後使用自實現DexClassLoader進行加載,這樣的思路與早期在windows平臺內存加載dll的一致,所以我也更喜歡將這種改進稱爲內存加載。但不論如何這種第一代的加固脫殼相對來說都比較簡單,通過Hook文件讀寫相關函數或者內存dump都可以達到脫殼目的。

  第二代加固後的app文件Dex文件其實可以說是“缺失”的,在運行態時將Dex中已經抽離的方法在動態開闢出來的內存裏映射補全,以此滿足函數調用時可正確調用。這種類型的加固需要在虛擬機處理時Hook部分函數得到具體執行的類與方法再進行內存dump並修正Dex文件結構來達到脫殼。

  我們是從虛擬化開始去設計的一期加固也就是上圖所說的第三代加固產品,這種類型的加固產品需要去設計自己的指令集解釋器,從設計實現的角度來說技術實現成本相對較高。結合上一代代碼抽取的思路,將需要保護的方法從Dex文件中抽取按照自研虛擬機解釋器的指令集規則轉譯成VOPCode(Virtual Operation Code)轉至native層通過java的jni接口進行調用,未被保護的方法將繼續運行在Android原生的虛擬機上執行。

  在我們研發加固的第一期後對自研加固及幾家知名加固進行了一次兼容性測試對比,測試的方式是依託於第三方測試平臺隨機抽取50臺熱門機型進行兼容性測試,具體數據如下:

  

  我們最終得到的測試數據顯示,自研產品及商業加固產品都存在一個對Android 4.x系統的部分機型適配不當的情況(在研發過程當中我們發現在一些機型上即使是原生未加固的apk文件也是會發生無法安裝或者運行失敗的狀況)。爲了解決這一問題提升安全產品的可用性,我們團隊將加固產品向java2c等更加穩定的方向研發,java2c即通過指令集轉譯的方式將java邏輯轉換爲c代碼,再用特定的編譯器和改造過的NDK(Native Development Kit)將其編譯成so文件,使用時通過jni接口進行調用,這樣既可以提高適配的兼容性同時也解決掉了傳統java2c後強度變弱的情況,經過QA的數據對比我們團隊自研加固的性能耗損大概爲業界同強度商業產品的十分之一。

  至於iOS上可以通過LLVM框架打造工具鏈,在前端編譯不同語言的代碼文件做詞法分析以形成抽象語法樹AST,然後將分析好的代碼轉換成LLVM IR(intermediate representation);再通過Pass優化器模塊對中間層IR進行優化混淆,通過一系列的Pass對IR做優化;後端負責將優化好的IR解釋成對應平臺的機器碼。LLVM的優點在於不同的前端語言最終都轉換成同一種的IR最終對應平臺的彙編指令集,且項目結構中的任意部分都可獨立拆分使用。

  因爲App Store上架審覈的關係,我們並不能像在其他平臺做加固、殼一樣進行動態解碼的操作。有一個開源項目叫做OLLVM,是瑞士一所大學實驗室的開源項目,該項目基於LLVM編譯器框架提供一套在編譯期進行代碼混淆的安全編譯器,以增加對逆向工程的難度。我們同樣選擇基於LLVM框架研發iOS加固產品,但因爲OLLVM項目的優秀,許多的逆向工程從業者也會去通過代碼相關文檔(wiki、註釋)去了解其功能的實現原理,所以我們不得不重構了其中虛假控制流 (bugosConstolFlow)、平坦流控制化(Flattening)和等價指令替換(substitution)功能,並新添了字符串加密及一些輔助檢測的PASS(本文寫於去年年中,時至今日我們已經完成了對LLVM後端的改造,不在僅限於前端)。

  除了代碼混淆以外還需要注意的就是符號混淆,我們團隊研發符號混淆的時間早於MT-OLLVM項目,在研發符號混淆的過程當中我們進行了幾次產品升級,最初我們致力於linker後對macho文件進行patch,這種方式沒那麼優雅且修正兼容性問題很耗時最終棄用。恰好在準備升級改造時我及我的團隊正在研發MT-OLLVM項目索性將這個功能合併入MT-OLLVM項目當中,但這種實現方案在產品可用性上會存在兩個問題,在公司中必然會存在大量的內部pod庫用於跨團隊使用,基於編譯期混淆無法支持pod庫並且我們在編譯期無法優雅的修改xlib和storyboard文件。爲了解決這兩個問題增強產品的可用性我們將前兩次的設計思想進行了整合從編譯後的macho文件進行符號提取,再介入預編譯階段重新編譯完成混淆以此來支持pod項目與動態修改xib和storyboard文件。我們在去年推廣產品覆蓋的過程當中反思一些實際投入使用後的發現項目弊端,在較大的項目當中編譯時間翻倍是比較困擾業務方的一個問題,最終我們確定的新的改進方案基於源碼文件做語法分析提取符號,替代了從macho文件中提取的原有方案極大的提升了可用性。

  通信層加固

  代碼混淆技術是對抗逆向分析最直接、有效的方式之一,但往往人們在只對二進制進行加固後,卻忽略了客戶端與服務端之間的通信也是攻擊的薄弱點,從而提供了攻擊行爲的切入點。我所見到的情況中往往都是對二進制進行加固後就覺得高枕無憂了,但這種認知存在“認知盲區”。例如如果分析者所關心的是如何通過APP進行數據爬取,又恰好APP在通信時的request和response中沒有需要算法加密或解密的字段,分析者輕而易舉就可以得到自己想要的東西。不過幸好現在的企業中移動端APP絕大部分重要接口或普通通信接口都會存在一些需要使用算法計算而出的字段,而這部分的保護就取決於APP自身所用加固產品的安全對抗能力是否足以擋掉初級、中級的分析者。

  在客戶端向服務端發起請求時對request進行校驗生成校驗值並添加到request中向後端進行傳輸,位於Nginx層有相應的校驗腳本對從客戶端發送過來的request進行校驗,失敗將終止本次請求、成功則將request放行至業務Server上。

  設計一款產品時我們通常會需要設計一套或多套兜底方案來保障其足以應對一些突發情況,而通信層的加固與代碼層加固之間的關係我覺得同理,即“互爲兜底”。不論多麼優秀的通信層加固也無法解決自身很容易被分析hook的弱點,而不論多麼優秀的代碼層加固也無法完全解決外部對其進行協議分析的隱患,但如果你的公司恰好有兩個優秀的產品那麼讓它們互爲犄角之勢是最好的選擇,同時也將極大的提升移動端產品的安全性。當然也可以增加一些安全對抗、環境檢測以及一些自校驗的相關功能,以此增加更多的防禦策略,更好的保障數據完整、機密的傳輸。

  後記

  攻防之間無絕對,在移動產品的生命週期內哪怕我們經過了基於客戶端做了完善的攻防對抗、上線前的安全檢測、業務運行中基於網絡通信層的完整性校驗、標準化的SDL檢測,但這一系列的防禦可以也只能幫我們擋住八成、九成的攻擊行爲,最後仍然會有一部分經驗老道的攻擊者可以去進行一些不可描述的事情。在互聯網企業移動端業務發展到一定規模後,移動安全方向所輸出的解決方案也需要更加貼合到保障公司業務的層面上,而非將自身及團隊侷限在傳統意義的攻防對抗困境中。所以,在將上文所述的移動安全基礎組件上線完畢後,我們除了在已有領域的深耕之外,會持續進行一些與業務安全緊密貼合的探索。

  合適的時間點會分享基於移動安全的業務安全實踐,下篇見~

  專欄

查看原文 >>
相關文章