Android官方開發文件Training系列課程中文版:Android的安全建議

sahadev發表於2016-10-27

原文地址:http://android.xsoftlab.net/training/articles/security-tips.html

Android系統內建的安全策略可以有效的降低應用程式的安全問題。所以預設建立的應用程式已經包含了一定程度的安全保護措施。

Android所包含的安全策略有:

  • 應用程式沙箱,它可以使APP的資料、程式碼與其它APP相互隔離。
  • 應用程式框架對於常見防護措施的強大實現,比如密碼、許可權以及安全的IPC機制。
  • 一些安全技術的應用,比如ASLR, NX, ProPolice, safe_iop, OpenBSD dlmalloc, OpenBSD calloc, 以及Linux mmap_min_addr,可以降低常見的記憶體管理錯誤風險。
  • 檔案加密系統可以保證裝置在丟失後其中的資料不被盜取。
  • 使用者許可權授予可以限制訪問系統特性以及使用者資料。
  • Android定義的許可權可以控制程式的資料只能在程式的控制範圍之內。

本文中所提及的Android安全策略對於日常開發非常重要,應養成良好的編碼習慣。在日常的編碼行為中使用以下策略可以有效降低無意中造成的安全風險。

資料儲存

應用程式常見的安全問題就是它們的資料是否可被其它應用程式訪問到。Android有3種基本的資料儲存方式。

內部儲存

預設情況下,由APP自己建立的檔案只能由APP本身訪問。這種防護措施由Android框架實現,也足以應對大多數應用的安全情況。

通常應當避免為IPC檔案使用MODE_WORLD_WRITEABLE模式或MODE_WORLD_READABLE模式,因為這些模式沒有對資料的訪問提供限制能力,也沒有在資料格式上做任何控制。如果需要在程式間共享資料,你應當考慮使用ContentProviderContentProvider對APP提供了資料讀寫許可權,也可以按照實際情況動態的授予許可權。

如果需要對一些較為敏感的資料提供額外的保護,你可以使用金鑰對本地檔案進行加密。比如,可以將金鑰放入KeyStore,並以使用者密碼將其保護起來。不過這種方案也不是萬能的:某些破解手段可以監視使用者所輸入的密碼,不過這種方式可以對丟失的裝置進行保護。

外部儲存

在外部儲存器上建立的檔案可被全域性讀寫。因為外部儲存器可被使用者移除,也可以被任何的應用程式修改,所以不應當在外部儲存器上儲存敏感的使用者資料。

正因為可能存在不可信資源,所以從外部儲存器中讀取資料時應當執行輸入驗證。我們強烈的建議不要在外部儲存器上儲存用於動態載入的可執行檔案或者類檔案。如果APP需要從外部儲存器上接收可執行檔案,那麼這些檔案應當是被簽過名的,在載入之前也應當對這些簽名進行校驗。

內容提供者

ContentProvider提供了一種結構化的儲存機制,這種機制可以對所有的應用程式形成一種約束。如果不打算使其它的應用程式訪問你的ContentProvider,那麼只需在程式的清單檔案中標記ContentProvider的屬性android:exported=false即可。否則,設定android:exported=true便意味著其它應用程式可以訪問其中的資料。

在建立ContentProvider時預設允許其它應用程式可以訪問其中的資料,你可以對讀或者寫進行單一許可權限制,也可以同時管理。

如果使用ContentProvider只是為了在自身的APP之間共享資料,更加合理的方式是將android:protectionLevel屬性值設定為”signature”。Signature許可權不需要使用者確認,所以這樣可以提供良好的使用者體驗,以及更多的控制力。

ContentProvider還可以通過android:grantUriPermissions屬性提供更細粒度的訪問能力。訪問者應當在Intent中使用FLAG_GRANT_READ_URI_PERMISSION標誌或FLAG_GRANT_WRITE_URI_PERMISSION標誌進行訪問。這些許可權的範圍可被做更進一步的限制。

當訪問ContentProvider時,使用引數化方法比如query(), update(), 及delete()可以避免不可信來源的SQL隱碼攻擊。

不要對寫入許可權的安全擁有錯誤的認知。考慮一下,寫入許可權允許執行SQL語句,這可能會使一些資料被確認。比如,一名攻擊者可能會在通話記錄中查詢一個指定的電話號碼是否存在,如果這條資料存在,那麼攻擊者只需進行修改便可得知結果。如果ContentProvider的資料結構可被猜測出,那麼寫入許可權就相當於也同時提供了讀取許可權。

