吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3885|回复: 29
收起左侧

[Web逆向] AST自动插桩jsvmp的简单实现

  [复制链接]
buluo533 发表于 2025-12-10 21:13
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责



项目地址:https://github.com/buluo533/vmp_demo
工作上有对vmp的需求,动态vmp手动插桩效率太低,听说大佬有ast的一键插桩,自己找了一个demo捣鼓了一下,没想到成了。

一、环境配置
   
[JavaScript] 纯文本查看 复制代码
npm install @babel/parsernpm install @babel/traverse
npm install @babel/generator
npm install @babel/types

        
[JavaScript] 纯文本查看 复制代码
let parse = require("@babel/parser").parse;let traverse = require("@babel/traverse").default;
let generator = require("@babel/generator").default;
let fs = require("fs");
let type = require("@babel/types");

let js_code = fs.readFileSync("vmpdemo2.js", encoding = "utf-8");
let ast = parse(js_code)

// 核心代码逻辑

let code = generator(ast).code
fs.writeFileSync("./vmp_demo2插桩后.js", code, "utf-8");



二、样本vmp代码逻辑
   需要对vmp有基本的了解,vmp的插桩点主要是在各种运算符和通过call和apply进行的函数调用,某皮的vmp同时带有控制流的混淆处理,需要先解混淆再进行插桩。以简单结构的vmp作为测试案例,分析vmp动态插桩的实现方案
      
1、前置处理
                  

vmp1

vmp1


   jsvmp通过拆分运算,通过伪字节码的形式实现运算,在加解密的过程中,异或加减运算都是算法的核心实现。所以我们需要对运算符进行插桩(有时间搓一个vmp案例),插桩效果如下:                 

vmp2

vmp2
      建议不了解vmp的大佬可以看看app的vmp项目或者windows的,效果更佳直观     首先是对计算形式的改变,k值在实现上动态的,需要根据计算的情况,推算k值的真实值,从左到右递减。其次是构造console.log节点插桩分析。
(1)console.log节点构造
         

vmp3

vmp3

      根据ast的解析,console.log本质上是两个Identifier节点组成的MemberExpression再进行调用,形成了CallExpression,参数部分是CallExpression的arguments数组填充。根据bebal官方文档直接进行节点构造。
            

vmp4

vmp4

            

vmp5

vmp5

           

vmp6

vmp6

           代码实现:
         

        
function insert_log(log_arguments) {
  if (!log_arguments instanceof Array) {
    Error("入参应该是数组类型")
  }
  let object_name = "console"
  let property_name = "log"
  let property_create = type.identifier(property_name)
  let object_create = type.identifier(object_name)
  // MemberExpression构建
  let MemberExpression_create = type.memberExpression(object_create, property_create)
  // callExpression
  let callExpression_create = type.callExpression(MemberExpression_create, log_arguments)
  return callExpression_create
}


    多次调用需要,封装成函数
      
(2)动态k值处理实现
         动态k值的处理涉及到插桩的核心逻辑变化,我是先改变原有的代码,使代码是需要的插桩形式,将插桩代码存入内存后,再还原之前的代码逻辑。首先是对自增节点的处理UpdateExpression对应的是++k的自增节点,定位节点特征父节点是SwitchCase,后兄弟节点是BreakStatement是这个语句的特点,再通过深度遍历,提取UpdateExpression节点出现次数用做真实动态值计算。
      
traverse(ast, {
  ExpressionStatement: function (path) {
    let {node, parentPath, getNextSibling} = path;
    if (!type.isSwitchCase(parentPath) && !type.isBreakStatement(getNextSibling)) return;
    let {expression} = node;
    if (!type.isAssignmentExpression(expression)) return;
    let updateExpressions = [];
    path.traverse({
      UpdateExpression(_path) {
        updateExpressions.push(_path);
      }
    });
    updateExpressions.reverse().forEach((_path, index) => {
      let countValue = index; // 或者 total - 1 - index
      let binaryExpression_create = type.binaryExpression(
        "-",
        type.identifier("k"),
        type.numericLiteral(countValue)
      );
      _path.parentPath.node.property = binaryExpression_create;
    });
  }
})


2、插桩处理
      首先 ast 实现的一个核心是对关键代码的一个定位,因为这个和 k 值的自增定位逻辑上是类似的,不过多赘述
        
let {node, parentPath, getNextSibling} = path;
if (!type.isSwitchCase(parentPath) && !type.isBreakStatement(getNextSibling)) return;
let {expression} = node;
if (!type.isAssignmentExpression(expression)) return;
let {left, right, operator} = expression;
if (!type.isMemberExpression(left)) return;
let return_string = "返回值===>"
let return_string_create = createSafeStringLiteral(return_string)
// 存在一个+=的计算
if (operator !== "=") return;

    当然代码存在一些遗漏和不足的地方,到时候 todo 啦{:301_998:}
      首先是处理运算符计算的插桩逻辑      
