吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2903|回复: 71
上一主题 下一主题
收起左侧

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

  [复制链接]
跳转到指定楼层
楼主
littlewhite11 发表于 2024-12-7 03:41 回帖奖励
本帖最后由 littlewhite11 于 2024-12-10 11:18 编辑

逆向目标

  • 网址:aHR0cHM6Ly9jdGJwc3AuY29tLyMvYnVsbGV0aW5MaXN0
  • 目标:查询参数type__1017,响应数据解密

抓包分析

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

响应数据也是加密的。

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

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

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

  1. 大数组
  2. 数组移位
  3. 解密函数
  • 大数组

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

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

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

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

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

那我们先还原字符串

思路:用一个数组保存解密函数及其引用的变量名,然后找到所有解密函数调用的地方进行还原,如: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))
            }
        }
    }
})

下一步,我们需要把对象中的字符串以及函数调用还原回去。

思路:首先用到的地方是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))
                                }
                            }
                        })
                    }
                })
            }
        }
    }
})

然后,我们对这样的控制流进行还原。

思路:拿到控制器和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即可。

可以看到替换后也能成功拿到数据。

逆向分析

我们需要的逆向的参数是type__1017,可以搜索type__,可以看到数组Jt有,那我们就可以大胆猜测下面的Ju应该是我们要的值了。

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

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

hook到响应数据的明文。

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

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

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

成功!!!

免费评分

参与人数 23吾爱币 +26 热心值 +21 收起 理由
奥喵 + 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;amp;#128077;&amp;amp;#127995;

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
maikale 发表于 2024-12-13 16:38
看到抓下来的数据了。小小伊吾县搞个智算中心舍得花钱啊,看来对那边企业对AI的需求还挺大
沙发
surepj 发表于 2024-12-7 08:02
3#
lastmu 发表于 2024-12-7 08:12
4#
义飞ing 发表于 2024-12-7 08:27
和楼上一样 路过 学习下
5#
ztz3421 发表于 2024-12-7 08:56
学习一下,认为很赞&#128077;&#127995;
6#
wanws 发表于 2024-12-7 09:24
学习,厉害
7#
SmileLoveSex 发表于 2024-12-7 09:58
感谢楼主分享。
8#
gzpenbeat 发表于 2024-12-7 09:59
感谢分享,不太熟悉AST,这个网站我硬扣的
9#
xiaoxiaotiao 发表于 2024-12-7 10:47
学习 学习
10#
 楼主| littlewhite11 发表于 2024-12-7 10:59 |楼主
gzpenbeat 发表于 2024-12-7 09:59
感谢分享,不太熟悉AST,这个网站我硬扣的

硬扣也很猛
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-14 17:57

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表