
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
前言
最近浏览私信时,发现有小伙伴在跑某眼查相关业务时,遇到一点问题,说自己被风控参数按住了,但是自己的解决办法又不是特别效率,特此来向 K 哥求助,K 哥会尽力满足粉丝需求,本文就对该参数进行逆向分析,仅作为学习研究:

逆向目标
-
目标:某眼查 if-match 参数
-
网址:
aHR0cHM6Ly93d3cudGlhbnlhbmNoYS5jb20vc2VhcmNoP2tleT3ku4rml6XlrpzkvJE=
抓包分析
通过调试可知,在触发风控以后的响应状态码为 429,同时返回一段经过混淆的 JS 代码,如下图所示:

此后,会调用 sorry/verifyCaptcha4.json 接口进行验证:

同时协议头里新增了 X-If-Match 参数:

校验通过的话则可以继续请求搜索接口,后续频繁请求可能会出现账号相关的若干问题。
逆向分析
综上所述,频繁请求搜索接口触发风控后,会给我们返回一段经过混淆的 JS,我们通过下 XHR 断点成功断下,进入一段 ts 代码中:

向上跟栈发现就是接口返回的混淆的 js,然后通过 setRequestHeader 方法将加密生成的 X-If-Match 置入请求头中:

发现是调用 _0x2ccea7 方法传入了 4 个参数,如下:
_0x3398f3 = _0x27abae[_0x5af2ca(0x1be, 0x2c7, 0x3f4, ‘!Ggl’, 0x16f)](this, _0x42da8e[_0x314dad(0x78, 0x221, 0x279, ‘xh$h’, 0x4e3) + ‘q’], _0x42da8e[_0x2724bb(‘d0we’, 0x60e, 0x3c2, 0x86c, 0x874) + ‘H’](_0x2ccea7, _0x17c050, _0x78dd78, _0x1f4a50, _0x2a3d89))
//_0x17c050 undefined
//_0x78dd78 lot_number
// _0x1f4a50 加密值,跟栈
// _0x2a3d89 固定参数 0
分析函数可知,最终他其实就是将传入的第三个参数返回:
function _0x18f1c6(_0x552ef8, _0x2e63db, _0x395a43, _0x35c1ad) {
{
var _0x50a86a = new Array(2);
_0x50a86a.splice(1, 0, _0x552ef8, _0x2e63db, _0x395a43, _0x35c1ad);
_0x50a86a.splice(1, 2);
_0x50a86a.splice(2, 1);
return _0x50a86a.join(’’);
}
}
即 _0x1f4a50 加密值为 X-If-Match,继续向上跟,发现 match 由以下生成:
_0x1f4a50 = _0x42da8e[_0x3f7ed9(0x5f4, ‘wgRL’, 0x589, 0x60c, 0x3c2) + ‘H’](_0x1f223f, _0x17c050, _0x78dd78, _0x2a3d89, _0x3398f3);
// _0x1f223f函数传入 _0x17c050 _0x78dd78(lot_number) 生成
接着向上跟栈,发现 _0x17c050 生成位置如下:

这是本文第一个难点,通过在混淆的 JS 里拿到里面的活值 _0x17c050。
方法一:可以用 AST 解混淆的办法将这个代码解下,然后将 32 位的字符串提取出来即可拿到这个活值:

解混淆代码如下:
const fs = require(‘fs’);
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const types = require("@babel/types");
const generator = require("@babel/generator").default;
jsname = “./temp"
const jscode = fs.readFileSync(jsname + “.js”, {
encoding: “utf-8”
});
let ast = parser.parse(jscode);xq
function deepCopy(obj) {
if (typeof obj !== ‘object’ || obj === null) {
return obj;
}
let copy = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
unieconsole = deepCopy(console)
unieconsole.time(“处理完毕,耗时”);
// 获取全局所有函数(字符串解析函数)
function getDecryptFuncNames(decryptCode) {
let funcNames = [];
let decryptAst = parser.parse(decryptCode);
traverse(decryptAst, {
FunctionDeclaration(path) {
let { parentPath, node } = path;
if (!parentPath.isProgram()) {
return;
}
let { id, params, body } = node;
let length = body.body.length;
if (params.length == 0 || length == 0 || !types.isReturnStatement(body.body[length - 1]) || params.length != 2) {
return;
}
funcNames.push(id.name);
}
});
return funcNames;
}
let globalparsefun = getDecryptFuncNames(jscode)[0];
unieconsole.log(“全局解析函数”, globalparsefun)
// 使用 Set
let strob = new Set();
let foundInCurrentPass = true;
// 循环 ob 替换,直到没有 ob
while (foundInCurrentPass) {
foundInCurrentPass = false;
let currentPassNames = new Set();
traverse(ast, {
VariableDeclarator(path) {
const node = path.node;
if (types.isIdentifier(node.init) &&
(node.init.name == globalparsefun || strob.has(node.init.name))) {
const varName = node.id.name;
currentPassNames.add(varName);
foundInCurrentPass = true;
let binding = path.scope.getOwnBinding(varName);
if (binding) {
for (let refPath of binding.referencePaths) {
if (refPath.parentPath.isCallExpression({ callee: refPath.node })) {
const callPath = refPath.parentPath;
callPath.node.callee = types.Identifier(globalparsefun);
}
}
}
path.remove();
}
}
});
// 合并到全局 Set
currentPassNames.forEach(name => strob.add(name));
if (foundInCurrentPass) {
unieconsole.log(“根据全局解析函数删除混淆的字符串解析函数数量”, currentPassNames.size);
}
}
let de_hexnum = 0, de_binnum = 0, de_membernum = 0;
// 构建成员变量缓存
const memberCache = new Map();
const firstPassVisitor = {
// 十六进制字符串和数字字面量
’StringLiteral|NumericLiteral’(path) {
path.replaceWith(types.valueToNode(path.node.value));
path.skip();
de_hexnum += 1;
},
// 一元表达式
UnaryExpression(path) {
const evalResult = path.evaluate();
if (evalResult.confident) {
path.replaceWith(types.valueToNode(evalResult.value));
path.skip();
de_hexnum += 1;
}
},
// 二项式解混淆
BinaryExpression(path) {
if (path.node.operator == “/” && path.node.right.value == 0) {
return;
}
const evalResult = path.evaluate();
if (evalResult.confident) {
path.replaceWith(types.valueToNode(evalResult.value));
de_binnum += 1;
}
},
// 成员变量混淆还原
MemberExpression(path) {
if (types.isUpdateExpression(path.parent) ||
(types.isAssignmentExpression(path.parent) && path.parent.left == path.node)) {
return;
}
// 字符串属性转标识符
if (types.isStringLiteral(path.node.property)) {
let membervalue = path.node.property.value;
if (membervalue.includes(”-") || membervalue.includes("@") ||
membervalue.includes("#") || membervalue.includes(" ") ||
membervalue === “” || membervalue == “toString”) {
return;
}
path.node.property = types.Identifier(membervalue);
path.node.computed = false;
}
if (types.isIdentifier(path.node.property) && types.isIdentifier(path.node.object)) {
const objName = path.node.object.name;
const propName = path.node.property.name;
const cacheKey = ${objName}.${propName};
if (memberCache.has(cacheKey)) {
const value = memberCache.get(cacheKey);
if (value !== null) {
try {
path.replaceWith(types.valueToNode(value));
de_membernum += 1;
return;
} catch(e) {}
}
return;
}
// 查找变量定义
let binding = path.scope.getBinding(objName);
if (binding && binding.path.isVariableDeclarator()) {
const init = binding.path.node.init;
if (init && init.properties) {
for (let i = 0; i < init.properties.length; i++) {
const prop = init.properties[i];
let proname;
if (types.isIdentifier(prop.key)) {
proname = prop.key.name;
} else if (types.isStringLiteral(prop.key)) {
proname = prop.key.value;
} else if (types.isNumericLiteral(prop.key)) {
proname = prop.key.value;
} else {
continue;
}
if ((types.isNumericLiteral(prop.value) || types.isStringLiteral(prop.value)) &&
proname == propName) {
try {
const value = prop.value.value;
memberCache.set(cacheKey, value);
path.replaceWith(types.valueToNode(value));
de_membernum += 1;
return;
} catch(e) {
memberCache.set(cacheKey, null);
}
}
}
}
}
memberCache.set(cacheKey, null);
}
}
};
traverse(ast, firstPassVisitor);
unieconsole.log(“第一次还原十六进制字符串解混淆数量”, de_hexnum);
unieconsole.log(“第一次还原二项式解混淆数量”, de_binnum);
unieconsole.log(“第一次还原成员变量混淆数量”, de_membernum);
// 函数花指令混淆
let de_callnum = 0;
const simpleFuncMap = new Map();
traverse(ast, {
FunctionDeclaration(path) {
const { id, body, params } = path.node;
const bodyStatements = body.body;
if (bodyStatements.length != 1 || !types.isReturnStatement(bodyStatements[0])) {
return;
}
// 检查是否包含this
let hasthis = false;
path.traverse({
ThisExpression() {
hasthis = true;
}
});
if (!hasthis) {
simpleFuncMap.set(id.name, {
params: params.map(p => p.name),
body: bodyStatements[0].argument
});
}
}
});
// 现在使用预先收集的信息处理调用
traverse(ast, {
CallExpression(path) {
const calleename = path.node.callee.name;
if (!calleename || types.isMemberExpression(path.node.callee)) {
return;
}
const funcInfo = simpleFuncMap.get(calleename);
if (!funcInfo) {
return;
}
const callparams = path.node.arguments;
const funparams = funcInfo.params;
const funbody = types.cloneNode(funcInfo.body, true);
try {
path.replaceWith(funbody);
} catch (e) {
return;
}
// 替换参数
path.traverse({
Identifier(path3) {
const idx = funparams.indexOf(path3.node.name);
if (idx != -1) {
path3.replaceWith(types.cloneNode(callparams[idx], true));
path3.skip();
}
}
});
de_callnum += 1;
}
});
unieconsole.log(“还原第二种函数花指令混淆数量”, de_callnum);
// 还原字符串解密函数混淆
let de_strnum = 0;
if (globalparsefun != null) {
try {
// 直接eval原始代码以获取解密函数
eval(jscode);
// 替换所有对解密函数的调用
traverse(ast, {
CallExpression(path) {
const callee = path.node.callee;
if (types.isIdentifier(callee) && callee.name == globalparsefun) {
try {
// 直接eval整个调用表达式
const value = eval(path.toString());
if (typeof value === “string”) {
path.replaceWith(types.valueToNode(value));
de_strnum += 1;
}
} catch(e) {
// 解密失败,保持原样
}
}
}
});
unieconsole.log(“还原字符串解密函数混淆数量”, de_strnum);
} catch(e) {
unieconsole.log(“字符串解密失败:”, e.message);
}
}
// 合并字符串拼接(需要循环直到没有新的合并)
let de_concatnum = 0;
let concatChanged = true;
while (concatChanged) {
concatChanged = false;
traverse(ast, {
BinaryExpression(path) {
if (path.node.operator === ‘+’) {
const { left, right } = path.node;
// 收集所有连续的字符串字面量
const parts = [];
let current = path.node;
let allStrings = true;
// 递归展开左侧
function collectLeft(node) {
if (types.isBinaryExpression(node) && node.operator === ‘+’) {
collectLeft(node.left);
collectRight(node.right);
} else if (types.isStringLiteral(node)) {
parts.push(node.value);
} else {
allStrings = false;
}
}
function collectRight(node) {
if (types.isBinaryExpression(node) && node.operator === ‘+’) {
collectLeft(node.left);
collectRight(node.right);
} else if (types.isStringLiteral(node)) {
parts.push(node.value);
} else {
allStrings = false;
}
}
collectLeft(left);
collectRight(right);
if (allStrings && parts.length > 1) {
const result = parts.join(’’);
path.replaceWith(types.stringLiteral(result));
de_concatnum += parts.length - 1;
concatChanged = true;
}
}
}
});
}
unieconsole.log(“合并字符串拼接数量”, de_concatnum);
// 第三次成员变量还原(字符串合并后,对象属性键已经是完整字符串)
de_membernum = 0;
memberCache.clear();
traverse(ast, {
MemberExpression: firstPassVisitor.MemberExpression
});
unieconsole.log(“第三次还原成员变量混淆数量”, de_membernum);
unieconsole.timeEnd(“处理完毕,耗时”);
code = generator(ast, {
jsescOption: { quotes: ‘single’ }
}).code;
fs.writeFile(jsname + “_deob2.js”, code, (err) => {
encoding: “utf-8”
});
// 第二轮合并优化
de_hexnum = 0;
de_binnum = 0;
de_membernum = 0;
traverse(ast, firstPassVisitor);
unieconsole.log(“第二次还原十六进制字符串解混淆数量”, de_hexnum);
unieconsole.log(“第二次还原二项式解混淆数量”, de_binnum);
unieconsole.log(“第二次还原成员变量混淆数量”, de_membernum);
temp 里把基本环境补齐,然后直接运行即可拿到含有 32 位字符串解混淆后的代码,耗时基本几百 ms。
方法二:通过十几组动态 JS 的观察,可以发现活值代码是有规律的,存在于第 8 个 window[ 到第 13 个 window[ 之间:
且活值最后一个为字符串常量,格式为 [变量名] = [混淆对象][[混淆函数(参数列表)] + 字符串常量];。
所以,我们可以直接通过正则精准定位:

const fs = require(‘fs’);
const path = require(‘path’);
const parser = require("@babel/parser");
const generator = require("@babel/generator").default;
function extractTargetAssignments(jsFilePath) {
try {
const jscode = fs.readFileSync(jsFilePath, { encoding: “utf-8” });
let ast = parser.parse(jscode);
let code = generator(ast, {
compact: false,
minified: false,
jsescOption: { quotes: ‘single’ },
}).code;
const windowRegex = /window[/g;
const windowPositions = [];
let match;
while ((match = windowRegex.exec(code)) !== null) {
windowPositions.push(match.index);
}
if (windowPositions.length < 13) {
console.log(文件中找到 ${windowPositions.length} 个 "window[",数量不足13个);
return;
}
const eighthWindowStart = windowPositions[7];
const thirteenthWindowStart = windowPositions[12];
const targetCodeRange = code.substring(eighthWindowStart, thirteenthWindowStart);
const assignRegex = /(_0x[a-f0-9]+)\s*=\s*(_0x[a-f0-9]+)[.?+\s’([^’]+)’/g;
const matchedAssignments = [];
let assignMatch;
while ((assignMatch = assignRegex.exec(targetCodeRange)) !== null) {
const matchStart = assignMatch.index;
const stmtStart = targetCodeRange.lastIndexOf(’\n’, matchStart) || 0;
let stmtEnd = targetCodeRange.indexOf(’,’, matchStart);
if (stmtEnd === -1) stmtEnd = targetCodeRange.indexOf(’;’, matchStart);
if (stmtEnd === -1) stmtEnd = matchStart + 300;
const fullAssignment = targetCodeRange.substring(stmtStart, stmtEnd).trim();
matchedAssignments.push({
variable: assignMatch[1],
baseObject: assignMatch[2],
stringConstant: assignMatch[3],
fullAssignment: fullAssignment
});
}
if (matchedAssignments.length === 0) {
console.log(‘未找到符合条件的赋值语句’);
} else {
console.log(找到 ${matchedAssignments.length} 条符合条件的赋值语句:);
matchedAssignments.forEach((item, idx) => {
console.log(\n第${idx+1}条:);
console.log(变量: ${item.variable});
console.log(基础对象: ${item.baseObject});
console.log(字符串常量: '${item.stringConstant}');
console.log(完整语句: ${item.fullAssignment});
});
}
return matchedAssignments;
} catch (error) {
console.error(‘处理出错:’, error.message);
return null;
}
}
const TARGET_JS_FILE = ‘./temp.js’;
extractTargetAssignments(TARGET_JS_FILE);
活值位置搞定后,直接插入 log 打印取出即可,耗时比 ast 略快一点。
活值搞定后分析 match 参数的主函数,进入一段花指令的函数:

继续单独跟进,发现最终由 _0x91b26[_0x535c98(0x716, 'L)%*', 0x4ec, 0x61b, 0x3fb) + 'F'](_0x3b0086, _0x3184cb, _0x167174) 生成的 match 参数:

经过分析后,发现这是一种位运算处理逻辑,逻辑如下,初始化 res=[] 循环 8 次,每次截取 4 个字符,作为一个 4 位十六进制段,分别从 s1 和 s2 中截取该段(若超出长度则取空字符串,转换为 0),将截取的十六进制字符串转为十进制整数 n1、n2,对 n1 和 n2 执行异或运算,将异或结果转为 4 位十六进制字符串,添加到 res:
def a(n):
s = hex(n)[2:]
return b(s, 4)
def b(s, l):
d = l - len(s)
if d <= 0:
return s
elif d == 1:
return ‘0’ + s
else:
return ‘0’ * d + s
def c(s1, s2):
res = []
for i in range(8):
st = i * 4
ed = (i + 1) * 4
p1 = s1[st:ed] if st < len(s1) else ''
n1 = int(p1, 16) if p1 else 0
p2 = s2[st:ed] if st < len(s2) else ''
n2 = int(p2, 16) if p2 else 0
res.append(a(n1 ^ n2))
return ‘’.join(res)
print(c(“611270d5e7fcb331e84ffb43f5144b74”, “08c4cf113fa04c1da4a2a9f64d2d5cef”))
可以将代码部分扣出或者导出到全局即可。
随时随地看视频