某手机银行被中间人劫持攻击实例

前言
该系统是国内比较大的手机网银开发商的开发的,国内多家城商行和至少2年全国性股份制商业银行采用了该产品。本文描述的缺陷是2014年4月的android版本,已经协助厂商修复。该产品存在的几个安全缺陷组合利用,可以实现wifi劫持后窃取资金的攻击。
安全缺陷一:客户端验证ssl证书不严格
这是一个在android app中普遍存在的漏洞(细节可以参考TSRC 的一个文章 http://security.tencent.com/index.php/blog/msg/41),在手机银行中也一样存在。这个漏洞为中间人攻击提供了可行的基础。
问题主要出在客户端代码中
文件:
行数:xxx行开始
代码分析(红色的为缺陷代码)
class EasyX509TrustManager implements X509TrustManager {

private X509TrustManager standardTrustManager = null;
//覆盖google默认的证书检查机制(X509TrustManager)
public EasyX509TrustManager(KeyStore keystore)
throws NoSuchAlgorithmException, KeyStoreException {
super();
TrustManagerFactory factory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
factory.init(keystore);
TrustManager[] trustmanagers = factory.getTrustManagers();
if (trustmanagers.length == 0) {
throw new NoSuchAlgorithmException(“no trust manager found”);
}
this.standardTrustManager = (X509TrustManager) trustmanagers[0];
}

public void checkClientTrusted(X509Certificate[] certificates,
String authType) throws CertificateException {
standardTrustManager.checkClientTrusted(certificates, authType);
}

public void checkServerTrusted(X509Certificate[] certificates,
String authType) throws CertificateException {
if ((certificates != null) && (certificates.length == 1)) {
certificates[0].checkValidity();
} else {
standardTrustManager.checkServerTrusted(certificates, authType);
}
}
//覆盖默认的证书检查机制后,没有对证书进行应有的安全性检查,直接接受了所有异常的https证书,不提醒用户存在安全风险,也不终止这次危险的连接。
public X509Certificate[] getAcceptedIssuers() {
return this.standardTrustManager.getAcceptedIssuers();
}
安全缺陷二:服务端敏感信息泄露
手机银行服务端程序,将大量配置文件存放在Web根路径下的xxxx目录下,可以通过浏览器直接访问,造成密钥等数据泄露。

安全缺陷三:密码rsa密文可重放
客户端登陆密码和交易密码等敏感数据在和服务器端程序交互时,使用了加密算法进行加密,测试发现客户端通过一次rsa加密对密码进行保护,然后再进行3des加密得到最终密文,将加密的密文发送给服务器端程序进行解密。由于利用缺陷二服务器端泄露了加密密钥,可以直接解密出密码的rsa密文。由于系统没有采用时间戳等措施防重放,该密文可以直接替代登录或者交易密码多次使用。

3des加密的处理过程
文件:
行数:xxx行开始
代码分析(蓝色的为关键代码)
if (subEl.getTagName().equals(“input”)) {
if (“password”.equals(subEl.getAttribute(“type”))) {
if (null == value || value.trim().length() < 1);
else{
if(ActivityStore.loginPwdrsa!=null&&ActivityStore.loginPwdrsa.length()>0){
try {
Class cla=this.getClass().getClassLoader().loadClass(ActivityStore.loginPwdrsa);
Constructor constructor = cla.getConstructor(new Class[]{String.class,String.class});
String rsam=subEl.getAttribute(“modulus”);
String rsae=subEl.getAttribute(“exponent”);
if(rsam!=null&&rsae!=null&&rsam.length()>0&&rsae.length()>0){
//调用rsa加密相关代码
RSAInterface rsa = (RSAInterface)constructor.newInstance(rsam,rsae);
value=rsa.encrypt(value);
}else{
RSAInterface rsa = (RSAInterface)constructor.newInstance(rendererContext.context.getResources().getString(R.string.loginpwdmodulus),rendererContext.context.getResources().getString(R.string.loginpwdpublicExponent));
value=rsa.encrypt(value);
}
} catch (Exception e) {
}
}
//使用客户端设备号、sessionid、随机数作为密钥进行加密
value = Encrypt3Des.encrypt3DesEncode(value, this.sourceElement.ownerDocument.getRendererContext().getNetClient().getProcessor().getDeviceKey(),
RequestProcessor.Sessionkey,
RequestProcessor.RandomKey);
}
}
}else if (subEl.getNodeName().equals(“password”)) {

加密过程用到了根据设备生成的设备号、当前jsessionid和服务器端生成的8位随机数,前两者是可以直接通过中间人攻击抓取到的。随机数从服务器下发到客户端的时候不是明文,而是使用缺陷二中的deskey进行了des加密处理,利用缺陷二我们可以解出明文随机数,进而获取到rsa密文。
安全缺陷四:转帐短信验证码可穷举
利用以上三个缺陷,还无法实现转帐,因为完成转帐需要短信验证码。遗憾的是短信验证码实现的也有缺陷。程序员设置的短信验证码为6位数字,有效期是三分钟,此外就没有其他保护措施了,三分钟穷举一个6位数字难度几乎为零。
文件:
行数:xx行开始
代码分析
从代码中,可以看到对于短信验证码的各种处理,如判断是否获取了短信验证码、短信验证码是否超时、短信验证码是否已经使用过、短信验证码是否正确,但是没有判断短信验证码错误次数:

public int doControl() throws XxException {
try {
String sms_input =(String)mapValue.get(“sms_yzm”);
String errMsg=(String)mapValue.get(“respmsg”);

String sms_yzm = priDataCache.getParam(“sms_yzm”);
String sms_time = priDataCache.getParam(“sms_yzm_time”);

if(sms_yzm==null || sms_time==null ||sms_time.equals(“”)||sms_yzm.equals(“”)){
priDataCache.setParam(“respcode”, “m2019”);
priDataCache.setParam(“respmsg”, “未获取短信验证码!”);
return -1;
}

if(System.currentTimeMillis()-180000>Long.parseLong(sms_time)){//三分钟
priDataCache.setParam(“respcode”, “m2020”);
priDataCache.setParam(“respmsg”, “短信验证码已经超时,请重新获取”);
return -1;
}

if(sms_input!=null&&sms_input.equals(sms_yzm)){
priDataCache.setParam(“sms_yzm”, “”);
priDataCache.setParam(“sms_yzm_time”, “”);
}else{
priDataCache.setParam(“respcode”, “m2021”);
priDataCache.setParam(“respmsg”, errMsg);
return -1;
}

}catch(Exception ex){
Log.getInstance().error(logId,ex.getMessage(),ex);
throw new XxException(“m2022”, “验证码输入不正确” + ex.toString());
}
return 0;
}