抓包分析
随便翻页,发起一个请求,可以看到表单参数type__1017可能需要逆向。
1.png (21.97 KB, 下载次数: 1)
下载附件
2024-12-7 03:32 上传
响应数据也是加密的。
2.png (15.38 KB, 下载次数: 1)
下载附件
2024-12-7 03:32 上传
浅浅从启动器进去看看,又是混淆的代码。
3.png (18.07 KB, 下载次数: 1)
下载附件
2024-12-7 03:32 上传
那我们这一章就来讲讲类OB混淆的反混淆吧,本人AST菜鸡一个,路过的大佬多多指点。
反混淆
用到的AST基本框架如下:
const parse = require('@babel/parser').parse
const generator = require('@babel/generator').default;
const traverse = require('@babel/traverse').default;
const types = require('@babel/types')
const fs = require('fs')
// 自行将后面讲的三个特征的代码放到这
// 待反混淆的文件
let jsCode = fs.readFileSync('./encode.js', { encoding: 'utf-8' })
let ast = parse(jsCode);
//////////////////
// 具体还原逻辑
//////////////////
// 语法数转JS代码
let { code } = generator(ast, {compact: false});
// 保存
fs.writeFile('./decode.js', code, (err) => {
});
我们先把代码整体复制到vscode中,简单捋一捋还原的思路。
首先,对于OB混淆,我们需要有一个认识:就是部分字符串会被所谓的加密函数进行了解密,在使用的时候就会调用相应的解密函数进行解密。
与解密函数相关的特征有如下三个:
[ol]
[/ol]
4.png (26.79 KB, 下载次数: 0)
下载附件
2024-12-7 03:32 上传
5.png (57.67 KB, 下载次数: 0)
下载附件
2024-12-7 03:32 上传
6.png (20.66 KB, 下载次数: 0)
下载附件
2024-12-7 03:32 上传
下面开始进行还原,思路仅供参考。。。
需要将前面三个特征的代码复制下来便于解密,记得把代码压缩一下。
我们先分析一下之后的代码,待解密的字符串有这样的特征,解密函数是引用的U,参数是从对象中取的数字。
7.png (35.98 KB, 下载次数: 0)
下载附件
2024-12-7 03:32 上传
那我们先还原字符串
思路:用一个数组保存解密函数及其引用的变量名,然后找到所有解密函数调用的地方进行还原,如:Jo(uM.J) 还原成 xxx字符串。
AST代码:
// 递归解密函数的引用,添加到数组中
traverse(ast, {
VariableDeclarator: function (path) {
if (
path.get('id').isIdentifier() &&
path.get('init').isIdentifier() &&
decodeFuncArr.indexOf(path.get('init.name').node) != -1
) {
decodeFuncArr.push(path.get('id.name').node)
}
}
})
// 字符串还原
let argsType = ['isNumericLiteral']
traverse(ast, {
CallExpression: {
exit: function (path) {
if (
path.get('callee').isIdentifier() &&
decodeFuncArr.indexOf(path.get('callee.name').node) != -1 &&
path.get('arguments').length === 1
) {
let argTypeTagArr = [] // 存储参数是否为指定类型的数组
for (let i = 0; i c)) {
// 如果符合指定的类型,就是需要解密的地方
let args = [] // 存储参数的值
for (let i = 0; i ', eval(`${startFuncName}(${args.join(',')})`))
path.replaceWith(types.valueToNode(eval(`${startFuncName}(${args.join(',')})`)))
}
}
}
}
})
还原后将部分字符串进行拼接。
AST代码:
// 字符串拼接
traverse(ast, {
BinaryExpression: {
exit: function (path) {
let left = path.get("left").node.value
let right = path.get("right").node.value
if (path.get("left").isStringLiteral() && path.get("right").isStringLiteral()) {
path.replaceInline(types.valueToNode(left + right))
}
}
}
})
下一步,我们需要把对象中的字符串以及函数调用还原回去。
8.png (57.1 KB, 下载次数: 0)
下载附件
2024-12-7 03:32 上传
思路:首先用到的地方是J["aZyay"]或J["oqDBR"](Jd, JP)类型,我们直接拿到对象的属性,然后去对应的对象判断属性值是字符串类型还是函数类型,进行替换。
AST代码:
// 排除一些不在对象的属性
let buildInFunc = [
'apply', 'slice', 'shift', 'which', 'split', 'index', 'input', 'clone', 'token', 'refer', 'scene', 'width',
'style', 'round', 'parse', 'match', 'catch'
]
// 从对象中取字符串还原
traverse(ast, {
MemberExpression: {
exit: function (path) {
if (
path.get('object').isIdentifier() &&
path.get('property').isStringLiteral()
) {
console.log(path.toString())
let identifier = path.get('object.name').node
let property = path.get('property.value').node
if (property.length !== 5) return
if (buildInFunc.indexOf(property) !== -1) return
if (!path.scope.getAllBindings()[identifier]) return
let property_nodes = path.scope.getAllBindings()[identifier].path.get('init.properties')
for (let i = 0; i ', property_nodes.get('value.value').node)
path.replaceWith(types.valueToNode(property_nodes.get('value.value').node))
}
}
}
}
}
})
// 从对象中取函数调用还原
traverse(ast, {
CallExpression: {
exit: function (path) {
if (
path.get('callee').isMemberExpression() &&
path.get('callee.property').isStringLiteral()
) {
console.log(path.toString())
let identifier = path.get('callee.object.name').node
let property = path.get('callee.property.value').node
if (property.length !== 5) return
if (buildInFunc.indexOf(property) !== -1) return
// 获取obj对象属性值,为操作符或函数
let property_paths = path.scope.getAllBindings()[identifier].path.get('init.properties')
property_paths = Array.from(property_paths)
property_paths.forEach(node_path => {
// 属性名称
let obj_property = node_path.get('key.value').node
if (
obj_property === property &&
node_path.get('value').isFunctionExpression()
) {
let func_bodys = node_path.get('value.body.body')
func_bodys = Array.from(func_bodys)
func_bodys.forEach(body => {
// 在return处才知道函数是操作符类型还是函数调用类型
if (body.isReturnStatement()) {
if (body.get('argument').isBinaryExpression()) {
// 操作符还原
let operator = body.get('argument.operator').node
let left = path.get('arguments.0')
let right = path.get('arguments.1')
console.log(path.toString(), '-->', left.toString(), operator, right.toString())
path.replaceWith(types.binaryExpression(operator, left.node, right.node))
} else if (body.get('argument').isCallExpression()) {
// 函数调用还原
let origin_args = path.get('arguments')
origin_args = Array.from(origin_args)
let args
if (origin_args.length === 1) {
args = [] // 没有参数
} else {
args = origin_args.slice(1).map(arg => arg.node)
}
let old_path_string = path.toString()
path.replaceWith(types.callExpression(origin_args[0].node, args))
console.log(old_path_string, '-->', path.toString())
} else if (body.get('argument').isLogicalExpression()) {
// 操作符还原
let operator = body.get('argument.operator').node
let left = path.get('arguments.0')
let right = path.get('arguments.1')
console.log(path.toString(), '-->', left.toString(), operator, right.toString())
path.replaceWith(types.logicalExpression(operator, left.node, right.node))
}
}
})
}
})
}
}
}
})
然后,我们对这样的控制流进行还原。
9.png (68 KB, 下载次数: 0)
下载附件
2024-12-7 03:32 上传
思路:拿到控制器和case节点,然后根据控制器的顺序对case节点进行排序。
AST代码:
let controler_code = {}
let controler = {}
traverse(ast, {
WhileStatement: {
exit: function (path) {
if (
path.get('test').isUnaryExpression() || (path.get('test').isArrayExpression() && path.get('test').toString() === '[]')
) {
if (path.get('body.body').length === 0) return // while循环体为空,直接返回
if (path.get('body.body.0').isTryStatement()) return
console.log(path.toString())
let switch_condition
try {
switch_condition = path.get('body.body.0.discriminant.object.name').node // 控制器名称
} catch (e) {
return
}
controler_code[switch_condition] = {} // 整体代码有多个控制流,需要分开
if (!path.scope.getAllBindings()[switch_condition].path.get('init.callee.object').isStringLiteral()) return
// 取控制器,var _0x41a9c6 = "1|4|3|0|2"["split"]('|')
eval(`controler['${switch_condition}'] = ` + path.scope.getAllBindings()[switch_condition].path.get('init').toString())
let cases_path = path.get('body.body.0.cases') // 拿到所有case节点,数组类型
for (var i = 0; i {
if (!c.isContinueStatement()) {
// 剔除case中的continue
controler_code[switch_condition][case_num].push(c)
}
})
}
let code_node = []
for (var i = 0; i {
code_node.push(n.node)
})
// code_node.push(controler_code[switch_condition][index][0].node)
}
path.replaceWithMultiple(code_node)
}
}
}
})
最后,再处理一下。
解编码:
const transform_literal = {
NumericLiteral({node}){
if (node.extra && /^0[obx]/i.test(node.extra.raw)){
node.extra = undefined;
}
},
StringLiteral({node}){
if (node.extra && /\\[ux]/gi.test(node.extra.raw)){
node.extra = undefined;
}
}
}
traverse(ast, transform_literal)
表达式计算:
traverse(ast, {
BinaryExpression: {
exit(path){
let {confident,value} = path.evaluate();
if(!confident)return
path.replaceInline({type:"NumericLiteral", value: value})
}
}
})
移除无用对象:
ast = parse(generator(ast, { compact: true }).code)
traverse(ast, {
VariableDeclarator: {
exit(path) {
let { init, id } = path.node;
if (!types.isObjectExpression(init) && !types.isIdentifier(id)) return;
let { scope } = path;
let binding = scope.getBinding(id.name);
if (binding.referencePaths.length !== 0) return;
path.remove();
}
}
})
成功将四千多行的代码还原到七百多行,而且逻辑也清晰多了(图不贴了)。
然后,我们再验证一下这代码能不能用,按道理来说,应该一步一验证的,但是我已经踩过坑了,所以直接一次性讲完。
具体验证方法就是去浏览器替换看能不能用,记得一定一定一定要压缩!!!保存的时候let { code } = generator(ast, {compact: true});将compact修改为true即可。
可以看到替换后也能成功拿到数据。
10.png (37.62 KB, 下载次数: 0)
下载附件
2024-12-7 03:32 上传
逆向分析
我们需要的逆向的参数是type__1017,可以搜索type__,可以看到数组Jt有,那我们就可以大胆猜测下面的Ju应该是我们要的值了。
11.png (34.23 KB, 下载次数: 0)
下载附件
2024-12-7 03:32 上传
Ju确实是我们要的值,这个值非常好跟,抠代码靠自己了。
12.png (21.07 KB, 下载次数: 0)
下载附件
2024-12-7 03:33 上传
然后我们看数据解密,老样子,我们尝试hook JSON.parse。
hook到响应数据的明文。
13.png (14.35 KB, 下载次数: 0)
下载附件
2024-12-7 03:33 上传
我们往上跟一个栈,很明显了,DES,剩下的就交给你们了。
14.png (13.95 KB, 下载次数: 0)
下载附件
2024-12-7 03:33 上传
总的来说,反混淆后就特别简单了。
加解密搞定后,我们模拟请求一下数据。
15.png (60.64 KB, 下载次数: 0)
下载附件
2024-12-7 03:33 上传
成功!!!