if (type.isBinaryExpression(right)) {
  let operator_list = ["+", "-", "*", "/", "%", "==", "===", "<", "<=", ">", "<<", ">>", "<<<", ">>>", "|", "^", "&", ">="]
  let in_right = type.cloneNode(right.right);
  let in_left =  type.cloneNode(right.left);
  let in_operator =right.operator;
  if (!operator_list.includes(in_operator)) return;
  let create_stringLiteral = "运算===>"
  let stringLiteral_create = createSafeStringLiteral(create_stringLiteral)
  let binaryExpression_create = type.binaryExpression(in_operator, in_left, in_right)
  let info_argument = [stringLiteral_create, binaryExpression_create, return_string_create,  type.cloneNode(left)]
  let call = insert_log(info_argument)
  keep_map.set(num, call)
  path.node.flag = type.NumericLiteral(num);
  num++;


首先是进行类型的判断提取BinaryExpression 表达式,因为在这里的运算符很明确,为了稳定可以判断一下是否包含,接下来就是构造 log 需要打印的内容,因为目前节点是已经变了,我们应该在还原节点之后再插入内容,我们在当前节点插入一个 flag,标记是我们要插桩的节点,同时用一个自增的 num 来存储 对应 map 结构
同理完成 函数调用 call 和 apply 的插桩逻辑  
else if (type.isCallExpression(right)) {
      // call apply 函数调用插桩处理
      if (!type.isMemberExpression(right.callee)) return;
      let in_object =  type.cloneNode(right.callee.object);
      let in_property_name = right.callee.property.name;
      let in_arguments = right.arguments;
      if (in_property_name === "apply") {
        let first_string = "func ===>"
        let first_stringLiteral_create = createSafeStringLiteral(first_string)
        let this_string = "this===>"
        let this_string_create = createSafeStringLiteral(this_string)
        let this_argument =  type.cloneNode(in_arguments[0])
        const value = type.cloneNode( in_arguments[1]);
        let insert_index = `参数===>`
        let insert_index_create = createSafeStringLiteral(insert_index)
        let apply_argument = [first_stringLiteral_create, in_object, this_string_create, this_argument, insert_index_create, value, return_string_create, type.cloneNode(left)]
        let apply_log = insert_log(apply_argument)
        keep_map.set(num, apply_log)
        path.node.flag = type.NumericLiteral(num);
        num++;
      } else if (in_property_name === "call") {
        let first_string = "func ===>"
        let first_stringLiteral_create = createSafeStringLiteral(first_string)
                let this_string = "this===>"
                let this_string_create = createSafeStringLiteral(this_string)
                let this_argument =  type.cloneNode(in_arguments[0])
                let call_arguments = [first_stringLiteral_create, in_object, this_string_create, this_argument]
                if (in_arguments.length > 1) {
                    for (let index = 1; index < in_arguments.length; index++) {
                        let call_argument = in_arguments[index]
                        let index_string_create = createSafeStringLiteral(`参数${index}====>`)
                        call_arguments.push(index_string_create)
                        call_arguments.push( type.cloneNode(call_argument))
                    }
                }
                call_arguments.push(return_string_create)
                call_arguments.push( type.cloneNode(left))
                let call_log = insert_log(call_arguments)
                keep_map.set(num, call_log)
                path.node.flag = type.NumericLiteral(num);
                num++;
            }
        }


3、代码还原
       在对 ++k 自增逻辑处理后,需要同理进行还原,直接遍历梭哈,构造节点
         
traverse(ast, {
  ExpressionStatement: function (path) {
    let {node, parentPath, getNextSibling} = path;
    if (!type.isSwitchCase(parentPath) && !type.isBreakStatement(getNextSibling)) return;
    let {expression} = node;
    if (!type.isAssignmentExpression(expression)) return;
    let operator = "++"
    let in_argument = type.identifier("k")
    path.traverse({
      BinaryExpression(_path) {
        _path.parentPath.node.property = type.updateExpression(operator, in_argument, true)
      }
    });
  }
})

4、提取插桩代码实现

然后就是 map 的存在 map 里的结构提取出来,塞到要插桩的位置
traverse(ast, {
  ExpressionStatement: function (path) {
    let {node} = path;
    if (!node.flage) return
    let insert_path = keep_map.get(node.flage.value)
    path.insertAfter(insert_path)
  }
})



三、总结
              相比于解混淆,插桩难点主要是在特征定位,还有就是插桩时二次调用函数或者结果导致走向错误分支(大佬提醒我的),感谢小伙伴在我卡点对我的指导,还有沙琪玛大佬提供的vmp样本。ast代码写的比较丑陋,最终目的还是实现功能。学ast还得是蔡老板,最后看一下效果,项目已经开源,大佬们可以玩玩。
      

vmp7

vmp7

        

vmp8

vmp8

免费评分

参与人数 16威望 +2 吾爱币 +116 热心值 +15 收起 理由
distrydoudou + 1 + 1 我很赞同!
QAQ~QL + 1 + 1 用心讨论,共获提升!
Bizhi-1024 + 1 用心讨论,共获提升!
ytfh1131 + 1 + 1 谢谢@Thanks!
imumu1239 + 1 + 1 热心回复!
ioyr5995 + 1 + 1 我很赞同!
yixi + 1 + 1 谢谢@Thanks!
zxzx307 + 1 + 1 谢谢@Thanks!
fengbolee + 1 + 1 用心讨论,共获提升!
allspark + 1 + 1 用心讨论,共获提升!
xifan520 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
涛之雨 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Yangzaipython + 1 + 1 鼓励转贴优秀软件安全工具和文档!
littlewhite11 + 3 + 1 我很赞同!
dandan946 + 1 + 1 我很赞同!
wocuole + 1 + 1 谢谢@Thanks!

查看全部评分

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

炫迈 发表于 2025-12-16 09:57
核心思路很对,VMP插桩关键就是定位运算符和函数调用点,你用AST遍历+特征匹配这招很稳。特别是处理动态k值那块,先改节点再还原的思路很巧妙,我之前搞某音VMP时也这么干过。

不过有几个地方可以优化下:

第一,flag标记方式太粗暴。用node.flag存num容易和其他AST节点冲突,建议用WeakMap或者path.set('instrument_id', num)这种Babel内置的元数据存储,安全得多。

第二,运算符插桩漏了复合运算。你只处理了BinaryExpression,但VMP里经常有a += b++这种复合操作,应该加个UnaryExpression的处理,特别是++和--操作符。

第三,函数调用插桩没考虑bind和new。现在只处理了call和apply,但VMP经常用bind改变this,new操作符也会改变执行上下文,建议加个判断:
if (callee.property.name === 'bind' || path.parentPath.isNewExpression()) {
// 特殊处理
}

第四,性能问题。现在每插一个桩都cloneNode,大文件会卡死。建议用path.scope.generateUidIdentifier生成唯一标识符,避免深拷贝。

第五,最致命的:没处理VMP的反调试。现在直接插console.log,遇到反调试直接GG。建议加个环境检测:
function wrapLog(args) {
return type.conditionalExpression(
type.memberExpression(type.identifier('window'), type.identifier('debugMode')),
insert_log(args),
type.identifier('void 0')
);
}

实战建议:

加个--no-log参数,只改k值不插桩,用于快速脱壳
用path.skip()跳过已经处理过的节点,避免重复遍历
对ArrayExpression里的运算也插桩,VMP经常把关键计算藏在数组里
用generator的comments选项保留原注释,方便对照分析
加个--output-dir参数,批量处理多个文件
代码里有个小bug:insert_log函数开头的判断应该用Array.isArray(log_arguments),现在那个instanceof判断永远为false。

老哥这个项目可以再深入做下去:

加个Chrome DevTools协议,自动注入到浏览器
用WebAssembly优化AST遍历速度
做个可视化界面,点击节点自动插桩
结合Frida实现动态补环境
最后提醒下,某书新版本VMP用了双重校验,单纯插桩会触发反制。建议在插桩后加个setTimeout延迟执行,绕过时间检测。期待老哥后续更新!
YIUA 发表于 2025-12-10 21:50
 楼主| buluo533 发表于 2025-12-10 21:51
YIUA 发表于 2025-12-10 21:54

我看你demo是tx,还以为是tx的一个网站,shopee跟dy混淆结构差不多,不过shopee混淆更强点
 楼主| buluo533 发表于 2025-12-10 21:58
YIUA 发表于 2025-12-10 21:54
我看你demo是tx,还以为是tx的一个网站,shopee跟dy混淆结构差不多,不过shopee混淆更强点

要麻烦很多了现在想想都是头皮发麻的
 楼主| buluo533 发表于 2025-12-10 21:59
YIUA 发表于 2025-12-10 21:54
我看你demo是tx,还以为是tx的一个网站,shopee跟dy混淆结构差不多,不过shopee混淆更强点

是tx的demo,那这个做案例,shopee的就不方便放出来了
YIUA 发表于 2025-12-10 22:01
buluo533 发表于 2025-12-10 21:58
要麻烦很多了现在想想都是头皮发麻的

有了思路很快的,就那几种类型的结构
 楼主| buluo533 发表于 2025-12-10 22:51
YIUA 发表于 2025-12-10 22:01
有了思路很快的,就那几种类型的结构

主要还是混淆的处理,插桩的难度也只在对结构的定位
renjw234 发表于 2025-12-11 10:43
有没有ast成套得教程?
Yangzaipython 发表于 2025-12-11 11:22
感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-2-19 07:39

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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