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

本文仅供学习交流,严禁用于非法用途,请于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 不变发送不同的请求依然能得到正确的回应。

结论

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

作者

uint128.com

发布于

2020-08-21

更新于

2022-08-22

许可协议

评论