WMCTF2025-Want2BecomeMagicalGirl 复现

作者彩蛋

将 apk 拖入模拟器,没想到是流氓软件,这就是魔法少女吗/(ㄒoㄒ)/~~

image-20251012154535340

作者好中二😄

image-20251012154610487

魔改 AES 加密

Jadx 打开找到 MainActivity ,很明显的 flutter 特征,同时还有一个 Magic 类,里面是标准的 xxtea ,最后定位是在 encryptToBase64(),这个加密函数无人调用,说明是在 native 层进行调用的

image-20251012160111136

先不管 Java 层的加密,去恢复 libapp.so 的符号和 dart 源码等

image-20251012161535131

在 /asm/magical_girl 里看到这些文件,看到这里就已经确定是 aes 加密了。

S 盒

据 GPT 所说 main.dart 里只有 Flutter 应用的启动过程,而在 aes_crypt_null_safe.dart 中,可以看到 aes 加密代码,但是这些汇编代码太难看了,GPT 也分析不出什么,所以应该到 ida 里去看这些代码。

而在这里的汇编中,已经标准好了这段代码对应的地址,我们在 libapp.so 可以找到

image-20251013133150633

对应到函数 magical_girl_aes_crypt_null_safe__Aes::_subBytes_2901b8()

image-20251013133605813

数一下这里的赋值操作,正好有 256 个,那是 S 盒无疑了。

Dart 在ida中的数值问题

但是为什么会有超过 255(0xFF) 的呢。

这是因为虽然 Dart 中一切皆为 Object,但是实际 Dart 在也是有底层优化的。

在 Dart 中最最常见的就是 int 类型。

int 分两种类型:

1.smi: 对于小数值的数字,Dart直接将原数值左移一位。

2.bigint: 对于大数值,Dart构建对象,其数值在对象偏移+7处。

这里一个字节的值显然是小数值,所以在底层硬编码时被左移了一位,现在需要右移回来。

顺便随便搜一些和 aes 有关的东西,搜到了如下,大致确定他是 ecb 模式的 aes 。

image-20251012165927609

加密逻辑

既然改了 S 盒,那肯定要检查一下是不是在加密逻辑上也进行了更改,同样定位到对应的代码逻辑,发现果然

magical_girl_aes_crypt_null_safe__Aes::aesEncryptBlock_28d5bc() 中,注意到

image-20251013163343190

他先做了轮密钥加,再做列混淆,颠倒了加密顺序。其他加密代码中倒是没有发现别的更改。

EditView.dart 文件中,找到了 check 逻辑,同时还有找到了密文。

image-20251012164621738

image-20251012164726149

You have become a magical girl ! !

这里注意到最后的密文是 Base64 格式的,而我们在 Java 看到的最后一层编码也是 Base64 ,可以猜测先是 native 层做 aes 加密,然后在 Java 层做 xxtea 加密。

根据 ai 的提示,现在去 libnative_add.so 找密钥,惊喜发现 libnative_add.so 没有抹符号表。

image-20251012170152593

genkey() 中,使用标准 rc4 加密生成的 key ,成功得到密钥

image-20251012171314845

aes 此时就成功的分析完了。

魔改 xxtea 加密

init_proc() 中,看到了非常多的 NEON SIMD 指令处理数据的代码,让 ai 写出字符解密代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import ida_bytes, ida_name

# 固定向量,根据你前面提供的数据
l = [0xF0] * 16
shellcode_end = [0x0D, 0x0E, 0x0F, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
0x06, 0x07, 0x05, 0x04, 0x02, 0x00, 0x00, 0x00]

# 所有在 init_proc 里出现的表
tables = [
"xmmword_10100", "xmmword_10110", "xmmword_10120", "xmmword_10130",
"xmmword_10140", "xmmword_10150", "xmmword_10160", "xmmword_10170",
"xmmword_10180", "xmmword_10190", "xmmword_101A0", "xmmword_101B0",
"xmmword_101C0", "xmmword_101D0", "xmmword_101E0", "xmmword_101F0",
"xmmword_10200", "xmmword_10210", "xmmword_10230", "xmmword_10240",
"xmmword_10250", "xmmword_10260", "xmmword_10270"
]

