青果教务系统APP网络协议加密算法分析

  1. 1. 抓包分析
  2. 2. 反编译分析
  3. 3. 结论

本文仅供学习交流,严禁用于非法用途,请于24小时内遗忘。

抓包分析

接上一篇文章成功逆向该 App 之后,开始分析其网络协议部分。这个 App 的网络部分非常有趣,数据请求是基于 HTTP 协议的,但是所有的数据查询请求的 URL 都是同一个。不仅如此,从这个 POST 请求的数据部分可以看出,请求的参数都被一种算法加密过了。

网络抓包截图1

网络抓包截图2

几乎所有请求都只有这四个参数,而且都发给了同一个 URL。很显然,真正的查询参数都被加密藏在这些参数里了。从命名来看,参数 token 应该是用来识别区分用户的,不过请求中也包含了 cookie ,而且 cookie 的内容和 token 不一样,所以还不好下结论,万一是用 cookie 来区分用户的呢。参数 param 和 param2 应该就是真正的查询参数了,可惜是被加密的密文,而且还看不出是什么加密算法。参数 appinfo 的内容是这个程序的版本号。

反编译分析

从反编译出来的代码中搜索 URL,找出和 HTTP 请求有关的代码。我是从登录的请求开始分析的,因为登录的前后有很多很多的流程,分析这些流程有助于梳理程序的逻辑。

搜索反编译代码

这个 App 是经过混淆处理的,给分析增加了很多的难度。分析的过程我就不写了,其实就是阅读代码,因为很多变量名称、函数名称都没了,所以需要自己寻找各个变量、函数的含义,最终找到和加密有关的函数。

下面这个函数是构建请求参数的函数:

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
public static Map<String, String> m11985a(
Map<String, String> map, // 原始参数字典
boolean z, // 未知 flag 猜测:是否已登录
Context context) { // ?

String str;

String a = Version.m1233a(context); // 返回 yt6n78 猜测是密钥

String str2 = "";
for (Map.Entry next : map.entrySet()) {
String trim = ((String) next.getKey()).trim();
if (next.getValue() == null) {
str = "";
} else {
str = (String) next.getValue();
}
LogUtil.m11834a(str.length() + "");
str2 = str2 + "&" + trim + "=" + str;
}

// 如果z==true,就加上 sfid 和 uuid 参数
if (z) {
str2 = (str2 + "&sfid=" + InterfaceTools.f17467a.userid) + "&uuid=" + InterfaceTools.f17467a.uuid;
}

// 去除开头的 & 字符
if (str2.indexOf("&") == 0) {
str2 = str2.substring(1);
}
// 构建加密后的字典
HashMap hashMap = new HashMap();
try {

MyLog.m11863a("as_str=", str2);
// param 来自这里
hashMap.put("param", m11979a(str2, a));
// param2 来自这里
hashMap.put("param2", m11996f(str2));

if (z) {
hashMap.put("token", InterfaceTools.f17467a.token);
hashMap.put("appinfo", Version.m1234b(context));
} else {
hashMap.put("token", "00000");
hashMap.put("appinfo", Version.m1234b(context));
}
return hashMap;

} catch (Exception unused) {
hashMap.put("param", "error");
hashMap.put("param2", "error");
if (z) {
hashMap.put("token", "error");
hashMap.put("appinfo", Version.m1234b(context));
}
return hashMap;
}
}

其中最关键的是这两行:

1
2
3
4
// param 来自这里
hashMap.put("param", m11979a(str2, a));
// param2 来自这里
hashMap.put("param2", m11996f(str2));

参数 param 是由 m11979a 这个函数生成的,所以进一步去分析这个函数。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public static String m11979a(String str, String str2) {
if (str != null) {
String str3 = "";
if (!str3.equals(str) && str2 != null && !str3.equals(str2)) {
int length = str2.length();
int length2 = str.length();
double d = (double) length2;
int ceil = (int) Math.ceil((1.0d * d) / ((double) length));
int ceil2 = (((int) Math.ceil((((d * 3.0d) * 6.0d) / 9.0d) / 6.0d)) * 6) % length;
int i = 0;
String str4 = str3;
for (int i2 = 0; i2 < ceil; i2++) {
for (int i3 = 1; i3 <= length; i3++) {
int i4 = (i2 * length) + i3;
String substring = str.substring(i4 - 1, i4);
String substring2 = str2.substring(i3 - 1, i3);
String str5 =
"000" +
String.valueOf(
Integer.valueOf(m11997g(substring)).intValue() +
Integer.valueOf(m11997g(substring2)).intValue() + ceil2);
str4 = str4 + str5.substring(str5.length() - 3, str5.length());
if (i4 == length2) {
break;
}
}
}
while (i < str4.length()) {
int i5 = i + 9;
String substring3 = str4.substring(i, i5 >= str4.length() ? str4.length() : i5);
String str6 = "000000" + m11977a(Long.valueOf(substring3).longValue());
str3 = str3 + str6.substring(str6.length() - 6, str6.length());
i = i5;
}
return str3;
}
}
return str;
}

