吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3480|回复: 30
上一主题 下一主题
收起左侧

[Web逆向] 某大厂waf逆向-- 用AST还原加强ob混淆壳

  [复制链接]
跳转到指定楼层
楼主
q8917749 发表于 2025-5-27 22:17 回帖奖励
本帖最后由 q8917749 于 2025-5-27 22:37 编辑

前言:
对于非标准OB的强混淆,有些时候硬着头皮去调试,往往把自己搞晕。 说起AST呢,以前我也不太爱用这个东西,因为觉得用起来还不如我直接调试。
但是没办法,现在的混淆绕的弯越来越大,硬着头皮调试其实还不如用一下 ASTAST往往就能起到事半功倍的效果
本文就讲一下 AST去混淆的一些小技巧。

混淆
长话短说,这边AST用的是js babel 所以直接把一整份源码抠出来放到VS
可以观察到 某个风控指纹的生成地方 长这样:



1.
首先我们去除一些静态计算的数值混淆 比如 0x42e * -0x6 +0x25c8 + -0xcb4 核心的api就是
const {confident, value } = path.evaluate();
path.evaluate() Babel 的静态求值方法,value就是返回值
如果confidenttrue 说明了 表达式里面都是静态值,没有动态值 、变量。是可以安全得到结果的
然后进行替换
[JavaScript] 纯文本查看 复制代码
traverse(ast, {
  BinaryExpression(path) {
    // 尝试计算表达式(支持数值、字符串、布尔值等)
    const { confident, value } = path.evaluate();
    
    if (confident) {
      // 如果是字符串,替换为 StringLiteral
      if (typeof value === "string") {
        path.replaceWith(t.stringLiteral(value));
      } 
      // 如果是数值,替换为 NumericLiteral
      else if (typeof value === "number") {
        path.replaceWith(t.numericLiteral(value));
      }
      // 其他类型(如布尔值)可以类似处理
    }
  }
});

2.去除数组引用混淆
最典型的
[JavaScript] 纯文本查看 复制代码
T9['BT'])(B[ci(Pm.B)], TW ? TB['BS'](B[ci(Pm.T)](B[ci(Pm.A)](B[ci(Pm.E)](B[ci(Pm.x)](B[ci(Pm.c)](B[ci(Pm.M)](TY[ci(Pm.s)]('^'), 'M'), Tn[ci(Pm.j)]), '^'), TJ[ci(Pm.j)]), '^'), TQ[ci(Pm.H)])) : TB['BS'](B[ci(Pm.k)](B[ci(Pm.y)](B[ci(Pm.X)](B[ci(Pm.U)](B[ci(Pm.K)](B[ci(Pm.S)](TY[ci(Pm.s)]('^'), 'P'), Tg[ci(Pm.H)]), '^'), Tb[ci(Pm.P)]), '^'), TQ[ci(Pm.H)])), Ax, TG),


你可以看到  ci函数里面 都是一些静态数组的值
比如 Pm.B  Pm.T 他们都是数组里面的整数值
这里处理起来也很简单
1.
收集数组定义
2.
替换成具体的值即可

按照思路 我们可以让AI帮我们写一份代码即可:
[JavaScript] 纯文本查看 复制代码
 
traverse(ast, {
    VariableDeclarator(path) {
        const { id, init } = path.node;
        if (t.isIdentifier(id) && t.isObjectExpression(init)) {
            const arrayName = id.name;
            init.properties.forEach(prop => {
                if (
                    t.isObjectProperty(prop) &&
                    t.isIdentifier(prop.key) &&
                    t.isNumericLiteral(prop.value) // 仅处理数值属性
                ) {
                    const fullKey = `${arrayName}.${prop.key.name}`;
                    numberMap[fullKey] = prop.value.value; // 记录数值
                }
            });
        }
    }
});

// 3. 替换所有 MemberExpression(如 Pm.B -> 2)
traverse(ast, {
    MemberExpression(path) {
        const { object, property } = path.node;
        if (
            t.isIdentifier(object) &&
            t.isIdentifier(property) &&
            numberMap[`${object.name}.${property.name}`] !== undefined
        ) {
            const fullKey = `${object.name}.${property.name}`;
            path.replaceWith(t.numericLiteral(numberMap[fullKey])); // 替换为数值
        }
    }
});





3.主动调用字符串解密函数经过前面两步,混淆已经变成了这个样子了:


大部分都是 类似
xxx
是一个数字
dT(xxx)  ci(xxx)   
的样式
其实不管dT 还是 ci  还是其他 func(xxx) 这样形式的函数
通过静态分析和动态调试验证都可以发现 他们指向共同的一个函数 B3

我是直接把代码压缩一下放在补环境框架下,把B3导出,在AST解析出参数的时候 ,把参数放进去主动调用即可得到 字符串解密的结果
当然也可以不需要放到补环境框架,直接通过playwright 这些框架, 直接在原界面 浏览器环境下调用B3函数就可以了

所以思路如下
1.
寻找所有引用B3的函数 ,把这些函数名 变回B3
2.
对所有B3函数进行RPC主动调用 还原值

