Android QQ 7.6.0~7.6.3 表情骂人BUG 的分析

5月27日晚,微博上看到些QQ 上发送 [菜刀]+数字+[表情]就会被转为骂人的话,28日中午就分析完了,但一直懒得发出来,今天补上。吐槽一下知乎上的某些沙雕分析文(小声 bb)

一、demo

演示视频挺多的,这没什么好被和谐的,随便贴两个视频链接吧

https://weibo.com/tv/v/GiE0hBfQe?fid=1034:2bd3171c50e53c24e1bcd6669c610e31

https://weibo.com/tv/v/GiDDnbtYD?fid=1034:4ff5a3565d2ee8b3cead9daca06a8902

是不是很刺激?第一反应是某开发不高兴,在里面内嵌了脏话,或者是测试时候用脏话测试的,忘了删掉了。

二、分析

1、先下载最新版的 QQ,确认一下情况,确实存在这种现象,先判断一下是本地还是远程的吧,幸运的是在本地配置的,在 classes2.dex 里搜到了硬编码的脏话,而且是一个脏话数组。(过于恶心的我就打码了)

本地试了一下,以2个testcase 为例,[菜刀]+"1"+[心] 被翻译为死胖子,[菜刀]+" "+[心] 被翻译为[跳舞]AmN you

2、硬编码位于 com.tencent.mobileqq.lovelanguage.LoveLanguageConfig ,附近还有LoveLanguageManager

dirtyList 的引用地方不多,分别是以下几处

a. String LoveLanguageConfig.convertCase(String) ,将前6个char改变大小写,这也是为什么”damn you”被转为了"[跳舞]AmN you" 的一个原因
b. boolean LoveLanguageConfig.isValidIndex(char) ,看是否下标越界
c. String LoveLanguageManager.AAA(String) 似乎是判断是否收到骂人的话,是的话就本地和谐掉,换成友好的表情
d. int LoveLanguageManager.BBB(EditText) 似乎是判断是否发出骂人的话,是的话就本地和谐掉,换成友好的表情
e. void LoveLanguageManager.CCC(EditText) 判断是否输入0x11,a,b,c,是的话就替换为脏话 这里有两个对 char 的运算

1
2
3
4
5
6
7
public static int a(char arg1) {
return arg1 - 30;
}

public static int a(int arg1) {
return arg1 + 30;
}

既然dirtyList 是数组,那么肯定是有对应关系的,这两个方法很可能与它有关,因为输入[菜刀]+"1"+[心] 被翻译为死胖子,[菜刀]+" "+[心] 被翻译为[跳舞]AmN you 。而"1"-" "是17,死胖子和 damn you 的间隔也是17,稍微猜一下就是线性的。

" " - (char)30 =2"damn you" 的index也是2,所以这个运算就是将 char 和 dirtyWord 对应起来。

3、LoveLanguageManager 这里面 Log 有很多,有两句给了我们提醒,handleLoveLanguageConverthandleLoveLanguageRevert ,似乎是转化之间的关系。

按顺序看吧,【其实我当时没看到后面那个方法】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public int a(EditText arg12) {
int v10 = 2;
this.d = false;
String v2 = arg12.getText().toString();
long v6 = System.currentTimeMillis();
int v1 = v2.length();
int v0 = 0;
int v3 = 0;
while(v0 < v1) {
if(v2.charAt(v0) == 17 && v0 < v1 - 3 && (LoveLanguageConfig.a(v2.charAt(v0 + 1)))) {
v1 = v0 + 4;
String v5 = LoveLanguageConfig.convertToDirty(v2.substring(v0, v1));
arg12.getEditableText().replace(v0, v1, ((CharSequence)v5));
v2 = arg12.getText().toString();
v1 = v2.length();
v0 = v0 + v5.length() - 1;
++v3;
}

++v0;
}

this.d = true;
if(QLog.isColorLevel()) {
Object[] v1_1 = new Object[6];
v1_1[0] = "love language handleLoveLanguageRevert count = ";
v1_1[1] = Integer.valueOf(v3);
v1_1[v10] = ",cost =";
v1_1[3] = Long.valueOf(Math.abs(System.currentTimeMillis() - v6));
v1_1[4] = ",send:";
v1_1[5] = v2;
QLog.d("LoveLanguageManager", v10, v1_1);
}

this.a();
return v3;
}

这段代码,安排明白了吧?看中间那段拿到输入框内容以后的逻辑,扫描一下,扫到 '\x11' + charA + charB + charC  这种格式的,调用 LoveLanguageConfig 的函数,进行转化后替换掉原来的文本,之后发出去。