// 每个字符用逗号隔开
public static String m11997g(String str) {
StringBuffer stringBuffer = new StringBuffer();
char[] charArray = str.toCharArray();
for (int i = 0; i < charArray.length; i++) {
if (i != charArray.length - 1) {
stringBuffer.append(charArray[i]);
stringBuffer.append(",");
} else {
stringBuffer.append(charArray[i]);
}
}
return stringBuffer.toString();
}

// 将整数 j 用36进制表示
private static String m11977a(long j) {
m11986a();
if (j < 0) {
return "-" + m11977a(Math.abs(j));
}
String str = "";
do {
// f17428a 的值是:0123456789abcdefghijklmnopqrstuvwxyz
String ch = f17428a.get(Integer.valueOf((int) (j % 36))).toString();
if ("".equals(str)) {
str = ch;
} else {
str = ch + str;
}
j /= 36;
} while (j > 0);
return str;
}

实践证明,函数 m11979a 确实是用来计算参数 param 的,但是这段反编译出来的代码其实是有问题的,不能直接用。

问题出在这一段:

1
2
3
4
5
String str5 = 
"000" +
String.valueOf(
Integer.valueOf(m11997g(substring)).intValue() +
Integer.valueOf(m11997g(substring2)).intValue() + ceil2);

函数 m11997g 的作用是用逗号将字符串的每一个字符分开,比如m11997g("ABC")的结果是"A,B,C",是一个字符串。而反编译出来的代码却对一个非数字字符串使用Integer.valueOf(),这是不符合逻辑的。如果 substring 和 substring2 是形如"123543"这样的数字字符串,那还是说得通的。但是 substring 和 substring2 的内容并不总是数字,大多数情况下是英文字符,很显然无法将英文解析成数字。

分析这一段的时候,简直怀疑人生,以为自己看漏了什么代码。后来实在没办法,其他代码里也找不到新的线索,我就猜测这两行是取 substring 和 substring2 第一个字符的 ASCII 码。按照这个思路重新实现这段代码,居然成功获得了加密的字符串。

以下是使用 C# 重新实现后的 param1 的计算算法:

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
static string charTable = "0123456789abcdefghijklmnopqrstuvwxyz";
private static string m11977a(long j)
{
if (j < 0)
{
return "-" + m11977a(Math.Abs(j));
}
var result = new StringBuilder();
do
{
string ch = charTable[(int)(j % 36)].ToString();
result.Insert(0, ch);
j /= 36;
} while (j > 0);
return result.ToString();
}

public static string CalcParam1(string str, string str2)
{
if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(str2)) return str;
int length1 = str.Length;
int length2 = str2.Length;
int ceil = (int)Math.Ceiling((1.0d * length1) / ((double)length2));
int ceil2 = ((int)Math.Ceiling(length1 * 3.0d * 6.0d / 9.0d / 6.0d)) * 6 % length2;
string str4 = string.Empty;
for (int i2 = 0; i2 < ceil; i2++)
{
for (int i3 = 1; i3 <= length2; i3++)
{
int i4 = (i2 * length2) + i3;
string substring = str.Substring(i4 - 1, 1);
string substring2 = str2.Substring(i3 - 1, 1);
string str5 = "000" + (substring[0] + substring2[0] + ceil2).ToString();
str4 = str4 + str5.Substring(str5.Length - 3);
if (i4 == length1)
{
break;
}
}
}
var result = new StringBuilder();
int i = 0;
while (i < str4.Length)
{
int i5 = i + 9;
int len = Math.Min(str4.Length, i5) - i;
string substring3 = str4.Substring(i, len);
string val = m11977a(Convert.ToInt64(substring3));
string str6 = $"000000{val}";
result.Append(str6.Substring(str6.Length - 6));
i = i5;
}
return result.ToString();
}

关于计算 param2 的函数,我没有仔细地研究,因为它在请求中似乎没有作用,我保持 param2 不变发送不同的请求依然能得到正确的回应。

结论

反编译出来的代码未必是正确的,还是得猜一下代码。