按照这份思路写一份代码 如下:
[JavaScript] 纯文本查看 复制代码
traverse(ast, {
    VariableDeclarator(path) {
        if (t.isIdentifier(path.node.init) && path.node.init.name === 'B3') {
            const binding = path.scope.getBinding(path.node.id.name);
            
            // 确保变量是常量且确实被引用了
            if (binding && binding.constant && binding.references > 0) {
                // 替换所有引用
                binding.referencePaths.forEach(ref => {
                    ref.replaceWith(t.identifier('B3'));
                });
               
                // 移除变量声明
                if (path.parent.declarations.length === 1) {
                    path.parentPath.remove(); // 整个 var 语句
                } else {
                    path.remove(); // 从多变量声明中移除单个声明
                }
            }
        }
    }
});

var B3=myExports.B3

traverse(ast, {
    CallExpression(path) {
      // 检查是否是 dT(数字) 这种调用
      if (t.isIdentifier(path.node.callee, { name: 'B3' }) &&
          path.node.arguments.length === 1 &&
          t.isNumericLiteral(path.node.arguments[0])) {
        
        const argValue = path.node.arguments[0].value;
        const result = B3(argValue);
        
        // 只替换dT调用部分,保留外部的成员表达式结构
        if (typeof result === 'string') {
          path.replaceWith(t.stringLiteral(result));
        } else if (typeof result === 'number') {
          path.replaceWith(t.numericLiteral(result));
        } else if (typeof result === 'boolean') {
          path.replaceWith(t.booleanLiteral(result));
        }
      }
    }
});


还原后发现存在一些静态字符串拼接的情况,类似第一步的情况,把字符串结果也一起静态计算出来


4.还原B函数混淆


现在所有的混淆就剩下了B["xxx"]这个混淆了

