对某网站加密混淆后的 javascript 代码也算分析了一段时间了,虽然还没搞出来,但多少有些心得,这里记录一下。
工具和资料
前一篇文章 – 记录了之前尝试的一些方案
awesome-java-crawler – 我收集的爬虫相关工具和资料
java-curl – 本人写的 java HTTP 库,可用来替换 chrome 网络后端,更方便控制底层行为,如缓存、代理、监控、修改请求和应答等
cdp4j – java 版的 Chrome Devtools Protocol 实现,用于控制 Chrome 浏览器。最大的特点就是没有“特点”,你懂的……
beautifier.io – js 代码在线格式化
estree – ECMAScript 抽象语法树 (AST) 业界标准
ECMAScript262 语言规范 – 帮助理解 estree
acornjs – ECMAScript 编译器前端,将 js 源码解析成 estree 格式的 AST
astring – ECMAScript 代码生成器,将 AST 重新还原成 js 源码
nashorn – java8 以上自带的 javascript 解释器,性能接近原生 node
java 中调用 npm 模块 – 我一直用的 java 和 kotlin,为了调用 js 原生库,需用这个方案
类似网站的破解 – 神箭手云的大佬写的
很早的一篇分析文 – 看特征是这种加密的早期版本
分析过程
获取 javascript 代码
加密的核心代码只有一小部分是直接写在网页的 <script> 里面的,有些代码是 eval 出来的
可以用 cdp4j 监听 Debugger.ScriptParsed 事件,并在监听器中调用 Debugger.getScriptSource 来获取 js 代码文本
这样是可以获取到所有前端 javascript 源码的,即使源码在网络应答中是加密的,但用 eval 执行前也必须还原为合法的 js 源码
为了方便分析,可将代码保存为文件。该网站 js 会用定时器不断重复 eval 一段代码,因此可以用 ScriptParsed.hash 作为文件名,避免重复保存文件
获取常量映射
拿到 js 之后,格式化一下,发现还是一团乱麻,所有的变量,函数都是 ”_$xx”,可读性约等于 0
在 Chrome 控制台里试了一下,发现全局变量和函数都保存在 window 中了
一部分无参调用的函数,其实返回的就是常量字符串
还有一些_$xx.call 的,看了一下,其实就是系统方法,比如 String.fromCharChode 等
因此可以编写一段代码,遍历 window 对象中所有形似_$xx 的成员,这样既可将常量字符串映射、系统方法映射等搞出来
可读性还原
拿到映射关系之后是不是简单用正则表达式替换回去就万事大吉了呢?哪有那么简单!函数的局部变量、局部函数有很大可能性和全局变量重名,如果用正则无脑替换回去绝对会被坑死!!要是代码少倒也罢了,这里可有 5000 行代码,差之毫厘谬以千里!
另外,不同函数的局部变量也存在大量重名,静态分析时干扰严重,因此,应该将局部变量也替换成唯一且更有意义的名字,比如 < 函数名 >_< 变量索引 >
因此,正确的方法是基于编译原理进行语法级别的替换,看到这里是不是要弃疗了?老子爬点数据还要写编译器?!
还好,js 上已经有很成熟的业界标准和若干老练的第三方库了,至少不用从龙书搞起……
我这里选择了 acornjs 和 astring,前者用于将 js 源码解析成抽象语法树 AST,后者将 AST 还原成 js 源码。当然,在 AST 上我们是可以上下其手的……
为了在 java 代码中运行 acornjs 和 astring,请参见参考中《java 中调用 npm 模块》一文。注意 astring 还依赖 endswith 和 repeat 两个 polyfill,均可以 npm 下载到
用 acorn.parse()搞到 AST 之后,用递归的方式扫描每个节点
进入每个 FunctionDeclaration/FunctionExpression 节点前,创建一个新的作用域对象放到栈顶,里面放该域内所有局部变量(含函数的参数)和新名称的映射表;退出时将栈顶弹出
遇到 Identifier 节点,首先在作用域栈中自顶向下依次寻找当前变量名,找到了,则是本方法局部变量或闭包外局部变量,用新名字替换之;否则,则是全局变量,去映射表中查找
注意,遇到 CallExpression 则要特殊处理,前面的 AST 变换只涉及修改标识符名,而为了将_$xx()变换为 ”xxx”,则涉及到结构变换,要把 CallExpression 节点修改为 Literal 节点并添加 value 属性
全部处理完成后,就可以用 astring.generate()产生还原后的代码了
代码分析
上面步骤完成后,这代码至少勉强能看了,别放松,后面还有无数的坑……还原前的代码只能是让人一脸懵逼,还原后的代码则足以让人咬牙切齿啊,多大仇啊,满满登登 5000 行全是正面硬钢的……我现在也就还卡在这一步,这里先记录一部分已经发现的反破解手法吧。
不断主动中断干扰调试,并检测是否有动态分析行为
var eI_v1 = window[“eval”](“(function() {var a = new Date(); debugger; return new Date() – a > 100;}())”);
_$n1 = _$n1 || eI_v1;
// 这个在上篇文章分析了,在这找到调用来源了。注意,在可读性还原之前这货长这样:
var _$pW = _$u9[_$mz()](_$oi());
_$n1 = _$n1 || _$pW;
js 代码动态混淆
上一篇文章已经说过了,每次刷新 js 代码都会完全变化,包括全局 / 局部变量名、函数排列顺序等
设断点会被干扰,而且代码无法重复执行对于调试意味着什么?
检查关键函数是否被注入替换
function __RW_checkNative(rh_p0, rh_p1) {// 函数名我手动改的
try {
var rh_v2 = Function[“prototype”][“toString”][“apply”](rh_p0);
var rh_v3 = new RegExp(“{\\s*\\[native code\\]\\s*}”);
if (typeof rh_p0 !== “function” || !rh_v3[“test”](rh_v2) || rh_p1 != undefined && rh_p0 !== rh_p1) __GL_undefined_$sy = true;
} catch (_$r0) {}
}
会用这个函数检测 eval, Function, setTimeout, setInterval 几个系统函数是不是被注入了
搞明白了,就可以用一些手段骗过去,不明白的话……
检测当前窗口是否隐藏状态
document[“addEventListener”](“visibilitychange”, _$r0);
会监控当前窗口是否在最上方,要是用 cdp4j 多开浏览器同时爬取……
检测 Selenium, WebDriver, PhantomJS
var rm_v5 = “_Selenium_IDE_Recorder,_selenium,callSelenium”
, rm_v6 = “__driver_evaluate,__webdriver_evaluate,__selenium_evaluate,__fxdriver_evaluate,__driver_unwrapped,__webdriver_unwrapped,__selenium_unwrapped,__fxdriver_unwrapped,__webdriver_script_func,__webdriver_script_fn”
, rm_v7 = [“selenium”, “webdriver”, “driver”];
if (_$un(window, “callPhantom,_phantom”)) {…}
看到这里想必就知道会发生些什么了……
Hook 住 AJAX
var ec_v4 = window[“XMLHttpRequest”];
if (ec_v4) {
var ec_v5 = ec_v4[“prototype”];
if (ec_v5) {
__GL_f_open = ec_v5[“open”];
__GL_f_send = ec_v5[“send”];
ec_v5[“open”] = function () {
_$t5();
arguments[1] = _$pK(arguments[1]);
return __GL_f_open[“apply”](this, arguments);
};
} else {…}
}
检查 navigator 是否是伪造的
var hi_v14 = window[“navigator”];
for (hi_v11 in hi_v14) {
try {
hi_v13 = hi_v14[“hasOwnProperty”](hi_v11);
} catch (_$r0) {
hi_v13 = false;
}
}
如果你的 navigator 对象是自己注入的水货版本,那就露馅了……
检查浏览器特征
这块代码很复杂,还没分析完,现在能看出来的包括:navigator.languages – 据说在 headless chrome 中是没有这个字段的 navigator.plugins – 据说无头和有头的 chrome 返回的插件列表不一样
WebGL 能力检查
有一大段代码是在 canvas 上用 webgl 绘图,没搞过 webgl,现在还不明白,但肯定也是检查浏览器特征手段之一
破解思路
其实并没有,还在抓瞎中……
一些很不成熟的点子:
可以拦截 window 对象的写入,把全局函数通过 toString()搞到源码,再通过 acorn/astring 大法搞事情之后替换成 eval 版本
但是函数名、变量名全部都混淆了,因此需要用一些特征来检测每个函数,目前想到的点子是将 AST 上所有节点分类计数连接成字符串,这样绝大多数方法的特征都是唯一的
需要将浏览器特征检测代码在桌面浏览器、手机浏览器、桌面无头浏览器分别运行,看看到底有啥区别,然后注入代码修改特征进行欺骗