猿人学第二届-第一题 逆向分析详解
一、抓包分析
API接口
https://match2023.yuanrenxue.cn/api/match2023/1
关键参数
token- 计算出的密钥now- 时间戳page- 页码
初步推测
根据参数特征,推测加密方式可能为 now + page 进行 AES 编码。
二、逆向过程
1. 定位token生成位置
要找到 token 的计算位置,只需找到接口的请求位置,查看请求体内的参数赋值过程即可定位到加密函数。这里采用最简单的方式:在开发者工具中找到接口请求,查看启动器位置(也就是请求走过的所有堆栈)快速定位发送位置。


2. 关键代码分析
g = '666yuanrenxue66' // AES密钥
// e = 时间戳, d = 页码, g = AES密钥
h = f['AES']['encrypt'](e + String(d), g, {
'mode': f['mode']['ECB'],
'padding': f['pad']['Pkcs7']
})
'token': f['MD5'](h['toString']())['toString']()
这是一个 AES-ECB 加密方式,加密内容为时间戳 + 页码,密钥为 g。但这里的 MD5 调用与正常的函数调用不一样,需要查看 f 指向的是什么。
3. 模块加载器分析
function a(b, c, d) {
function f(j, k) {
if (!c[j]) {
if (!b[j]) {
var l = 'function' == typeof require && require;
if (!k && l)
return l(j, !0x0);
if (g)
return g(j, !0x0);
var m = new Error('Cannot\x20find\x20module\x20\x27' + j + '\x27');
throw m['code'] = 'MODULE_NOT_FOUND', m;
}
var q = c[j] = {
'exports': {}
};
b[j][0x0]['call'](q['exports'], function(s) {
var v = b[j][0x1][s];
return f(v || s);
}, q, q['exports'], a, b, c, d);
}
return c[j]['exports'];
}
for (var g = 'function' == typeof require && require, h = 0x0; h < d['length']; h++)
f(d[h]);
return f;
}
f = a('crypto-js')
f 获取了 a 内的 crypto-js 模块,而 a 是一个自制的模块加载器。可以理解为 f 等同于 CryptoJS = require('crypto-js')。
4. 魔改加密函数陷阱
网站使用自定义模块加载器而不直接用 require('crypto-js'),这里存在一个坑:魔改加密函数。
网站将原生加密函数库代码写到本地,修改其中的逻辑后调用,导致使用未魔改的导入库与已魔改的出参不一致。
可以用以下代码验证:
const plaintext = e + String(d);
const key = g;
const h = CryptoJS.AES.encrypt(plaintext, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
三、补环境方案
方案选择
有两种方式:
- 将函数找齐,保存到本地重命名后调用
- 直接补环境,将 a 导出到全局变量中,在外部调用
我们采用方式二:补环境。
1. 复制代码并处理报错
将代码全文复制到本地 JS 中运行,发现报错在发送网络请求的地方。因为我们不需要发送网络请求,将此块注释掉即可。


2. 导出模块到全局变量
let cyjs // 定义全局变量
(function() {
function a(b, c, d) {
function f(j, k) {
if (!c[j]) {
if (!b[j]) {
var l = 'function' == typeof require && require;
if (!k && l)
return l(j, !0x0);
if (g)
return g(j, !0x0);
var m = new Error('Cannot\x20find\x20module\x20\x27' + j + '\x27');
throw m['code'] = 'MODULE_NOT_FOUND', m;
}
var q = c[j] = {
'exports': {}
};
b[j][0x0]['call'](q['exports'], function(s) {
var v = b[j][0x1][s];
return f(v || s);
}, q, q['exports'], a, b, c, d);
}
return c[j]['exports'];
}
for (var g = 'function' == typeof require && require, h = 0x0; h < d['length']; h++)
f(d[h]);
return f;
}
return a;
}()({
0x1: [function(a, b, c) {}, {}],
0x2: [function(a, b, c) {
cyjs = a // 导出模块
console.log("模块获取", cyjs)
// ...
}
})
3. 编写测试逻辑
time = '1765086350541'
page = '1'
sign = '666yuanrenxue66'
f = cyjs('crypto-js')
h = f['AES']['encrypt'](time + String(page), sign, {
'mode': f['mode']['ECB'],
'padding': f['pad']['Pkcs7']
})
token = f['MD5'](h['toString']())['toString']()
if(token === "2a1de47d1e0d001c595ab047ddd3c3cb"){
console.log("成功:", token)
} else {
console.log("失败", token)
}
输出:
模块获取 [Function (anonymous)]
失败 9333a5a18a763c9955653eccdcd8b166
参数保持一致,但计算出来的数据还是不一样。这就需要进入补环境的核心:针对环境检测进行针对性补入,模拟真实浏览器。
4. 补入三件套
window = global
Window = function Window(){}
Object.setPrototypeOf(window, Window.prototype)
delete global;
document = {}
Document = function Document(){}
Object.setPrototypeOf(document, Document.prototype)
navigator = {}
Navigator = function Navigator(){}
Object.setPrototypeOf(navigator, Navigator.prototype)
location = {}
Location = function Location(){}
Object.setPrototypeOf(location, Location.prototype)
运行后发现加密参数变动了,说明补的环境是有作用的。接下来需要接路由,查看函数中到底获取了哪些属性。
5. 路由函数监控
function watch(obj, obj_name) {
if (obj === undefined || obj === null) {
console.log(`警告: "${obj_name}" 是 ${obj === null ? 'null' : 'undefined'},跳过代理`);
return obj;
}
if (typeof obj !== 'object' && typeof obj !== 'function') {
console.log(`警告: "${obj_name}" 不是对象或函数类型,而是 ${typeof obj},跳过代理`);
return obj;
}
const handler = {
get(target, property, receiver) {
if (property === 'toJSON') {
return target[property];
}
console.log(
`方法: get | 对象: "${obj_name}" | 属性: ${typeof property === 'symbol' ? property.toString() : property} | 属性类型: ${typeof property} | 属性值类型: ${typeof target[property]}`
);
return Reflect.get(target, property, receiver);
},
// ... 其他 handler 方法
};
return new Proxy(obj, handler);
}
location = watch(location, 'location')
输出:
方法: getPrototypeOf | 对象: "navigator" | 获取原型链
方法: get | 对象: "location" | 属性: href | 属性类型: string | 属性值类型: undefined
失败 932497bfb68d0ba03c0b071493e0e9a6
6. 补入 location.href
在浏览器控制台查看真实环境:
location.href
// 'https://match2023.yuanrenxue.cn/topic/1'
补入这个值:
location = {
'href': 'https://match2023.yuanrenxue.cn/topic/1'
}
Location = function Location(){}
Object.setPrototypeOf(location, Location.prototype)
location = watch(location, 'location')
输出:
方法: get | 对象: "location" | 属性: href | 属性类型: string | 属性值类型: string
失败 06ada6373be1f4b15da20f9fbb239d01
补入 href 后值发生了极大变化,说明环境补对了,但密钥依然错误。此时已经没有其他调用了,需要去读代码,而不是盲目添加更多浏览器对象。
7. 发现可疑代码
for (var B = 0x0; B < z; B++) {
if (B < x)
A[B] = w[B];
else {
u = A[B - 0x1];
if (!(B % x))
u = u << 0x8 | u >>> 0x18,
u = i[u >>> 0x18] << 0x18 | i[u >>> 0x10 & 0xff] << 0x10 | i[u >>> 0x8 & 0xff] << 0x8 | i[u & 0xff],
u ^= s[B / x | 0x0] << 0x18;
else
x > 0x6 && B % x == 0x4 && (delete window,
window = 0x0,
u = window ? i[u >>> 0x1a] << 0x18 | i[u >>> 0x10 & 0xff] << 0x10 | i[u >>> 0x8 & 0xff] << 0x8 | i[u & 0xff] : i[u >>> 0x16] << 0x18 | i[u >>> 0x10 & 0xff] << 0x10 | i[u >>> 0x8 & 0xff] << 0x8 | i[u & 0xff]);
A[B] = A[B - x] ^ u;
}
}
这里的 delete window, window = 0x0 在浏览器中是无效的,所以不会导致 window = 0。据此推理,当 window = 1 时,我们的代码与浏览器流程相同。
8. 修改代码
for (var B = 0x0; B < z; B++) {
if (B < x)
A[B] = w[B];
else {
u = A[B - 0x1];
if (!(B % x))
u = u << 0x8 | u >>> 0x18,
u = i[u >>> 0x18] << 0x18 | i[u >>> 0x10 & 0xff] << 0x10 | i[u >>> 0x8 & 0xff] << 0x8 | i[u & 0xff],
u ^= s[B / x | 0x0] << 0x18;
else
x > 0x6 && B % x == 0x4 && (
u = 1 ? i[u >>> 0x1a] << 0x18 | i[u >>> 0x10 & 0xff] << 0x10 | i[u >>> 0x8 & 0xff] << 0x8 | i[u & 0xff] : i[u >>> 0x16] << 0x18 | i[u >>> 0x10 & 0xff] << 0x10 | i[u >>> 0x8 & 0xff] << 0x8 | i[u & 0xff]);
A[B] = A[B - x] ^ u;
}
}
删除 delete window, window = 0x0,将值固定为 1。
输出:
模块获取 [Function (anonymous)]
成功: 2a1de47d1e0d001c595ab047ddd3c3cb
此时我们的值与浏览器出值一致,成功还原出请求 token。
四、总结
这道题主要考验读代码的能力。代码无强混淆,逻辑基本都能看通。关键点在于:
- 理解自定义模块加载器的作用
- 认识到魔改加密函数的存在
- 正确补入浏览器环境
- 通过路由函数监控环境调用
- 仔细阅读代码找出环境检测逻辑
整个逆向过程需要耐心分析,不能盲目补环境,要结合实际代码逻辑进行针对性处理。
685

被折叠的 条评论
为什么被折叠?



