JavaScript逆向补环境
什么是补环境
补环境是JS逆向中的一个重要手段。何谓补环境?浏览器的中的JS是在浏览器环境中执行的,逆向得出的JS是在本地Node.js环境中执行的,这里就有环境的差异。Node.js中是没有BOM和DOM的,为了让在浏览器中可以执行的代码在Node.js中也能正常执行,有时我们就要补上Node.js缺少的一些BOM和DOM对象等,这就是补环境。
补环境会遇到什么问题
有人说补环境是比较吃经验的,补多了很快就能搞定,新手则半天搞出来还是个有问题的环境。也有人补环境靠猜,运气好就能搞出来,运气不好就不行。还有人会直接使用开源网站上的补环境框架,但是这些框架往往不是万能的,出了问题新手往往又不知道如何把缺少的环境给补上。本文的目的就是利用一个案例,梳理出一套清晰的流程,从零开始补一个完整可用的环境,让大家在面对补环境问题时有一套解决问题的方案。
补环境需要的工具
工欲善其事,必先利其器。这里先介绍几个帮助我们完成补环境的工具。
Web文档
这里我们指的是MDN Web Docs。这里可以查看所有JS对象相关的文档,可以在补环境的时候让我们知道是补一个对象还是函数等等。比如
我们可以知道createElement()是Document对象的一个方法。
Proxy对象
Proxy可以理解为,在目标对象之前设一层"拦截",外界对该对象的访问,都必须通过这层拦截,可以对外界的访问进行过滤和改写(表示可以用它"代理"某些操作,可以翻为“代理器")。详细文档可以参考链接: Proxy - JavaScript | MDN。这里主要在一个函数中使用Proxy对象在本地代码中去监控代码中某些对象的属性获取和赋值,并打印出日志,从而我们就能得知本地代码缺少了哪些属性和对象。函数代码如下
function get_enviroment(proxy_array) {
for (var i = 0; i < proxy_array.length; i++) {
handler = '{\n' +
' get: function(target, property, receiver) {\n' +
' console.log("方法:", "get ", "对象:", ' +
'"' + proxy_array[i] + '" ,' +
'" 属性:", property, ' +
'" 属性类型:", ' + 'typeof property, ' +
// '" 属性值:", ' + 'target[property], ' +
'" 属性值类型:", typeof target[property]);\n' +
// 'if (typeof target[property] == "undefined"){debugger}' +
' return target[property];\n' +
' },\n' +
' set: function(target, property, value, receiver) {\n' +
' console.log("方法:", "set ", "对象:", ' +
'"' + proxy_array[i] + '" ,' +
'" 属性:", property, ' +
'" 属性类型:", ' + 'typeof property, ' +
// '" 属性值:", ' + 'target[property], ' +
'" 属性值类型:", typeof target[property]);\n' +
' return Reflect.set(...arguments);\n' +
' }\n' +
'}'
eval('try{\n' + proxy_array[i] + ';\n'
+ proxy_array[i] + '=new Proxy(' + proxy_array[i] + ', ' + handler + ')}catch (e) {\n' + proxy_array[i] + '={};\n'
+ proxy_array[i] + '=new Proxy(' + proxy_array[i] + ', ' + handler + ')}')
}
}
proxy_array = ['window', 'document', 'location', 'navigator', 'history', 'screen']
get_enviroment(proxy_array)
proxy_array里是默认需要监控的一些常见重要的浏览器对象,这个数组可以自己增加新的需要监控的对象。
Node.js调试工具node-inspect
node-inspect 是Node.js 的内置命令行调试工具,它是一个纯命令行的调试客户端,但是我们可以配合 Chrome DevTools 进行图形化调试。用法很简单,直接在终端输入node-inspect yourscript.js
,然后我们打开chrome的开发者工具
点击这里产生的按钮,则打开一个新的浏览器调试工具窗口,可以在其中调试yourscript.js。
补环境的流程
这里我们以某冶这个网站为例,介绍下补环境的流程。这里涉及瑞数,我们不过多介绍。整个补环境有个总体思想,就是先保证js运行不报错补一些空方法空对象,然后再根据需要将这些空方法空对象更精细的补充完整。
补充基础环境
这里我们先把一些一定缺少的,或者大概率会被检测的环境补上。其中navigator和location需要去网页上的调试工具获取。
// 基础环境
window = global;
window.top = window;
window.addEventListener = function () { }
window.setInterval = function () { }
window.setTimeout = function () { }
window.clearInterval = function () { }
window.clearTimeout = function () { }
navigator = {
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
}
location = {
"ancestorOrigins": {},
"href": "https://www.ouyeel.com/steel/search?pageIndex=0&pageSize=50",
"origin": "https://www.ouyeel.com",
"protocol": "https:",
"host": "www.ouyeel.com",
"hostname": "www.ouyeel.com",
"port": "",
"pathname": "/steel/search",
"search": "?pageIndex=0&pageSize=50",
"hash": ""
}
// 核心必删项
try { delete require; } catch (e) { }
try { delete module; } catch (e) { }
try { delete exports; } catch (e) { }
// 推荐删除项
try { delete Buffer; } catch (e) { }
try { delete setImmediate; } catch (e) { }
try { delete clearImmediate; } catch (e) { }
// 原始目标变量
try { delete __dirname; } catch (e) { }
try { delete __filename; } catch (e) { }
加上代理函数
加上文介绍的利用Proxy跟踪对象的函数,默认跟踪window、document、navigator、history、screen这几个对象
// 基础环境
....
// 代理
function get_enviroment(proxy_array) {
for (var i = 0; i < proxy_array.length; i++) {
handler = '{\n' +
' get: function(target, property, receiver) {\n' +
' console.log("方法:", "get ", "对象:", ' +
'"' + proxy_array[i] + '" ,' +
'" 属性:", property, ' +
'" 属性类型:", ' + 'typeof property, ' +
// '" 属性值:", ' + 'target[property], ' +
'" 属性值类型:", typeof target[property]);\n' +
// 'if (typeof target[property] == "undefined"){debugger}' +
' return target[property];\n' +
' },\n' +
' set: function(target, property, value, receiver) {\n' +
' console.log("方法:", "set ", "对象:", ' +
'"' + proxy_array[i] + '" ,' +
'" 属性:", property, ' +
'" 属性类型:", ' + 'typeof property, ' +
// '" 属性值:", ' + 'target[property], ' +
'" 属性值类型:", typeof target[property]);\n' +
' return Reflect.set(...arguments);\n' +
' }\n' +
'}'
eval('try{\n' + proxy_array[i] + ';\n'
+ proxy_array[i] + '=new Proxy(' + proxy_array[i] + ', ' + handler + ')}catch (e) {\n' + proxy_array[i] + '={};\n'
+ proxy_array[i] + '=new Proxy(' + proxy_array[i] + ', ' + handler + ')}')
}
}
proxy_array = ['window', 'document', 'location', 'navigator', 'history', 'screen']
get_enviroment(proxy_array)
加上需要逆向的核心js代码
这里是瑞数相关的代码,这里就不贴上来了,感兴趣可以在这里下载,最终结果是要输出cookie
// 基础环境
......
// 代理
......
// 核心代码
......
// 逆向结果输出
console.log(document.cookie)
// 对外暴露函数
function RS_5() {
const tm = document.cookie.toString().split(';')[0].split('=')
return {name: tm[0], value: tm[1]}
}
本地执行js根据日志提示补充环境
使用node-inspect执行js后存在几种不同的情况,我们会采取不同的应对方案,然后再重新使用node-inspect执行js直到得到我们想要的逆向结果
没有任何报错
例如下图
日志没有任何报错,这时我们需要重点关注属性值类型为undefined的属性。这里我们需要用到上面介绍的两个工具Proxy和node-inspect。首先我们打开代理函数get_enviroment中的注释
'if (typeof target[property] == "undefined"){debugger}' +
这行代码可以在获取的属性值为undefined是断点停住,然后我们使用node-inspect运行我们的js,在浏览器调试工具中调试。在每个undefined的断点处,我们也给真实的网页上的js打上断点,然后刷新页面,对比我们的js和网页上的js相应的属性是否都是undefined。若不是,我们再利用我们介绍的另外一个工具Web文档,这里我们可以查询到document的createElement是一个我们需要补的方法。
// 基础环境
......
// 补充环境
document = {
createElement(args) {
console.log('document createElement:', args)
debugger;
}
}
// 代理
......
注意,我们在补充的方法里加上了日志和debugger,这些都是帮助我们后续如果需要更精细的补充这些方法时能够帮到我们。
进入到了补充方法的断点中
如果执行js进入到了我们补充的方法加入的debugger处,我们一般需要根据打印的日志,结合Web文档和和node-inspect调试工具,找到浏览器中真实js对应的方法的返回值,并在我们的js中补上同样类型的返回值,例如
刚刚补上的document的createElement方法,传入的参数是div,我们根据调用堆栈找到方法调用处,并在真实浏览器的同样位置打上断点,执行代码我们可以得知document的createElement方法传入参数为div时,返回值是一个对象
此时我们在自己的环境中的document的createElement方法增加如下代码
// 基础环境
......
// 补充环境
div = {
ss: 'div'
}
document = {
createElement(args) {
console.log('document createElement:', args)
if (args === 'div') {
return div
}
debugger;
}
}
// 代理
......
proxy_array = ['window', 'document', 'location', 'navigator', 'history', 'screen', 'div']
get_enviroment(proxy_array)
这里注意三个细节,第一createElement方法中debugger仍保留放在最后,因为可能createElement会接受不同的参数,我们仍需按照上述方法进入到方法内部并最终添加更多的if-else来返回不同的返回值。第二div对象我添加了一个自定义的属性ss(这里可以随意命名,注意不要有冲突),用来标记这个是什么对象方便调试。第三我们将div对象也加入了代理监控中。
遇到未捕获的异常
如果执行js直接报错,我们可以直接利用node-inspect进行调试,例如出现如下报错
直接使用node-inspect,在浏览器调试工具中打开“遇到未捕获的异常时暂停”,运行代码调试工具会在异常处暂停
这里我们之前添加的自定义属性ss派上用场了!可以知道是我们创建的div对象缺少了getElementsByTagName属性,通过查Web文档得出getElementsByTagName是一个方法,从而补上环境代码
// 基础环境
......
// 补充环境
div = {
ss: 'div',
getElementsByTagName(args){
console.log('div getElementsByTagName:', args)
debugger;
}
}
总结
综上,补环境基本思路就是使用node-inspect和Web文档,再加上代理监控对比本地js的属性和网站真实js的属性,两边保持一致。完整补环境代码参考JS逆向-补环境-欧冶金,因为涉及瑞数仅供参考。