Android應用安全防護的點點滴滴

yukuoyuan發表於2019-03-01

前言

facebook資料洩露,國內某公司資訊洩露,國內某酒店開房記錄洩露...,近年來,資訊保安越來越讓人堪憂,作為移動開發人員,也是憂心忡忡,在經理的指示下,開始Android資訊保安防護的旅程

一. webView

在現在安卓應用原生開發中,為了追求開發的效率以及移植的便利性,使用WebView作為業務內容展示與互動的主要載體是個不錯的折中方案。

那麼在這種Hybrid(混合式) App中,難免就會遇到頁面JS需要與Java相互呼叫,呼叫Java方法去做那部分網頁JS不能完成的功能。網上的方法可以告訴我們這個時候我們可以使用addjavascriptInterface來注入原生介面到JS中,但是在安卓4.2以下的系統中,這種方案卻我們的應用帶來了很大的安全風險。攻擊者如果在頁面執行一些非法的JS(誘導使用者開啟一些釣魚網站以進入風險頁面),極有可能反彈拿到使用者手機的shell許可權。接下來攻擊者就可以在後臺默默安裝木馬,完全洞穿使用者的手機

==那麼如何避免呢?我們從以下幾個方面進行優化webView.==

1. 謹慎支援JS功能,避免不必要的麻煩

提到對於Android4.2以下的JS任意程式碼執行漏洞 , 還有包括證照版本過低不安全的因素 . 對於低版本 , 對於2019年未來來說,建議可以放棄支援.

2. 請使用https的連結

  • 第一是安全;
  • 第二是避免被噁心的運營商劫持,插入廣告,影響使用者體驗

我覺得這個很有必要,不僅僅是因為安全,包括微信公眾號,googleplay 都在強制要求開發者必須使用https

3. 處理file協議安全漏洞

//若不需支援,則直接禁止 file 協議
setAllowFileAccess(false);
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
複製程式碼

4. 密碼明文儲存漏洞

由於webView預設開啟密碼儲存功能,所以在使用者輸入密碼時,會彈出提示框,詢問使用者是否儲存。若選擇儲存,則密碼會以明文形式儲存到 ==/data/data/com.package.name/databases/webview.db==中,這樣就有被盜取密碼的危險。所以我們應該禁止網頁儲存密碼,設定

WebSettings.setSavePassword(false)
複製程式碼

5. 開啟安全瀏覽模式

<manifest> 
		<meta-data
    android:name="android.webkit.WebView.EnableSafeBrowsing"
    android:value="true" />
  		<application> ... </application>
 </manifest>

複製程式碼

啟用安全瀏覽模式後,WebView 將參考安全瀏覽的惡意軟體和釣魚網站資料庫檢查訪問的 URL ,在使用者開啟之前給予危險提示,體驗類似於Chrome瀏覽器 . 如果遇到不安全網站,會有如圖所示情況 Android應用安全防護的點點滴滴

二. 通訊安全

這個也是重中之重,大多數的資料安全都是通過網路攻擊造成的,那麼我們如何去避免呢?

1. 使用HTTPS協議

HTTPS的主要思想是在不安全的網路上建立一安全通道,並可在使用適當的加密包和伺服器證照可被驗證且可被信任時,對竊聽和中間人攻擊提供合理的防護。可以說是非常基礎的安全防護級別了。

2. 驗證證照.

android中實現Https基本就這兩種方式,一種是不驗證證照,一種是有驗證證照(預防釣魚)。

第二種驗證證照稍微複雜一點,這種方式也只能簡單的防止釣魚,不能有效的防止釣魚。防止釣魚最終還是靠使用者分辨,在正規渠道下載應用。

==但是如果把證照放在apk中也是一件很危險的事情,因為現在的反編譯技術不得不服,所以目前覺得最好的方式,就是放在.so檔案中.==可以進一步的降低風險 .

3. 通訊資料儘量不使用明文形式.

前後端進行自定義演算法進行加密,md5 base64 AES RSA 等等,==演算法推薦.so和java相互呼叫的形式==.

4. 防抓包

System.getProperty("http.proxyHost");  
System.getProperty("http.proxyPort");  

複製程式碼

正常這兩行程式碼獲取的是null,如果返回不為空,就是掛代理了,那麼就可以考慮是否不給資料了

5. token等等進一步的防護措施.

道高一尺,魔高一丈,未來的路還會很長.

三. 資料儲存安全

有了資料就得存放,如果存放,就會被別人發現.所以得存好嘍

1. 隱藏資料儲存位置

