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

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

缺陷编号:wooyun-2015-0141784

漏洞标题:360免费WIFI加密签名破解获取他人无线明文密码(还可刷金币换运营商流量)

相关厂商:奇虎360

漏洞作者: 瘦蛟舞

提交时间:2015-09-17 16:30

修复时间:2015-12-16 18:18

公开时间:2015-12-16 18:18

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

危害等级:高

自评Rank:15

漏洞状态:厂商已经确认

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

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2015-09-17: 细节已通知厂商并且等待厂商处理中
2015-09-17: 厂商已经确认,细节仅向厂商公开
2015-09-20: 细节向第三方安全合作伙伴开放
2015-11-11: 细节向核心白帽子及相关领域专家公开
2015-11-21: 细节向普通白帽子公开
2015-12-01: 细节向实习白帽子公开
2015-12-16: 细节向公众公开

简要描述:

360免费 wifi是一款类似 wifi 万能钥匙的产品.其也以插件的形式存在于360手机卫士,360手机助手等其他360产品上.

详细说明:

第一回合:加解密破除
根据特征定位功能点代码
根据 http 请求中method=Wifi.password 为特征,定位请求封装代码存在 qE 类中

wifipassword.png


接下来对函数进行hook 分析

hookpara.png


追踪到其加密方式为DESede (DESede 是由DES对称加密算法改进后的一种对称加密算法。使用 168 位的密钥对资料进行三次加密的一种机制 ),实现类为 vA

des.png


接下要做的就是定位DESede key 就可以破解完成 http 请求加解密的整个过程并且自己用代码实现.

init.png


现在开始跟踪 key 的生成算法.从以下代码可以得知 init 中的 key 是由 public static String a(String arg2) 方法获取的. 从代码中得知 key 的生成实际是由com.qihoo.freewifi.utils.SecurityUtils完成的

getkey.png


其中 getkey 是一个 native 方法 (可以看出 app 的保护工作主要放在保护 key 上)

public static native String getKey(String paramString1, String paramString2);


接下来就要分析 so 咯.

» strings libsecurity.so |grep -i getkey
Java_com_qihoo_freewifi_utils_SecurityUtils_getKey


然后再对 IDA F5中参数类型进行修复就得到了如下

getkeyn.png


一个 hash 过程,这个函数在so 没有啥包含可以直接引用
主导 key 生成因子2的就是 getKey(String paramString1, String paramString2) 第二个参数了. (因子1就是一些固定 method 字符串)
SecurityUtils#b(Context arg1, String arg2, String arg3)的 arg3引入
回看vA#a代码

public static String a(String arg2) {
return SecurityUtils.b(Application.a(), arg2, vB.a(Application.a()));
}


vB#a 得到类似 UUID 的东西传入 SecurityUtils#getKey 得到 key. (这里在可以否定前面的推断 类UUID 的字符串比和服务端有关.不然服务端无法得到 key 解密,客户端会把生成 key 的因子2传到服务端,在 http 请求中是 m2 参数. 这样服务端就有了生成 key 的两个因子来生成 key 进行解密了)

param2.png


设备关联 id 生成函数vB#a 分析 ,主要逻辑如下:将 imei,deviceid,android_id拼接后进行 md5

v0 = vB.b(arg6);
String v1 = Settings$System.getString(arg6.getContentResolver(), "android_id");
String v2 = vB.a();
vB.a = vP.a("" + v0 + v1 + v2);


其中 v0是由vB#b(Context arg2)生成 ,一种情况是返回vB.b = arg2.getSystemService("phone").getDeviceId() ,如果若空则返回"360_DEFAULT_IMEI"

public static String b(Context arg2) {
try {
vB.b = wa.a(arg2, "APP_STORE_IMEI0", "");
if(!TextUtils.isEmpty(vB.b)) {
String v0_1 = vB.b;
return v0_1;
}
vB.b = arg2.getSystemService("phone").getDeviceId();
if(TextUtils.isEmpty(vB.b)) {
return "360_DEFAULT_IMEI";
}
wa.b(arg2, "APP_STORE_IMEI0", vB.b);
return vB.b;
}
catch(Exception v0) {
}
return "360_DEFAULT_IMEI";
}


v1是由 Settings$System.getString(arg6.getContentResolver(), "android_id") 生成
v2是由 vB#a() 生成,连接之后传入vP#a(String arg1) 算出 md5 值.即得到生成的 key 因子2.

