//EZMiniAPP — Mobile CTF writeup
>Summary
This writeup documents a full local analysis and solution for the EZMiniAPP Mobile CTF challenge found in the provided __APP__.wxapkg archive. The target is a WeChat Mini Program package. I inspected the package, located the input verification routine in the app's JavaScript, reversed the check, and recovered the flag.
Flag: flag{JustEasyMiniProgram}
>Environment & artifacts
- Host: Linux (local workspace)
- Challenge archive unpacked:
__APP__.wxapkg(placed at repository root) - Solution artifacts (created during analysis):
solution/001_unwxapkg.py— helper script used to inspect the package structure (index dumper)solution/002_recover_flag.py— script to invert the app's transform and recover the accepted inputsolution/003_notes.txt— short notes and summarysolution/WRITEUP.md— this writeup
>High-level approach
- Extract and inspect
__APP__.wxapkglocally. - Search and extract JavaScript resources inside the wxapkg to locate the input validation logic.
- Identify the transformation applied to user input and the target byte-array the app compares against.
- Invert the transformation to recover the input string which results in the target byte-array.
- Verify locally with the provided script.
>Key findings (analysis)
-
The mini program includes a front-end check in
pages/index/index.js— anenigmaticTransformationfunction (an XOR/rotate based obfuscation) and anonCheckcomparison against a constant array. -
The check uses a fixed key string
"newKey2025!"and computes a shift valuec= sum(ascii(key)) % 8. The produced array is then compared (JSON stringified) with the hardcoded array:[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]
-
By reversing the rotation and XOR operations we can compute the original input string accepted by the mini program.
>The reversal logic (explanation)
The check follows these conceptual steps (simplified):
- Convert the
keystring into bytes. - Compute
c = sum(key_bytes) % 8(a rotate count in 0..7). - For each input character at index
i:- Let K = key[i % len(key)] (the corresponding key byte)
- Let u = resultByte ^ K, but the stored
resultBytein the target had been rotated left bycwhen generating the stored array. - Therefore, to recover input character charByte we rotate right by
cthe stored value (undoing the left-rotation performed by the app), then XOR with K.
In code terms (recovery):
- For each target byte T:
- rotated = (T >> c) | ((T & ((1<<c)-1)) << (8-c)) # rotate-right by c
- charByte = rotated ^ keyByte
Concatenate the resulting charBytes into ASCII text: this yields flag{JustEasyMiniProgram}.
>Files: code listings (exact)
Below I include the exact code I used during the analysis. You can reproduce everything locally with these files.
solution/001_unwxapkg.py (wxapkg index dumper)
#!/usr/bin/env python3
import struct
from pathlib import Path
PACKAGE_PATH = Path(__file__).resolve().parent.parent / "__APP__.wxapkg"
with PACKAGE_PATH.open('rb') as fh:
data = fh.read()
cursor = 16
print("Parsing file index starting at offset 0x10")
entries = []
index = 0
while cursor < len(data):
if cursor + 6 > len(data):
break
tag, = struct.unpack_from('>H', data, cursor)
cursor += 2
attr, = struct.unpack_from('>H', data, cursor)
cursor += 2
name_len, = struct.unpack_from('>H', data, cursor)
cursor += 2
if name_len == 0 or cursor + name_len > len(data):
break
name_bytes = data[cursor:cursor + name_len]
cursor += name_len
name = name_bytes.decode('utf-8', errors='replace')
if cursor + 8 > len(data):
break
data_offset_entry, = struct.unpack_from('>I', data, cursor)
cursor += 4
data_size, = struct.unpack_from('>I', data, cursor)
cursor += 4
entries.append({
'index': index,
'tag': tag,
'attr': attr,
'name_len': name_len,
'name': name,
'data_offset': data_offset_entry,
'data_size': data_size,
})
index += 1
if data_offset_entry == 0 and data_size == 0:
break
print(f"parsed {len(entries)} entries")
for entry in entries[:10]:
print(entry)Note: This script was used during exploration to inspect embedded entries inside the
.wxapkg. It prints a short list of parsed entries (paths, offsets and sizes). The package format varies across versions; this helper was used only for quickly discovering embedded JS resources.
solution/002_recover_flag.py (flag recovery script)
#!/usr/bin/env python3
"""Recover the original input string that satisfies the mini program check."""
KEY = "newKey2025!"
TARGET = [
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,
]
shift = sum(ord(ch) for ch in KEY) % 8
flag_bytes = []
for idx, value in enumerate(TARGET):
key_byte = ord(KEY[idx % len(KEY)])
if shift == 0:
rotated = value
else:
rotated = ((value >> shift) | ((value & ((1 << shift) - 1)) << (8 - shift))) & 0xFF
flag_bytes.append(rotated ^ key_byte)
flag = bytes(flag_bytes).decode('ascii')
print(flag)This is the exact script I used to invert the obfuscation. It prints the recovered input which matches the string the mini program expects.
solution/003_notes.txt (analysis notes)
Step 1: Extracted the provided archive and inspected the __APP__.wxapkg package header.
Step 2: Identified the JavaScript logic inside the package by dumping raw content; located the encryption check in pages/index/index.js.
Step 3: Reimplemented the transformation in Python (see 002_recover_flag.py) and inverted the operation to derive the accepted input string.
Recovered flag: flag{JustEasyMiniProgram}>How to reproduce (commands)
Run these from the repository root (/home/noigel/Desktop/XCTF/MOBILE):
# (optional) inspect parsed entries
python3 solution/001_unwxapkg.py
# recover the flag
python3 solution/002_recover_flag.pyExpected output from 002_recover_flag.py:
flag{JustEasyMiniProgram}
>Conclusion
I recovered the accepted input by reverse-engineering the JavaScript in the package and inverting the transform with 002_recover_flag.py. The recovered flag is reproduced above.