在Andoid裝置中,以'.'開標頭檔案或者資料夾是不可見的,並且也可以進行讀寫操作.如果隱藏了儲存了檔案位置,就可以避免使用者誤操作和被發現的機會.

2. 儲存內容不要使用明文

就算被找到,內容如果以密文的形式,也會降低被洩漏的風險

3. 程式碼中禁止硬編碼重要資訊內容

硬編碼很容易被反編譯找到.建議儲存到.so中(雖然.so也可以被破解,但是相較於java更安全點)

4. 儲存位置推薦

儘量儲存到手機內部儲存上,不要儲存到外部儲存卡上(因為手機內部儲存只對相應的應用開放,外部儲存對所有的應用都開放)

四. 元件安全

規範安卓標準元件(Activity、Service、Receiver、Provider)的訪問許可權

1. 設定許可權開放屬性:android:exported=["true" | "false"]

exported屬性為四大元件共有屬性,其中含義大同小異。預設值由其包含 ==== 與否決定。若未包含====,預設為“false”,若存在至少一個====,則預設值為“true”。

  • 在Activity中:

表示是否允許外部應用元件啟動。若為“false”,則 Activity 只能由同一應用或同一使用者 ID 的不同應用啟動。

  • 在Service中:

表示是否允許外部應用元件呼叫服務或與其進行互動。若為“false”,則 Activity 只能由同一應用或同一使用者 ID 的不同應用啟動。

  • 在Receiver中:

表示是否可以接收來自其應用程式之外的訊息,如來自系統或或其他應用的廣播。若為“ false”,則廣播接收器只能接收具有相同使用者ID的相同應用程式或應用程式的元件傳送的訊息。

  • 在Provider中:

表示是否允許其他應用程式訪問內容提供器。若為“false”,則具有與Provider相同的使用者ID(UID)的應用程式才能訪問它。如果需要給其他應用程式提供內容,則應當限定讀寫許可權。

2. 配置自定義許可權

自定義許可權,限制自己的元件訪問,詳情看Android自定義許可權與使用

3. 使用更加安全高效的LocalBroadcastManager

區別基於Binder實現的BroadcastReceiver,LocalBroadcastManager 是基於Handler實現的,擁有更高的效率與安全性。安全性主要體現在資料僅限於應用內部傳輸,避免廣播被攔截、偽造、篡改的風險。簡單瞭解下用法:

(1).自定義BroadcastReceiver

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        //Do SomeThing Here
    }
}
複製程式碼

(2).註冊Receive

MyReceiver myReceiver = new MyReceiver();
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this);
IntentFilter filter = new IntentFilter();
filter.addAction("MY_ACTION");
localBroadcastManager.registerReceiver(myReceiver, filter);
複製程式碼

(3).傳送本地廣播

Bundle bundle = new Bundle();
bundle.putParcelable("DATA", content);
Intent intent = new Intent();
intent.setAction("MY_ACTION");
intent.putExtras(bundle);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
複製程式碼

(4).在Activity銷燬時取消註冊

@Override
protected void onDestroy() {
    super.onDestroy();
    localBroadcastManager.unregisterReceiver(myReceiver);
}
複製程式碼

4. Application相關屬性配置

(1). debugable屬性 android:debuggable=["true" | "false"]

很多人說要在釋出的時候手動設定該值為false,其實根據官方文件說明,預設值就是false。

(2). allowBackup屬性 android:allowBackup=["true" | "false"]

設定是否支援備份,預設值為true,應當慎重支援該屬性,避免應用內資料通過備份造成的洩漏問題。

5. 自定義鍵盤.

對於操作密碼等危險輸入的情況,可以考慮自定義鍵盤處理,防止通過鍵盤被盜用密碼.

五. 其他防護措施

多做一層安全措施,少一點風險.

1. 程式碼安全

  • 加固 360加固寶 等等
  • 混淆

2. 控制日誌輸出

自定義工具類,不要線上上出現敏感的資訊

3. 漏洞檢測工具

各種雲測平臺進行測試

  • testin 雲測
  • 阿里雲測 免費的
  • 騰訊的 wetest

4. 防止模擬器

判斷手機是否包含藍芽等模組,一些資訊是否跟手機真機不一致等.防止通過模擬器篡改資訊

5. 二次打包

通過判斷簽名資訊,防止,被二次打包,除錯應用資訊

6. 賬號與裝置繫結

賬號與相應裝置進行繫結,如果發現與常用裝置不符合,增加簡訊登入形式進行重新登入

7. dex檔案的校驗

