某data网逆向和解混淆

查看 7|回复 0
作者:zzyzy   
本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!
目标:实现登录,翻页,得到结果。
网站:aHR0cHM6Ly9jbi5yb290ZGF0YS5jb20vcGVvcGxl
[color=]1.注意:这个网站需要挂梯子,国内访问限制。进入登录页面,先随便输入账号密码尝试登录,可以看到登录时需要两个参数,content和sign。


image.png (87.65 KB, 下载次数: 0)
下载附件
2025-5-25 11:38 上传

2.先hook到参数生成的地方,方法有很多的,直接搜索 ,hook JSON ,或者在启动器中跟栈。搜索这两个参数结果是有很多,选择直接hook JSON。
[JavaScript] 纯文本查看 复制代码(function () {
    var stringify = JSON.stringify;
    JSON.stringify = function (params) {
        console.log("Hook JSON.stringify ——> ", params);
        debugger;
        return stringify(params);
    }
})();
(function () {
    var parse = JSON.parse;
    JSON.parse = function (params) {
        console.log("Hook JSON.parse ——> ", params);
        debugger;
        return parse(params);
    }
})();


image.png (31.04 KB, 下载次数: 0)
下载附件
2025-5-25 12:28 上传

3.调试几下可以看到账号密码的明文,往下跟两步,进入js文件,可以看到JSON.stringify的地方,往下走就可以看到content的生成地方,打上断点分析一下js代码。


image.png (108.26 KB, 下载次数: 0)
下载附件
2025-5-25 12:32 上传



image.png (60.45 KB, 下载次数: 0)
下载附件
2025-5-25 12:35 上传

4. (0x0, _0x109419['encryptData'])(_0x3191dd)
_0x3191dd 传入参数--账号密码明文  调用_0x109419下的encryptData 方法。怎么调用这个方法呢
方法一:
  直接把整个js保存下载,定义一个全局变量赋值_0x109419 调用里面的方法。
方法二:
  还是分析算法直接js或者python还原。
5.复制全部代码,在Notepad++ 中分析,上面是一些定义和自执行函数,主要看下面的webpackJsonp的定义,我们需要的_0x109419就在里面。在控制打印一下_0x109419,跳转进去,key值是0x4b6。


image.png (25.61 KB, 下载次数: 0)
下载附件
2025-5-25 12:48 上传



image.png (45.04 KB, 下载次数: 0)
下载附件
2025-5-25 12:51 上传



image.png (45.27 KB, 下载次数: 0)
下载附件
2025-5-25 12:55 上传

6.这网站之前还没有添加混淆,为了看的方便先进行一波变量名的还原,自上而下分析js代码,可以得到一个规律,想要获得方法名都是走的函数a41_0x4cab返回的结果。函数中显示定义_0x4b41de,结果是一个大数组,a41_0x4cab返回的是function(_0x2d7ed8, _0x220a59) {
        _0x2d7ed8 = _0x2d7ed8 - 0x8f;
        var _0x3f6884 = _0x4b41de[_0x2d7ed8];
        return _0x3f6884;
    } 两个参数,第二个没有使用,第一个就是数组的下标,其中进行一下偏移减去0x8f,返回数组中对应索引的值。代码太多了简单写一个小demo进行解混淆测试。


image.png (111.08 KB, 下载次数: 0)
下载附件
2025-5-25 13:08 上传



image.png (26.72 KB, 下载次数: 0)
下载附件
2025-5-25 13:11 上传

7.AST解混淆是通过将混淆的JavaScript代码转成抽象语法树,利用遍历和修改节点的方式,系统性地还原代码的原始结构和逻辑,从而提升代码可读性和逆向效率的技术。
源文件代码太多,写个demo测试自己的ast代码,大致有有以下几种情况
[JavaScript] 纯文本查看 复制代码const builtinMethods = ['push', 'pop', 'shift', 'unshift', 'slice', 'splice', 'map', 'filter', 'reduce', 'forEach'];
var a41_0x24115f = getBuiltinMethod;
function getBuiltinMethod(index, index2) {
    if (index = builtinMethods.length) {
        return '索引超出范围';
    }
    return builtinMethods[index];
}
function a() {
    var aTest = getBuiltinMethod;
    const ss = aTest(1, 1).substr(0, 1);
    console.log(ss);
}
a();
function b(param1, param2) {
    var bTest = param1;
    const ss = bTest(5, 1).substr(0, 5);
    console.log(ss);
}
b(getBuiltinMethod, '');
function c() {
    var cTest = getBuiltinMethod;
    const ss = cTest(5, 1).substr(0, 5);
    console.log(ss);
    function fun() {
        var cTestNest = cTest;
        const ss = cTestNest(1, 1).substr(0, 5);
        console.log(ss);
    }
    fun();
}
c();
function d() {
    var dTest = a41_0x24115f;
    const ss = dTest(5).substr(0, 5);
    console.log(ss);
    function fun() {
        var dTestNest = dTest;
        const ss = dTestNest(1).substr(0, 5);
        console.log(ss);
    }
    fun();
}
d();
(function (param1, param2) {
    var headLesstest = param1;
    const ss = headLesstest(4, 1).substr(0, 5);
    console.log(ss);
}(getBuiltinMethod, ''))
不管怎么样最终结果都是调用最终返回数组的函数。
  7.1先是定位到数组的地方,提取数组元素。也可以手动赋值。