# ---------- 基础函数 ----------
def read_bytes(ea, size=16):
return [ida_bytes.get_byte(ea + i) for i in range(size)]

def veorq_s8(v1, v2):
return [(v1[i] ^ v2[i]) & 0xFF for i in range(16)]

def vqtbl1q_s8(table, index):
return [(table[i] if i < len(table) else 0) for i in index]

def get_vector(label):
ea = ida_name.get_name_ea(0, label)
if ea == idaapi.BADADDR:
print(f"[!] 找不到符号 {label}")
return None
return read_bytes(ea, 16)

def to_ascii(bs):
out = []
for b in bs:
if 32 <= b < 127:
out.append(chr(b))
elif b == 0:
out.append('\0')
return ''.join(out)

# ---------- 主逻辑 ----------
decoded_total = []

for lbl in tables:
tbl = get_vector(lbl)
if not tbl:
continue
step1 = veorq_s8(l, shellcode_end)
step2 = vqtbl1q_s8(tbl, shellcode_end)
final = veorq_s8(step1, step2)
s = to_ascii(final)
print(f"{lbl:15s}: {s}")
decoded_total.extend(final)

# ---------- 输出 ----------
joined = to_ascii(decoded_total)
print("\n======== 合并结果 ========\n")
print(joined)

得到解密结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xmmword_10100  : EEbPNS_9ArtMeooo
xmmword_10110 : _ZN3art9ArtMeooo
xmmword_10120 : Lb0EEEbPNS_9AMMM
xmmword_10130 : PKNS_11Instruiii
xmmword_10140 : er12IsDebuggaeee
xmmword_10150 : _ZN3art11interrr
xmmword_10160 : b
xmmword_10170 : Ev
xmmword_10180 : dEPNS_6ThreadNNN
xmmword_10190 : onEtbPNS_6JVaeee
xmmword_101A0 : dERNS_11Shadorrr
xmmword_101B0 : lueE
xmmword_101C0 : _ZNK3art9OatHddd
xmmword_101D0 : S_11ShadowFraEEE
xmmword_101E0 : ethodEPNS_6Thaaa
xmmword_101F0 : gic.fi
xmmword_10200 : uctionEtPNS_6aaa
xmmword_10210 : d12PrettyMethEEE
xmmword_10230 : ameEPKNS_11Inrrr
xmmword_10240 : gic.toB
xmmword_10250 : libart.so
xmmword_10260 : E
xmmword_10270 : eter6DoCallILEEE

注意到解密之后有两个特殊的字符串,gic.figic.toB ,查他们交叉引用,看到是在 sub_1A51C() 函数里被用到

image-20251013161352031

sub_1A51C() 在函数 init_proc() 函数里有如下操作

image-20251013155511706

我一开始做复现的时候就做到这里了,我到处翻找一直卡在这里,我认为是AES已经完全逆完了,这一段 init_proc() 的内容,大概是跟 Java 层那个没人调用的 encryptToBase64() 有关,那两个特殊字符串应该是提示,但是完全没领会到意思。官方 wp 里作者也没有给详细的解题思路(其实是没认真看官方 wp 里也写了 hook 了 DoCall函数😭) ,只好搁置。

后来题目作者在自己博客重新发了一篇详解

image-20251013175736883

看到这里真是恍然大悟,(之前一直想找 ArtMethod::Invoke 函数,因为作者说像抽取壳,难怪卡住了),现在赶紧去学一学安卓字节码执行的解释模式和 DoCall 函数。

学习后,按照我的理解如下

在 Android 的解释执行模式(Interpreter Mode)下,Java 字节码中的 invoke-* 指令最终就是通过 DoCall 函数(及其相关函数)来完成方法调用的执行的。

