找回密码
 立即注册
首页 业界区 业界 解析2025强网拟态EZMiniAPP

解析2025强网拟态EZMiniAPP

纪晴丽 前天 12:15
微信小程序逆向分析与加密算法破解

一、题目背景与初步分析

1.1 题目描述

本题是一道Mobile类别的CTF挑战题,题目提供了一个文件:__APP__.wxapkg。
1.2 什么是wxapkg文件

.wxapkg是微信小程序的打包文件格式。微信小程序是运行在微信客户端内的轻量级应用程序,其代码包就以这种特殊格式分发。
wxapkg文件的特点

  • 二进制格式,无法直接用文本编辑器查看
  • 包含小程序的所有资源:JavaScript代码、页面模板、样式表、配置文件等
  • 有特定的文件结构:包含文件头、索引区和数据区
1.3 解题思路


  • 解包wxapkg文件,提取其中的代码
  • 分析JavaScript代码,找到加密逻辑
  • 理解加密算法的工作原理
  • 编写解密脚本,获取flag
二、wxapkg文件格式详解

2.1 文件结构分析

一个标准的wxapkg文件由三部分组成:
  1. ┌─────────────────────────────────────┐
  2. │          文件头部 (Header)           │
  3. ├─────────────────────────────────────┤
  4. │  - First Mark (1字节): 标识字节     │
  5. │  - Info1 (4字节): 信息段            │
  6. │  - Info2 (4字节): 信息段            │
  7. │  - Data Offset (4字节): 数据区偏移  │
  8. │  - Reserved (1字节): 保留字节       │
  9. ├─────────────────────────────────────┤
  10. │         索引区 (Index Section)       │
  11. ├─────────────────────────────────────┤
  12. │  - File Count (4字节): 文件数量     │
  13. │  - File List: 文件列表              │
  14. │    * Name Length (4字节)            │
  15. │    * Name (变长): 文件名            │
  16. │    * Offset (4字节): 文件偏移       │
  17. │    * Size (4字节): 文件大小         │
  18. ├─────────────────────────────────────┤
  19. │         数据区 (Data Section)        │
  20. ├─────────────────────────────────────┤
  21. │  各个文件的实际数据内容              │
  22. └─────────────────────────────────────┘
复制代码
关键技术点

  • 多字节整数使用大端序(Big-Endian)存储
  • 文件偏移量是从wxapkg文件开头计算的绝对位置
  • 文件名是UTF-8编码的字符串
2.2 为什么需要解包

wxapkg是二进制打包格式,直接查看只能看到乱码。我们需要:

  • 解析文件头,获取文件列表信息
  • 根据偏移量和大小,提取每个文件的数据
  • 还原成原始的目录结构
三、实战:解包wxapkg文件

3.1 编写解包工具

我们使用Python的struct模块来解析二进制数据:
  1. #!/usr/bin/env python3
  2. import struct
  3. import os
  4. def unpack_wxapkg(wxapkg_file, output_dir):
  5.     """解包微信小程序 wxapkg 文件"""
  6.     with open(wxapkg_file, 'rb') as f:
  7.         # 读取头部信息
  8.         first_mark = struct.unpack('B', f.read(1))[0]
  9.         f.read(4)  # 跳过Info1
  10.         f.read(4)  # 跳过Info2
  11.         # 读取数据区偏移量 (大端序,用'>I'表示)
  12.         data_section_offset = struct.unpack('>I', f.read(4))[0]
  13.         f.read(1)  # 跳过保留字节
  14.         # 读取文件数量
  15.         file_count = struct.unpack('>I', f.read(4))[0]
  16.         # 读取文件列表
  17.         file_list = []
  18.         for i in range(file_count):
  19.             # 文件名长度
  20.             name_len = struct.unpack('>I', f.read(4))[0]
  21.             # 文件名 (UTF-8编码)
  22.             name = f.read(name_len).decode('utf-8')
  23.             # 文件偏移和大小
  24.             offset = struct.unpack('>I', f.read(4))[0]
  25.             size = struct.unpack('>I', f.read(4))[0]
  26.             file_list.append({
  27.                 'name': name,
  28.                 'offset': offset,
  29.                 'size': size
  30.             })
  31.         # 创建输出目录
  32.         if not os.path.exists(output_dir):
  33.             os.makedirs(output_dir)
  34.         # 解包每个文件
  35.         for file_info in file_list:
  36.             name = file_info['name'].lstrip('/')
  37.             file_path = os.path.join(output_dir, name)
  38.             file_dir = os.path.dirname(file_path)
  39.             # 创建文件所在目录
  40.             if file_dir and not os.path.exists(file_dir):
  41.                 os.makedirs(file_dir)
  42.             # 读取并写入文件数据
  43.             f.seek(file_info['offset'])
  44.             file_data = f.read(file_info['size'])
  45.             with open(file_path, 'wb') as out_f:
  46.                 out_f.write(file_data)
  47.             print(f"Extracted: {file_info['name']}")
