记录黑客技术中优秀的内容, 传播黑客文化,分享黑客技术精华

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

2015-12-17 01:00
漏洞标题 360免费WIFI加密签名破解获取他人无线明文密码(还可刷金币换运营商流量)
相关厂商 奇虎360
漏洞作者 瘦蛟舞
提交时间 2015-09-17 16:30
公开时间 2015-12-16 18:18
漏洞类型 设计错误/逻辑缺陷
危害等级
自评Rank 15
漏洞状态 厂商已经确认
Tags标签 客户端程序设计错误,逻辑错误

漏洞详情

第一回合:加解密破除

根据特征定位功能点代码

根据 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

修复方案:

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

知识来源: www.secpulse.com/archives/42008.html

阅读:160747 | 评论:0 | 标签:漏洞 客户端程序设计错误 逻辑错误 加密

想收藏或者和大家分享这篇好文章→复制链接地址

“360免费WIFI加密签名破解获取他人无线明文密码(还可刷金币换运营商流量)”共有0条留言

发表评论

姓名:

邮箱:

网址:

验证码:

公告

九层之台,起于累土;黑客之术,始于阅读

推广

工具

标签云