1
2
3
4
5
6
7
8
9
10
// Docall函数定义
template<bool is_range>
NO_STACK_PROTECTOR
bool DoCall(ArtMethod* called_method, //要被调用的方法(目标方法)
Thread* self, // 当前线程对象
ShadowFrame& shadow_frame, //当前解释器的 shadow frame(栈帧)
const Instruction* inst, //当前字节码指令
uint16_t inst_data, //解析指令所需的数据
bool is_string_init, //是否是特殊处理的 String.<init> 构造函数
JValue* result)// 调用结果

而本题通过 hook DoCall 函数去得到内存中的 dex 文件,如作者所说

把内存中函数的字节码转为 Instruction* ,就可以利用其中函数读取操作码和操作数。那么通过 inst 拿到的指针就是 dex 的指针,只要把指针向前回溯到 dex 头就可以操作正在运行的整个 dex 文件。

而我们在ida里看到的sub_1A51C() ,正是 DoCallonEnter 处理函数

image-20251015145610503

知道了原理,我们很容易就可以看出来,,第一个 gic.fi 分支里的操作是对 dex 文件进行修改,而 gic.toB 分支里的操作是恢复 dex 文件,而 xmmword_41F60 显然是原始字节码的储存处。

既然这样,我们作者到题的思路就非常明显且简单了,要么在进入 gic.toB 分支前将 dex dump下来,要么去 trace Smali,对比看看他改了什么,当然最简单的思路是作者给出的。

第二次 strstr 后的还原逻辑 NOP

这样,dex 文件就不会被改回去了。

我的做法相当暴力,我给里面的逻辑全 nop 了,如图

image-20251015150233599

重新打包签名后,安装到手机上,随便输入一次按回车之后,我们直接使用 frida-dexdump 就能得到 被修改之后的 dex 文件了

在这里我使用的是 hluda ,并且改了端口,因为不改会闪退,应该有端口检测,我做 Android 已经习惯用 hluda server 了,不知是不是绕过了其他检测

image-20251015150356223

得到的 dex 如图

image-20251015151230114

最后 xxtea 的 key 就很容易获得了,frida 脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 使弹窗可关闭
Java.perform(function () {
let a = Java.use("android.widget.Button");
a["setEnabled"].implementation = function (enable) {
console.log("Button set enable")
this["setEnabled"](true);
};
});
// 以上代码来自作者
Java.perform(function () {
var Magic = Java.use("work.pangbai.magic.magical_girl.Magic");

Magic.encrypt.overload('[B', '[B').implementation = function (bArr, bArr2) {
// 打印第二个参数(bArr2)
var keyBytes = Java.array('byte', bArr2);
var keyStr = "";
for (var i = 0; i < keyBytes.length; i++) {
keyStr += String.fromCharCode(keyBytes[i] & 0xff);
}

console.log("[*] Hooked Magic.encrypt");
console.log(" [+] 密钥 (bArr2) 原始字节: " + keyBytes);
console.log(" [+] 密钥 (bArr2) 字符串形式: " + keyStr);

// 调用原函数
var result = this.encrypt(bArr, bArr2);
return result;
};
});

如图

image-20251015151810179

最后我们的 exp 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import base64
import struct

def to_uint32_list(data, include_length):
n = len(data) // 4
v = list(struct.unpack('<' + 'I' * n, data[:n*4])) # 保持你原先的小端
if include_length:
v.append(len(data))
return v

def to_bytes(v, include_length):
n = len(v)
if include_length:
m = v[-1]
n = n - 1
else:
m = n << 2
return struct.pack('<' + 'I' * n, *v)[:m]

def xxtea_decrypt(v, key):
n = len(v)
if n < 2:
return v
delta = 0x9e3779b9
rounds = 6 + 52 * n
s = (rounds * delta) & 0xffffffff
y = v[0]
while s:
e = (s >> 2) & 3
for p in range(n - 1, 0, -1):
z = v[p - 1]
mx = (((z >> 5) ^ (y >> 2)) + ((y >> 3) ^ (z >> 4))) ^ ((s ^ y) + (key[(p & 3) ^ e] ^ z))
v[p] = (v[p] - mx) & 0xffffffff
y = v[p]
z = v[-1]
mx = (((z >> 5) ^ (y >> 2)) + ((y >> 3) ^ (z >> 4))) ^ ((s ^ y) + (key[(0 & 3) ^ e] ^ z))
v[0] = (v[0] - mx) & 0xffffffff
y = v[0]
s = (s - delta) & 0xffffffff
return v

