news 2025/12/31 6:51:31

解析2025强网拟态EZMiniAPP

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
解析2025强网拟态EZMiniAPP

题目背景与初步分析

1.1 题目描述

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

1.2 什么是wxapkg文件

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

wxapkg文件的特点:

二进制格式,无法直接用文本编辑器查看

包含小程序的所有资源:JavaScript代码、页面模板、样式表、配置文件等

有特定的文件结构:包含文件头、索引区和数据区

1.3 解题思路

解包wxapkg文件,提取其中的代码

分析JavaScript代码,找到加密逻辑

理解加密算法的工作原理

编写解密脚本,获取flag

二、wxapkg文件格式详解

2.1 文件结构分析

一个标准的wxapkg文件由三部分组成:

┌─────────────────────────────────────┐

│ 文件头部 (Header) │

├─────────────────────────────────────┤

│ - First Mark (1字节): 标识字节 │

│ - Info1 (4字节): 信息段 │

│ - Info2 (4字节): 信息段 │

│ - Data Offset (4字节): 数据区偏移 │

│ - Reserved (1字节): 保留字节 │

├─────────────────────────────────────┤

│ 索引区 (Index Section) │

├─────────────────────────────────────┤

│ - File Count (4字节): 文件数量 │

│ - File List: 文件列表 │

│ * Name Length (4字节) │

│ * Name (变长): 文件名 │

│ * Offset (4字节): 文件偏移 │

│ * Size (4字节): 文件大小 │

├─────────────────────────────────────┤

│ 数据区 (Data Section) │

├─────────────────────────────────────┤

│ 各个文件的实际数据内容 │

└─────────────────────────────────────┘

关键技术点:

多字节整数使用大端序(Big-Endian)存储

文件偏移量是从wxapkg文件开头计算的绝对位置

文件名是UTF-8编码的字符串

2.2 为什么需要解包

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

解析文件头,获取文件列表信息

根据偏移量和大小,提取每个文件的数据

还原成原始的目录结构

三、实战:解包wxapkg文件

3.1 编写解包工具

我们使用Python的struct模块来解析二进制数据:

#!/usr/bin/env python3

import struct

import os

def unpack_wxapkg(wxapkg_file, output_dir):

"""解包微信小程序 wxapkg 文件"""

with open(wxapkg_file, 'rb') as f:

# 读取头部信息

first_mark = struct.unpack('B', f.read(1))[0]

f.read(4) # 跳过Info1

f.read(4) # 跳过Info2

# 读取数据区偏移量 (大端序,用'>I'表示)

data_section_offset = struct.unpack('>I', f.read(4))[0]

f.read(1) # 跳过保留字节

# 读取文件数量

file_count = struct.unpack('>I', f.read(4))[0]

# 读取文件列表

file_list = []

for i in range(file_count):

# 文件名长度

name_len = struct.unpack('>I', f.read(4))[0]

# 文件名 (UTF-8编码)

name = f.read(name_len).decode('utf-8')

# 文件偏移和大小

offset = struct.unpack('>I', f.read(4))[0]

size = struct.unpack('>I', f.read(4))[0]

file_list.append({

'name': name,

'offset': offset,

'size': size

})

# 创建输出目录

if not os.path.exists(output_dir):

os.makedirs(output_dir)

# 解包每个文件

for file_info in file_list:

name = file_info['name'].lstrip('/')

file_path = os.path.join(output_dir, name)

file_dir = os.path.dirname(file_path)

# 创建文件所在目录

if file_dir and not os.path.exists(file_dir):

os.makedirs(file_dir)

# 读取并写入文件数据

f.seek(file_info['offset'])

file_data = f.read(file_info['size'])

with open(file_path, 'wb') as out_f:

out_f.write(file_data)

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 执行解包

运行解包脚本:

python3 unpacker.py

输出结果:

Unpacking __APP__.wxapkg...

First mark: 190

Data section offset: 170832

File count: 24

File 1: /__debug__/__jscore-debug__.png, offset: 907, size: 178

...

File 11: /chunk_0.appservice.js, offset: 65008, size: 15834

...

Extracted: /chunk_0.appservice.js

...

Done!

成功解包出24个文件!其中最关键的是chunk_0.appservice.js。

3.3 解包后的文件结构

unpacked/

├── __debug__/ # 调试文件

├── app-config.json # 小程序配置

├── app-service.js # 服务层主文件

├── appservice.app.js # 应用逻辑

├── chunk_0.appservice.js # ★ 关键:包含页面逻辑

├── chunk_1.appservice.js # 代码分块