截取一部分B函数,他大概长这样
[JavaScript] 纯文本查看 复制代码
[/size]B = {
        'wccbh': function(c, M, s) {
            return c(M, s);
        },
        'trPRA': function(c, M) {
            return c + M;
        },
        'MoDGs': function(c, M) {
            return c(M);
        },
        'alnwk': function(c, M) {
            return c(M);
        },
        'WJPpE': function(c, M) {
            return c(M);
        },
        'LWatF': "acw_sc__v2",
        'IymRT': "[15,35, 29, 24, 33, 16, 1, 38, 10, 9, 19, 31, 40, 27, 22, 23, 25, 13, 6,11,39,18,20,8, 14, 21, 32, 26, 2, 30, 7, 4, 17, 5, 3, 28, 34, 37, 12, 36]",
        'wpbEy': "3000176000856006061501533003690027800375",
        'nSGQU': function(c, M) {
            return c < M;
        },
        'DSwHf': function(c, M) {
            return c == M;
        },
        'wICbM': function(c, M) {
            return c + M;
        },
        'ZJLgP': function(c, M) {
            return c ^ M;
        },
        'lPpIn': function(c, M, s) {
            return c(M, s);
        }[size=10.5pt],

B
函数有字符串,有函数调用,也有一个表达式
当然我们希望还原的效果

B["wICbM"](a,b) ==>  a+b  
B["LWatF"]   ==>"acw_sc__v2",
B["lPpIn](a,b,c)==>a(b,c);

思路大概如下:
第一次遍历 收集B对象的所有的属性定义
根据类型(比如字符串、函数、表达式)分类存储到BMap

然后第二次遍历
根据分类 替换所有B[...]的引用

思路明确了 剩下就是交给AI帮我们写一份代码了


[JavaScript] 纯文本查看 复制代码
const BMap = new Map();

traverse(ast, {
  // 处理变量声明(如 const B = {...})
  VariableDeclarator(path) {
    // 检查是否是 B 对象的定义
    if (
      t.isIdentifier(path.node.id, { name: 'B' }) && // 变量名是 B
      t.isObjectExpression(path.node.init)          // 值是一个对象 {...}
    ) {
      // 遍历 B 对象的所有属性
      path.node.init.properties.forEach((prop) => {
        // 获取属性名(如 'wccbh')
        const key = t.isIdentifier(prop.key) ? prop.key.name : prop.key.value;
        
        // 情况1:属性值是字符串(如 'LWatF': "acw_sc__v2")
        if (t.isStringLiteral(prop.value)) {
          BMap.set(key, { 
            type: 'string', 
            value: prop.value.value // 存储字符串值
          });
        } 
        // 情况2:属性值是函数(如 'wccbh': function(c, M, s) {...})
        else if (t.isFunctionExpression(prop.value) || t.isArrowFunctionExpression(prop.value)) {
          const body = prop.value.body;
          
          // 检查函数体是否是 return 语句(如 return c(M, s);)
          if (t.isBlockStatement(body) && body.body.length === 1 && t.isReturnStatement(body.body[0])) {
            const returnExpr = body.body[0].argument;
            
            // 子情况1:函数返回的是调用表达式(如 return c(M, s))
            if (t.isCallExpression(returnExpr)) {
              BMap.set(key, { 
                type: 'call', 
                args: returnExpr.arguments.map(arg => arg.name) // 提取参数名 [c, M, s]
              });
            } 
            // 子情况2:函数返回的是二元运算(如 return c + M)
            else if (t.isBinaryExpression(returnExpr)) {
              BMap.set(key, { 
                type: 'binary', 
                operator: returnExpr.operator // 提取运算符(如 '+')
              });
            }
          }
        }
      });
    }
  },
});

// 5. 第二次遍历 AST,替换所有 B["xxx"] 的引用
traverse(ast, {
  // 1. 处理函数调用(如 B["wccbh"](a, b, c))
  CallExpression(path) {
    const { callee } = path.node;
    
    // 防御性检查:确保 callee 合法
    if (!callee || !t.isMemberExpression(callee)) return;

    // 检查是否为 B["xxx"] 或 B.xxx 的形式
    if (
      t.isIdentifier(callee.object, { name: 'B' }) &&
      (t.isStringLiteral(callee.property) || t.isIdentifier(callee.property))
    ) {
      const key = t.isStringLiteral(callee.property)
        ? callee.property.value
        : callee.property.name;
      const mapping = BMap.get(key);
      if (!mapping) return;

      // 情况1:替换为直接调用(如 c(M) → func(args))
      if (mapping.type === 'call') {
        // 确保至少有一个参数(函数)
        if (path.node.arguments.length < 1) return;

        const func = path.node.arguments[0];
        // 取第一个参数之后的所有参数
        const args = path.node.arguments.slice(1);

        path.replaceWith(t.callExpression(func, args));
      }
      // 情况2:替换为运算符(如 a + b)
      else if (mapping.type === 'binary' && path.node.arguments.length >= 2) {
        const [left, right] = path.node.arguments;
        path.replaceWith(t.binaryExpression(mapping.operator, left, right));
      }
    }
  },

  // 2. 处理属性访问(如 B["LWatF"])
  MemberExpression(path) {
    const { object, property } = path.node;
    
    // 确保是 B 对象的属性访问
    if (!t.isIdentifier(object, { name: 'B' })) return;

    let key;
    if (t.isStringLiteral(property)) {
      key = property.value;
    } else if (t.isIdentifier(property)) {
      key = property.name;
    } else {
      return; // 非字符串或标识符属性不处理
    }

    const mapping = BMap.get(key);
    if (mapping?.type === 'string') {
      path.replaceWith(t.stringLiteral(mapping.value));
    }
  },
});


5
完结

至此大部分混淆已经去除 如图


其实很多混淆壳 大多大同小异,AST也不必学到精益求精,学会几个常见的处理方式,帮我们把代码简化到大致能看的地步就好了

免费评分

参与人数 16吾爱币 +14 热心值 +15 收起 理由
QAQ~QL + 1 + 1 用心讨论,共获提升!
liquan8909 + 1 热心回复!
lin5789 + 1 热心回复!
ioyr5995 + 1 + 1 我很赞同!
Shengkissyou + 1 + 1 我很赞同!
Yao2903 + 1 + 1 谢谢@Thanks!
yixi + 1 + 1 谢谢@Thanks!
wdj500 + 1 + 1 我很赞同!
fengbolee + 2 + 1 用心讨论,共获提升!
gaosld + 1 + 1 热心回复!
ZeLin99 + 1 我很赞同!
allspark + 1 + 1 用心讨论,共获提升!
melooon + 1 + 1 热心回复!
zaixia + 1 我很赞同!
q25l + 1 + 1 谢谢@Thanks!
laozhang4201 + 1 + 1 热心回复!

查看全部评分

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

推荐
涛之雨 发表于 2025-5-28 07:03
用AST写针对性的脚本还是比较轻松的,但是,写通用性的代码就比较难了,(而且很容易被针对)

使用AI解的话由于AI的鲁棒性并不固定,存在一定的误码率,(这也是AI下面都会有一句“AI生成的内容,仅供参考”的原因)

但是话又说回来了,AI也是一个工具,当使用AI来辅助撰写AST脚本,就会变得很简单了

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
ajk147 + 1 + 1 谢谢@Thanks!

查看全部评分

沙发
phil0314 发表于 2025-5-28 06:53
4#
linix 发表于 2025-5-28 08:32
5#
悦来客栈的老板 发表于 2025-5-28 08:50
其实就是简单的ob混淆。使用AI编写ast反混淆插件,我只能说一言难尽。简单的还好,稍微复杂一点的,它就找不到北了。
6#
stone102 发表于 2025-5-28 09:23
自己会ast的一些基本原理 然后通过分析代码 让ai去写一个就会好很多  你直接扔一个混淆的代码 让ai调  肯定说不过去
7#
n0rma1playe2 发表于 2025-5-28 09:28
感谢分享
8#
RielCcc 发表于 2025-5-28 09:55
前端的混淆经常看得眼都花
9#
cnryb 发表于 2025-5-28 10:20
感谢分享!
10#
pengfangbin 发表于 2025-5-28 15:02
感谢分享!混淆的代码太难了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2025-6-15 15:03

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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