quickjs程序逆向
字数 1554 2025-08-06 18:07:40

QuickJS程序逆向分析教学文档

1. QuickJS简介

QuickJS是一个轻量级的JavaScript引擎,具有以下特点:

  • 体积小且速度快
  • 支持ES2020规范
  • 可以将JS编译为字节码
  • 包含解释器(qjs)和编译器(qjsc)

主要组件:

  • qjs: JavaScript解释器,直接执行JS代码
  • qjsc: 编译器,将JS文件编译为C文件(包含字节码)
  • qjscalc: JS计算器

2. QuickJS编译流程分析

2.1 编译过程

  1. 从QuickJS官网下载并编译选定版本

  2. 使用qjsc将JS文件编译为C文件:

    ./qjsc -e -o output.c input.js
    
    • -e选项输出C文件,否则直接编译为可执行文件
  3. 编译生成的C文件包含:

    • 字节码数组(如qjsc_hello)
    • 运行时初始化代码
    • 主函数调用链:main -> eval_file -> eval_buf -> JS_EvalFunction -> JS_EvalFunctionInternal -> JS_CallFree -> JS_CallInternal

2.2 字节码结构

字节码存储在C文件的数组中,如:

const uint8_t qjsc_hello[78] = {
 0x02, 0x04, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f,
 0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x16, 0x4c,
 // ... 更多字节码
};

3. QuickJS逆向技术

3.1 字节码提取与反编译

  1. 修改QuickJS源码以启用字节码dump功能:

    • 取消quickjs.cDUMP_BYTECODE宏的注释
    • JS_ReadFunctionTag函数的return前插入js_dump_function_bytecode调用
  2. 关键patch点

// patch1: 启用字节码dump
#define DUMP_BYTECODE  (1)

// patch2: 在适当位置插入dump调用
#if DUMP_BYTECODE 
js_dump_function_bytecode(ctx, b);  
#endif
  1. 重新编译QuickJS后,执行时会输出易读的字节码

3.2 字节码分析

QuickJS虚拟机特点:

  • 基于栈的虚拟机
  • 操作码定义在quickjs-opcode.h
  • 常见指令:
    • call: 调用函数,后跟参数个数
    • call_method: 调用对象方法
    • get: 获取参数入栈
    • push: 直接压入栈
    • put: 将栈中数据存放至变量

4. 实战案例:2022YCB-easyre逆向

4.1 题目分析

  1. 识别QuickJS编译的可执行文件特征:

    • 字符串中包含"quickjs.c"
    • 存在大量js_开头的函数
  2. 提取字节码:

    • 从二进制中提取4513字节的机器码
    • 替换自定义C文件中的字节码数组
  3. 编译并运行修改后的程序,获取反编译输出

4.2 加密流程分析

程序主要逻辑:

data3 = [5911837666743816200, 5133585975960501272, ...] # 15个元素的数组
data4 = [2101524238053948931, 9154814254531429383, 8941618984500083987]
data5 = [0]*12

# 加密流程
data2 = myenc.encode64(enc1(input, k))  # 魔改XXTEA + 换表base64

for i in range(len(data2)):  # 异或处理
    data2[i] ^= data2[(i+2)%len(data2)]

for i in range(0, len(data2), 8):  # 数据转换
    if i%32 != 8:
        arg = [data2[i+j] for j in range(8)]
        data5[index] = func5(arg)
    else:
        data5[index] = data4[index1]
        index1 += 1
    index += 1

# 校验
z = 0
for i in range(3):
    if func6(i):
        z += 1
if z == 3:
    print('suc')
else:
    print('nono')

4.3 关键函数分析

4.3.1 encode64函数

  • 换表base64编码
  • 使用自定义字母表:
    keystr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/="
    

4.3.2 enc1函数

  • 魔改XXTEA加密
  • 修改参数:
    • DELTA = 289739796
    • 位运算参数:6、3、4、5
  • 密钥:k = "welcometoycb2022"

4.3.3 func6校验函数

  • 将输入分为4个8字节一组(x,y,z,w)
  • 进行复杂逻辑运算后与data5比对
  • 约束条件:每组第二个数据(y)必须等于data4中的数据

