名华慕课App算法解密过程以及软件实现

  • 测试环境 Windows 10 x64
  • Fiddler 5.0.20182.28034
  • ApkIDE 3.5.0.0
  • 测试样本 名华慕课 Android 版 1.0.3
  • Python 3.6.8

捕获数据包

使用 Fiddler 截获手机客户端的 HTTP 封包 , 如何使用 Fiddler 截获手机封包请自行 Google 这里不做过多阐释

拦截到的数据包

request
1
2
3
4
5
6
7
8
9
10
POST http://api.mooc.minghuaetc.com/v1 HTTP/1.1
Cookie: moocvk=*************;moocsk=**************;
Content-Length: 89
Content-Type: application/x-www-form-urlencoded
Host: api.mooc.minghuaetc.com
Connection: Keep-Alive
User-Agent: Mozilla/5.0 (Linux; U; Android 8.0.0; en-gb; SM-G9650 Build/R16NW) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
Accept-Encoding: gzip

cmd=course.subject&client=chinamoocs&level=2&spoc=1&sign=125d379b0b53f2c43214721ad3732e4d

出于安全考虑已经将 Cookies 信息做处理实际测试中以实际值为准 , 可以看到提交的字段有 cmdclientlevel、spocsign

搜索方法

打开 ApkIDE 加载名华慕课安卓客户端 , 然后使用全局搜索 cmd 字段内容 course.subject

搜索方法

可以看到使用了 course.subject 的有两个类

com/wisedu/mooccloud/mhaetc/phone/ui/HomeActivity
com/wisedu/mooccloud/mhaetc/phone/service/ZhiTuService

根据类名推测分析 , 我们需要进行分析的为 HomeActivity

查看其 Java 源代码 , 静态分析函数 eP

查看Java源代码

发现 sign 是由 localObject2 赋值 , localObject2localObject1 赋值 , localObject1 由函数 ig.d赋值 , 于是继续追踪函数

追踪函数

查看 ig.d

查看igd函数

发现 return 处由另外函数 aE 赋值 , 继续查看 aE 函数 , 其中又使用了 e 函数进行复制

查看aE函数

发现是一个 摘要算法 MD5 的加密

查看e函数

sign 算法解密

结合前面的 Java 源码中的函数 eP , 静态分析出传递给 ig.d 方法的参数为 POST 除 sign 字段的所有字段值加上一个常量值 xF3m1m4CrvQd3VsfsEpIf6s0CPWT7sJu

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
private void eP()
{
RequestParams localRequestParams = new RequestParams();
localRequestParams.setHeader("Cookie", hz.a(this.pw));
localRequestParams.addBodyParameter("cmd", "course.subject");
localRequestParams.addBodyParameter("client", "chinamoocs");
localRequestParams.addBodyParameter("level", "2");
localRequestParams.addBodyParameter("spoc", String.valueOf(gt.c(this.pE)));
Object localObject1 = new ArrayList();
((List)localObject1).add("course.subject");
((List)localObject1).add("chinamoocs");
((List)localObject1).add("2");
((List)localObject1).add(String.valueOf(gt.c(this.pE)));
((List)localObject1).add("xF3m1m4CrvQd3VsfsEpIf6s0CPWT7sJu");
Object localObject2 = "";
try
{
localObject1 = ig.d((List)localObject1);
localObject2 = localObject1;
}
catch (Exception localException)
{
for (;;) {}
}
localRequestParams.addBodyParameter("sign", ((String)localObject2).toLowerCase());
new HttpUtils().send(HttpRequest.HttpMethod.POST, gr.pn, localRequestParams, new RequestCallBack()
{
public void onFailure(HttpException paramAnonymousHttpException, String paramAnonymousString) {}

public void onStart() {}

public void onSuccess(ResponseInfo<String> paramAnonymousResponseInfo)
{
hz.a(paramAnonymousResponseInfo, HomeActivity.this.pw);
ie.d(HomeActivity.this.TAG, "我的课程 返回: " + (String)paramAnonymousResponseInfo.result);
hz.a(paramAnonymousResponseInfo, HomeActivity.this.pw);
try
{
JSONObject localJSONObject = new org/json/JSONObject;
localJSONObject.<init>((String)paramAnonymousResponseInfo.result);
String str = localJSONObject.optString("code");
localJSONObject.optString("message", "服务器或网络出现错误,请稍后再试~");
if ("000000".equals(str))
{
hh.c(localJSONObject.getJSONObject("result").optJSONArray("subjects"), HomeActivity.this.pw.qH);
hz.a(paramAnonymousResponseInfo, HomeActivity.this.pw);
}
return;
}
catch (JSONException paramAnonymousResponseInfo)
{
for (;;)
{
paramAnonymousResponseInfo.printStackTrace();
}
}
}
});
}

重新组合文本并使用 md5 加密 , 可以得出 HTTP 请求中一致的 sign 字段值 到此 sign 算法解密结束

sign解密验证

password 算法解密

根据 Fiddler 截获的封包中 , 发现登录的 HTTP 封包如下

password封包

request
1
2
3
4
5
6
7
8
9
10
POST http://api.mooc.minghuaetc.com/v1 HTTP/1.1
Cookie: moocvk=**************;
Content-Length: 121
Content-Type: application/x-www-form-urlencoded
Host: api.mooc.minghuaetc.com
Connection: Keep-Alive
User-Agent: Mozilla/5.0 (Linux; U; Android 8.0.0; en-gb; SM-G9650 Build/R16NW) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
Accept-Encoding: gzip

cmd=sys.login.no&client=chinamoocs&orgid=**&user=**&password=dk******h1DI%3D%0A&sign=***********************

