本文仅供学习交流,严禁用于非法用途,请于24小时内遗忘。
抓包分析
接上一篇文章成功逆向该 App 之后,开始分析其网络协议部分。这个 App 的网络部分非常有趣,数据请求是基于 HTTP 协议的,但是所有的数据查询请求的 URL 都是同一个。不仅如此,从这个 POST 请求的数据部分可以看出,请求的参数都被一种算法加密过了。
几乎所有请求都只有这四个参数,而且都发给了同一个 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);
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; }
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); hashMap.put("param", m11979a(str2, a)); 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
| hashMap.put("param", m11979a(str2, a));
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(); }
private static String m11977a(long j) { m11986a(); if (j < 0) { return "-" + m11977a(Math.abs(j)); } String str = ""; do { 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 不变发送不同的请求依然能得到正确的回应。
结论
反编译出来的代码未必是正确的,还是得猜一下代码。