某音X-Bogus参数逆向分析与JSVMP算法还原
1. 逆向目标
目标:某音网页端用户信息接口X-Bogus参数
接口:aHR0cHM6Ly93d3cuZG91eWluLmNvbS9hd2VtZS92MS93ZWIvdXNlci9wcm9maWxlL290aGVyLw==
2. JSVMP基础概念
JSVMP全称Virtual Machine based code Protection for JavaScript,即JS代码虚拟化保护方案。
2.1 JSVMP核心思想
- 在JavaScript代码保护过程中引入代码虚拟化思想
- 将目标代码转换成自定义的字节码
- 这些字节码只有特殊的解释器才能识别
- 隐藏目标代码的关键逻辑
2.2 JSVMP保护流程
- 服务器端读取JavaScript代码
- 词法分析
- 语法分析
- 生成AST语法树
- 生成私有指令
- 生成对应私有解释器
- 将私有指令加密与私有解释器发送给浏览器
- 浏览器边解释边执行
3. JSVMP逆向方法
目前JSVMP的逆向方法主要有三种:
- RPC远程调用:通过远程调用的方式绕过保护
- 补环境:模拟完整的浏览器环境
- 日志断点还原算法(插桩):找到关键位置,输出关键参数的日志信息,从结果往上倒推生成逻辑
本文主要介绍插桩还原算法的方法。
4. 抓包分析
在博主主页抓包,可以发现一个返回JSON数据的接口,包含博主信息。请求参数中包含:
X-Bogus:28个字符组成的参数,每次请求会改变sec_user_id:博主主页URL后面的一串webid:请求主页返回内容中包含msToken:与cookie有关
经测试,该接口不验证webid和msToken,可置空。
5. 逆向分析流程
5.1 定位关键JS文件
- 下XHR断点,当URL中包含X-Bogus参数时断下
- 跟栈找到
webmssdk.js文件,这是生成参数的主要JS逻辑(JSVMP实现) - 使用AST解混淆(可使用v_jstools插件)
5.2 关键代码分析
还原混淆后,发现以下关键点:
this.openArgs[1]是携带了X-Bogus的完整URL- 代码中有大量三元表达式
- 当
M的值为15时,会走到生成X-Bogus的逻辑 S数组参与整个生成过程,不断被增删改查for循环中的I值决定后续if语句的走向
5.3 插桩实现
在关键位置添加日志断点:
// 位置1日志断点
"位置1", "索引I", I, "索引A", A, "值S: ", JSON.stringify(S, function(key, value) {
if (value == window) {
return undefined
}
return value
})
// 位置2日志断点
"位置2", "索引I", I, "索引A", A, "值S: ", JSON.stringify(S, function(key, value) {
if (value == window) {
return undefined
}
return value
})
JSON.stringify处理说明:
- 使用replacer函数处理循环引用问题
- 当value为window时返回undefined,排除该成员
- 确保能完整输出S数组内容
5.4 日志分析
导出日志后分析X-Bogus生成过程:
- X-Bogus由28个字符组成,分为7组,每组4个字符
- 每组字符的生成逻辑相同
- 每个字符通过以下步骤生成:
- 从乱码字符串获取指定位置的Unicode编码
- 进行位运算
- 从固定字符串
short_str中取出对应字符
5.5 字符生成算法
function getXBogus(originalString) {
// 生成乱码字符串
var garbledString = getGarbledString(originalString);
var XBogus = "";
// 依次生成七组字符串
for (var i = 0; i <= 20; i += 3) {
var charCodeAtNum0 = garbledString.charCodeAt(i);
var charCodeAtNum1 = garbledString.charCodeAt(i + 1);
var charCodeAtNum2 = garbledString.charCodeAt(i + 2);
var baseNum = charCodeAtNum2 | charCodeAtNum1 << 8 | charCodeAtNum0 << 16;
// 依次生成四个字符
var str1 = short_str[(baseNum & 16515072) >> 18];
var str2 = short_str[(baseNum & 258048) >> 12];
var str3 = short_str[(baseNum & 4032) >> 6];
var str4 = short_str[baseNum & 63];
XBogus += str1 + str2 + str3 + str4;
}
return XBogus;
}
其中short_str为固定字符串:
Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=
6. 乱码字符串生成逻辑
6.1 初始处理
- 对URL参数进行两次MD5、两次转Uint8Array处理:
var md5 = require("md5");
// 字符串转换为Uint8Array对象
_0x5960a2 = function(a) {
for (var c = a.length >> 1, e = c << 1, b = new Uint8Array(c), d = 0, f = 0; f < e;) {
b[d++] = _0x511f86[a.charCodeAt(f++)] << 4 | _0x511f86[a.charCodeAt(f++)];
}
return b;
}
// originalString: URL后面的原始参数
var uint8Array = _0x5960a2(md5(_0x5960a2(md5(originalString))));
6.2 生成两个大数
fixedString1:时间戳(1663385262240 / 1000 = 1663385262.24)fixedString2:通过特定方法生成(536919696)
function _0x2996f8() {
try {
return _0x4b3b53 || (_0xb55f3e.perf ? -1 : (_0x4b3b53 = _0x229792(3735928559), _0x4b3b53));
} catch (a) {
return -1;
}
}
6.3 生成两个数组
array1(19个元素):
- [0]至[3]:固定值
- [4]:uint8Array[14]
- [5]:uint8Array[15]
- [6]至[7]:固定值
- [8]至[9]:与UA有关
- [10]至[13]:来自fixedString1的位运算
- [14]至[17]:来自fixedString2的位运算
- [18]:array1所有元素的异或结果
完整值示例:
[64,1.00390625,1,8,9,185,69,63,74,125,99,73,3,241,32,0,190,144,100]
array2:由array1元素交换位置得来
[array1[0], array1[2], array1[4], array1[6], array1[8], array1[10], array1[12], array1[14], array1[16], array1[18], array1[1], array1[3], array1[5], array1[7], array1[9], array1[11], array1[13], array1[15], array1[17]]
完整值示例:
[64,1,9,69,74,99,3,32,190,100,1.00390625,8,185,63,125,73,241,0,144]
6.4 生成乱码字符串
通过三个关键函数处理:
function _0x2f2740(a, c, e, b, d, f, t, n, o, i, r, _, x, u, s, l, v, h, g) {
let w = new Uint8Array(19);
return w[0] = a, w[1] = r, w[2] = c, w[3] = _, w[4] = e, w[5] = x, w[6] = b, w[7] = u, w[8] = d, w[9] = s, w[10] = f, w[11] = l, w[12] = t, w[13] = v, w[14] = n, w[15] = h, w[16] = o, w[17] = g, w[18] = i, String.fromCharCode.apply(null, w);
}
function _0x46fa4c(a, c) {
let e, b = [], d = 0, f = "";
for (let a = 0; a < 256; a++) {
b[a] = a;
}
for (let c = 0; c < 256; c++) {
d = (d + b[c] + a.charCodeAt(c % a.length)) % 256, e = b[c], b[c] = b[d], b[d] = e;
}
let t = 0;
d = 0;
for (let a = 0; a < c.length; a++) {
t = (t + 1) % 256, d = (d + b[t]) % 256, e = b[t], b[t] = b[d], b[d] = e, f += String.fromCharCode(c.charCodeAt(a) ^ b[(b[t] + b[d]) % 256]);
}
return f;
}
function _0x583250(a) {
return String.fromCharCode(a);
}
function _0x2b6720(a, c, e) {
return _0x583250(a) + _0x583250(c) + e;
}
处理步骤:
_0x2f2740.apply(null, array2)→ 生成中间字符串_0x46fa4c.apply(null, [key, 中间字符串])→ 进一步处理_0x2b6720.apply(null, [2, 255, 上一步结果])→ 最终乱码字符串
7. 完整算法还原步骤
- 获取URL参数并进行两次MD5和Uint8Array转换
- 生成时间戳和特定数值
- 构建array1和array2
- 通过三个关键函数处理生成乱码字符串
- 从乱码字符串中获取字符编码
- 通过位运算和固定字符串映射生成X-Bogus的28个字符
8. 注意事项
- 日志中的行号、变量值可能会变化,但逻辑一致
- JSON.stringify处理后的日志可能与实际值有差异
- 需要仔细定位关键断点位置
- 算法中的q数组可以写死固定值
- 每组字符的生成逻辑相同,可以抽象为通用方法