public static String a() {
String v0_3;
String v1 = "";
try {
Class v0_1 = Class.forName("android.os.SystemProperties");
Object v0_2 = v0_1.getMethod("get", String.class).invoke(v0_1, "ro.serialno");
}
catch(Exception v0) {
v0_3 = v1;
}
return v0_3;
}


响应包的加密方法和请求包的加密方法一致.直接解密响应包

passwd.png


其中 pwd 参数就是 wifi 的密码了,若未查询到则为空.
梳理一下整个流程:
1.通过拼接imie/android_id/deviceid 再进行 md5得 m2
2.将 m2 和 method 传入 native 函数getkey 得到对称密钥 key
3.通过 key 对请求相应包内的 data 进行 DESede 加解密
自此数据的加解密工作已经基本完成.进入第二个环节:签名
---
第二回合:签名破除
其中 sign 参数由

v0_1.add(new BasicNameValuePair("sign", tM.b(v0_1, v1)));


函数生成,v0_1为 url 中参数的 list 集合,而 v1则由tM.b()方法生成的一个32位字符串
tm#b生成字符串的的过程又和tM.c(); 以及 vA.b(v1, "User.getConfig");有关联

private static String b() {
String v0 = "";
String v1 = tM.c();
if(!TextUtils.isEmpty(((CharSequence)v1))) {
try {
v0 = vA.b(v1, "User.getConfig"); // DESede 解密函数
}
catch(Exception v1_1) {
v1_1.printStackTrace();
}
}
return v0;
}


而在 tM.c()方法中主要是调用了wa.a(Application.a().getBaseContext(), "qlink_secret_key", "")

private static String c() {
String v0;
if(!TextUtils.isEmpty(tM.a)) {
v0 = tM.a;
}
else {
tM.a = wa.a(Application.a().getBaseContext(), "qlink_secret_key", "");
v0 = tM.a;
}
return v0;
}


wa#a(Context arg1, String arg2, String arg3)方法调用了sV#b(Context arg2, String arg3, String arg4)

public static String a(Context arg1, String arg2, String arg3) {
return sV.b(arg1, arg2, arg3);
}


hook sV#b(Context arg2, String arg3, String arg4) 输入输出如下

09-15 11:02:27.749    1293-1293/? I/QihooWifi﹕ in sV#b(C|s|s) = last_update_date| para2 =
09-15 11:02:27.749 1293-1293/? I/QihooWifi﹕ out sV#b(C|s|s) = 20150915
09-15 11:56:55.622 2114-3083/? I/QihooWifi﹕ in sV#b(C|s|s) = qlink_secret_key| para2 =
09-15 11:56:55.632 2114-3083/? I/QihooWifi﹕ out sV#b(C|s|s) = Rd36RTbNXij5tjaqHZiEQY7ulZdvnrjbRWFtcIUBivz6wPxjdAMfYw== //DESede 加密后的32位字符串
09-15 11:57:00.032 1244-1244/? I/QihooWifi﹕ in sV#b(C|s|s) = APP_STORE_IMEI| para2 =
09-15 11:57:00.032 1244-1244/? I/QihooWifi﹕ out sV#b(C|s|s) = 78856d3517649f2de7ceb7dc3e4a0e9 //m2


其中qlink_secret_key字符串将传入sV#b(Context arg7, String arg8)

private static Cursor b(Context arg7, String arg8) {
Cursor v0_2;
Cursor v6 = null;
if(arg7 != null && !TextUtils.isEmpty(((CharSequence)arg8))) {
ContentResolver v0 = arg7.getContentResolver();
try {
v0_2 = v0.query(SharedPrefProvider.a, null, "key=\'" + arg8 + "\'", null, null);
}
catch(Throwable v0_1) {
v0_2 = v6;
}
if(v0_2 == null) {
try {
String v0_3 = "select value from sharedpref where key=?";
if(sV.b == null || !sV.b.isOpen()) {
sV.b = new SPDBHelper(arg7).getReadableDatabase();
}
v0_2 = sV.b.rawQuery(v0_3, new String[]{arg8});
}
catch(Throwable v0_1) {
v0_2 = v6;
}
}
else {
}
}
else {
v0_2 = v6;
}
return v0_2;
}


从代码中可以看出qlink_secret_key的值是从表 sharepref 中取出

sharedpref.png


那这个值又是如何生成并存储到 sqlite 中的了?
会看tM#c()方法,当成员变量 tM.a 为空的时候是通过 tM#a(String paramString) 来赋值的.