├── common.app.js # 公共代码

├── pages/ # 页面目录

│ ├── index/ # 首页

│ │ ├── index.html

│ │ └── index.wxss

│ └── logs/ # 日志页

│ ├── logs.html

│ └── logs.wxss

└── page-frame.html # 页面框架

四、代码分析:定位加密逻辑

4.1 查看小程序配置

首先查看app-config.json了解小程序结构:

{

"entryPagePath": "pages/index/index.html",

"pages": ["pages/index/index", "pages/logs/logs"],

...

}

可以看到入口页面是pages/index/index,这应该是我们的重点分析对象。

4.2 分析关键文件

打开chunk_0.appservice.js,这个文件包含了index页面的逻辑代码。虽然代码经过了混淆,但我们仍能识别出关键函数。

在第2行找到了核心逻辑(为便于阅读,这里进行了格式化):

Page({

data: {

inputValue: "",

animationData: {}

},

// 输入框变化处理

onInputChange: function(a) {

this.setData({inputValue: a.detail.value});

},

// ★ 关键:加密函数

enigmaticTransformation: function(a, t) {

// a: 明文

// t: 密钥

// ... 加密逻辑 ...

},

// 自定义加密入口

customEncrypt: function(a, t) {

return this.enigmaticTransformation(a, t);

},

// ★ 验证逻辑

onCheck: function() {

var a = this.data.inputValue;

if ("" !== a.trim()) {

var t = this.customEncrypt(a, "newKey2025!");

console.log(t);

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])

? wx.showToast({title: "Right", icon: "success", duration: 2e3})

: wx.showToast({title: "Wrong", icon: "error", duration: 2e3});

}

}

});

关键发现:

密钥:"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 完整提取加密逻辑

enigmaticTransformation: function(a, t) {

// 步骤1: 将密钥转换为ASCII码数组

i = Array.from(t).map(function(a) {

return a.charCodeAt(0);

});

s = i.length;

// 步骤2: 计算循环移位参数c

c = function(a) {

for (var t = 0, e = 0; e < a.length; e++) {

switch(e % 4) {

case 0: t += 1 * a[e]; break;

case 1: t += a[e] + 0; break;

case 2: t += 0 | a[e]; break; // 按位或0

case 3: t += 0 ^ a[e]; break; // 按位异或0

}

}

return t;

}(i) % 8;

// 步骤3: 逐字符加密

r = [];

for (o = 0; o < a.length; o++) {

var u;

// 3.1: 异或运算

switch(o % 3) {

case 0:

u = a.charCodeAt(o) ^ i[o % s];

break;

case 1:

u = i[o % s] ^ a.charCodeAt(o);

break;

case 2:

e = a.charCodeAt(o);

n = i[o % s];

u = e ^ n;

break;

}

// 3.2: 循环左移

var h;

switch(c) {

case 0: h = u; break;

case 1: h = 255 & (u << 1 | u >> 7 & 1); break;

case 2: h = 255 & (u << 2 | u >> 6 & 3); break;

case 3: h = 255 & (u << 3 | u >> 5 & 7); break;

case 4: h = 255 & (u << 4 | u >> 4 & 15); break;

case 5: h = 255 & (u << 5 | u >> 3 & 31); break;

case 6: h = 255 & (u << 6 | u >> 2 & 63); break;

case 7: h = 255 & (u << 7 | u >> 1 & 127); break;

default: h = 255 & (u << c | u >> (8 - c)); break;

}

// 3.3: 添加到结果数组

r.push(h);

}

return r;

}

5.2 算法流程图

输入: 明文字符串, 密钥字符串

步骤1: 密钥处理

- 将密钥转为ASCII码数组

- key = "newKey2025!" → [110, 101, 119, 75, 101, 121, 50, 48, 50, 53, 33]

步骤2: 计算移位参数

- 对密钥数组各元素求和

- sum = 110+101+119+75+101+121+50+48+50+53+33 = 861

- c = 861 % 8 = 5

步骤3: 逐字符加密

对于每个明文字符:

3.1 异或运算

- plain_char ^ key[i % key_length] → u

3.2 循环左移

- rotate_left(u, c) → h

3.3 添加到结果

- result.append(h)

输出: 密文字节数组

5.3 关键技术点详解

5.3.1 异或运算(XOR)

基本性质:

A ^ B = C 则 C ^ B = A(自反性)

A ^ 0 = A

A ^ A = 0

为什么用异或:

加密和解密使用相同的运算

简单高效

数学上具有对称性

代码中的混淆:

虽然代码中有三种switch-case分支:

case 0: u = a.charCodeAt(o) ^ i[o % s];

case 1: u = i[o % s] ^ a.charCodeAt(o);

case 2: u = (a.charCodeAt(o)) ^ (i[o % s]);

但由于异或的交换律(A ^ B = B ^ A),这三种方式结果完全相同!这是一种代码混淆技巧,目的是增加逆向分析的难度。

5.3.2 循环左移(Rotate Left)

什么是循环左移:

将一个字节的所有位向左移动n位,左侧溢出的位移到右侧。

原始: a b c d e f g h

↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

左移3位: d e f g h a b c

实现原理:

以左移5位为例(本题中c=5):

h = 255 & (u << 5 | u >> 3 & 31)

分解步骤:

假设 u = 0b10110011 (179)

步骤1: u << 5 (左移5位)

10110011 → 01100000 (96)

(左侧5位溢出)

步骤2: u >> 3 (右移3位,8-5=3)

10110011 → 00010110 (22)

步骤3: (u >> 3) & 31 (取低5位)

00010110 & 00011111 = 00010110 (22)

步骤4: 左移结果 | 右移结果

01100000 | 00010110 = 01110110 (118)

步骤5: & 255 (确保在0-255范围)

01110110 & 11111111 = 01110110 (118)

结果: 10110011 循环左移5位 → 01110110

图示说明:

原始字节: 1 0 1 1 0 0 1 1

╰─────────╯╰──╯

↓ ↓

左移5位后: 0 1 1 0 0 0 0 0 (左移部分)

右移3位后: 0 0 0 1 0 1 1 0 (溢出部分)

↓ 按位或

最终结果: 0 1 1 1 0 1 1 0

5.3.3 完整加密示例

让我们完整演示flag第一个字符'f'的加密过程:

明文字符: 'f'

1. 获取ASCII码

'f' → 102 → 0b01100110

2. 异或运算 (位置0,使用key[0]='n'=110)

102 ^ 110 = 0b01100110 ^ 0b01101110

= 0b00001000

= 8

3. 循环左移5位

u = 8 = 0b00001000

左移5位: 8 << 5 = 0b00000000 = 0

右移3位: 8 >> 3 = 0b00000001 = 1

取低5位: 1 & 31 = 1

按位或: 0 | 1 = 1

h = 1

4. 输出密文

cipher[0] = 1 ✓

验证成功!预期密文的第一个元素确实是1。

六、逆向解密:编写解密脚本

6.1 解密思路

加密过程是:明文 → 异或 → 循环左移 → 密文

解密过程是逆运算:密文 → 循环右移 → 异或 → 明文

关键认识:

循环左移的逆运算是循环右移

异或的逆运算仍是异或(因为 (A ^ B) ^ B = A)

6.2 实现循环右移

def rot_right(x, n):

"""

循环右移函数

参数:

x: 待移位的字节值

n: 右移位数

返回:

循环右移n位后的结果

"""

x &= 0xFF # 确保在0-255范围内

return ((x >> n) | (x << (8 - n))) & 0xFF

原理说明:

循环右移 = 右移n位 | 左移(8-n)位

例如右移5位:

原始: a b c d e f g h

↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

右移5位: 0 0 0 0 0 a b c (右侧5位溢出)

左移3位: f g h 0 0 0 0 0 (将溢出位移回)

↓ 按位或

结果: f g h 0 0 a b c

6.3 完整解密函数

def decrypt(cipher, key):

"""

解密函数

参数:

cipher: 密文字节数组

key: 密钥字符串

返回:

解密后的明文字符串

"""

# 步骤1: 密钥转ASCII码数组

key_array = [ord(c) for c in key]

key_length = len(key_array)

# 步骤2: 计算移位参数 (与加密时相同)

c = sum(key_array) % 8

# 步骤3: 逐字节解密

plaintext = []

for position, cipher_byte in enumerate(cipher):

cipher_byte &= 0xFF

# 3a: 还原循环左移 → 执行循环右移

after_rotate = rot_right(cipher_byte, c)

# 3b: 还原异或 (异或是自反运算)

plain_code = after_rotate ^ key_array[position % key_length]

# 3c: 转换为字符

plaintext.append(chr(plain_code))

return ''.join(plaintext)

6.4 执行解密

# 从小程序代码中提取的数据

cipher = [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]

key = "newKey2025!"

# 解密

flag = decrypt(cipher, key)

print(f"Flag: {flag}")

运行结果:

======================================================================

CTF题目: EZMiniAPP - 微信小程序逆向解密

======================================================================

[输入] 密文数组:

[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]