绝大部分情况下,用户肯定不会输入 \x11 这个字符,所以猜测是 [菜刀] 表情编码中带有\x11,然后拼接了后面的几个任意的 char,就会被替换成脏话。【后来发现菜刀确实是 \x14\x11 组成的】

记住这个函数啊,叫 int a(EditText) ,附近还有个函数叫 void a(EditText) ,后面这个函数是将脏话替换为表情的。

验证一下猜想,输入[菜刀]+"111" ,触发;输入[菜刀]+1234567 ,发现123消失了。

所以,我觉得是手滑把这两个函数写反了,毕竟参数一样,返回值可有可无这种。

4、上 xposed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
ClassLoader loader = loadPackageParam.classLoader;
Log.d(TAG, "start hook");
XposedHelpers.findAndHookMethod("com.tencent.qphone.base.util.QLog", loader, "d", String.class, int.class, String.class, new XC\_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.d(TAG, param.args[0] + "\\t" + param.args[2]);
}

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});
XposedHelpers.findAndHookMethod("com.tencent.qphone.base.util.QLog", loader, "isColorLevel", new XC\_MethodReplacement() {

@Override
protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
return true;
}
});

XposedHelpers.findAndHookMethod("com.tencent.mobileqq.lovelanguage.LoveLanguageManager", loader, "a", EditText.class, new XC\_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
return 0;
}
});


XposedHelpers.findAndHookMethod("com.tencent.mobileqq.lovelanguage.LoveLanguageManager", loader, "a", EditText.class, new XC\_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);

Object obj = param.thisObject;
Method[] methods = obj.getClass().getDeclaredMethods();
for (Method m : methods) {
Log.e(TAG, m.toGenericString() + "===" + printHexString(m.getName()));
}

EditText editText = (EditText) param.args[0];
String s = editText.getText().toString();
Log.d(TAG, "input=" + s);
Log.d(TAG, "input=" + printHexString(s));
}

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
EditText editText = (EditText) param.args[0];
String s = editText.getText().toString();
Log.d(TAG, "after=" + s);
Log.d(TAG, "after=" + printHexString(s));
}
});


Log.d(TAG, "end hook");

我们输入:[菜刀]1[心]的时候 输入输出的Log:

1
2
3
4
5
05-28 04:24:03.716 1511-1511/com.tencent.mobileqq D/LDB: input=1J
05-28 04:24:03.716 1511-1511/com.tencent.mobileqq D/LDB: input=141131144a
05-28 04:24:03.866 1511-1511/com.tencent.mobileqq D/LDB: LoveLanguageManager love language report 0X8009167
05-28 04:24:03.866 1511-1511/com.tencent.mobileqq D/LDB: after=死胖子
05-28 04:24:03.866 1511-1511/com.tencent.mobileqq D/LDB: after=14e6adbbe88396e5ad90

我们输入:[菜刀]123 的时候

1
2
3
4
05-28 05:04:28.338 1511-1511/com.tencent.mobileqq D/LDB: input=123
05-28 05:04:28.338 1511-1511/com.tencent.mobileqq D/LDB: input=1411313233
05-28 05:04:28.347 1511-1511/com.tencent.mobileqq D/LDB: after=死胖子
05-28 05:04:28.347 1511-1511/com.tencent.mobileqq D/LDB: after=14e6adbbe88396e5ad90

我们输入:[菜刀]空格[心]的时候

1
2
3
4
05-28 05:11:06.107 1511-1511/com.tencent.mobileqq D/LDB: input= J
05-28 05:11:06.107 1511-1511/com.tencent.mobileqq D/LDB: input=141120144a
05-28 05:11:06.120 1511-1511/com.tencent.mobileqq D/LDB: after=dAmN you
05-28 05:11:06.120 1511-1511/com.tencent.mobileqq D/LDB: after=1464416d4e20796f75

5、[菜刀]表情包含2个 char,是\\x14\\x11 ,既然如此,这里又有一个细节,其实转化为脏话以后,第一个 char 还是保留着的,当它与字母拼接时,是可以组合成 QQ 快捷表情发出去的,我们可以通过复制粘贴在聊天界面里获得它。

三、总结

开发手滑,两个函数调用弄混了,虽然\\x11这个字符不容易写出来,但[菜刀]加上任意3个字符还是很常见的,经过3个小版本,还是被用户不小心测出来了,哈哈哈,就看谁来背这个锅吧。