逆向爬虫之补环境专题
一、补环境的原理
浏览器环境和node环境对比:
浏览器下:
node.js
下
当我们辛苦将浏览器环境的加密或者解密入口找到,把加密或者解密的JS的代码拷贝到本地,由node解释器驱动执行的时候,会因为拷贝的JS代码中包括只能由浏览器调用的API,现在被node执行就会报错,为了解决这个问题,我们需要在拷贝的代码环境中模拟补充需要的前端对象,所以我们就非常有必要掌握浏览器接口对象常用的八大前端对象
二、Window对象
-
window:窗口对象,也是最重要的一个对象
addEventListener:f setInterval:f setTimeout:f name selfdocument navigator location screen historytoString:f
window是顶级变量,在浏览器中,任何在全局作用域中声明的变量或函数都会成为 window 对象的属性。例如,如果你声明一个全局变量 var yuan = 10;,你可以通过 window.yuan。
window
仅在浏览器环境中存在。在 Node.js 等其他环境中,顶级对象是global
。 -
document:文档对象:第二重要对象
body documentElement cookiegetElementById:f getElementByTagName:f getElementByClassName:f createElement:ftoString:f
-
navigator:导航对象
userAgent toString:f
-
location:地址栏对象
href toString:f
-
screen:屏幕对象
availHeight availLeft availTop availWidth toString:f
-
history:历史对象
back forward go toString:f
-
localStorage和sessionStorage:本地存储对象
getItem:f setItem:f removeItem:f toString:f
-
element:泛指,标签对象
# 常用标签对象 h1-h6标签 p标签 input标签 div和span标签 img标签 table标签 form标签 canvas标签:toDataURL:f# 标签对象常用属性和方法 ele.style ele.innerHtml ele.getAttribute() ele:ele.getElementById:fele.getElementByTagName:fele.getElementByClassName:f
为了更好的让大家理解我们接下来的实战补环境案例,我先给大家模拟了一套简单的JS逆向代码
function get_sign() {// 黑匣子,省略很多代码// (1) BOM和DOM正常的前端动作window.addEventListener("test")// 搜索框let kw = document.getElementById("kw")let _class = kw.getAttribute("class")// 创建画布let canvas = document.createElement("canvas")let ctx = canvas.getContext("2d");ctx.fillRect(10, 10, 100, 100);// (2) 基于DOM和BOM进行环境校验if (navigator.toString() === '[object Navigator]') {let navLength = navigator.userAgent.lengthreturn "u82d1660a" + navLength // Yuan老师的微信,想深入学逆向爬虫的联系我,结一段善缘!} else {return false}
}console.log(get_sign())
在这里,整层的JS加密代码对于我们而言就是一个黑匣子,有千千万万行代码,甚至做了混淆处理,我们不能去一行行读,看看整个过程到底用到了哪些对象以及对应的属性和方法的。所以这里就涉及到了到了补环境最终的一件事情:代理监控
三、Proxy代理
JavaScript中的Proxy是一种内置对象,它允许你在访问或操作对象之前拦截和自定义底层操作的行为。通过使用Proxy,你可以修改对象的默认行为,添加额外的逻辑或进行验证,以实现更高级的操作和控制。
Proxy对象包装了另一个对象(目标对象),并允许你定义一个处理程序(handler)来拦截对目标对象的操作。处理程序是一个带有特定方法的对象,这些方法被称为"捕获器"(traps),它们会在执行相应的操作时被调用。
var yuan = {username: "yuan",age: 22,wx: "u82d1660a"
}
yuan = new Proxy(yuan, {get(target, p, receiver) {console.log(target, "查询了属性", p)// return window['username'];/// 这里如果这样写. 有递归风险的...// return Reflect.get(...arguments);return Reflect.get(target, p);},set(target, p, value, receiver) {console.log(target, "设置了属性", p, "值为:", value)Reflect.set(target, p, value);}
});yuan.username;
yuan.age;
yuan.username = "rain"
yuan.age = 18
基于Proxy的特性,衍生了基本补环境思路:在本地,用Proxy代理监控浏览器所有的BOM、DOM对象,相当于在node.js中,对整个浏览器环境对象进行了代理,拷贝的加密JS代码使用任何浏览器 api都能被我们所拦截。然后我们针对拦截到的环境检测点去补对应属性和方法。
因为接下来的补环境中,涉及到需要监控的前端对象还是比较多的 ,如果每一个都按着上面对yuan的Proxy代理,就会导致监控代码大量重复不简洁。所以,在这里,爬虫工程师都需要一个简单的功能函数,对需要监控的数组对象循环监控:
function setProxyArr(proxyObjArr) {for (let i = 0; i < proxyObjArr.length; i++) {const objName = proxyObjArr[i];const handler = {get(target, property, receiver) {console.log("方法:", "get", "对象:", objName, "属性:", property, "属性类型:", typeof property, "属性值:", target[property], "属性值类型:", typeof target[property]);return target[property];},set(target, property, value, receiver) {console.log("方法:", "set", "对象:", objName, "属性:", property, "属性类型:", typeof property, "属性值:", value, "属性值类型:", typeof target[property]);return Reflect.set(target, property, value, receiver);}};// 检查并初始化对象let targetObject = global[objName] || {}; // 在 Node.js 环境中使用 globalglobal[objName] = new Proxy(targetObject, handler); // 在 Node.js 中使用 global}
}
接下来,我们就将这段Proxy监控函数注入到刚才我们的案例中:
window = global
window.addEventListener = function () {
}
kw = {getAttribute() {}
}
ctx = {fillRect: function () {}
}
canvas = {getContext() {return ctx}
}
document = {getElementById(id) {console.log("document getElementById by:", id)if (id === "kw") {return kw}},createElement(ele) {console.log("document createElement ", ele)return canvas}
}
navigator = {userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',toString: function () {return '[object Navigator]'}
}function setProxyArr(proxyObjArr) {for (let i = 0; i < proxyObjArr.length; i++) {const objName = proxyObjArr[i];const handler = {get(target, property, receiver) {console.log("方法:", "get", "对象:", objName, "属性:", property, "属性类型:", typeof property, "属性值:", target[property], "属性值类型:", typeof target[property]);return target[property];},set(target, property, value, receiver) {console.log("方法:", "set", "对象:", objName, "属性:", property, "属性类型:", typeof property, "属性值:", value, "属性值类型:", typeof target[property]);return Reflect.set(target, property, value, receiver);}};// 检查并初始化对象let targetObject = global[objName] || {}; // 在 Node.js 环境中使用 globalglobal[objName] = new Proxy(targetObject, handler); // 在 Node.js 中使用 global}
}setProxyArr(["window", "document", "navigator"])function get_sign() {// 黑匣子,省略很多代码// (1) BOM和DOM正常的前端动作window.addEventListener("test")// 搜索框let kw = document.getElementById("kw")let _class = kw.getAttribute("class")// 创建画布let canvas = document.createElement("canvas")let ctx = canvas.getContext("2d");ctx.fillRect(10, 10, 100, 100);// (2) 基于DOM和BOM进行环境校验if (navigator.toString() === '[object Navigator]') {let navLength = navigator.userAgent.lengthreturn "u82d1660a" + navLength // Yuan老师的微信,想深入学逆向爬虫的联系我,结一段善缘!} else {return false}
}console.log(get_sign())
环境验证的JS代码其实一共有两个思路,思路一是基于当前环境是不是浏览器环境,是的话正常执行。思路二是当前环境是不是node环境,如果不是,则正常执行,扩展案例:
window = globaldelete global
delete process
delete Buffer
delete __dirname
delete __filenamewindow.addEventListener = function () {
}
kw = {getAttribute() {}
}
ctx = {fillRect: function () {}
}
canvas = {getContext() {return ctx}
}
document = {getElementById(id) {console.log("document getElementById by:", id)if (id === "kw") {return kw}},createElement(ele) {console.log("document createElement ", ele)return canvas}
}
navigator = {userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',toString: function () {return '[object Navigator]'}
}function setProxyArr(proxyObjArr) {for (let i = 0; i < proxyObjArr.length; i++) {const objName = proxyObjArr[i];const handler = {get(target, property, receiver) {console.log("方法:", "get", "对象:", objName, "属性:", property, "属性类型:", typeof property, "属性值:", target[property], "属性值类型:", typeof target[property]);return target[property];},set(target, property, value, receiver) {console.log("方法:", "set", "对象:", objName, "属性:", property, "属性类型:", typeof property, "属性值:", value, "属性值类型:", typeof target[property]);return Reflect.set(target, property, value, receiver);}};// 检查并初始化对象// let targetObject = global[objName] || {}; // 在 Node.js 环境中使用 global// global[objName] = new Proxy(targetObject, handler); // 在 Node.js 中使用 globallet targetObject = window[objName] || {}; // 在 Node.js 环境中使用 globalwindow[objName] = new Proxy(targetObject, handler); // 在 Node.js 中使用 global}
}setProxyArr(["window", "document", "navigator"])function get_sign() {// 黑匣子,省略很多代码// (1) BOM和DOM正常的前端动作window.addEventListener("test")// 搜索框let kw = document.getElementById("kw")let _class = kw.getAttribute("class")// 创建画布let canvas = document.createElement("canvas")let ctx = canvas.getContext("2d");ctx.fillRect(10, 10, 100, 100);let navLength = navigator.userAgent.length// (2) 基于DOM和BOM进行环境校验if (navigator.toString() === '[object Navigator]') {// (3) 基于node关键字判断是不是浏览器环境const isNode = typeof process !== 'undefined' && typeof global !== 'undefined';if (isNode) {console.log("当前环境是 Node.js");return false} else {console.log("当前环境不是 Node.js");return "u82d1660a".toUpperCase() + navLength // Yuan老师的微信,想深入学逆向爬虫的联系我,结一段善缘!}} else {return false}
}console.log(get_sign())
四、瑞数案例
yuan老师微信
:u82d1660a
1、瑞数介绍
瑞数信息是一家专注于提供互联网动态业务应用安全防护解决方案的公司
瑞数动态安全 Botgate(机器人防火墙)以“动态安全”技术为核心,通过动态封装、动态验证、动态混淆、动态令牌等技术对服务器网页底层代码持续动态变换,增加服务器行为的“不可预测性”,实现了从用户端到服务器端的全方位“主动防护”,为各类 Web、HTML5 提供强大的安全保护。
瑞数可以理解为是我们进入到JS逆向世界的标志
过瑞数的方法基本上有以下几种
- 自动化工具
- 补环境
- 纯算
2、瑞数流程
- rs的网站会请求两次page_url(文档请求),第1次page_url(文档请求)会返回一个cookie1,和一个响应体(HTML源码),以及请求响应码是202或者 412
- 响应体(HTML源码)包括3个部分:
- 一个meta标签,其content内容很长且是
动态
的(每次请求会变化),会在eval执行第二层JS代码时使用到; - 一个ts代码,下面的自执行函数会解密文件内容生成eval执行时需要的JS源码,也就是第二层vm代码;
- 一个大自执行函数(每次请求首页都会
动态
变化),给window添加一些属性如$_ts,会在vm中使用; - 这三个要素用于在本地生成一个cookie2,用于第2次请求
- 第2次page_url(文档请求)携带cookie1和ncookie2,获取真正的页面内容!
瑞数 3、4 代有以 S 和 T 结尾的两个 Cookie,其中以 S 结尾的 Cookie 是第一次的 那个请求返回的,以 T 结尾的 Cookie 是由 JS 生成的,动态变化的,T 和 S 前面一般会跟 80 或 443 的数字,Cookie 值第一个数字为瑞数的版本。
瑞数 5 、6代也有以 S 和 T 结尾的两个 Cookie,但有些特殊的 5 代瑞数也有以 O 和 P 结尾的,同样的,以 O 结尾的是第一次的 412 那个请求返回的,以 P 结尾的是由 JS 生成的,Cookie 值第一个数字同样为瑞数的版本。
3、瑞数案例解析
【1】基本结构与流程
【2】VM环境与入口
先定位入口,这里涉及一个VM环境
-
“VM”表示的是Virtual Machine(虚拟机),这些文件通常表示由浏览器生成和执行的虚拟机脚本环境中的临时脚本。这些脚本并不是项目源代码的一部分,也不是实际存在的物理文件。 它们在浏览器的内存中创建并执行。
-
比如说,当你在调试一个网页时,如果在某些动态生成并执行的JS代码上设定了断点,Chrome调试器会在一个以"VM"开头的文件中显示这些代码,例如"VM111.js"。这个"VM"文件的存在只是为了调试目的,它并不存在于服务器端,也不会被存储在本地,而是存在于浏览器内存中。一般情况下,这类文件的出现是因为浏览器对JavaScript代码的处理方式,如动态编译或者JavaScript堆栈跟踪。
-
通过eval函数或者new Function方法,Chrome浏览器会创建一个"VM"文件来展示这段临时执行的代码
通过脚本断点可以断住ts和自执行函数,因为第一次响应的index.html是动态的,所以为了后面调试,我们先试用文件替换本地化。
接下来在index.html中直接使用正则快速定位入口函数:
# 搜索.call
这个位置通常在分析瑞数的时候作为入口,图中 _\(jL== 实际上是 ==eval== 方法,传入的第一个参数 ==_\)iC 是 Window 对象,第二个对象 _$eB 是我们前面看到的 VM 虚拟机中的 IIFE 自执行代码。
最后在入口位置单步调试,即可进入到VM环境!
后面的断点调试就在这个环境中。
【3】补环境
初始化补环境文件
metaContent = "pDcfDN8f3jz1r7XPqFGsPL6q9rSYtQFpgNsbPWxFArYi81IxUjQqVJWLCSbyMjvmpPyvkJOGoZQizOfI8SGYRSg.ZWDc2MpBkbeiYeTC0T2n07RtcD7cCyQ_xeajMYzzU28381JavZM7FlN4Pz3whmOha766Uu_TEYu3H4pK.j6M9UQzVj.Y2G"self = top = window = global;
window.setTimeout = function () {
}
window.setInterval = function () {
}
window.clearInterval = function () {
}
window.addEventListener = function () {
}
window.XMLHttpRequest = function () {
}
window.HTMLFormElement = function () {}window.ActiveXObject = function ActiveXObject() {
}div = {getElementsByTagName() {return []}
}
script = {getAttribute(attr) {if (attr === "r") {return "m"}},parentElement: {removeChild: function () {}}
}meta = {content: metaContent,getAttribute(attr) {if (attr === "r") {return "m"}},parentNode: {removeChild: function () {}}}document = {createElement: function (ele) {console.log("document createElement", ele)if (ele === "div") {return div} else if (ele === "form") {return {getElementsByTagName: function () {return []}}} else {return {}}},getElementsByTagName(ele) {console.log("document getElementsByTagName", ele)if (ele === "script") {return [script, script]}if (ele === "meta") {return [meta, meta]} else {return []}},appendChild: function () {},removeChild: function () {},documentElement: {addEventListener: function () {}},getElementById() {return {}}
}location = {"ancestorOrigins": {},"href": "https://www.ouyeel.com/search-ng/queryResource/index","origin": "https://www.ouyeel.com","protocol": "https:","host": "www.ouyeel.com","hostname": "www.ouyeel.com","port": "","pathname": "/search-ng/queryResource/index","search": "","hash": ""
}navigator = {appCodeName: "Mozilla",appName: "Netscape",appVersion: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",webdriver: false
}