复制代码
技术要点解释

  • struct.unpack('B', data):解包1个无符号字节
  • struct.unpack('>I', data):解包4字节无符号整数(大端序)

    • >表示大端序
    • I表示无符号整数(unsigned int)

  • decode('utf-8'):将字节序列解码为UTF-8字符串
3.2 执行解包

运行解包脚本:
  1. python3 unpacker.py
复制代码
输出结果:
  1. Unpacking __APP__.wxapkg...
  2. First mark: 190
  3. Data section offset: 170832
  4. File count: 24
  5. File 1: /__debug__/__jscore-debug__.png, offset: 907, size: 178
  6. ...
  7. File 11: /chunk_0.appservice.js, offset: 65008, size: 15834
  8. ...
  9. Extracted: /chunk_0.appservice.js
  10. ...
  11. Done!
复制代码
成功解包出24个文件!其中最关键的是chunk_0.appservice.js。
3.3 解包后的文件结构
  1. unpacked/
  2. ├── __debug__/                  # 调试文件
  3. ├── app-config.json             # 小程序配置
  4. ├── app-service.js              # 服务层主文件
  5. ├── appservice.app.js           # 应用逻辑
  6. ├── chunk_0.appservice.js       # ★ 关键:包含页面逻辑
  7. ├── chunk_1.appservice.js       # 代码分块
  8. ├── common.app.js               # 公共代码
  9. ├── pages/                      # 页面目录
  10. │   ├── index/                  # 首页
  11. │   │   ├── index.html
  12. │   │   └── index.wxss
  13. │   └── logs/                   # 日志页
  14. │       ├── logs.html
  15. │       └── logs.wxss
  16. └── page-frame.html             # 页面框架
复制代码
四、代码分析:定位加密逻辑

4.1 查看小程序配置

首先查看app-config.json了解小程序结构:
  1. {
  2.   "entryPagePath": "pages/index/index.html",
  3.   "pages": ["pages/index/index", "pages/logs/logs"],
  4.   ...
  5. }
复制代码
可以看到入口页面是pages/index/index,这应该是我们的重点分析对象。
4.2 分析关键文件

打开chunk_0.appservice.js,这个文件包含了index页面的逻辑代码。虽然代码经过了混淆,但我们仍能识别出关键函数。
在第2行找到了核心逻辑(为便于阅读,这里进行了格式化):
  1. Page({
  2.     data: {
  3.         inputValue: "",
  4.         animationData: {}
  5.     },
  6.     // 输入框变化处理
  7.     onInputChange: function(a) {
  8.         this.setData({inputValue: a.detail.value});
  9.     },
  10.     // ★ 关键:加密函数
  11.     enigmaticTransformation: function(a, t) {
  12.         // a: 明文
  13.         // t: 密钥
  14.         // ... 加密逻辑 ...
  15.     },
  16.     // 自定义加密入口
  17.     customEncrypt: function(a, t) {
  18.         return this.enigmaticTransformation(a, t);
  19.     },
  20.     // ★ 验证逻辑
  21.     onCheck: function() {
  22.         var a = this.data.inputValue;
  23.         if ("" !== a.trim()) {
  24.             var t = this.customEncrypt(a, "newKey2025!");
  25.             console.log(t);
  26.             JSON.stringify(t) === JSON.stringify([1, 33, 194, 133, 195, 102, 232, 104, 200, 14, 8, 163, 131, 71, 68, 97, 2, 76, 72, 171, 74, 106, 225, 1, 65])
  27.                 ? wx.showToast({title: "Right", icon: "success", duration: 2e3})
  28.                 : wx.showToast({title: "Wrong", icon: "error", duration: 2e3});
  29.         }
  30.     }
  31. });