sbox = [
0x20,0x7b,0x18,0xa7,0x42,0x44,0xd7,0x4a,0xcd,0x32,0xd1,0xec,0xf3,0x81,0xa5,0x89,
0x0e,0x91,0x4b,0xf0,0xe9,0x5d,0x8d,0xf5,0x46,0xfc,0x31,0x36,0xb6,0xac,0x9b,0xb9,
0x26,0x09,0xe6,0x40,0xd4,0xb0,0x51,0x4f,0x9c,0x3e,0xe7,0x79,0x30,0x88,0xb1,0x3c,
0x7a,0x5c,0xd3,0x14,0x5a,0xab,0x56,0xc0,0x04,0x29,0xd0,0x3b,0x1f,0xf9,0xa3,0x57,
0x00,0x8a,0x84,0x16,0xf4,0x1a,0xea,0x64,0xa6,0xd6,0x2e,0xbe,0x2f,0x17,0xc4,0xe0,
0x1e,0x02,0x3a,0x22,0x8f,0x9f,0xcb,0xa8,0x2c,0x67,0x34,0x25,0xd5,0xff,0xef,0xf6,
0xe2,0xaa,0xd9,0x72,0xfe,0xce,0xa1,0x78,0x85,0x96,0x2a,0x77,0xca,0xc1,0x37,0x74,
0xa2,0x5e,0x6c,0xfd,0xb8,0x4d,0x7d,0x70,0xb3,0xdd,0xcf,0x71,0x73,0x61,0xf8,0x19,
0x48,0xe3,0x63,0x33,0x3d,0x15,0xae,0x98,0xe5,0x80,0xbd,0xbc,0x82,0xc6,0x94,0x01,
0xe4,0xde,0x06,0x50,0x95,0xdf,0x47,0xf7,0x90,0x8b,0x45,0x9a,0x6e,0x07,0xad,0x1c,
0x35,0x83,0x68,0x03,0x6f,0x5b,0xb7,0xfb,0x1d,0xc5,0x10,0x7c,0xd8,0x6a,0xcc,0x69,
0x8e,0x24,0x4c,0x39,0xb4,0xa0,0x0b,0x52,0xe8,0xa9,0xb2,0x8c,0x0a,0xbf,0x28,0x86,
0x6d,0xaf,0xda,0x41,0xfa,0x75,0xb5,0x43,0xc3,0x60,0x62,0x2b,0x55,0xf2,0x9e,0x2d,
0x12,0x23,0x0d,0xdb,0x6b,0xc7,0x38,0x7f,0x5f,0x97,0x08,0xed,0xe1,0xbb,0xee,0x9d,
0xd2,0x92,0x49,0x3f,0xdc,0x58,0x87,0xc2,0xba,0x99,0xc9,0x4e,0xf1,0x21,0xeb,0x13,
0x65,0x59,0x76,0x0c,0xc8,0x05,0xa4,0x54,0x93,0x1b,0x66,0x11,0x27,0x53,0x7e,0x0f
]
inv_sbox = [
0x40,0x8f,0x51,0xa3,0x38,0xf5,0x92,0x9d,0xda,0x21,0xbc,0xb6,0xf3,0xd2,0x10,0xff,
0xaa,0xfb,0xd0,0xef,0x33,0x85,0x43,0x4d,0x02,0x7f,0x45,0xf9,0x9f,0xa8,0x50,0x3c,
0x00,0xed,0x53,0xd1,0xb1,0x5b,0x20,0xfc,0xbe,0x39,0x6a,0xcb,0x58,0xcf,0x4a,0x4c,
0x2c,0x1a,0x09,0x83,0x5a,0xa0,0x1b,0x6e,0xd6,0xb3,0x52,0x3b,0x2f,0x84,0x29,0xe3,
0x23,0xc3,0x04,0xc7,0x05,0x9a,0x18,0x96,0x80,0xe2,0x07,0x12,0xb2,0x75,0xeb,0x27,
0x93,0x26,0xb7,0xfd,0xf7,0xcc,0x36,0x3f,0xe5,0xf1,0x34,0xa5,0x31,0x15,0x71,0xd8,
0xc9,0x7d,0xca,0x82,0x47,0xf0,0xfa,0x59,0xa2,0xaf,0xad,0xd4,0x72,0xc0,0x9c,0xa4,
0x77,0x7b,0x63,0x7c,0x6f,0xc5,0xf2,0x6b,0x67,0x2b,0x30,0x01,0xab,0x76,0xfe,0xd7,
0x89,0x0d,0x8c,0xa1,0x42,0x68,0xbf,0xe6,0x2d,0x0f,0x41,0x99,0xbb,0x16,0xb0,0x54,
0x98,0x11,0xe1,0xf8,0x8e,0x94,0x69,0xd9,0x87,0xe9,0x9b,0x1e,0x28,0xdf,0xce,0x55,
0xb5,0x66,0x70,0x3e,0xf6,0x0e,0x48,0x03,0x57,0xb9,0x61,0x35,0x1d,0x9e,0x86,0xc1,
0x25,0x2e,0xba,0x78,0xb4,0xc6,0x1c,0xa6,0x74,0x1f,0xe8,0xdd,0x8b,0x8a,0x4b,0xbd,
0x37,0x6d,0xe7,0xc8,0x4e,0xa9,0x8d,0xd5,0xf4,0xea,0x6c,0x56,0xae,0x08,0x65,0x7a,
0x3a,0x0a,0xe0,0x32,0x24,0x5c,0x49,0x06,0xac,0x62,0xc2,0xd3,0xe4,0x79,0x91,0x95,
0x4f,0xdc,0x60,0x81,0x90,0x88,0x22,0x2a,0xb8,0x14,0x46,0xee,0x0b,0xdb,0xde,0x5e,
0x13,0xec,0xcd,0x0c,0x44,0x17,0x5f,0x97,0x7e,0x3d,0xc4,0xa7,0x19,0x73,0x64,0x5d
]
rcon_words = [0x01000000,0x02000000,0x04000000,0x08000000,0x10000000,
0x20000000,0x40000000,0x80000000,0x1B000000,0x36000000]

