吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 926|回复: 13
上一主题 下一主题
收起左侧

[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、前置处理
                  

   jsvmp通过拆分运算,通过伪字节码的形式实现运算,在加解密的过程中,异或加减运算都是算法的核心实现。所以我们需要对运算符进行插桩(有时间搓一个vmp案例),插桩效果如下:                        建议不了解vmp的大佬可以看看app的vmp项目或者windows的,效果更佳直观     首先是对计算形式的改变,k值在实现上动态的,需要根据计算的情况,推算k值的真实值,从左到右递减。其次是构造console.log节点插桩分析。
(1)console.log节点构造
         
      根据ast的解析,console.log本质上是两个Identifier节点组成的MemberExpression再进行调用,形成了CallExpression,参数部分是CallExpression的arguments数组填充。根据bebal官方文档直接进行节点构造。
            
            
           
           代码实现:
         

        
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还得是蔡老板,最后看一下效果,项目已经开源,大佬们可以玩玩。
      
        

免费评分

参与人数 8威望 +2 吾爱币 +109 热心值 +7 收起 理由
fengbolee + 1 + 1 用心讨论,共获提升!
allspark + 1 + 1 用心讨论,共获提升!
xifan520 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
涛之雨 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Yangzaipython + 1 + 1 鼓励转贴优秀软件安全工具和文档!
littlewhite11 + 3 + 1 我很赞同!
dandan946 + 1 + 1 我很赞同!
wocuole + 1 + 1 谢谢@Thanks!

查看全部评分

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

沙发
YIUA 发表于 2025-12-10 21:50
某皮是哪个
3#
 楼主| buluo533 发表于 2025-12-10 21:51 |楼主
4#
YIUA 发表于 2025-12-10 21:54

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

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

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

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

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

本版积分规则

返回列表

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

GMT+8, 2025-12-12 09:05

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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