声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
逆向目标
- 目标:某音网页端用户信息接口 X-Bogus 参数
- 接口:
aHR0cHM6Ly93d3cuZG91eWluLmNvbS9hd2VtZS92MS93ZWIvdXNlci9wcm9maWxlL290aGVyLw==
什么是 JSVMP?
JSVMP 全称 Virtual Machine based code Protection for java script,即 JS 代码虚拟化保护方案。
JSVMP 的概念最早应该是由西北大学2015级硕士研究生匡开圆,在其2018年的学位论文中提出的,论文标题为:《基于 WebAssembly 的 java script 代码虚拟化保护方法研究与实现》,同年还申请了国家专利,专利名称:《一种基于前端字节码技术的 java script 虚拟化保护方法》,网上可以直接搜到,也可在公众号【K哥爬虫】后台回复 JSVMP,免费获取原版高清无水印的论文和专利。本文就简单介绍一下 JSVMP,想要详细了解,当然还是建议去读一下这篇论文。
JSVMP 的核心是在 java script 代码保护过程中引入代码虚拟化思想,实现源代码的虚拟化过程,将目标代码转换成自定义的字节码,这些字节码只有特殊的解释器才能识别,隐藏目标代码的关键逻辑。在匡开圆的论文中,利用 WebAssembly 技术实现了特殊的虚拟解释器,通过编译隐藏解释器的执行逻辑。JSVMP 的保护流程如下图所示:
一个完整的 JSVMP 保护系统,大致的架构应该是这样子的:服务器端读取 java script 代码 —> 词法分析 —> 语法分析 —> 生成AST语法树 —> 生成私有指令 —> 生成对应私有解释器,将私有指令加密与私有解释器发送给浏览器,然后一边解释,一边执行。
JSVMP 有哪些学习资料?
除了匡开圆的论文以外,还有以下文章也值得学习:
JSVMP 逆向方法有哪些?
就目前来讲,JSVMP 的逆向方法有三种(自动化不算):RPC 远程调用,补环境,日志断点还原算法,其中日志断点也称为插桩,找到关键位置,输出关键参数的日志信息,从结果往上倒推生成逻辑,以达到算法还原的目的,RPC 技术K哥以前写过文章,补环境的方式以后有时间再写,本文主要介绍如何使用插桩来还原算法。
抓包情况
随便来到某个博主主页,抓包后搜索可发现一个接口,返回的是 JSON 数据,里面包含了博主某音号,认证信息、签名,关注、粉丝、获赞等,请求 Query String Parameters
里包含了一个 X-Bogus
参数,每次请求会改变,此外还有 sec_user_id
是博主主页 URL 后面那一串,webid
直接请求主页返回内容里就有,msToken
与 cookie 有关,清除 cookie 访问,就没这个参数了,实测该接口不验证 webid
和 msToken
,直接置空即可。
逆向分析
这条请求是 XHR 请求,所以直接下个 XHR 断点,当 URL 中包含 X-Bogus
参数时就断下:
往前跟栈,来到一个叫 webmssdk.js 的 JS 文件,这里就是生成参数的主要 JS 逻辑了,也就是 JSVMP,整体上做了一个混淆,这里可以使用 AST 来解混淆,K哥以前同样也写过 AST 的文章,这里还原混淆不是重点,咱们直接使用 V 佬的插件 v_jstools 来还原:
还原后使用浏览器的 Overrides 替换功能将 webmssdk.js 替换掉,往上跟栈,如下图所示,到 W 这里就已经生成了 X-Bogus
了,this.openArgs[1]
就是携带了 X-Bogus
的完整 URL,仔细观察这段代码,有很多三元表达式,当 M 的值为 15 时,就会走到这段逻辑,U 的值生成之后,有一个 S[C] = U
的操作。
再往上看代码,S 是一个数组,单步调试的话会发现代码会一直走这个 if-else
的逻辑,几乎每一步都有 S 数组的参与,不断往里面增删改查值,for 循环里面的 I 值,决定着后续 if 语句的走向,这里也就是插桩的关键所在,如下图所示:
插桩分析
大的 for 循环和 if-else 逻辑有两个地方,为了保证最后的日志更加详细完整,在这两个地方都下个日志断点(右键 Add logpoint
),断点内容为:
"位置 1", "索引I", I, "索引A", A, "值S: ", JSON.stringify(S, function(key, value) {if (value == window) {return undefined} return value})
"位置 2", "索引I", I, "索引A", A, "值S: ", JSON.stringify(S, function(key, value) {if (value == window) {return undefined} return value})
插桩输出 S 的时候为什么要写这么长一串呢?首先 JSON.stringify()
方法的作用是将 java script 值转换为 JSON 字符串,基础语法是 JSON.stringify(value[, replacer [, space]])
,如果不将其转换成 JSON,那么 S 的值,输出可能是这样的:[empty, Array(26), 1, Array(0)]
,你看不到 Array 数组里面具体的值,该方法有个可选参数 replacer,如果 replacer 为函数,则 JSON.stringify
将调用该函数,并传入每个成员的键和值,在函数中可以对成员进行处理,最后返回处理后的值,如果此函数返回 undefined,则排除该成员,举个例子:
var obj1 = {key1: 'value1', key2: 'value2'}
function changeva lue(key, value) {
if (value == 'value2') {
return '公众号:K哥爬虫'
} return value
}
var obj2 = JSON.stringify(obj1, changeva lue)
console.log(obj2)
// 输出:{"key1":"value1","key2":"公众号:K哥爬虫"}
上面的代码中 JSON.stringify
传入了一个函数,当 value
为 value2
的时候就将其替换成字符串 公众号:K哥爬虫
,接下来我们演示一下当 value
为 window
时,会发生什么:
根据报错我们可以看到这里由于循环引用导致异常,要知道在插桩的时候,如果插桩内容有报错,就会导致不能正常输出日志,这样就会缺失一部分日志,这种情况我们就可以加个函数处理一下,