重編譯apk其實就是重編譯了classes.dex檔案,重編譯後,生成的classes.dex檔案的hash值就改變了,因此我們可以通過檢測安裝後classes.dex檔案的hash值來判斷apk是否被重打包過。

(1). 讀取應用安裝目錄下/data/app/xxx.apk中的classes.dex檔案並計算其雜湊值,將該值與軟體釋出時的classes.dex雜湊值做比較來判斷客戶端是否被篡改。

(2). 讀取應用安裝目錄下/data/app/xxx.apk中的META-INF目錄下的MANIFEST.MF檔案,該檔案詳細記錄了apk包中所有檔案的雜湊值,因此可以讀取該檔案獲取到classes.dex檔案對應的雜湊值,將該值與軟體釋出時的classes.dex雜湊值做比較就可以判斷客戶端是否被篡改。

為了防止被破解,軟體釋出時的classes.dex雜湊值應該存放在伺服器端。

8.偵錯程式檢測

為了防止apk被動態除錯,可以檢測是否有偵錯程式連線。在Application類中提供了isDebuggerConnected()方法用於檢測是否有偵錯程式連線,如果發現有偵錯程式連線,可以直接退出程式。

9.是否root

檢測是否包含su程式,和ro.secure是否為1,如果root了,可以禁止某些核心功能 檢測是否root的程式碼

    public boolean isRoot() {
        int secureProp = getroSecureProp();
        if (secureProp == 0)//eng/userdebug版本,自帶root許可權
            return true;
        else return isSUExist();//user版本,繼續查su檔案
    }

    private int getroSecureProp() {
        int secureProp;
        String roSecureObj = CommandUtil.getSingleInstance().getProperty("ro.secure");
        if (roSecureObj == null) secureProp = 1;
        else {
            if ("0".equals(roSecureObj)) secureProp = 0;
            else secureProp = 1;
        }
        return secureProp;
    }

    private boolean isSUExist() {
        File file = null;
        String[] paths = {"/sbin/su",
                "/system/bin/su",
                "/system/xbin/su",
                "/data/local/xbin/su",
                "/data/local/bin/su",
                "/system/sd/xbin/su",
                "/system/bin/failsafe/su",
                "/data/local/su"};
        for (String path : paths) {
            file = new File(path);
            if (file.exists()) return true;//可以繼續做可執行判斷
        }
        return false;
    }

複製程式碼

10. 是否裝有xposd框架

檢測是否安裝有xposd框架,如果有提示並隱藏核心功能模組.介面禁用某些功能 所有的方案迴歸到一點:==判斷xposed的包是否存在。== (1).是通過主動丟擲異常查棧資訊; (2).是主動反射呼叫。

    private static final String XPOSED_HELPERS = "de.robv.android.xposed.XposedHelpers";
    private static final String XPOSED_BRIDGE = "de.robv.android.xposed.XposedBridge";

    //手動丟擲異常,檢查堆疊資訊是否有xp框架包
    public boolean isEposedExistByThrow() {
        try {
            throw new Exception("gg");
        } catch (Exception e) {
            for (StackTraceElement stackTraceElement : e.getStackTrace()) {
                if (stackTraceElement.getClassName().contains(XPOSED_BRIDGE)) return true;
            }
            return false;
        }
    }

    //檢查xposed包是否存在
    public boolean isXposedExists() {
        try {
            Object xpHelperObj = ClassLoader
                    .getSystemClassLoader()
                    .loadClass(XPOSED_HELPERS)
                    .newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
            return true;
        } catch (IllegalAccessException e) {
            //實測debug跑到這裡報異常
            e.printStackTrace();
            return true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return false;
        }

        try {
            Object xpBridgeObj = ClassLoader
                    .getSystemClassLoader()
                    .loadClass(XPOSED_BRIDGE)
                    .newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
            return true;
        } catch (IllegalAccessException e) {
            //實測debug跑到這裡報異常
            e.printStackTrace();
            return true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    //嘗試關閉xp的全域性開關,親測可用
    public boolean tryShutdownXposed() {
        if (isEposedExistByThrow()) {
            Field xpdisabledHooks = null;
            try {
                xpdisabledHooks = ClassLoader.getSystemClassLoader()
                        .loadClass(XPOSED_BRIDGE)
                        .getDeclaredField("disableHooks");
                xpdisabledHooks.setAccessible(true);
                xpdisabledHooks.set(null, Boolean.TRUE);
                return true;
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
                return false;
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                return false;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                return false;
            }
        } else return true;
    }

複製程式碼

參考資料

相關文章