4.4 解题步骤

  1. 约束求解

    • 使用Z3求解器解func6的方程
    • 确保y值等于data4中的对应值
    from z3 import *
    
    data3 = [...] # 原始数据
    data4 = [...] # 约束数据
    
    for i in range(3):
        x = BitVec('x',65)
        y = BitVec('y',65)
        z = BitVec('z',65)
        w = BitVec('w',65)
        s = Solver()
        s.add((~x)&z == data3[5*i])
        s.add(w ^ (((~y)&x) | ((~y)&z) | (x&y)| ((~x)&z)) == data3[5*i+1])
        # 添加更多约束...
        s.add(y == data4[i])
        print(s.check())
        m = s.model()
        # 获取解...
    
  2. 数据转换

    • 将求解结果转换为字节序列
    • 逆向处理异或操作:
      for i in range(len(ms)):
          ms[i] ^= ms[(i+2)%len(ms)]
      
  3. Base64解码

    • 使用自定义字母表逆向换表base64
  4. XXTEA解密

    • 实现魔改版XXTEA解密算法:
      DELTA = 289739796
      
      def decrypt(v, n, k):
          rounds = 6 + int(52 / n)
          sum = c_uint32(rounds * DELTA)
          y = v[0].value
          while rounds > 0:
              e = (sum.value >> 2) & 3
              p = n - 1
              while p > 0:
                  z = v[p - 1].value
                  v[p].value -= (((z >> 6 ^ y << 3) + (y >> 4 ^ z << 5)) ^ ((sum.value ^ y) + (k[(p & 3) ^ e] ^ z)))
                  y = v[p].value
                  p -= 1
              z = v[n - 1].value
              v[0].value -= (((z >> 6 ^ y << 3) + (y >> 4 ^ z << 5)) ^ ((sum.value ^ y) + (k[(p & 3) ^ e] ^ z)))
              y = v[0].value
              sum.value -= DELTA
              rounds -= 1
      

5. 总结与防御建议

5.1 逆向技巧总结

  1. 识别QuickJS特征:字符串、函数名等
  2. 修改源码启用字节码dump功能
  3. 理解基于栈的虚拟机工作原理
  4. 分析关键加密函数(如魔改XXTEA)
  5. 使用约束求解器处理复杂逻辑

5.2 防御建议

  • 对关键代码进行混淆
  • 使用更复杂的加密算法组合
  • 增加反调试机制
  • 使用原生代码实现核心算法

6. 参考资源

  1. QuickJS官方源码
  2. QuickJS内部实现分析文章
  3. XXTEA算法标准实现
  4. Z3求解器官方文档
QuickJS程序逆向分析教学文档 1. QuickJS简介 QuickJS是一个轻量级的JavaScript引擎,具有以下特点: 体积小且速度快 支持ES2020规范 可以将JS编译为字节码 包含解释器(qjs)和编译器(qjsc) 主要组件: qjs : JavaScript解释器,直接执行JS代码 qjsc : 编译器,将JS文件编译为C文件(包含字节码) qjscalc : JS计算器 2. QuickJS编译流程分析 2.1 编译过程 从QuickJS官网下载并编译选定版本 使用qjsc将JS文件编译为C文件: -e 选项输出C文件,否则直接编译为可执行文件 编译生成的C文件包含: 字节码数组(如 qjsc_hello ) 运行时初始化代码 主函数调用链: main -> eval_file -> eval_buf -> JS_EvalFunction -> JS_EvalFunctionInternal -> JS_CallFree -> JS_CallInternal 2.2 字节码结构 字节码存储在C文件的数组中,如: 3. QuickJS逆向技术 3.1 字节码提取与反编译 修改QuickJS源码 以启用字节码dump功能: 取消 quickjs.c 中 DUMP_BYTECODE 宏的注释 在 JS_ReadFunctionTag 函数的return前插入 js_dump_function_bytecode 调用 关键patch点 : 重新编译QuickJS后,执行时会输出易读的字节码 3.2 字节码分析 QuickJS虚拟机特点: 基于栈的虚拟机 操作码定义在 quickjs-opcode.h 中 常见指令: call : 调用函数,后跟参数个数 call_method : 调用对象方法 get : 获取参数入栈 push : 直接压入栈 put : 将栈中数据存放至变量 4. 实战案例:2022YCB-easyre逆向 4.1 题目分析 识别QuickJS编译的可执行文件特征: 字符串中包含"quickjs.c" 存在大量 js_ 开头的函数 提取字节码: 从二进制中提取4513字节的机器码 替换自定义C文件中的字节码数组 编译并运行修改后的程序,获取反编译输出 4.2 加密流程分析 程序主要逻辑: 4.3 关键函数分析 4.3.1 encode64函数 换表base64编码 使用自定义字母表: 4.3.2 enc1函数 魔改XXTEA加密 修改参数: DELTA = 289739796 位运算参数:6、3、4、5 密钥: k = "welcometoycb2022" 4.3.3 func6校验函数 将输入分为4个8字节一组(x,y,z,w) 进行复杂逻辑运算后与data5比对 约束条件:每组第二个数据(y)必须等于data4中的数据 4.4 解题步骤 约束求解 : 使用Z3求解器解func6的方程 确保y值等于data4中的对应值 数据转换 : 将求解结果转换为字节序列 逆向处理异或操作: Base64解码 : 使用自定义字母表逆向换表base64 XXTEA解密 : 实现魔改版XXTEA解密算法: 5. 总结与防御建议 5.1 逆向技巧总结 识别QuickJS特征:字符串、函数名等 修改源码启用字节码dump功能 理解基于栈的虚拟机工作原理 分析关键加密函数(如魔改XXTEA) 使用约束求解器处理复杂逻辑 5.2 防御建议 对关键代码进行混淆 使用更复杂的加密算法组合 增加反调试机制 使用原生代码实现核心算法 6. 参考资源 QuickJS官方源码 QuickJS内部实现分析文章 XXTEA算法标准实现 Z3求解器官方文档