安卓應用常被用於處理非常敏感的資料。開發者有責任確保使用者提供的資訊不被居心不良者輕易擷取。開放式 Web 應用安全專案(OWASP)[9,10]嘗試著列舉移動應用潛在的安全問題。
其中一些是系統架構師的責任(例如弱伺服器端控制有關的問題),一些是後端開發者的責任(授權檢查相關問題),最後一些就是純粹與移動應用本身有關。本文我們將關注通過安卓開發者努力可以解決的問題。
因此我們將在這裡提出三個潛在的漏洞源:使用網路服務(WS)通訊時的風險、在儲存裝置上儲存資料時潛在的資訊洩漏以及第三方軟體能輕易編輯應用程式的漏洞。
一、安全的網路服務呼叫
對於使用WS的敏感應用,最重要的就是保證使用者分享在後臺的資料是安全的。事實上,如果網路上的請求能被輕易擷取,最安全的應用程式也是毫無用處的。
威脅:中間人攻擊(MITM)
應用程式遭受中間人攻擊有兩個主要的風險。
1.資訊洩漏
如果竊取者控制了使用者使用應用的本地網路,他暗地裡輕易就能擷取到 app 和 WS 的所有通訊。
2.網路服務(WS)模仿
對 WS 有一定認識的人可以阻塞應用呼叫並且提供偽造的回應,這時使用者認為他們的請求已經執行,然而請求根本沒有到達後臺。
測試應用在中間人攻擊時的脆弱性相當簡單:你只需要使用一個代理軟體(例如CharlesProxy[12]),然後建立裝置來使用安裝了這個代理的機器。如果應用不能阻止中間人攻擊,你將能看到它執行的每個請求。現在,想象你的 app 使用者通過“不安全”的網路連線到你的網路服務:竊取者可以毫不費力地將代理安裝在網路路由器上,就能嗅到所有不加密的請求。
攻擊源:TLS/SSL證照鏈
保證通訊安全至少要使用 HTTPS 協議,也就是說使用安全傳輸層協議(TLS)或是它的前身安全套接層協議(SSL)加密的通訊。
然而,如果有必要,這並不是系統需要遵守的唯一條件。為了理解清楚,我們來看看 SSL 協議的工作原理。
SSL 證照(至少)包括三個證照:
- 根證照。這是由證照認證機構(CA)頒發的,也就是一個可以確保整個通訊時安全的值得信任的組織。
- 中間證照。有許多箇中間證照。它們建立終端使用者證照和根證照的連線,是為提供WS的伺服器服務,由根證照籤名的證照。
- 終端使用者證照。終端使用者證照是適用於WS物理伺服器的證照。
安卓 SSL 本地保護:
安卓網路層有一個內建的CA 證照列表(超過一百個,你可以在裝置引數中檢查這個列表)。每個HTTPS網路呼叫需要在證照鏈上有一個CA證照。
然而,沒有辦法確保其餘的鏈適合我們想要連線的伺服器。例如一個竊取者可以向CA購買中間證照實施中間人攻擊。所有的網路事務都將被系統視作無效。這種漏洞非常常見:一個研究表明[1]73%使用HTTPS 協議的應用並沒有用正確的方式檢查證照。
怎樣保證連線的是我們的後臺並且這個連線是安全?
解決上述問題的方式是手動檢查中間證照(適用於特定伺服器)是已知證照。這意味著我們需要將特定的伺服器證照儲存在應用中,可以將它作為常數儲存在資原始檔或直接放在原始碼中來實現。
我們可能會疑惑為什麼需要檢查中間證照而不是終端使用者。這裡有兩個理由,第一,終端使用者證照生存期很短。第二點是安全理由:想象一下,一個黑客完全控制了系統,那他將擁有你的私鑰(伺服器需要私鑰簽名請求)。應用會認為請求是由正確的終端使用者私鑰簽名,並且會允許連線。如果認證在中間伺服器檢查時實現,那就有可能通過聯絡中間CA遠端廢除證照。
SSLSocketFactory 類可以驗證一個 SSL 連線是否安全。建立一個執行中間證照檢查的 SSLSocketFactory,我們需要執行以下步驟。
1.建立一個繼承於 X509TrustManger 的類。這是一個 java.net.ssl 包中的抽象類,用於檢查伺服器端 SSL socket 的合法性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class MyX509TrustManager implements X509TrustManager { private X509Certificate certificate; public MyX509TrustManager (InputStream knownIntermediateCertificate) throws CertificateException { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); certificate = certFactory.generateCertificate(knownIntermediateCertificate); } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // Do nothing. We only want to check server side certificate. } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // Verify that the certificate domain name matches the expected one if (!chain[0].getIssuerDN().equals(certificate.getSubjectDN())) { throw new CertificateException("Parent certificate of server was different than expected signing certificate"); } try { // Verifiy that the certificate key matches the expected one chain[0].verify(certificate.getPublicKey()); // Verify that the certificate has not expired chain[0].checkValidity(); } catch (Exception e) { throw new CertificateException("Parent certificate of server was different than expected signing certificate"); } } @Override public X509Certificate[] getAcceptedIssuers() { // Do nothing return new X509Certificate[0]; } } |
2.設定一個新的預設 SSLSocketFactory。程式碼需要在網路呼叫前執行。
1 2 3 4 5 6 7 8 9 10 11 12 |
TrustManager[] trustManagerArray = new TrustManager[1]; MyTrustManager trustManager = new MyTrustManager(TRUSTED_CERTIFICATE); trustManagerArray[0] = trustManager; final SSLContext sslc; // TLS is the last SSL protocol and is used by all the CA sslc = SSLContext.getInstance("TLS"); // We only need to give a TrustManager list as we don't need to perform client authentification sslc.init(null, trustManagers, null); HttpsURLConnection.setDefaultSSLSocketFactory(sslc.getSocketFactory()); |
證照檢查潛在缺陷
1.中間證照會過期(其生存期大概是10年)。在證照過期前將新的證照新增到白名單可以有效檢測證照的變化。
2.中間證照認證機構會失效。如果中間證照認證機構失效,它所提供的安全機制將會完全無效。事實上,如果黑客保留了中間 CA ,他將能夠偽造證照鏈,擁有和你的證照鏈相同的中間證照。此時他就能實施中間人攻擊。儘管CA理論上是安全的,依然可能發生像2011年Dignotar證照失效的事件。一旦發生這種情況,唯一能做的事情就是改變伺服器上的SSL證照鏈,並且釋出一個內嵌新的中間證照的版本。
3.SSLSocketFactory 認為它的策略適用於應用中的所有網路呼叫。如果sdk是內建的,那就有必要為遠端伺服器的這些sdk嵌入中間證照。存在的不確定性是無法輕易察覺伺服器證照的變化。
動態植入證照會遇到這種問題。應用只允許一個證照(主伺服器一個)並且在應用執行時獲取一個授權中間證照的動態列表,然後將這些證照新增到 SSLContext 的 trust manager中。
總而言之,在大多數情況下,中間檢查機制能夠防禦中間人攻擊。黑客竊聽通訊時,他必須將自己的證照列入證照鏈, TrustManager不能識別這個證照,於是拒絕 HTTPS 連線。
二、裝置上的儲存安全
多虧了SharedPreferences 介面,安卓平臺提供了一種方便的方式儲存引數以及大檔案。儘管儲存在shared preferences中的資料隱藏在隱藏目錄中,如果裝置獲得了root許可權,那就可以輕而易舉地恢復資料。
因此,如果儲存在應用中的是敏感資料,有必要加密shared preferences中的資料。有兩種方式:
1.使用密碼庫加密/解密 SharedPreferences 的 values(最後是key)。有許多形式的 java 密碼庫,例如 javax.crypto、Bouncycastle[2] 和 Concealed[3]。
2.使用提供 SharedPreferences 包裝類的庫。這些密碼庫使用很方便,開發者不需要關心使用什麼演算法。然而,使用這些庫會失去靈活性並且有些並不使用安全演算法。因此,它們並不適合儲存敏感的資料。SecurePreference[4] 是其中一個最常用的提供這種包裝特性的庫。如果你選擇這種方式,你可以用非常直接的方式例項化一個繼承自SharedPreferences 的 SecurePreferences 類:
1 |
SecurePreferences securePreferences = new SecurePreferences(context, "MyPassword", null); |
這兩種方法是基於例如AES(有一個合適的key 大小)的對稱密碼學。帶來的疑問是:我們應該使用哪種key?事實上,如果我們使用一個靜態key,可以通過反編譯應用解密引數。所以,最好的解決方式是在應用啟動時使用使用者輸入的pin碼/通行碼。或者使用 Fingerprint API [15](需要API 23以上版本),可以提供一種安全流暢的認證方式。
不幸的是,這種方法不能滿足每個應用的使用者體驗。例如如果我們想要在pin碼輸入前顯示儲存的資訊,那我們就不能使用安全加密系統。
幸好,安卓提供了一種安全的方式來為一些應用程式/裝置生成特定的key: KeyStore。安卓 KeyStore 是為了允許應用將私鑰放在不能被其他應用的地方,或者是不能通過實質性訪問儲存在裝置上的資料獲取私鑰的地方。
機制非常簡單:首先,執行應用檢查應用相關的私鑰是否存在。如果不存在,就生成一個並儲存在KeyStore 中。如果私鑰存在,它可以成為安全金鑰來解析SharedPreferences 的資料,這多虧了上文描述的演算法。
Obaro Ogbo寫了一篇詳細的文章深刻描繪瞭如何使用 KeyStore 生成私鑰/公鑰對。KeyStore 主要的缺點是隻允許API18以上版本使用。但是有一個相容API14以上版本的補丁庫(這不是“官方”的布丁,所以你必須自己承擔後果)。
因此,我們建議當需要決定優先使用哪種系統時可以參考下面的策略圖:
三、防止應用被原始碼分析和修改
有時,安卓開發者不希望應用被其他人分析,解讀,最後被修改。這種要求有各種理由:
- 我們不希望黑客移除用於阻止非付費使用者使用某些功能的應用鎖。
- 我們開發的敏感應用時的風險是,黑客會將應用修改成所有輸入資訊都會返回給他。儘管應用商店不容易發生這種情況,但是使用者可以去許多其他地方下載到偽造的應用,這些應用會用完全透明的方式偷取他所有的資料。
每個安卓開發者都應該注意,當開發敏感應用時,經驗豐富的人反編譯一個安卓應用是相當簡單的。特別是使用“本地”應用配置的情況。事實上,因為大多數安卓app的特性(使用 Java 位元組碼),反編譯位元組碼後解讀,改正,最後重建一個修改過的應用非常簡單。
這部分我們會強調一些可以規避風險的技術工具和構建原則。同時,我們需要注意,因為應用程式執行在客戶端裝置上,我們並沒有百分之百確信的方法來規避這些風險。
1.有價值的演算法寫在伺服器端
下面是構建指南。如果你的應用程式價值是以演算法為基礎的,你當然不希望有人能輕易解讀、複製並且將你的演算法嵌入自己的應用程式中。此時,最好的解決方式是在伺服器端實現演算法。應用程式只需提供待處理的資料給伺服器,然後獲取演算法的返回值。這種結構的明顯缺點是,在離線狀態下無法使用app 的核心功能。
2.防止WS完全開放
如果應用程式的功能是藉助 WS 獲取資料,你可以傳送在認證階段獲得的session token 或者在每個請求中授予user / password 來保證WS 安全。如果僅僅是在app 引數中使用認證標誌,將這個標誌設定成“always connected”就可以輕鬆修改應用程式程式碼。這麼做的風險是使用者需要定期輸入使用過的id和密碼來延長會話。
3.使用Proguard混淆程式碼
Proguard 是 Java 工程中常用的工具。Proguard 工具執行三個操作:壓縮步驟(移除無用程式碼)、優化步驟(內聯方法、移除無用方法等)、混淆步驟。在最後一個步驟中,Proguard 會重新命名Java 檔案中所有類、屬性、方法名,為了使它們在位元組碼被反編譯後難以辨認。Proguard 當然也確保 JVM 能夠區分不同的編譯元素。
這個工具的有趣之處在於它使反編譯位元組碼可讀性變差了很多。然而,儘管程式碼元素被重新命名,通過逆向工程化應用的方式依然可能猜測出混淆後的方法和屬性的功能。Proguard 也生成了一個 mapping 檔案,可以將混淆的 stacktrace 轉換成可讀狀態[6]。
網上有許多詳細解釋如何配置Proguard 的指導,比如在安卓系統文件中的配置[7]。
4.使用編譯庫
通過Java本地介面(JNI),我們可以使用 C/C++ 語言寫的原生程式碼(已編譯程式碼)並且與Java 程式碼互動。開發安卓程式時會更簡單,因為 NDK 提供了可以在應用程式中使用已編譯程式碼的工具。整體機制很簡單:編譯你的 C/C++程式碼(必須包含標準的JNI 入口點),獲得一個 .so 檔案。這時你就在應用程式工程中包含了庫和java 介面。
編譯庫的主要優勢是,反編譯程式碼可讀性更差,因為 .so 檔案使用本地機器語言而不是java 位元組碼。用 C/C++ 開發應用程式高度敏感的部分(例如最高機密演算法或是安全層)然後與其餘用傳統 Java 編寫的部分互動,這將是一個不錯的實踐(如果實際上很方便)。
使用 NDK 依然有許多缺點:我們必須為應用程式針對的不同型別的硬體結構編譯本地庫,這樣就放棄了產生 crash 時得到適當stacktrace 的可能性,同時它也大大增加了程式碼結構。
總結
在本文,我們建議的解決方法覆蓋了 3 個 OWASP 中排名前十的手機安全問題[9]。像我們介紹中提到的,只有當系統結構是安全的,應用程式才是安全的。一個人可以開發技術上安全的應用,但如果伺服器沒有授權良好的認證系統,所有的努力都是無意義的。同時,確保完美的安全邊界是手機開發者的責任,這篇文章給出了覆蓋安全邊界的解決方法。
參考
[2] http://www.bouncycastle.org/
[4] https://github.com/scottyab/secure-preferences
[5] http://geeknizer.com/decompile-reverse-engineer-android-apk/
[6] http://proguard.sourceforge.net/manual/retrace/examples.html
[7] http://developer.android.com/tools/help/proguard.html
[9] https://www.owasp.org/index.php/OWASP_Mobile_Security_Project#tab=Top_10_Mobile_Risks
[10] https://www.owasp.org/index.php/About_OWASP
[11] http://www.androidauthority.com/use-android-keystore-store-passwords-sensitive-information-623779/
[12] http://www.charlesproxy.com/
[13] https://threatpost.com/final-report-diginotar-hack-shows-total-compromise-ca-servers-103112/77170/
[14] https://github.com/pprados/android-keychain-backport
[15] https://developer.android.com/about/versions/marshmallow/android-6.0.html#fingerprint-authentication
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式