image.png (42.05 KB, 下载次数: 0)
下载附件
2025-5-25 14:41 上传

7.2 在代码中找到所有调用目标函数的地方,根据传入参数的索引替换成对应的变量名,(或者直接拿着js代码扔给gpt)
注意的是函数通过变量赋值,参数传递等多层绑定,调用时用的函数名不一定是目标函数,需要递归判断变量名是否最终绑定的到目标函数。
[JavaScript] 纯文本查看 复制代码function isBoundToTargetFunc(path, name, visited = new Set()) {
    if (cache.has(name)) return cache.get(name);
    if (visited.has(name)) {
        cache.set(name, false);
        return false;
    }
    visited.add(name);
    const binding = path.scope.getBinding(name);
    if (binding) {
        const node = binding.path.node;
        if (t.isVariableDeclarator(node)) {
            const init = node.init;
            if (!init) {
                cache.set(name, false);
                return false;
            }
            if (t.isIdentifier(init, {name: FUNC_NAME})) {
                cache.set(name, true);
                return true;
            }
            if (t.isIdentifier(init)) {
                const result = isBoundToTargetFunc(binding.path, init.name, visited);
                cache.set(name, result);
                return result;
            }
            cache.set(name, false);
            return false;
        }
    }
    const funcPath = path.findParent(p => p.isFunction() || p.isArrowFunctionExpression());
    if (funcPath) {
        const params = funcPath.node.params;
        for (let i = 0; i
8.直接保存2a6fadd文件,这个文件是动态的哦,上面跟一下栈就找到那个了或者直接搜索encryptionScheme,文件里面只有一个参数的,也可以定位到。以下是解混淆完整代码。


image.png (36.76 KB, 下载次数: 0)
下载附件
2025-5-25 15:13 上传

[JavaScript] 纯文本查看 复制代码const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const t = require('@babel/types');
const code = fs.readFileSync('./2a6fadd.js', 'utf-8');
const FUNC_NAME = 'a41_0x4cab';
const ARRAY_NAME = '_0x4be818';
const ast = parser.parse(code, {
  sourceType: 'module',
  plugins: ['jsx']
});
// 缓存,避免重复递归
const cache = new Map();
/**
* 递归判断变量名是否绑定到目标函数 FUNC_NAME
* 支持变量声明赋值、函数参数绑定、多层变量赋值链
* @Param {NodePath} path 当前路径
* @param {string} name 变量名
* @param {Set} visited 防止递归死循环
* @returns {boolean}
*/
function isBoundToTargetFunc(path, name, visited = new Set()) {
  if (cache.has(name)) {
    return cache.get(name);
  }
  if (visited.has(name)) {
    cache.set(name, false);
    return false;
  }
  visited.add(name);
  // 1. 查找作用域绑定
  const binding = path.scope.getBinding(name);
  if (binding) {
    const node = binding.path.node;
    // 变量声明赋值
    if (t.isVariableDeclarator(node)) {
      const init = node.init;
      if (!init) {
        cache.set(name, false);
        return false;
      }
      if (t.isIdentifier(init, { name: FUNC_NAME })) {
        cache.set(name, true);
        return true;
      }
      if (t.isIdentifier(init)) {
        const result = isBoundToTargetFunc(binding.path, init.name, visited);
        cache.set(name, result);
        return result;
      }
      // 如果是函数表达式或箭头函数直接赋值,暂不处理
      cache.set(name, false);
      return false;
    }
    // 处理函数参数(函数声明、表达式、箭头函数)不在这里,下面统一处理
  }
  // 2. 判断是否为函数参数绑定(支持普通函数、箭头函数、匿名函数)
  const funcPath = path.findParent(p => p.isFunction() || p.isArrowFunctionExpression());
  if (funcPath) {
    const params = funcPath.node.params;
    for (let i = 0; i  {
//         if (t.isStringLiteral(el)) {
//           return el.value;
//         }
//         return null;
//       });
//     }
//   }
// });
// 替换调用表达式
traverse(ast, {
  CallExpression(path) {
    const callee = path.node.callee;
    if (t.isIdentifier(callee)) {
      if (
        path.node.arguments.length >= 1 &&
        t.isNumericLiteral(path.node.arguments[0]) &&
        isBoundToTargetFunc(path, callee.name)
      ) {
        const index = path.node.arguments[0].value;
        if (index >= 0 && index  {
    if (err) {
        console.error('写入文件时发生错误:', err);
    } else {
        console.log('文件已成功写入:', fileName);
    }
});
需要注意的是,索引偏移量,还有数组内容,我这是直接赋值了,在原js代码中数组有一次更改顺序的,我是直接在浏览器脚本代码断运行打上断点直接复制数组。放到ast代码中,看一下结果没啥问题。我这限制了一下,字符串长度和类型,/^[a-zA-Z]+$/.test(builtinArray[index-0x8f]) && builtinArray[index-0x8f].length 100,直接使用浏览器的本地替换。再次定位到content的地方。


image.png (61.81 KB, 下载次数: 0)
下载附件
2025-5-25 15:17 上传



image.png (145.56 KB, 下载次数: 0)
下载附件
2025-5-25 15:19 上传

若是用方法一,可以看一下之前大佬写的文章,WEB前端逆向在nodejs环境中复用webpack代码 哇,写的老全面的。我就老老实实用第二种复原算法吧。进去可以看到这是rsa加密。里面定义了公钥私钥,填充方式,公钥用来生成content,私钥是用来签名的生成sign。


image.png (73.7 KB, 下载次数: 0)
下载附件
2025-5-25 15:42 上传



image.png (48.44 KB, 下载次数: 0)
下载附件
2025-5-25 15:47 上传



image.png (84.72 KB, 下载次数: 0)
下载附件
1
2025-5-25 15:43 上传

先是根据账号明文生成content,然后转成md5,在使用私钥生成sign,直接简化到最简。以下是python代码。测试一下没有什么问题。
[Python] 纯文本查看 复制代码# 公钥和私钥
PUBLIC_KEY_PEM = b"""-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----"""
PRIVATE_KEY_PEM = b"""-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----"""
def encrypt_data(data: str) -> str:
    public_key = load_pem_public_key(PUBLIC_KEY_PEM)
    encrypted = public_key.encrypt(
        data.encode('utf-8'),
        padding.PKCS1v15()
    )
    return base64.b64encode(encrypted).decode('utf-8')
def sign_data(data: str) -> str:
    private_key = load_pem_private_key(PRIVATE_KEY_PEM, password=None)
    signature = private_key.sign(
        data.encode('utf-8'),
        padding.PKCS1v15(),
        hashes.SHA1()
    )
    return base64.b64encode(signature).decode('utf-8')
def md5_encrypt(data: str) -> str:
    md5 = hashlib.md5()
    md5.update(data.encode('utf-8'))
    return md5.hexdigest()
def get_data(data_to_encrypt: str) -> str:
    encrypted_data = encrypt_data(data_to_encrypt)
    sign_code = 'content=' + encrypted_data
    sign_hex = md5_encrypt(sign_code)
    signed_data = sign_data(sign_hex)
    import json
    return json.dumps({
        "content": encrypted_data,
        "sign": signed_data
    }, ensure_ascii=False, separators=(',', ':'))


image.png (22.21 KB, 下载次数: 0)
下载附件
2025-5-25 16:00 上传

9.页面上登录账号后,找到x上top人物,先hook,在翻页,相对于登录,我看了一下content和sign是一样的,就响应data有所区别,定住解析后结果直接从结果分析,向上跟栈。


image.png (37.44 KB, 下载次数: 0)
下载附件
2025-5-25 16:07 上传



image.png (18.08 KB, 下载次数: 0)
下载附件
2025-5-25 16:07 上传



image.png (65.23 KB, 下载次数: 0)
下载附件
2025-5-25 16:15 上传

10. 可以很清晰的看到是aes加解密,模式 填充 密钥都有,我直接上python代码。测试一下结果 ok了,没有什么问题(其实翻页不带登录cookies也可以,还没强校验,只是页面先是翻页要登录。)
至此该网站分析完了,大致不难,感谢观看,给点热心值吧,感谢老铁们。


image.png (58.93 KB, 下载次数: 0)
下载附件
2025-5-25 16:21 上传



image.png (65.38 KB, 下载次数: 0)
下载附件
2025-5-25 16:22 上传



image.png (67.62 KB, 下载次数: 0)
下载附件
2025-5-25 16:24 上传

下载次数, 下载附件

您需要登录后才可以回帖 登录 | 立即注册

返回顶部