长度: 25 字节

[输入] 密钥:

newKey2025!

长度: 11 字符

======================================================================

开始解密...

======================================================================

[调试] 密钥ASCII码数组: [110, 101, 119, 75, 101, 121, 50, 48, 50, 53, 33]

[调试] 密钥数组和: 861

[调试] 移位参数c: 5

[调试] 位置0:

密文字节: 1 (0b00000001)

右移5位后: 8 (0b00001000)

密钥字节: 110

异或结果: 102 (ASCII: 'f')

[调试] 位置1:

密文字节: 33 (0b00100001)

右移5位后: 9 (0b00001001)

密钥字节: 101

异或结果: 108 (ASCII: 'l')

[调试] 位置2:

密文字节: 194 (0b11000010)

右移5位后: 22 (0b00010110)

密钥字节: 119

异或结果: 97 (ASCII: 'a')

======================================================================

解密完成!

======================================================================

[结果] Flag: flag{JustEasyMiniProgram}

======================================================================

6.5 验证解密正确性

为了确保解密结果正确,我们实现完整的加密函数,将解密得到的flag重新加密:

def encrypt(plaintext, key):

"""完整复现JavaScript的加密算法"""

key_array = [ord(c) for c in key]

key_length = len(key_array)

c = sum(key_array) % 8

result = []

for position in range(len(plaintext)):

# 异或

plain_code = ord(plaintext[position])

u = plain_code ^ key_array[position % key_length]

# 循环左移

if c == 5:

h = 255 & (u << 5 | u >> 3 & 31)

# ... 其他case ...

result.append(h)

return result

# 验证

encrypted = encrypt("flag{JustEasyMiniProgram}", "newKey2025!")

original = [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]

if encrypted == original:

print("✓ 验证成功!解密结果正确!")

运行结果:

======================================================================

加密验证 - 验证解密结果的正确性

======================================================================

[输入] 明文: flag{JustEasyMiniProgram}

[输入] 密钥: 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]

[对比] 原始密文:

[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]

======================================================================

✓ 验证成功!解密结果正确!

======================================================================

Flag: flag{JustEasyMiniProgram}

完美!验证通过,证明我们的解密算法完全正确。

七、知识点总结与技术深化

7.1 二进制文件解析技术

Python struct模块常用格式:

格式字符 C类型 Python类型 字节数

B unsigned char integer 1

H unsigned short integer 2

I unsigned int integer 4

Q unsigned long long integer 8

字节序标识:

标识 字节序 说明

< 小端序 Little-Endian

> 大端序 Big-Endian

= 本机序 Native

示例:

# 大端序读取4字节无符号整数

offset = struct.unpack('>I', f.read(4))[0]

# 小端序读取2字节无符号短整数

value = struct.unpack('<H', f.read(2))[0]

7.2 位运算详解

7.2.1 异或运算(XOR)

运算规则:

0 ^ 0 = 0

0 ^ 1 = 1

1 ^ 0 = 1

1 ^ 1 = 0

重要性质:

交换律:A ^ B = B ^ A

结合律:(A ^ B) ^ C = A ^ (B ^ C)

自反性:A ^ B ^ B = A

恒等律:A ^ 0 = A

归零律:A ^ A = 0

在加密中的应用:

# 加密

cipher = plaintext ^ key

# 解密(使用相同的key)

plaintext = cipher ^ key

# 证明:

# plaintext ^ key ^ key = plaintext

7.2.2 移位运算

左移(<<):

x << n # 左移n位,右侧补0

5 << 2 # 0b00000101 → 0b00010100 (5 → 20)

右移(>>):

x >> n # 右移n位,左侧补0

20 >> 2 # 0b00010100 → 0b00000101 (20 → 5)

循环移位的实现:

# 循环左移n位

def rotate_left(x, n):

return ((x << n) | (x >> (8 - n))) & 0xFF

# 循环右移n位

def rotate_right(x, n):

return ((x >> n) | (x << (8 - n))) & 0xFF

7.2.3 位掩码

作用:提取或保留特定的位

x & 0xFF # 保留低8位 (0-255)

x & 0x0F # 保留低4位 (0-15)

x & 0x01 # 保留最低位 (0或1)

# 示例

value = 0b11010110

low_4_bits = value & 0x0F # 0b00000110 = 6

7.3 代码混淆识别

本题中使用的混淆技巧:

7.3.1 等价分支混淆

switch(e % 4) {

case 0: t += 1 * a[e]; break; // 等价于 t += a[e]

case 1: t += a[e] + 0; break; // 等价于 t += a[e]

case 2: t += 0 | a[e]; break; // 等价于 t += a[e]

case 3: t += 0 ^ a[e]; break; // 等价于 t += a[e]

}