使用許可權

因為Android每個應用都處於沙箱之內,所以如果需要共享資源與資料的話,應用必須顯式的宣告自有許可權。

請求許可權

我們推薦應用程式所需的許可權越少越好。這樣可以降低許可權濫用的風險,也更易讓使用者接受,也可以減少黑客的攻擊入口。通常情況下,如果某個許可權不是必要的,那就不要去請求它。

如果應用程式可以做到不需要任何許可權,那麼這是最完美的。比如,如果需要通過訪問裝置資訊的方式來建立唯一識別符號的話,我們更推薦GUID。又比如,相比於將資料儲存於外部儲存器,我們更推薦內部儲存器。

另外在請求許可權時,可以使用< permissions>來保護IPC。IPC對於安全特別敏感、薄弱,並且它會被暴露給其它應用程式,比如ContentProvider。 除了可能需要使用者確認的許可權之外,我們更推薦使用訪問控制,因為這些許可權可能會使使用者感到困惑、不解。比如,可以考慮對個人開發者的應用程式使用”signature“的IPC許可權保護等級。

不要洩露受保護的許可權資料。這種情況僅會發生在通過IPC共享資料時:因為它擁有特殊的許可權,並且對於任何的IPC介面的客戶端也沒有要求出示該許可權。

More details on the potential impacts, and frequency of this type of problem is provided in this research paper published at USENIX:http://www.cs.berkeley.edu/~afelt/felt_usenixsec2011.pdf

建立許可權

通常情況下應當儘量少的使用許可權,少用許可權就意味著更安全。建立許可權這種事情對於大多數應用來說是用不到的,因為系統定義的許可權足以涵蓋所有情況,這些許可權會提供訪問檢查。

如果必須建立許可權,考慮是否可以使用”signature”保護等級完成你的所需任務。”signature”會將自身完全暴露給使用者,只允許具有相同簽名的應用程式訪問。

如果要建立”dangerous”保護等級的許可權,那麼有些東西需要考慮在內:

  • 許可權必須提供一段簡短的描述該許可權安全的字串。
  • 描述許可權的字串必須提供不同地區的語言。
  • 如果許可權的描述含糊不清或者使用者認為這會為其帶來風險,那麼使用者可能會選擇不安裝應用。
  • 如果許可權的生成器沒有安裝的話,應用程式可能會請求許可權。

上面的每一條對於作為程式設計師的你都是一項重要的非技術性挑戰,這樣做可能會使使用者感到困惑,這就是為什麼我們不鼓勵使用”dangerous”許可權等級的原因。

網路安全

網路傳輸本身就存在安全風險,因為它會包括使用者的隱私資料。人們越來越關心移動裝置的隱私問題,尤其是執行網路傳輸時,所以APP需要至始至終以最佳的安全方案保護使用者的資料安全。

使用IP網路

Android所處的網路環境與其它的Linux系統有著很大的不同。主要考慮的就是選用適用於敏感資料傳輸的協議。比如HttpsURLConnection用於安全的WEB通訊。我們推薦在支援HTTPS,因為移動裝置會頻繁的連線到不可信網路,比如公共的Wi-Fi熱點。

經認證,Socket等級的加密通訊可以使用SSLSocket類簡單實現。鑑於Android裝置會頻繁的連線到不安全的無線網路,所以我們強力的建議對所有的應用程式都使用安全的網路實現。

我們也見過一些應用在處理敏感的IPC上使用了localhost網路埠。我們不鼓勵使用這種方式,因為這些介面能被其它應用程式訪問到,應當使用Android的IPC機制。

還有一個常見的問題就是,根證照重複用於驗證從HTTP或者其它網路上下載的不可信資料。這也包括WebView以及HTTP請求響應的輸入驗證。

使用電話網路

SMS(短訊息服務)協議主要用於個人對個人之間的通訊,它並不適用於APP的資料傳輸。由於SMS的限制,我們強烈的推薦使用Google Cloud Messaging(GCM)及IP網路來傳輸伺服器與裝置之間的資料。

要注意,SMS既沒有對網路和裝置進行加密也沒有對其進行相關驗證。尤其是,任何的SMS接收器應當考慮到會有一位惡意的使用者會傳送SMS給你的應用,不要相信沒有驗證過的SMS資料,並用它們來執行一些敏感操作。還有,你應當意識到SMS可能會在網路上被攔截並被篡改。在Android裝置內,SMS訊息是由廣播意圖傳送的,所以這些資料可以被擁有READ_SMS許可權的應用程式讀取到。