public static void a(String paramString)
{
a = paramString;
wa.b(Application.a().getBaseContext(), "qlink_secret_key", paramString);
}


追踪到 wa#b(Context paramContext, String paramString1, String paramString2)

public static void b(Context paramContext, String paramString1, String paramString2)
{
sV.a(paramContext, paramString1, paramString2);
}


继续跟踪sV#a(Context paramContext, String paramString1, String paramString2)

public static void a(Context paramContext, String paramString1, String paramString2)
{
if (paramContext == null) {}
for (;;)
{
return;
paramContext = paramContext.getContentResolver();
ContentValues localContentValues = new ContentValues();
localContentValues.put("key", paramString1);
localContentValues.put("value", paramString2);
try
{
if (paramContext.update(SharedPrefProvider.a, localContentValues, "key=?", new String[] { paramString1 }) == 0)
{
paramContext.insert(SharedPrefProvider.a, localContentValues);
return;
}
}
catch (Throwable paramContext) {}
}
}


可以确定是通过tM#c()方法写入qlink_secret_key这个键值的,现在又要用xref 来查看改方法的调用了. (猜测一:这个值是服务端返回的,恰巧返回包就是用 DESede 加密的,此处也是加密的. 猜测二:之前提到的 update_key可能是更新此处的 key)
有两处引用,其中一处是rn#a(rp arg3) ,其中 arg3.c 是通过取 json 中的 data 下 url 键值.(可以佐证猜测一)
</code>
public void a(rp arg3) {
if(arg3 != null && arg3.c != null) {
try {
String v0_1 = arg3.c.optString("url");
if(TextUtils.isEmpty(((CharSequence)v0_1))) {
goto label_9;
}
tM.a(v0_1);
}
catch(Exception v0) {
}
}
label_9:
rh.a(this.a, arg3);
}
</code>
第二处应用在tk#a(tz arg7) , 其中 tk.a = "http://api.free.wifi.360.cn/intf.php"; method 阐述为User.getConfig(继续佐证猜测一这个返回值是通过此处 url 请求到的)

public static void a(tz arg7) {
vO.a("ApiHelper", "getUserConfig");
tL v0 = tK.a(tk.a, "User.getConfig", null, null, null, null); //请求 api
vO.a("ApiHelper", "getUserConfig end:" + v0.h);
JSONObject v1 = tk.a(v0); //从返回值中取出 data 字段 既为 qlink_secret_key
if(v1 == null) {
tk.a(v0, arg7, new Object[0]);
}
else {
String v1_1 = v1.optString("url");
if(!TextUtils.isEmpty(((CharSequence)v1_1))) {
tM.a(v1_1);
}
tk.a(v0, arg7, new Object[0]);
}
}


接下来清除 app 数据后抓包来捕获此请求,东静态结合提高效率.

dataurl.png


重置应用后第一个请求就是获取此参数,那么问题来了.知道那是用来计算 sign 的,那么第一请求包的sign是如何来的了? (猜测:和另外两个 native 函数有关)
回看tM#a(String arg9, String arg10, List arg11, String arg12, boolean arg13) 方法,若 method 为User.getConfig那么 v1 便为空来计算 sign....

v1 = "User.getConfig".equals(arg10) ? "" : tM.b();
v0_1.add(new BasicNameValuePair("sign", tM.b(v0_1, v1)));


回看tM#a(Context paramContext, String paramString1, String paramString2) 方法,若 para2为空则调用的是SecurityUtils.b(paramContext, paramString1)函数
</code>
private static String a(Context paramContext, String paramString1, String paramString2)
{
if (TextUtils.isEmpty(paramString1)) {
return "";
}
for (;;)
{
synchronized (rz.a)
{
if (TextUtils.isEmpty(paramString2))
{
paramContext = SecurityUtils.b(paramContext, paramString1);
paramContext = paramContext.toLowerCase();
return paramContext;
}
}
paramContext = SecurityUtils.a(paramContext, paramString1, paramString2);
}
}
</code>
而 tM#b(Context paramContext, String paramString)

public static String b(Context paramContext, String paramString)
{
if ((paramContext == null) || (paramString == null) || (paramString.equals("")))
{
if (paramContext == null) {
return "contextisnull";
}
if ((paramString == null) || (paramString.length() == 0)) {
return "signstrisnull";
}
}
if (!a)
{
a(paramContext);
if (!a) {
return paramString + "&loadsoerror=true";
}
}
try
{
paramContext = initnew(paramContext.getApplicationContext(), paramString, "", false);
return paramContext;
}
catch (Throwable paramContext) {}
return "";
}