所有分支实际效果相同!

7.3.2 冗余操作混淆

switch(o % 3) {

case 0: u = a ^ b; break;

case 1: u = b ^ a; break; // 与case 0相同

case 2: u = a ^ b; break; // 与case 0相同

}

利用异或的交换律,制造"不同"的假象。

7.3.3 变量命名混淆

使用无意义的单字母变量名:a, t, e, n, r, i, s, c, o, u, h

识别方法:

分析每个分支的实际计算结果

简化位运算表达式

找出运算的数学本质

7.4 逆向分析方法论

┌─────────────────┐

│ 静态分析 │ 阅读代码,理解逻辑

├─────────────────┤

│ 动态分析 │ 运行代码,观察行为

├─────────────────┤

│ 数学分析 │ 找出运算的逆运算

├─────────────────┤

│ 验证测试 │ 确认解密正确性

└─────────────────┘

具体步骤:

识别加密算法类型:对称/非对称、流密码/分组密码

提取关键参数:密钥、初始向量、轮数等

理解运算流程:每一步的数学含义

推导逆运算:找到每个步骤的逆操作

实现解密:编写代码实现逆运算

验证结果:重新加密检验

八、完整解题流程回顾

步骤总结

第一步:文件分析

├─ 识别wxapkg格式

└─ 了解文件结构

第二步:解包提取

├─ 编写解包工具 (Python + struct)

├─ 解析文件头和索引

└─ 提取所有文件

第三步:代码定位

├─ 查看小程序配置

├─ 找到入口页面

└─ 定位加密函数

第四步:算法分析

├─ 提取enigmaticTransformation函数

├─ 理解加密流程

│ ├─ 密钥处理

│ ├─ 参数计算

│ └─ 逐字符加密

└─ 识别代码混淆

第五步:逆向解密

├─ 推导逆运算

│ ├─ 循环左移 → 循环右移

│ └─ 异或 → 异或

├─ 实现解密函数

└─ 获取flag

第六步:验证结果

├─ 实现加密函数

├─ 重新加密flag

└─ 对比原始密文

九、扩展学习资源

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/23 21:29:20

wangEditor处理ppt动画效果转网页兼容

企业级文档处理解决方案评估与实施方案 项目背景与需求分析 作为河南某集团企业的项目负责人&#xff0c;我近期正在评估为后台管理系统增加文档处理功能的解决方案。根据项目需求和集团技术架构&#xff0c;我们需要以下核心功能&#xff1a; 核心需求 Word粘贴功能&#…

作者头像 李华
网站建设 2025/12/28 19:32:52

深度残差网络在智能垃圾分类中的技术实践与性能分析

深度残差网络在智能垃圾分类中的技术实践与性能分析 【免费下载链接】基于深度残差网络的图像识别垃圾分类系统 本项目使用 Python 和深度学习库 Keras 构建了一个基于深度残差网络&#xff08;ResNet&#xff09;的图像识别垃圾分类系统。该系统能够识别并分类六种不同类型的垃…

作者头像 李华
网站建设 2025/12/23 11:38:51

wangEditor导入MathType公式保留矢量格式

《苏州大二程序员的暑假“渡劫”日记&#xff1a;Word图片转存样式保留大作战》 日期&#xff1a;2023年7月18日 星期二 苏州 桑拿天&#xff08;空调房里敲代码&#xff0c;命是奶茶给的&#xff09; 第一章&#xff1a;需求暴击——客户爸爸的“变态”要求 “同学&#xff0…

作者头像 李华
网站建设 2025/12/29 16:59:50

Node.js BFF层实战:对接天远综合多头借贷/逾期/欺诈聚合接口

一、在 BFF 层重塑风控数据结构 在现代金融 SaaS 平台或信贷管理后台的开发中&#xff0c;前端往往需要展示一个可视化的“借款人风险仪表盘”。然而&#xff0c;上游风控接口为了追求传输效率和扩展性&#xff0c;通常返回扁平化的数据结构。 天远API 的“综合多头”接口&am…

作者头像 李华
网站建设 2025/12/27 8:02:57

System Informer 终极指南:从零掌握Windows系统监控神器

System Informer 终极指南&#xff1a;从零掌握Windows系统监控神器 【免费下载链接】systeminformer A free, powerful, multi-purpose tool that helps you monitor system resources, debug software and detect malware. Brought to you by Winsider Seminars & Soluti…

作者头像 李华