Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
AndroidEasy

Ezminiapp

CTF writeup for Ezminiapp from xCTF

//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 input
    • solution/003_notes.txt — short notes and summary
    • solution/WRITEUP.md — this writeup

>High-level approach

  1. Extract and inspect __APP__.wxapkg locally.
  2. Search and extract JavaScript resources inside the wxapkg to locate the input validation logic.
  3. Identify the transformation applied to user input and the target byte-array the app compares against.
  4. Invert the transformation to recover the input string which results in the target byte-array.
  5. Verify locally with the provided script.

>Key findings (analysis)

  • The mini program includes a front-end check in pages/index/index.js — an enigmaticTransformation function (an XOR/rotate based obfuscation) and an onCheck comparison against a constant array.

  • The check uses a fixed key string "newKey2025!" and computes a shift value c = 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 key string 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 resultByte in the target had been rotated left by c when generating the stored array.
    • Therefore, to recover input character charByte we rotate right by c the 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)

python
#!/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)

python
#!/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)

text
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):

bash
# (optional) inspect parsed entries
python3 solution/001_unwxapkg.py

# recover the flag
python3 solution/002_recover_flag.py

Expected 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.