Android應用安全防護的點點滴滴
前言
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瀏覽器 . 如果遇到不安全網站,會有如圖所示情況
二. 通訊安全
這個也是重中之重,大多數的資料安全都是通過網路攻擊造成的,那麼我們如何去避免呢?
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;
}
複製程式碼