輸入驗證

無論程式執行在哪個平臺上,沒有執行充分的輸入驗證都是影響程式安全的主要原因之一。Android提供了平臺等級的安全策略,這可以降低應用程式輸入驗證問題的暴露率,應用程式應當儘可能的使用這些平臺特性。還應該注意:安全性語言的選擇傾向於減少輸入驗證問題的可能性。

如果你在使用原生程式碼,那麼從檔案中讀取的資料、從網路上接收的資料或從IPC接收的資料都可能會引起安全問題。最常見的問題就是緩衝區溢位,使用釋放後的物件等等。Android提供了大量的相關技術手段比如ASLR(Address Space Layout Randomization)、DEP(Data Execution Prevention),這些技術手段可以降低這些錯誤的出現頻率,但是它們不會解決根本的內在問題。你可以謹慎的處理指標或管理緩衝區來防止這些問題的出現。

如果使用SQL資料庫或者ContentProvider進行資料查詢,那麼SQL隱碼攻擊可能是個問題。避免這些問題最好使用引數化的查詢方法。將許可權降為只讀或只寫可以降低SQL隱碼攻擊帶來的相關風險。

如果不能夠使用以上提及的安全建議,那麼我們強烈推薦使用結構良好的資料格式,並在使用時對這些資料格式進行驗證。

處理使用者資料

通常來說,對於使用者的資料安全最好的方法就是儘量少使用可以訪問到敏感資料或者使用者資料的API。如果可以避免儲存或者傳輸這些資訊,那麼就不要傳輸這些資料。可考慮應用程式是否能夠實現這麼一種邏輯:使用資料的雜湊碼或者一種不可逆的資料形態。比如,程式可能會實現這麼一種邏輯:將電子郵件地址的雜湊碼作為一個關鍵的值,這樣可以避免使用原有的電子郵件地址,這可以降低暴露資料的機會,也可以降低攻擊者入侵應用程式的機會。

如果程式需要訪問使用者的個人資訊,比如密碼或者使用者名稱等等,要記住一些元件可能會要求你提供相關的隱私政策,來解釋這些資料的使用與儲存。所以接下來資料訪問實踐可以簡化遵循規則。

你還應當考慮應用程式是否有無意中將使用者的個人資料暴露給了第三方。如果你不清楚這些第三方元件或服務為什麼要使用使用者的資訊,那麼就不要提供給它們。通常降低個人資訊的訪問可以降低這塊的安全風險。

如果必須要訪問敏感資料,那麼評估一下這些資訊是否有必要傳輸到伺服器,或者是否這些操作只需在客戶端執行就可以。考慮凡是涉及到使用者敏感資料的程式碼都在客戶端執行,這樣可以避免不必要的網路傳輸和安全洩漏問題。

還有,要確保沒有將使用者資料通過IPC、全域性可讀寫檔案或者網路Socket暴露給其它應用程式。

如果需要GUID,可以建立一個大的唯一的資料將其儲存下來。不要使用與電話有關的數字,比如電話號碼、IMEI,這些資訊可能會與使用者資訊有關。

寫入裝置日誌時要當心。在Android中,日誌是共享資源,可被具有READ_LOGS許可權的應用讀取到。儘管日誌資料是臨時的,但是不恰當的使用者資訊日誌可能會無意中將資訊洩露給其它應用程式。

WebView的使用

因為WebView會解析像HTML和JavaScript這樣的web內容,所以不正確的使用會帶來常見的安全隱患。Android提供了大量機制來收縮這些潛在問題的範圍,比如通過限制WebView的能力來達到最低的執行需求。

如果程式沒有直接使用到WebView中的JavaScript,那麼不要呼叫setJavaScriptEnabled()。一些示例程式碼可能會使用該方法,所以如果不需要的話,在你的程式中關閉它。預設情況下,WebView不會執行JavaScript,所以不會發生跨站指令碼攻擊。

使用addJavaScriptInterface()需要特別當心,因為它會允許JavaScript呼叫預留給Android原生應用的方法。如果你使用它,要確認所有的Web的相關內容都是可信的。如果不可信的內容允許進入,那麼不可信的 JavaScript 可能會呼叫App內的相關方法。一般我們推薦在APK內包含JavaScript程式碼的時候使用addJavaScriptInterface()。

如果程式通過WebView訪問敏感資料,你可能需要使用clearCache()方法來刪除儲存在本地的所有檔案。我們可以使用HTTP頭部的某些屬性比如no-cache來表示應用程式不應當快取某些特殊的內容。

