android怎么调试已经上线的H5呢? 这里以某APP为例, 展示了如何HOOK WebView, 以及如何调式JS加密算法
准备工作
在android中, 一般对H5进行调试, 是打开chrome 输入 chrome://inspect/#devices
然后选择调试的app内嵌网页即可 但是对于已经发布的release版本, 如果Webview未开启调试, chrome是看不见可调试H5的
具体API
android.webkit.WebView.setWebContentsDebuggingEnabled(boolean enabled)
对于release的app, 一般有几种方式打开这个标识
- app增加设置功能
- 修改smail代码二次打包
- 通过xposed hook api 三种方案难易程度来看, 第三种最难, 那我们就直接上最难的方式, 本次案例以某友商电商app为例
笔者这里选择pixel进行root&刷入xposed框架. Hook的api
Class clz = loader.loadClass("android.webkit.WebView"); Method method = clz.getMethod("setWebContentsDebuggingEnabled", boolean.class); method.invoke(clz, true);}
loader
传入为hook app的classloader 这里如果app<font color=Red>**有壳 **</font>, <font color=Red>**有壳 **</font>, <font color=Red>**有壳 **</font>重要的事儿说三遍, 还需要绕一下~
思路是先hook ClassLoader
XposedHelpers.findAndHookMethod("java.lang.ClassLoader", loader, "loadClass", String.class, Boolean.TYPE, this);
然后在回调hook相关的class
@Override protected void afterHookedMethod(MethodHookParam param) { ... Class = (Class) param.getResult(); ClassLoader tmpLoader = loadClass.getClassLoader(); Class cls = null; try { cls = XposedHelpers.findClass(clsName, tmpLoader); } catch (Throwable e) { e.printStackTrace(); } if (cls != null) { //your code } .... }
Hook完成之后, 打开相关H5, 在chrome就能看到inspect选项啦~
分析点
so, lets begin 分析接口算法 我们要分析的接口如下 ![Alt text](https://haitao.nos.netease.com/9ee8bf01-b8ba-4511-95ab-6e0c321de7e8_2252_1338.jpeg) 主要是探究这个dfpToken是如何生成的, 这里我们提取关键字
getDrip.do` 下面分析会用到 在chrome点击inspect, 会打开一个新窗口, 第一次可以cmd+R 进行reload一次(加载H5源码)
然后分析H5的框架层, 加载了哪些JS
在上图的的js文件里面, 搜索到了关键字getDrip.do
断点1
HttpPostAsync("/m/newsign/getDrip.do", { dfpToken: window.riskManage.getDfpToken(), channelType: window.myDevice }).catch(function(t) { Et.enQueue(X.GetDripTimeOut) });
从上面代码中, 知道是一个POST请求, path是/m/newsign/getDrip.do
, 参数有dfpToken
& channelType
其中dfpToken
是调用riskManage.getDfpToken(), 这里是我们希望分析的 因此在代码中dfpToken: window.riskManage.getDfpToken()
这里我们进行分析. 在console里面输入 window.riskManage
回车, 看看这个riskManage到底是个什么鬼~ 这里依次展开定位为index.min.js
断点2
这里js方法为
key: "getDfpToken", value: function() { if (window._dfp && window._dfp.getToken) return _dfp.getToken(); s.error("getDfpToken() error: no _dfp or _dfp.getToken") }
继续分析window._dfp.getToken
, 定位到下一段JS代码
断点3
如上图我们在断点3下断. console里面执行window._dfp.getToken()
, 会在断点3暂停
在断点3处, 我们继续输入E
, p()
发现结果如下:
分析到E
是内存缓存, p()
是真正生成Token的方法, 因此每次在p()
下断的时候, 需要在console把E
手动置为undefined
断点4
在console, 输入p
, 点击输出的函数, 可定位到函数p()
并且下断设为断点4
在console我们先执行E = undefined
, 然后继续执行到p
函数 执行SY0
,qsK
,Shl
等对象, 发现console输出的都是Xu2
对象, 在Xu2
对象里面, 找到toString()方法如下
!(SY0 = (n1p[(n1p.toString = function() { return d7c(this.s)}
断点5
在断点5进行下断 继续执行到断点5 执行命令d7c
在d7c
函数下断如下 继续执行, 这里大致格式化了下函数如下
function(n, r, t, o) { if (C[n]) return C[n]; r = "" t = NiV0(n).fb5$(lMWq[0]).split("") for (o = 0; o < t.length; o++) { r += i.charAt(e.indexOf(t[o])); } return C[n] = r}
对其中NiV0(n).fb5$(lMWq[0])
进行初步判断是一个无意义函数, 增加阅读难度而已
NiV0(n).fb5$(lMWq[0]) -> "0x0Io" (n='0x0Io')NiV0("netease").fb5$(lMWq[0]) -> "netease"NiV0(".*&").fb5$(lMWq[0]) -> ".*&"
其中e
, i
都是常量
e -> "VF=hydBigRU9.fAOS/&p4_qawLsG1rmtnck leZJ-j?P5T6EWbu7MDzI3v2,8KXCQHoN0x:Y"i -> "- ,:&=?zxwvqkjZYXWVUSRQPONMLKJGFDA9876543210.HT_hCbusfiadIyBnmEeg/lctorp"
因此上面的函数也就是解混淆的过程. 再次回到断点4 去混淆的代码翻译如下:
function p(n){ var minddle = inner() return "TH"+ minddle + MD5(middle).substr(0, 4);}function inner(){ var r = 19; var t = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""); var o = new Date().getTime().toString(16); var i = 8; if (i >= r) return o.substr(0, r); var C = []; for (e = 0; e < i; e++) C[e] = t[0 | Math.random() * t.length]; if (4 <= i) return C.slice(0, 4).join("") + o + C.slice(4).join(""); return C.join("") + o}
这段函数的大致思路是
生成25位定长字符串,格式为TH+$middle$+MD5($middle$).substr(0, 4)middle = 随机4位+16进制时间戳+随机4位
p
函数解析完毕, 回到断点3, 继续分析
函数大致还原如下
function(n) {//n = undefined E || (E = p()); try { M < ++t && (E = p(), _fp.collect && _fp.collect(E)) } catch (r) { } return typeof n === "function" && n(E), E}
这里简单理解就是判断是否存在E存在直接返回, 不存在调用p
函数生成dfpToken
至此, js算法还原完毕.