复制代码
关键发现

  • 密钥:"newKey2025!"
  • 预期密文:[1, 33, 194, 133, 195, 102, 232, 104, 200, 14, 8, 163, 131, 71, 68, 97, 2, 76, 72, 171, 74, 106, 225, 1, 65]
  • 加密函数:enigmaticTransformation
五、深入分析

5.1 完整提取加密逻辑
  1. enigmaticTransformation: function(a, t) {
  2.     // 步骤1: 将密钥转换为ASCII码数组
  3.     i = Array.from(t).map(function(a) {
  4.         return a.charCodeAt(0);
  5.     });
  6.     s = i.length;
  7.     // 步骤2: 计算循环移位参数c
  8.     c = function(a) {
  9.         for (var t = 0, e = 0; e < a.length; e++) {
  10.             switch(e % 4) {
  11.                 case 0: t += 1 * a[e]; break;
  12.                 case 1: t += a[e] + 0; break;
  13.                 case 2: t += 0 | a[e]; break;  // 按位或0
  14.                 case 3: t += 0 ^ a[e]; break;  // 按位异或0
  15.             }
  16.         }
  17.         return t;
  18.     }(i) % 8;
  19.     // 步骤3: 逐字符加密
  20.     r = [];
  21.     for (o = 0; o < a.length; o++) {
  22.         var u;
  23.         // 3.1: 异或运算
  24.         switch(o % 3) {
  25.             case 0:
  26.                 u = a.charCodeAt(o) ^ i[o % s];
  27.                 break;
  28.             case 1:
  29.                 u = i[o % s] ^ a.charCodeAt(o);
  30.                 break;
  31.             case 2:
  32.                 e = a.charCodeAt(o);
  33.                 n = i[o % s];
  34.                 u = e ^ n;
  35.                 break;
  36.         }
  37.         // 3.2: 循环左移
  38.         var h;
  39.         switch(c) {
  40.             case 0: h = u; break;
  41.             case 1: h = 255 & (u << 1 | u >> 7 & 1); break;
  42.             case 2: h = 255 & (u << 2 | u >> 6 & 3); break;
  43.             case 3: h = 255 & (u << 3 | u >> 5 & 7); break;
  44.             case 4: h = 255 & (u << 4 | u >> 4 & 15); break;
  45.             case 5: h = 255 & (u << 5 | u >> 3 & 31); break;
  46.             case 6: h = 255 & (u << 6 | u >> 2 & 63); break;
  47.             case 7: h = 255 & (u << 7 | u >> 1 & 127); break;
  48.             default: h = 255 & (u << c | u >> (8 - c)); break;
  49.         }
  50.         // 3.3: 添加到结果数组
  51.         r.push(h);
  52.     }
  53.     return r;
  54. }
复制代码
5.2 算法流程图
  1. 输入: 明文字符串, 密钥字符串
  2.   ↓
  3. 步骤1: 密钥处理
  4.   - 将密钥转为ASCII码数组
  5.   - key = "newKey2025!" → [110, 101, 119, 75, 101, 121, 50, 48, 50, 53, 33]
  6.   ↓
  7. 步骤2: 计算移位参数
  8.   - 对密钥数组各元素求和
  9.   - sum = 110+101+119+75+101+121+50+48+50+53+33 = 861
  10.   - c = 861 % 8 = 5
  11.   ↓
  12. 步骤3: 逐字符加密
  13.   对于每个明文字符:
  14.     3.1 异或运算
  15.       - plain_char ^ key[i % key_length] → u
  16.     3.2 循环左移
  17.       - rotate_left(u, c) → h
  18.     3.3 添加到结果
  19.       - result.append(h)
  20.   ↓
  21. 输出: 密文字节数组
复制代码
5.3 关键技术点详解

5.3.1 异或运算(XOR)

基本性质

  • A ^ B = C 则 C ^ B = A(自反性)
  • A ^ 0 = A
  • A ^ A = 0
为什么用异或

  • 加密和解密使用相同的运算
  • 简单高效
  • 数学上具有对称性
代码中的混淆
虽然代码中有三种switch-case分支:
  1. case 0: u = a.charCodeAt(o) ^ i[o % s];
  2. case 1: u = i[o % s] ^ a.charCodeAt(o);
  3. case 2: u = (a.charCodeAt(o)) ^ (i[o % s]);
复制代码
但由于异或的交换律(A ^ B = B ^ A),这三种方式结果完全相同!这是一种代码混淆技巧,目的是增加逆向分析的难度。
5.3.2 循环左移(Rotate Left)

