- 测试环境 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 这里不做过多阐释
request1 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 信息做处理实际测试中以实际值为准 , 可以看到提交的字段有 cmd、client、level、spoc、sign
搜索方法
打开 ApkIDE 加载名华慕课安卓客户端 , 然后使用全局搜索 cmd 字段内容 course.subject
可以看到使用了 course.subject 的有两个类
com/wisedu/mooccloud/mhaetc/phone/ui/HomeActivity
com/wisedu/mooccloud/mhaetc/phone/service/ZhiTuService
根据类名推测分析 , 我们需要进行分析的为 HomeActivity
查看其 Java 源代码 , 静态分析函数 eP
发现 sign 是由 localObject2 赋值 , localObject2 由 localObject1 赋值 , localObject1 由函数 ig.d赋值 , 于是继续追踪函数
追踪函数
查看 ig.d
发现 return 处由另外函数 aE 赋值 , 继续查看 aE 函数 , 其中又使用了 e 函数进行复制
发现是一个 摘要算法 MD5 的加密
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 算法解密结束
password 算法解密
根据 Fiddler 截获的封包中 , 发现登录的 HTTP 封包如下
request1 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 信息和用户凭据字段做处理实际测试中以实际值为准 , 将其 URLDecode 后 dk**h1DI%3D%0A为 dk**h1DI=**
因此推测是一串 Base64 编码的字符串 , 先对其进行直接解码后发现为乱码 , 因此这不是一个单纯的 Base64 编码与百度贴吧客户端不同 , 再次搜索方法 , 找到位于类
com/wisedu/mooccloud/mhaetc/phone/ui/UnifyLucherActivity
中的 as函数 , 静态分析发现 , password 是由 localObject1 赋值 , localObject1 是由函数 ig.aF 赋值
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) 的对称算法
其中 , 发现 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 和 Java 的 byte[] 不一样 , 我们将 zC 转换成了 Base64 字符串 DrZPGgL9WHkZrVQ0DT2bASoZE0Z8oc4s , 并在解密的时候再解码使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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
至此整个过程结束 , 一份名华慕课的工具在我的 GitHub 需要的可以自行 git
总结
通过逆向过程我们发现名华慕课的安卓客户端没有 release 版本进行加密、函数混淆或对加密的函数使用so调用 , 这是不符合产品安全规范的