def LOAD32H(b, off):
return ((b[off] << 24) | (b[off+1] << 16) | (b[off+2] << 8) | b[off+3]) & 0xFFFFFFFF

def BYTE(x, n): # 低字节序提取第 n 个字节
return (x >> (8 * n)) & 0xFF

def MIX(x):
return ((sbox[BYTE(x,2)] << 24) ^ (sbox[BYTE(x,1)] << 16) ^
(sbox[BYTE(x,0)] << 8) ^ (sbox[BYTE(x,3)])) & 0xFFFFFFFF

def key_expansion_words(key16):
w = [0]*44
for i in range(4):
w[i] = LOAD32H(key16, 4*i)
for round_idx in range(10):
b = round_idx*4
w[b+4] = (w[b] ^ MIX(w[b+3]) ^ rcon_words[round_idx]) & 0xFFFFFFFF
w[b+5] = (w[b+1] ^ w[b+4]) & 0xFFFFFFFF
w[b+6] = (w[b+2] ^ w[b+5]) & 0xFFFFFFFF
w[b+7] = (w[b+3] ^ w[b+6]) & 0xFFFFFFFF
return w

def make_dec_key_words(eK):
dK = [0]*44
for j in range(11):
src = (10 - j) * 4
dst = j * 4
dK[dst:dst+4] = eK[src:src+4]
return dK

def load_state_array(block16):
state = [[0]*4 for _ in range(4)]
idx = 0
for col in range(4):
for row in range(4):
state[row][col] = block16[idx]
idx += 1
return state

def store_state_array(state):
out = bytearray(16)
idx = 0
for col in range(4):
for row in range(4):
out[idx] = state[row][col]
idx += 1
return bytes(out)