出于安全考虑已经 Cookies 信息和用户凭据字段做处理实际测试中以实际值为准 , 将其 URLDecodedk**h1DI%3D%0Adk**h1DI=

因此推测是一串 Base64 编码的字符串 , 先对其进行直接解码后发现为乱码 , 因此这不是一个单纯的 Base64 编码与百度贴吧客户端不同 , 再次搜索方法 , 找到位于类

com/wisedu/mooccloud/mhaetc/phone/ui/UnifyLucherActivity

中的 as函数 , 静态分析发现 , password 是由 localObject1 赋值 , localObject1 是由函数 ig.aF 赋值

查看as函数

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
75
76
77
78
private void as(final String paramString)
{
final String str1 = this.uC.getText().toString();
final String str2 = this.uD.getText().toString();
Object localObject1 = ig.aF(str2);
RequestParams localRequestParams = new RequestParams();
localRequestParams.setHeader("Cookie", hz.a(this.pw));
localRequestParams.addBodyParameter("cmd", "auth.login");
localRequestParams.addBodyParameter("client", "chinamoocs");
localRequestParams.addBodyParameter("user", str1);
localRequestParams.addBodyParameter("password", (String)localObject1);
Object localObject2 = new ArrayList();
((List)localObject2).add("auth.login");
((List)localObject2).add("chinamoocs");
((List)localObject2).add(str1);
((List)localObject2).add(localObject1);
((List)localObject2).add("xF3m1m4CrvQd3VsfsEpIf6s0CPWT7sJu");
localObject1 = "";
try
{
localObject2 = ig.d((List)localObject2);
localObject1 = localObject2;
}
catch (Exception localException)
{
for (;;) {}
}
localRequestParams.addBodyParameter("sign", ((String)localObject1).toLowerCase());
new HttpUtils().send(HttpRequest.HttpMethod.POST, paramString, localRequestParams, new RequestCallBack()
{
public void onFailure(HttpException paramAnonymousHttpException, String paramAnonymousString)
{
Toast.makeText(UnifyLucherActivity.this, "获取认证信息失败...", 0).show();
UnifyLucherActivity.a(UnifyLucherActivity.this).ed();
}

public void onStart()
{
UnifyLucherActivity.a(UnifyLucherActivity.this).m(UnifyLucherActivity.this, "正在认证,请稍后...");
}

public void onSuccess(ResponseInfo<String> paramAnonymousResponseInfo)
{
hz.a(paramAnonymousResponseInfo, UnifyLucherActivity.a(UnifyLucherActivity.this));
try
{
Object localObject1 = new org/json/JSONObject;
((JSONObject)localObject1).<init>((String)paramAnonymousResponseInfo.result);
Object localObject2 = ((JSONObject)localObject1).optString("code");
paramAnonymousResponseInfo = ((JSONObject)localObject1).optString("message", "服务器或网络出现错误,请稍后再试~");
if ("000000".equals(localObject2))
{
localObject2 = ((JSONObject)localObject1).getJSONObject("result");
paramAnonymousResponseInfo = ((JSONObject)localObject2).optString("data");
localObject1 = ((JSONObject)localObject2).optString("from");
localObject2 = ((JSONObject)localObject2).optString("key");
UnifyLucherActivity.a(UnifyLucherActivity.this, str1, str2, UnifyLucherActivity.b(UnifyLucherActivity.this), paramString, paramAnonymousResponseInfo, (String)localObject1, (String)localObject2);
}
for (;;)
{
UnifyLucherActivity.a(UnifyLucherActivity.this).ed();
return;
Toast.makeText(UnifyLucherActivity.this, paramAnonymousResponseInfo, 0).show();
UnifyLucherActivity.a(UnifyLucherActivity.this).ed();
}
}
catch (JSONException paramAnonymousResponseInfo)
{
for (;;)
{
paramAnonymousResponseInfo.printStackTrace();
Toast.makeText(UnifyLucherActivity.this, "认证解析出错...", 0).show();
UnifyLucherActivity.a(UnifyLucherActivity.this).ed();
}
}
}
});
}

继续追踪函数进行分析 , 静态分析发现 aF 是一个 DESede (3DES) 的对称算法

查看aF函数

其中 , 发现 3DES 的密匙存放于一个字节数组 zC

1
static final byte[] zC = { 14, -74, 79, 26, 2, -3, 88, 121, 25, -83, 84, 52, 13, 61, -101, 1, 42, 25, 19, 70, 124, -95, -50, 44 };

随后我们使用 Python 进行测试 , 通过解密验证

Python测试

其中由于 Python 和 Java 的 byte[] 不一样 , 我们将 zC 转换成了 Base64 字符串 DrZPGgL9WHkZrVQ0DT2bASoZE0Z8oc4s , 并在解密的时候再解码使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64

import pyDes


def pwd_encrypt(password):
key = "DrZPGgL9WHkZrVQ0DT2bASoZE0Z8oc4s"
key = base64.standard_b64decode(key)
k = pyDes.triple_des(key, pyDes.ECB, IV=None, pad=None, padmode=pyDes.PAD_PKCS5)
d = k.encrypt(password)
res = base64.b64encode(d)
return bytes.decode(res, "utf8")


print(pwd_encrypt("your password"))

注意记得安装第三方库 pyDes

1
pip install pyDes

至此整个过程结束 , 一份名华慕课的工具在我的 GitHub 需要的可以自行 git

总结

通过逆向过程我们发现名华慕课的安卓客户端没有 release 版本进行加密、函数混淆或对加密的函数使用so调用 , 这是不符合产品安全规范的