Android 4.4之前版本的webkit含有大量的安全問題。如果App執行在這些版本上,應當確認WebView所渲染的內容都是可信的。如果應用必須渲染開放的Web內容,考慮實現一個獨有的渲染器,這樣可以及時更新相關的安全補丁。

管理證照

通常來說,我們不推薦頻繁的要求使用者憑證,推薦使用授權令牌。

使用者名稱及密碼通常不應該存在本地。應當使用使用者輸入的使用者名稱及密碼進行初始化驗證,然後使用一個許可權Token進行通訊。

如果一個賬戶需要被多個程式訪問,那麼應當使用AccountManager。如果可能的話,使用AccountManager與服務進行互動,絕不要將密碼存在裝置上。

如果證照只是用作於你建立的應用程式,那麼可以使用checkSignature()方法進行程式訪問驗證。如果只有一個程式使用了證照,那麼KeyStore可能更適合你。

使用密碼

除了資料隔離、檔案系統加密、安全通訊通道等保護手段之外,Android還提供了一系列通過演算法加密的安全保護措施。

通常情況下,Android內建的最高等級的安全實現已經可以支援各種安全情況。如果需要從一個已知的位置接受一個檔案,那麼HTTPS URI已經足夠。如果需要一條安全通道,考慮使用HttpsURLConnectionSSLSocket

如果發現確實需要實現自己的安全協議,不建議自己實現加密演算法。而應當使用已有的加密演算法比如在Cipher中提供的AES加密演算法或RSA加密演算法。

使用安全隨機數生成器SecureRandom來初始化金鑰KeyGenerator。如果不使用由SecureRandom生成的金鑰的話,則會大大減弱加密演算法的健壯性,也更容易遭受線下攻擊。

如果需要將金鑰存在本地以便後續使用,那麼可以使用KeyStore類似的加密機制。

使用程式間通訊

一些App嘗試使用傳統的Linux技術比如網路Socket和共享檔案來實現IPC。我們對此推薦使用Android為IPC實現的系統功能,例如IntentBinderMessengerBroadcastReceiver。Android的IPC機制允許驗證連線到你IPC的程式的身份,並且可以對每個IPC機制設定安全策略。

許多安全元素通過IPC機制共享。如果你的IPC機制的目的不是為了供其他應用程式使用,那麼請將對應元素的android:exported的屬性設定為false。這對於由相同UID的多程式組成的應用很有幫助,或者對後期再開啟該功能也有一定的幫助。

如果IPC的目的是為了可被其他應用訪問,你可以通過使用< permission>元素指定安全策略。如果IPC只是為了在持有相同key的兩個自有應用中使用,那麼android:protectionLevel=”signature”則更為適合。

使用Intent

Intent是非同步IPC的首選機制。這取決於程式的需求,你可能會使用sendBroadcast(), sendOrderedBroadcast()或者顯式Intent來指定應用程式元件。

要注意,由於有序廣播可以被接收方消耗掉,所以這些廣播可能不會被分發給所有的應用程式。如果你傳送了一個廣播,而該廣播必須被指定接收器接收的,那麼必須使用顯式Intent,並且該Intent還需指明廣播接收器的名稱。

傳送Intent的一方可以驗證接收方是否允許非空的許可權方法呼叫。只有含有該許可權的應用才會接收到該Inetnt。如果廣播中的Intent的資料是敏感的,那麼應當考慮這裡所使用的許可權不會被非法的應用程式攔截。在這種情況下,你應當考慮直接呼叫接收器,而不是使用廣播。

**Note:**Intent filters不應當被視作為一種安全特性:因為元件可能會由顯式的Intent調起。你應當在收到Intent的地方執行輸入驗證來確認該Intent是否被格式化為適用於呼叫廣播接收器,服務或者Activity的格式。

使用服務

Service經常被用作支援其他程式的功能。每個Service必須在它的清單檔案中有相應的宣告。

預設情況下,Service不是開放的,也不能夠被其它應用程式調起。然而,如果在Service的宣告中新增了IntentFilter,那麼預設就會被暴露出去。最好的辦法就是顯式的宣告android:exported屬性為你需要的屬性。Service還可以由android:permission屬性保護。如果這麼做了,那麼其它的程式則需要在它們的清單檔案中宣告對應的許可權才可以啟動、停止或者繫結該服務。

