文章目录
- JS逆向高阶补充
- eval函数
- Hook函数
- 案例1: Hook eval
- 案例2: Hook JSON.stringify
- 案例3: Hook JSON.parse
- 案例4: Hook Cookie
- Promise对象
- (1)回调函数
- (2)基本语法
- (3)then函数
- (4)链式应用
- (5)Promise与Ajax请求
- webpack
- 字体加密
- 什么是字体加密
- 补环境
- 【1】补环境介绍
- 【2】Proxy代理
- 初始补环境代码
- 瑞数
- 瑞数之无限debugger
- 瑞数流程
- 瑞数5案例解析
- 入口定位
- 补环境
JS逆向高阶补充
eval函数
eval() 函数计算 JavaScript 字符串,并把它作为脚本代码来执行。
如果参数是一个表达式,eval() 函数将执行表达式。如果参数是Javascript语句,eval()将执行 Javascript 语句。
eval(string)
//
eval('[1,2,3,4,5].map(x=>x*x)')
http://tools.jb51.net/password/evalencode
Hook函数
在 JS 逆向中,我们通常把替换原函数的过程都称为 Hook。
function foo() {
console.log("foo功能...")
return 123
}
foo()
var _foo = foo
foo = function () {
console.log("截断开始...")
debugger;
_foo()
console.log("截断结束...")
}
function foo(a, b, c, d, e, f) {
console.log("foo功能...")
console.log(a, b, c, d, e, f)
return 123
}
var _foo = foo
foo = function () {
console.log("截断开始...")
// _foo(arguments)
_foo.apply(this,arguments)
console.log("截断结束...")
}
foo(1,2,3,4,5,6,7)
案例1: Hook eval
console.log("程序开始")
// eval("console.log('yuan')")
window["e"+"v"+"a"+"l"]("console.log('eval yuan')")
console.log("程序结束")
var _eval = eval
eval = function (src) {
console.log("eval截断开始...")
debugger;
_eval.apply(this, src)
console.log("eval截断结束...")
}
案例2: Hook JSON.stringify
JSON.stringify() 方法用于将 JavaScript 值转换为 JSON 字符串,在某些站点的加密过程中可能会遇到,以下代码演示了遇到 JSON.stringify() 时,则插入断点:
(function() {
var stringify = JSON.stringify;
JSON.stringify = function(params) {
console.log("Hook JSON.stringif:::", params);
debugger;
return stringify(params);
}
})();
案例3: Hook JSON.parse
JSON.parse() 方法用于将一个 JSON 字符串转换为对象,在某些站点的加密过程中可能会遇到,以下代码演示了遇到 JSON.parse() 时,则插入断点:
(function() {
var parse = JSON.parse;
JSON.parse = function(params) {
console.log("Hook JSON.parse::: ", params);
debugger;
return parse(params);
}
})();
案例4: Hook Cookie
一般使用Object.defineProperty()来进行属性操作的hook。那么我们了解一下该方法的使用。
Object.defineProperty(obj, prop, descriptor)
// 参数
obj:对象;
prop:对象的属性名;
descriptor:属性描述符;
一般hook使用的是get和set方法,下边简单演示一下
var people = {
name: '张三',
};
var count
Object.defineProperty(people, 'age', {
get: function () {
console.log('获取值!');
return count;
},
set: function (val) {
console.log('设置值!',val);
count = val;
},
});
console.log(people.age);
people.age = 18;
console.log(people.age);
通过这样的方法,我们就可以在设置某个值的时候,添加一些代码,比如 debugger;,让其断下,然后利用调用栈进行调试,找到参数加密、或者参数生成的地方,需要注意的是,网站加载时首先要运行我们的 Hook 代码,再运行网站自己的代码,才能够成功断下,这个过程我们可以称之为 Hook 代码的注入。
(function(){
'use strict'
var _cookie = "";
Object.defineProperty(document, 'cookie', {
set: function(val) {
console.log(val);
debugger
_cookie = val;
return val;
},
get: function() {
return _cookie;
},
});
})()
Promise对象
- Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
- Promise 是一个 ECMAScript 6 提供的类,目的是更加优雅地书写复杂的异步任务。
- Promise是一个构造函数,通过new来实例化,主要解决异步编程。
- 一个Promise对象有三种状态:pending(等待中)、fulfilled(已成功)或rejected(已失败)。当Promise对象处于pending状态时,它表示尚未完成,但可能会在未来某个时间完成。
(1)回调函数
了解promise应该先懂回调,简单理解回调函数能够实现异步编程(可以控制函数调用顺序)。紧接着你应该知道回调地狱,或者函数瀑布,就类似如下的代码:
setTimeout(function () {
console.log("apple");
setTimeout(function () {
console.log("banana");
setTimeout(function () {
console.log("cherry");
}, 1000);
}, 2000);
}, 3000);
console.log("下一个操作")
Promise 的出现就能够将上面嵌套格式的代码变成了较为有顺序的从上到下风格的代码。
(2)基本语法
new Promise(function(resolve, reject) { });
- Promise 构造函数只有一个参数,这个参数是一个函数,这个函数在构造之后会直接被异步运行,所以我们称之为起始函数。起始函数包含两个参数 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。异步任务执行成功时调用resolve函数返回结果,反之调用reject。
- Promise对象的then方法用来接收处理成功时响应的数据,catch方法用来接收处理失败时相应的数据。
案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
function sayHi() {
var promise = new Promise(function (resolve, reject) {
// 模拟Ajax请求
// var data = "hello world"
// console.log("请求完成,响应数据为:", data)
// resolve(data)
setTimeout(function () {
var data = "hello world"
resolve(data)
}, Math.random() * 5000)
})
return promise
}
// 出发promise对象
s = sayHi()
s.then(item => console.log(item + "!!!")) // 异步操作,不影响下面继续执行
// 控制台打印s
/* async function test() {
try {
const result = await sayHi();
console.log("result:::", result)
} catch (error) {
// 当 Promise 状态变为 rejected 时执行的逻辑
// 可以访问到错误信息 error
}
}
test();*/
console.log("下一个操作!")
</script>
</head>
<body>
</body>
</html>
(3)then函数
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数**。**
Promise实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
.then(function (){}, function (){});
如果初始函数里面没有指定resolve或者reject函数,那么 .then 函数是不会被调用的,因为之后状态变成了resolved或者rejected才会调用对应的回调函数。
const promise = new Promise((resolve, reject) => {
console.log("apple");
//resolve()
});
promise.then(() => {
console.log("banana");
});
console.log("cherry");
// 输出 "apple" "cherry"
// 这里没有输出"banana",因为promise实例的状态没有改变,所以没有调用对应状态的回调函数
放开resolve()函数再试一下。结果先cherry后banana是因为 .then方法是微任务,宏任务执行完毕后开始才开始执行微任务。
把上面的resolve函数换成reject()函数:
const promise = new Promise((resolve, reject) => {
console.log("apple");
reject()
});
promise.then(() => {
console.log("banana");
});
console.log("cherry");
因为.then方法并没有定义rejected状态相对应的回调函数。因为.then方法的第2个参数才是rejected状态的回调函数。所以改为
const promise = new Promise((resolve, reject) => {
console.log("apple");
reject()
});
promise.then(() => {
console.log("banana success");
},() => {
console.log("banana reject");
});
console.log("cherry");
如果我们此时再在reject后面加上resolve输出结果和上面一样。这是因为状态只会改变一次,之后不会更改的。如果把最开始的代码resolve放中间会怎样?
const promise = new Promise((resolve, reject) => {
console.log("apple");
resolve("apple") // resolve仅仅设置了状态
console.log("APPLE");
});
promise.then((res) => {
console.log(res+" banana");
}, () => {
console.log("banana");
});
console.log("cherry");
promise执行流程:
1. 构造函数中的输出执行是同步的,输出 apple,执行 resolve 函数,将 Promise 对象状态置为resolved,输出APPLE。
2. 注册这个Promise对象的回调then函数。
3. 宏任务继续,打印cherry,整个脚本执行完,stack 清空。
4. eventloop 检查到 stack为空,再检查 microtask队列中是否有任务,发现了 Promise 对象的 then 回调函数产生的 microtask,推入stack,执行。输出apple banana,eventloop的列队为空,stack为空,脚本执行完毕。
(4)链式应用
使用Promise可以更好地处理异步操作,例如网络请求,文件读取等。它避免了回调地狱(callback hell)的问题,使得代码更加容易理解和维护。
案例1:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
function A() {
var p = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("A请求")
var res = "A-data"
resolve(res)
}, 1000)
})
return p
}
function B() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("B请求")
var res = "B-data"
resolve(res)
}, 2000)
})
}
function C() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("C请求")
var res = "C-data"
resolve(res)
}, 3000)
})
}
function D() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("D请求")
var res = "D-data"
resolve(res)
}, 3000)
})
}
function E() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("E请求")
var res = "E-data"
resolve(res)
}, 3000)
})
}
/*var p1 = A()
var p2 = p1.then(function (res) {
console.log("A获取结果:", res)
return B()
})
var p3 = p2.then(function (res) {
console.log("B获取结果:", res)
return C()
})
p3.then(function (res) {
console.log("C获取结果:", res)
})*/
A().then(function (res) {
console.log("A获取结果:", res)
return B()
}).then(function (res) {
console.log("B获取结果:", res)
return C()
}).then(function (res) {
console.log("C获取结果:", res)
return D()
}).then(function (res) {
console.log("D获取结果:", res)
return E()
}).then(function (res) {
console.log("E获取结果:", res)
})
</script>
</head>
<body>
</body>
</html>
案例2:
// 第一层:获取用户信息
function getUserInfo(userId) {
return new Promise((resolve, reject) => {
// 模拟异步操作,获取用户信息
setTimeout(() => {
const userInfo = {
id: userId,
name: "John Doe",
email: "johndoe@example.com"
};
resolve(userInfo);
}, 1000);
});
}
// 第二层:获取用户订单列表
function getUserOrders(userId) {
return new Promise((resolve, reject) => {
// 模拟异步操作,获取用户订单列表
setTimeout(() => {
const orders = [
{id: 1, product: "Product A"},
{id: 2, product: "Product B"},
{id: 3, product: "Product C"}
];
resolve(orders);
}, 2000);
});
}
// 第三层:获取订单详情
function getOrderDetails(orderId) {
return new Promise((resolve, reject) => {
// 模拟异步操作,获取订单详情
setTimeout(() => {
const orderDetails = {
id: orderId,
status: "Delivered",
address: "123 Main St"
};
resolve(orderDetails);
}, 3000);
});
}
// 应用示例
const userId = 123;
getUserInfo(userId)
.then(userInfo => {
console.log("User Info:", userInfo);
return getUserOrders(userInfo.id);
})
.then(orders => {
console.log("User Orders:", orders);
const orderId = orders[0].id;
return getOrderDetails(orderId);
})
.then(orderDetails => {
console.log("Order Details:", orderDetails);
})
.catch(error => {
console.error("Error:", error);
});
console.log("后续操作!!!")
(5)Promise与Ajax请求
原生Ajax:
function fetchData(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function () {
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(Error(xhr.statusText));
}
};
xhr.onerror = function () {
reject(Error('Network Error'));
};
xhr.send();
});
}
// 调用fetchData()函数并处理Promise对象的结果
fetchData('https://v0.yiketianqi.com/api?unescape=1&version=v9&appid=47284135&appsecret=jlmX3A6s').then(function (response) {
// 如果Promise对象成功解决,执行这里的代码
var data = JSON.parse(response)
console.log("data:::", data)
}).catch(function (error) {
// 如果Promise对象被拒绝,执行这里的代码
console.log('Error loading data:', error);
});
jQuery版本:
function fetchData(url) {
return new Promise(function (resolve, reject) {
$.ajax({
url: url,
success: function (res) {
console.log("res:", res)
resolve(res)
}
})
});
}
// 调用fetchData()函数并处理Promise对象的结果
url="https://v0.yiketianqi.com/api?unescape=1&version=v9&appid=47284135&appsecret=jlmX3A6s"
fetchData(url).then(function (response) {
// 如果Promise对象成功解决,执行这里的代码
const container = $('#container');
const html = response.data.map(item => `<div><span>${item.day}</span><span>${item.wea}</span></div>`).join('');
container.append(html);
})
webpack
Webpack是一个现代的静态模块打包工具,它主要用于前端开发中的模块化打包和构建。通过Webpack,开发者可以将多个模块(包括JavaScript、CSS、图片等)进行打包,生成优化后的静态资源文件,以供在浏览器中加载和运行。
Webpack的主要功能和特点包括:
- 模块化支持:Webpack将应用程序拆分为多个模块,通过模块化的方式管理和加载依赖关系。它支持CommonJS、ES module、AMD等多种模块化规范,并且能够将这些模块打包成最终的静态资源文件。
- 打包和压缩:Webpack可以将多个模块打包成一个或多个最终的静态资源文件。它支持对JavaScript、CSS、图片等资源进行压缩、合并和优化,以减小文件大小,提升加载速度和性能。
- 资源加载管理:Webpack可以处理各种类型的资源文件,例如JavaScript、CSS、图片、字体等。通过加载器(Loader)的配置,Webpack可以对这些资源文件进行转换和处理,使其能够被应用程序正确地引用和加载。
/*
!function(形参){加载器}([模块1,模块2,...])
!function(形参){加载器}({"k1":"模块1","k2":"模块2"})
*/
window = global;
!function (e) {
var t = {};
function n(r) {
if (t[r])
return t[r].exports;
var o = t[r] = {
i: r,
l: !1,
exports: {}
};
e[r].call(o.exports, o, o.exports, n);
return o.exports.exports;
}
window.loader = n;
// n("1002");
}([ function () {
console.log("foo");
this.exports = 100;
},
function () {
console.log("bar");
this.exports = 200;
}]
);
console.log(window.loader(0));
console.log(window.loader(1));
window = global;
!function (e) {
var t = {};
function n(r) {
if (t[r])
return t[r].exports;
var o = t[r] = {
i: r,
l: !1,
exports: {}
};
e[r].call(o.exports, o, o.exports, n);
return o.exports.exports; // 返回 o.exports.exports,而不是整个 o.exports 对象
}
window.loader = n;
// n("1002");
}({
"1001": function () {
console.log("foo");
this.exports = 100; // 直接修改 exports 变量
},
"1002": function () {
console.log("bar");
this.exports = 200;
}
});
console.log(window.loader("1001"));
注 有时候copy过来的webpack里面的代码,可以判断下是否是加/解密所需,不需要,并且处理起来麻烦的话可以选择删掉
字体加密
什么是字体加密
字体加密是页面和前端字体文件想配合完成的一个反爬策略。通过css对其中一些重要数据进行加密,使我们在代码获取的和在页面上看到的数据是不同的。
前端人员通过使用font-face来达到这个目的,font-face是CSS3中的一个模块,他主要是把自己定义的Web字体嵌入到你的网页中。而font-face的格式为:
@font-face {
font-family: <FontName>; # 定义字体的名称。
src: <source> [<format>][, []]*; # 定义该字体下载的网址,包括ttf,eof,woff格式等
}
我们要打开ttf,eof,woff这些格式的字体文件有两种方式:
- https://font.qqe2.com/#
- https://www.bejson.com/ui/font/
字体解密可以基于fontTools 库和dddocr 库来识别出加密值和实际值的对应关系
from fontTools.ttLib import TTFont
import ddddocr
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
def convert_cmap_to_image(cmap_code, font_path_or_data):
"""
:param cmap_code:
:param font_path_or_data: 文件路径/字节流
:return:
"""
img_size = 1024
img = Image.new("1", (img_size, img_size), 255) # 创建一个黑白图像对象
draw = ImageDraw.Draw(img) # 创建绘图对象
# 根据输入类型选择加载方式
if isinstance(font_path_or_data, str): # 文件路径
font = ImageFont.truetype(font_path_or_data, int(img_size * 0.6)) # 调整大小
elif isinstance(font_path_or_data, bytes): # 二进制数据
font = ImageFont.truetype(BytesIO(font_path_or_data), img_size) # 调整大小
else:
raise ValueError("Unsupported input type. Expected str or bytes.")
character = chr(cmap_code) # 将 cmap code 转换为字符
bbox = draw.textbbox((0, 0), character, font=font) # 获取文本在图像中的边界框
width = bbox[2] - bbox[0] # 文本的宽度
height = bbox[3] - bbox[1] # 文本的高度
draw.text(((img_size - width) // 2, (img_size - height) // 7), character, font=font, fill=0) # 绘制文本,并居中显示 需要根据文件调整长宽和大小
return img
def extract_text_from_font(font_input):
"""
:param font_input: 文件地址/字节流对象
:return:
"""
if isinstance(font_input, str): # 处理文件路径的情况
font = TTFont(font_input)
elif isinstance(font_input, bytes): # 处理二进制数据流的情况
font = TTFont(BytesIO(font_input))
else:
raise ValueError("Unsupported input type. Expected str or bytes.")
# font.saveXML("xxx.xml")
ocr = ddddocr.DdddOcr(beta=True, show_ad=False) # 实例化 ddddocr 对象
font_map = {}
for cmap_code, glyph_name in font.getBestCmap().items():
bytes_io = BytesIO()
image = convert_cmap_to_image(cmap_code, font_input) # 将字体字符转换为图像
image.save(bytes_io, "PNG")
text = ocr.classification(bytes_io.getvalue()) # 图像识别
# image.save(f"./img/{text}.png", "PNG") # 保存图像
print(f"Unicode码点:{cmap_code} - Unicode字符:{glyph_name},识别结果:{text}")
font_map[cmap_code] = text
return font_map
补环境
【1】补环境介绍
浏览器环境: 是指 JS代码在浏览器中的运行时环境,它包括V8自动构建的对象(即ECMAScript的内容,如Date、Array),浏览器(内置)传递给V8的操作DOM和BOM的对象(如document、navigator);
Node环境:是基于V8引擎的Js运行时环境,它包括V8与其自己的内置API,如fs,http,path;
Node环境 与 浏览器环境 的异同点可以简单概括如图:
所以我们所说的 “补浏览器环境” 其实是补浏览器有 而Node没有的环境,即 补BOM和DOM的对象;
当我们每次把辛辛苦苦扣出来的 “js加密算法代码”,并且放在浏览器环境中能正确执行后,就需要将它放到Node环境 中去执行,而由于Node环境与浏览器环境之间存在差异,会导致部分JS代码在浏览器中运行的结果 与在node中运行得到的结果不一样,从而影响我们最终逆向成果;
附注:window对象结构图
【2】Proxy代理
JavaScript中的Proxy是一种内置对象,它允许你在访问或操作对象之前拦截和自定义底层操作的行为。通过使用Proxy,你可以修改对象的默认行为,添加额外的逻辑或进行验证,以实现更高级的操作和控制。
Proxy对象包装了另一个对象(目标对象),并允许你定义一个处理程序(handler)来拦截对目标对象的操作。处理程序是一个带有特定方法的对象,这些方法被称为"捕获器"(traps),它们会在执行相应的操作时被调用。
var window = {
username: "tt",
age: 22
}
window = new Proxy(window, {
get(target, p, receiver) {
console.log("target: ", target);
console.log("p: ", p);
// return window['username'];/// 这里如果这样写. 有递归风险的...
// return Reflect.get(...arguments);
return Reflect.get(target, p);
},
set(target, p, value, receiver) {
console.log("设置操作")
Reflect.set(target, p, value);
}
});
console.log(window.username);
console.log(window.age);
window.username = "json"
window.age = 18
function browser_proxy(obj) {
return new Proxy(obj, {
get: function (target, property1) {
console.log('获取对象-->', obj, '属性-->', property1, '值-->', target[property1])
debugger
return Reflect.get(target, property1)
},
set: function (target, property1, value) {
console.log('设置对象-->', obj, '属性-->', property1, '值-->', target[property1])
debugger
Reflect.set(target, property1, value)
}
})
}
基于Proxy的特性,衍生了两种补环境思路:
- Proxy代理浏览器所有的BOM、DOM对象及其属性,再配合node vm2模块提供的纯净V8环境,就相当于在node中,对整个浏览器环境对象进行了代理,JS代码使用任何浏览器环境 api都能被我们所拦截。然后我们针对拦截到的环境检测点去补。
- 搭建补环境框架,用JS模拟浏览器基于原型链去伪造实现各个BOM、DOM对象,然后将这些JS组织起来,形成一个纯JS版浏览器环境,补环境越完善,越接近真实浏览器环境,能通杀的js环境检测就越多。
基于上面的语法,我们构建一套完整的代理补环境的功能函数
function setProxyArr(proxyObjArr) {
for (let i = 0; i < proxyObjArr.length; i++) {
const handler = `{
get: function(target, property, receiver) {
console.log("方法:", "get ", "对象:", "${proxyObjArr[i]}", " 属性:", property, " 属性类型:", typeof property, ", 属性值:", target[property], ", 属性值类型:", typeof target[property]);
return target[property];
},
set: function(target, property, value, receiver) {
console.log("方法:", "set ", "对象:", "${proxyObjArr[i]}", " 属性:", property, " 属性类型:", typeof property, ", 属性值:", value, ", 属性值类型:", typeof target[property]);
return Reflect.set(...arguments);
}
}`;
eval(`try {
${proxyObjArr[i]};
${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler});
} catch (e) {
${proxyObjArr[i]} = {};
${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler});
}`);
}
}
初始补环境代码
delete __dirname
delete __filename
top = self = window = global
delete global
document = {}
location = {}
navigator = {}
history = {}
screen = {}
function getEnv(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,' 属性值类型:',typeof target[property]);
return target[property];
},
set: function(target, property, value, receiver){\n
console.log('方法:set',' 对象:${proxy_array[i]}',' 属性:',property,' 属性类型:',typeof property,' 属性值类型:',typeof target[property]);
return Reflect.set(...arguments);
}
}`
eval(`
try {
${proxy_array[i]};
${proxy_array[i]} = new Proxy(${proxy_array[i]}, ${handler});
} catch (e) {
${proxy_array[i]} = {};
${proxy_array[i]} = new Proxy(${proxy_array[i]}, ${handler});
}
`)
}
}
proxy_array = ['window', 'document', 'location', 'navigator', 'history', 'screen']
getEnv(proxy_array)
瑞数
瑞数信息是一家专注于提供互联网动态业务应用安全防护解决方案的公司**
瑞数动态安全 Botgate(机器人防火墙)以“动态安全”技术为核心,通过动态封装、动态验证、动态混淆、动态令牌等技术对服务器网页底层代码持续动态变换,增加服务器行为的“不可预测性”,实现了从用户端到服务器端的全方位“主动防护”,为各类 Web、HTML5 提供强大的安全保护。
瑞数可以理解为是我们进入到JS逆向世界的标志
过瑞数的方法基本上有以下几种
- 自动化工具
- 补环境
- 纯算
瑞数之无限debugger
// (1)
let _Function = Function;
Function = function (s) {
if (s === "debugger") {
console.log(s)
return null
}
return _Function(s)
}
// (2)
let _constructor = constructor
Function.prototype.constructor = function (s) {
if (s === "debugger") {
console.log(s)
return null
}
return _constructor(s)
}
// (3)
Function.prototype._constructor = Function.prototype.constructor;
Function.prototype.constructor = function () {
if (arguments && typeof arguments[0] === 'string') {
if ("debugger" === arguments[0]) {
return
}
}
return Function.prototype._constructor.apply(this, arguments)
};
瑞数流程
- rs的网站会请求两次page_url(文档请求),第1次page_url(文档请求)会返回一个cookie1,和一个响应体(HTML源码),以及请求响应码是 202(瑞数3、4代)或者 412(瑞数5代、6代)
- 响应体(HTML源码)包括3个部分:
- 一个meta标签,其content内容很长且是
动态
的(每次请求会变化),会在eval执行第二层JS代码时使用到; - 一个外链js文件,一般同一页面中其内容是
固定
的,下面的自执行函数会解密文件内容生成eval执行时需要的JS源码,也就是第二层vm代码; - 一个大自执行函数(每次请求首页都会
动态
变化),主要是解密外链JS内容,给window添加一些属性如$_ts,会在vm中使用; - 这三个要素用于在本地生成一个cookie2,用于第2次请求
- 第2次page_url(文档请求)携带cookie1和ncookie2,获取真正的页面内容!
瑞数 3、4 代有以 T 和 S 结尾的两个 Cookie,其中以 S 开头的 Cookie 是第一次的 201 那个请求返回的,以 T 开头的 Cookie 是由 JS 生成的,动态变化的,T 和 S 前面一般会跟 80 或 443 的数字,Cookie 值第一个数字为瑞数的版本。
瑞数 5 代也有以 T 和 S 结尾的两个 Cookie,但有些特殊的 5 代瑞数也有以 O 和 P 结尾的,同样的,以 O 开头的是第一次的 412 那个请求返回的,以 P 开头的是由 JS 生成的,Cookie 值第一个数字同样为瑞数的版本。
瑞数5案例解析
入口定位
先定位入口,这里涉及一个VM环境
-
“VM”表示的是Virtual Machine(虚拟机),这些文件通常表示由浏览器生成和执行的虚拟机脚本环境中的临时脚本。这些脚本并不是项目源代码的一部分,也不是实际存在的物理文件。 它们在浏览器的内存中创建并执行。
-
比如说,当你在调试一个网页时,如果在某些动态生成并执行的JS代码上设定了断点,Chrome调试器会在一个以"VM"开头的文件中显示这些代码,例如"VM111.js"。这个"VM"文件的存在只是为了调试目的,它并不存在于服务器端,也不会被存储在本地,而是存在于浏览器内存中。一般情况下,这类文件的出现是因为浏览器对JavaScript代码的处理方式,如动态编译或者JavaScript堆栈跟踪。
-
通过eval函数或者new Function方法,Chrome浏览器会创建一个"VM"文件来展示这段临时执行的代码
通过脚本断点可以断住ts和自执行函数,因为第一次响应的index.html是动态的,所以为了后面调试,我们先试用文件替换本地化。
接下来在index.html中直接使用正则快速定位入口函数:
# 1. 搜索call
# 2. 正则搜索
\S{4} = \S{4}\[\S{4}\[\d{2}\]\]\(\S{4},
代码混淆后,找对应入口会比较麻烦,如果上述两个方式都不凑效,那么就只能通过Hook 来找了
这个位置通常在分析瑞数的时候作为入口,图中 _$BH 实际上是 eval 方法,传入的第一个参数 _KaTeX parse error: Expected group after '_' at position 30: …w 对象==,第二个对象 ==_̲wt 是我们前面看到的 VM 虚拟机中的 IIFE 自执行代码。
debug 到这块后, 在入口位置单步调试,即可进入到VM环境!
后面的断点调试就在这个环境中。
补环境
注意content的值和auto.js是关联的,必须保持一致!
然后就是一步步补充需要的环境,基本上set 相关返回空值, get 相关根据情况判断,用得上的就在浏览器中把需要的值扣出来,用不上的返回空值就行,补环境要点就在于,跳过与加密解密相关的逻辑,不报错就行,相关的就要补全