【JS逆向】某招标公告逆向分析

文章正文
发布时间:2025-08-21 12:07

网址:aHR0cHM6Ly9jdGJwc3AuY29tLyMvYnVsbGV0aW5MaXN0

目标:查询参数type__1017,响应数据解密

抓包分析

随便翻页,发起一个请求,可以看到表单参数type__1017可能需要逆向。

1.png (21.97 KB, 下载次数: 3)

下载附件

2024-12-7 03:32 上传

响应数据也是加密的。

2.png (15.38 KB, 下载次数: 3)

下载附件

2024-12-7 03:32 上传

浅浅从启动器进去看看,又是混淆的代码。

3.png (18.07 KB, 下载次数: 3)

下载附件

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混淆,我们需要有一个认识:就是部分字符串会被所谓的加密函数进行了解密,在使用的时候就会调用相应的解密函数进行解密。

与解密函数相关的特征有如下三个:

大数组

数组移位

解密函数

大数组

4.png (26.79 KB, 下载次数: 1)

下载附件

2024-12-7 03:32 上传

数组移位(一般是一个自执行函数,将大数组当参数传进去)

5.png (57.67 KB, 下载次数: 1)

下载附件

2024-12-7 03:32 上传

解密函数(会用到大数组),这里U是解密函数

6.png (20.66 KB, 下载次数: 1)

下载附件

2024-12-7 03:32 上传

下面开始进行还原,思路仅供参考。。。

需要将前面三个特征的代码复制下来便于解密,记得把代码压缩一下。

我们先分析一下之后的代码,待解密的字符串有这样的特征,解密函数是引用的U,参数是从对象中取的数字。

7.png (35.98 KB, 下载次数: 1)

下载附件

2024-12-7 03:32 上传

那我们先还原字符串

思路:用一个数组保存解密函数及其引用的变量名,然后找到所有解密函数调用的地方进行还原,如:Jo(uM.J) 还原成 xxx字符串。

AST代码:

// 递归解密函数的引用,添加到数组中 let startFuncName = 'U' let decodeFuncArr = [startFuncName ] 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 < argsType.length; i++) {                     argTypeTagArr.push(path.get(`arguments.${i}`)[argsType[i]]())                 }                 if (argTypeTagArr.every(c => c)) {                     // 如果符合指定的类型,就是需要解密的地方                     let args = []  // 存储参数的值                     for (let i = 0; i < argsType.length; i++) {                         args.push(path.get(`arguments.${i}.value`).node)                     }                     console.log(path.toString(), '-->', 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.length; i++) {                     let obj_property = property_nodes[i].get('key.value').node                     if (                         obj_property === property &&                         property_nodes[i].get('value').isStringLiteral()                     ) {                         console.log(path.toString(), '-->', property_nodes[i].get('value.value').node)                         path.replaceWith(types.valueToNode(property_nodes[i].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, 下载次数: 1)

下载附件

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 < cases_path.length; i++) {                     let case_num = cases_path[i].get('test.value').node  // case的值                     controler_code[switch_condition][case_num] = []  // 控制流的代码                     let case_content = cases_path[i].get('consequent')  // case的内容                     case_content = Array.from(case_content)                     case_content.forEach(c => {                         if (!c.isContinueStatement()) {                             // 剔除case中的continue                             controler_code[switch_condition][case_num].push(c)                         }                     })                 }                 let code_node = []                 for (var i = 0; i < controler[switch_condition].length; i++) {                     let index = controler[switch_condition][i]                     controler_code[switch_condition][index].forEach(n => {                         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, 下载次数: 1)

下载附件

2024-12-7 03:32 上传

Ju确实是我们要的值,这个值非常好跟,抠代码靠自己了。

12.png (21.07 KB, 下载次数: 1)

下载附件

2024-12-7 03:33 上传

然后我们看数据解密,老样子,我们尝试hook JSON.parse。

hook到响应数据的明文。

13.png (14.35 KB, 下载次数: 1)

下载附件

2024-12-7 03:33 上传

我们往上跟一个栈,很明显了,DES,剩下的就交给你们了。

14.png (13.95 KB, 下载次数: 1)

下载附件

2024-12-7 03:33 上传

总的来说,反混淆后就特别简单了。

加解密搞定后,我们模拟请求一下数据。

15.png (60.64 KB, 下载次数: 1)

下载附件

2024-12-7 03:33 上传

成功!!!

 

免费评分 参与人数 30吾爱币 +32 热心值 +24 理由

wzh103103
  + 1     我很赞同!  

hellohack000
  + 1     我很赞同!  

c4mpj007
  + 1     用心讨论,共获提升!  

xml2069
  + 1   + 1   我很赞同!  

rccxkj
  + 1     我很赞同!  

Q7gui
  + 1   + 1   我很赞同!  

lin5789
    + 1   谢谢@Thanks!  

奥喵
  + 1   + 1   用心讨论,共获提升!  

weidechan
  + 1     欢迎分析讨论交流,吾爱破解论坛有你更精彩!  

hamilemon
  + 1   + 1   我很赞同!  

li156
  + 1   + 1   谢谢@Thanks!  

Bizhi-1024
    + 1   谢谢@Thanks!  

lanyun86
  + 1   + 1   用心讨论,共获提升!  

meet52
  + 1   + 1   用心讨论,共获提升!  

alun120
    + 1   谢谢@Thanks!  

qu1024
  + 1   + 1   谢谢@Thanks!  

fengbolee
  + 2   + 1   欢迎分析讨论交流,吾爱破解论坛有你更精彩!  

iokeyz
  + 3   + 1   用心讨论,共获提升!  

心比天傲
  + 1   + 1   我很赞同!  

liyitong
  + 1   + 1   欢迎分析讨论交流,吾爱破解论坛有你更精彩!  

sixnology233
  + 1     谢谢@Thanks!  

liuxuming3303
  + 1   + 1   谢谢@Thanks!  

allspark
  + 1   + 1   用心讨论,共获提升!  

xhtdtk
  + 3   + 1   用心讨论,共获提升!  

无问且问
  + 1   + 1   谢谢@Thanks!  

杨辣子
  + 1   + 1   用心讨论,共获提升!  

FitContent
  + 1   + 1   感谢发布原创作品,吾爱破解论坛因你更精彩!  

dream20241111
  + 1   + 1   谢谢@Thanks!  

yxnwh
  + 1   + 1   感谢发布原创作品,吾爱破解论坛因你更精彩!  

ztz3421
  + 1   + 1   学习了,我认为很赞&amp;#128077;&amp;#127995;  

查看全部评分