又要到查看 so 的时候啦

initnewn.png


这里显然做了个包判断,若像getkey 一样直接调用肯定会返回 none,所以得分析下isVaild函数来尝试绕过这个限制.
这时有两个选择 1.path so 在判断处将 BEQ 改成 BNE 2.最后一个布尔型变量传 true..
这里选择第二种简单的方法.算出如下结果

09-15 18:29:20.060  11255-11255/org.wooyun.qihoowifi I/Hi360﹕ MySign = 5c1c5d32e47638e464bd2ee7f8beea1b


对比之前 hook 的结果,证明猜测是正确的

09-15 15:09:14.610    6110-6110/? I/QihooWifi﹕ in tM#a(c|s|s) = channel=100001&devtype=android&inviter_qid=0&m2=78856d3517649f2de7ceb7dc3e4a0e94&manufacturer=LGE&method=User.getConfig&model=Nexus%205&nance=1442300954619&nettype=WIFI&os=4.4.4&qid=0&v=237 | para3 =
09-15 15:09:14.610 6110-6110/? I/QihooWifi﹕ out tM#a(c|s|s) = 5c1c5d32e47638e464bd2ee7f8beea1b


nance 为时间戳生成方式如下:

v3[18] = "nance";
v3[19] = String.valueOf(System.currentTimeMillis());


至此 sign 的破解基本完成.简单梳理下整个流程
1.先将 url 中参数按序拼接成字符串

2.若 method为User.getConfig将拼接字符串传入native 函数 initnew 计算出 sign 值,并且得到 sign 的加密盐值 data:url
3.若 method 不等于 User.getConfig 则将拼接字符串传入 native 函数 sign 中, 并且对2得到的加密盐进行 DESede 解密后一同传入. (注意大小写敏感)

漏洞证明:

有了上面的分析就可以自己写程序刷下金币了..再加上之前搞过万能钥匙...现在不用装两个应用.可以集成到一个 app 里啦.

wywifi1.png


coin.png


wywifi2.png


修复方案:

版权声明:转载请注明来源 瘦蛟舞@乌云


漏洞回应

厂商回应:

危害等级:中

漏洞Rank:5

确认时间:2015-09-17 18:16

厂商回复:

确认存在此问题,感谢您对360免费wifi产品的关注,您提交的问题不是产品的安全漏洞,根据您的建议我们会发布新版本加强产品的安全性。目前业界包括同行对于逆向破解还没有彻底的解决方案,我们会更努力从产品自身加强安全性,同时对服务端业务流程进行排查监控,防止黑客的恶意查询和恶意刷金币行为。

最新状态:

暂无


漏洞评价:

评论

  1. 2015-09-22 05:20 | 大漠長河 ( 实习白帽子 | Rank:66 漏洞数:10 | ̷̸̨̀͒̏̃ͦ̈́̾( 天龙源景区枫叶正...)

    还有这功能。一直没分清免费WIFI与随身WIFI。

  2. 2015-09-25 17:30 | 瓦解° ( 路人 | Rank:12 漏洞数:5 | web渗透学习小菜一枚。)

    @大漠長河 免费wifi是一款软件app,随身wifi是硬件。另外针对洞主提出的这个洞,厂商确认了却让我觉得很意外。免费wifi这款app的功能其实和wifi万能钥匙功能差不多,搜索附近的wifi,云里有保存的密码就可以直接连接,而360因为有很多人用360免费wifi创建热点,这些热点的密码360的服务器全有,所以你在电脑上用360wifi创建一个热点无论你怎么改密码,手机上安装个360免费wifi的app随时都可以连,因为360的服务器在随时更新用户创建热点的密码数据库提供给360免费wifi手机用户。至于积分换流量这是360wifi的一个活动,我暑假还用积分换了几十个小时的ChinaNet呢,我说的对吧?@奇虎 360

  3. 2015-09-25 17:31 | 瓦解° ( 路人 | Rank:12 漏洞数:5 | web渗透学习小菜一枚。)

    @奇虎360

  4. 2015-11-11 18:29 | mango ( 核心白帽子 | Rank:1721 漏洞数:255 | 我有个2b女友!)

    求APP !!!