什么是循环左移
将一个字节的所有位向左移动n位,左侧溢出的位移到右侧。
  1. 原始:     a b c d e f g h
  2.           ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
  3. 左移3位:  d e f g h a b c
复制代码
实现原理
以左移5位为例(本题中c=5):
  1. h = 255 & (u << 5 | u >> 3 & 31)
复制代码
分解步骤:
  1. 假设 u = 0b10110011 (179)
  2. 步骤1: u << 5 (左移5位)
  3.   10110011 → 01100000 (96)
  4.   (左侧5位溢出)
  5. 步骤2: u >> 3 (右移3位,8-5=3)
  6.   10110011 → 00010110 (22)
  7. 步骤3: (u >> 3) & 31 (取低5位)
  8.   00010110 & 00011111 = 00010110 (22)
  9. 步骤4: 左移结果 | 右移结果
  10.   01100000 | 00010110 = 01110110 (118)
  11. 步骤5: & 255 (确保在0-255范围)
  12.   01110110 & 11111111 = 01110110 (118)
  13. 结果: 10110011 循环左移5位 → 01110110
复制代码
图示说明
  1. 原始字节: 1 0 1 1 0 0 1 1
  2.          ╰─────────╯╰──╯
  3.               ↓       ↓
  4. 左移5位后: 0 1 1 0 0 0 0 0  (左移部分)
  5.           ↓
  6. 右移3位后: 0 0 0 1 0 1 1 0  (溢出部分)
  7.           ↓ 按位或
  8. 最终结果: 0 1 1 1 0 1 1 0
复制代码
5.3.3 完整加密示例

让我们完整演示flag第一个字符'f'的加密过程:
  1. 明文字符: 'f'
  2.   ↓
  3. 1. 获取ASCII码
  4.    'f' → 102 → 0b01100110
  5. 2. 异或运算 (位置0,使用key[0]='n'=110)
  6.    102 ^ 110 = 0b01100110 ^ 0b01101110
  7.              = 0b00001000
  8.              = 8
  9. 3. 循环左移5位
  10.    u = 8 = 0b00001000
  11.    左移5位: 8 << 5 = 0b00000000 = 0
  12.    右移3位: 8 >> 3 = 0b00000001 = 1
  13.    取低5位: 1 & 31 = 1
  14.    按位或:  0 | 1 = 1
  15.    h = 1
  16. 4. 输出密文
  17.    cipher[0] = 1  ✓
复制代码
验证成功!预期密文的第一个元素确实是1。
六、逆向解密:编写解密脚本

6.1 解密思路

加密过程是:明文 → 异或 → 循环左移 → 密文
解密过程是逆运算:密文 → 循环右移 → 异或 → 明文
关键认识

  • 循环左移的逆运算是循环右移
  • 异或的逆运算仍是异或(因为 (A ^ B) ^ B = A)
6.2 实现循环右移
  1. def rot_right(x, n):
  2.     """
  3.     循环右移函数
  4.     参数:
  5.         x: 待移位的字节值
  6.         n: 右移位数
  7.     返回:
  8.         循环右移n位后的结果
  9.     """
  10.     x &= 0xFF  # 确保在0-255范围内
  11.     return ((x >> n) | (x << (8 - n))) & 0xFF
复制代码
运行结果:
  1. 循环右移 = 右移n位 | 左移(8-n)位
  2. 例如右移5位:
  3. 原始:     a b c d e f g h
  4.          ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
  5. 右移5位:  0 0 0 0 0 a b c  (右侧5位溢出)
  6. 左移3位:  f g h 0 0 0 0 0  (将溢出位移回)
  7.          ↓ 按位或
  8. 结果:     f g h 0 0 a b c
复制代码
完美!验证通过,证明我们的解密算法完全正确。
七、知识点总结与技术深化

7.1 二进制文件解析技术

Python struct模块常用格式
格式字符C类型Python类型字节数Bunsigned charinteger1Hunsigned shortinteger2Iunsigned intinteger4Qunsigned long longinteger8字节序标识
标识字节序说明</tdtd小端序/tdtdLittle-Endian/td/trtrtd>大端序Big-Endian=本机序Native示例
[code]# 大端序读取4字节无符号整数offset = struct.unpack('>I', f.read(4))[0]# 小端序读取2字节无符号短整数value = struct.unpack('

相关推荐

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