def add_round_key(state, key_words4):
for col in range(4):
w = key_words4[col]
for row in range(4):
state[row][col] ^= BYTE(w, 3 - row)

def ROR32(x, n):
n %= 32
return ((x >> n) | ((x << (32 - n)) & 0xFFFFFFFF)) & 0xFFFFFFFF

def inv_shift_rows(state):
for row in range(4):
x = ((state[row][0] << 24) | (state[row][1] << 16) | (state[row][2] << 8) | state[row][3]) & 0xFFFFFFFF
x = ROR32(x, 8 * row)
state[row][0] = (x >> 24) & 0xFF
state[row][1] = (x >> 16) & 0xFF
state[row][2] = (x >> 8) & 0xFF
state[row][3] = x & 0xFF

def inv_sub_bytes(state):
for i in range(4):
for j in range(4):
state[i][j] = inv_sbox[state[i][j]]

def GMul(u, v):
p = 0
for _ in range(8):
if u & 1:
p ^= v
flag = v & 0x80
v = (v << 1) & 0xFF
if flag:
v ^= 0x1B
u >>= 1
return p

def inv_mix_columns(state):
M = [
[0x0E, 0x0B, 0x0D, 0x09],
[0x09, 0x0E, 0x0B, 0x0D],
[0x0D, 0x09, 0x0E, 0x0B],
[0x0B, 0x0D, 0x09, 0x0E],
]
tmp = [[state[i][j] for j in range(4)] for i in range(4)]
for i in range(4):
for j in range(4):
state[i][j] = GMul(M[i][0], tmp[0][j]) ^ GMul(M[i][1], tmp[1][j]) ^ GMul(M[i][2], tmp[2][j]) ^ GMul(M[i][3], tmp[3][j])

def aes_ecb_decrypt_all(ciphertext: bytes, key16: bytes) -> bytes:
assert len(key16) == 16
eK = key_expansion_words(key16)
dK = make_dec_key_words(eK)

out = bytearray()
for off in range(0, len(ciphertext), 16):
block = ciphertext[off:off+16]
state = load_state_array(block)

rk_idx = 0
add_round_key(state, dK[rk_idx:rk_idx+4])
for _round in range(1, 10):
rk_idx += 4
inv_shift_rows(state)
inv_sub_bytes(state)
inv_mix_columns(state)
add_round_key(state, dK[rk_idx:rk_idx+4])

inv_shift_rows(state)
inv_sub_bytes(state)
add_round_key(state, dK[rk_idx+4:rk_idx+8])

out += store_state_array(state)
return bytes(out)


def main():
ciphertext_b64 = "8sAFX45zT7uc0vSUyFNNly1h/d5zTt89tV3kcVr5P5n7lRKPyYtxg31zYNB2lPV0c5nf/x2/IK94XV9Ufs9XfaDG5IXxMlZy+Z2nE+ZZRFBSpMoKzQXfUq2TSjJJfQxV"
ciphertext = base64.b64decode(ciphertext_b64)
key_bytes = b"16929"
key_bytes = (key_bytes + b"\0"*16)[:16]
key = list(struct.unpack('<4I', key_bytes))
v = to_uint32_list(ciphertext, False)
v = xxtea_decrypt(v, key)
ciphertext2 = to_bytes(v, False)
ciphertext2 = bytes.fromhex(ciphertext2.decode())
# print("XXTEA 解密结果:", ciphertext2.hex())
aes_key = bytes.fromhex("7a25c524c6334c30f362afac3f2303d5")
plaintext = aes_ecb_decrypt_all(ciphertext2, aes_key)
print("AES 解密后:", plaintext)

if __name__ == "__main__":
main()
# WMCTF{I_R4@11y_w@n7_70 _84c0m4_@_m@gic@1_Gir1}

参考文章

https://blog.wm-team.cn/index.php/archives/86/#Want2BecomeMagicalGirl

https://pangbai.work/CTF/Reverse/Want2BecomeMagicalGirl/

https://www.ctfiot.com/261979.html