Service還可以保護在其許可權內的IPC呼叫,在執行這個呼叫的實現之前呼叫checkCallingPermission()進行必要檢查。我們通常推薦使用在清單檔案中宣告的許可權,因為有很多漏洞會被忽略。

使用Binder及Messenger介面

BinderMessenger是遠端過程呼叫的首要IPC機制。它們提供了一種定義良好的介面:可以進行端對端相互認證。

我們強烈推薦以不需要特定的許可權檢查的方式設計介面。由於BinderMessenger並不是在應用的清單檔案中宣告過的,因此不能對其採用特定許可權。它們的許可權通常來自於對應的Service或Activity所宣告的許可權。如果你建立了一個用於請求驗證或者訪問控制器的介面,那麼這些控制器必須顯式的新增在BinderMessenger的介面中。

如果建立了一個需要訪問控制器的介面,使用checkCallingPermission()驗證呼叫者是否含有所需的許可權。這在訪問服務的遠端代理之前尤其重要,正如你的應用的身份需要傳給其它介面一樣。如果呼叫一個由Service提供的介面,那麼如果你沒有訪問給定服務所需的許可權,那麼bindService()的呼叫可能會失敗。如果呼叫一個由自身APP提供的一個本地的介面,那麼clearCallingIdentity()則可以滿足內部安全檢查的需求。

有關更多執行與服務有關的IPC的相關資訊,請參見Bound Services

使用廣播接收器

BroadcastReceiver用於處理由Intent發起的非同步請求。

預設情況下,接收器可以被任何應用調起。如果你的廣播接收器的作用是給其它程式使用,那麼可能需要對接收器採取一些安全措施:在清單檔案中新增相應的安全許可權。這可以防止沒有正確許可權的應用程式傳送Intent給廣播接收器。

動態載入程式碼

Dalvik是Android的執行時虛擬機器。雖然Dalvik專用於Android,但是其它虛擬機器上的相關安全問題也同樣適用於Android。一般不需要關心與虛擬機器相關的安全問題,因為Android的應用程式執行在安全的沙箱環境中,所以系統上的其它程式訪問不到程式的程式碼或者私有資料。

如果你對更深的虛擬機器安全課題感興趣,那麼推薦熟悉一些現有的有關這一課題的相關文獻。其中最受歡迎的兩個資源如下:
http://www.securingjava.com/toc.html
https://www.owasp.org/index.php/Java_Security_Resources

這篇文件主要關注於Android的特殊之處和與其它虛擬機器環境有什麼不同。對於對其它虛擬機器很有經驗的開發者來說,這裡有兩個很主要的Android的不同之處:

  • 一些虛擬機器,比如JVM或.net執行時,扮演了一個安全邊界的角色,從底層作業系統將程式碼、功能隔離。然而在Android中Dalvik虛擬機器並沒有這樣的功能——應用程式沙箱實現與作業系統層面,所以Dalvik可以與應用的原生程式碼進行互動,而沒有任何的安全限制。
  • 由於移動裝置有限的儲存空間,一些開發者可能需要通過模組化構建應用程式,並使用動態載入技術。如果這麼做,那麼則需要考慮在哪接收應用的邏輯程式碼?又應當將這些程式碼存在哪?不要使用沒有經過驗證的程式碼,比如從不安全的網路資源上或者是外部儲存器中載入的程式碼,因為這些程式碼很有可能會被其它程式篡改。

原生程式碼的安全

一般我們推薦使用Android SDK來進行應用程式開發,而不是使用原生程式碼開發。由原生程式碼構建的程式會更加複雜,也缺少了靈活性,也更容易產生像緩衝區溢位等常見的記憶體洩露錯誤。

因為Android建立於Linux kernel基礎之上,所以如果使用原生程式碼的話,那麼Linux開發中所遇到的安全問題也同樣適用於此。由於Linux安全相關超出了本文的範圍,所以這裡提供了很受歡迎的“Linux和Unix如何安全程式設計”的相關資源,相關地址:http://www.dwheeler.com/secure-programs.

Android與其它大部分Linux環境最大的不同就在於程式沙箱。在Android中,所有的程式都執行在程式沙箱中,也包括那些原生程式碼。在最基本的層面上,對熟悉Linux的開發者來說,不同之處就是Android的每個應用程式都有一個唯一UID,也擁有少量的許可權。如果要使用原生程式碼開發的話,那麼應當對應用的許可權極為了解才對。

PS: 翻譯的相關原始檔皆已開源,開源地址:https://code.csdn.net/u011064099/android-training-chinese-version/tree/master,歡迎fork、star。


相關文章