← Back to Writeups
CUSTOMMediumReversing

Вернуть 2007й (Return 2007)

XESXOR9/2/20269 min read
#reversing#custom#medium#custom_vm_opcode_reversing#encrypted_bytecode_decryption#java_bytecode_to_python_reimpl#javap_bytecode_disassembly#per_character_bruteforce_with_#pin_bruteforce_via_title_hint#linux

Вернуть 2007й (Return 2007)

Platform: bug-makers | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-27 | Status: Solved Techniques: custom_vm_opcode_reversing, encrypted_bytecode_decryption, java_bytecode_to_python_reimplementation, javap_bytecode_disassembly, per_character_bruteforce_with_evolving_state, pin_bruteforce_via_title_hint

Summary

Task: J2ME MIDlet JAR with custom StageVM bytecode interpreter verifying a flag via encrypted opcodes and a 4-digit PIN. Solution: decompile with javap, decrypt VM bytecode using XOR with STATIC_KEY, derive PIN=2007 from title hint, brute-force each character through slowCharMix with evolving state variables.

Вернуть 2007й (Return 2007) — Bug-Makers CTF

Description

Запусти это приложение для платформы J2ME и проверь флаг!

English summary: A J2ME MIDlet application (FlagChecker.jar) implements a flag checker. The user enters a 4-digit PIN, then a flag string which is verified by a custom bytecode VM with encrypted instructions. The goal is to reverse the verification logic and recover the flag.

Analysis

JAR Structure

The JAR targets CLDC-1.1/MIDP-2.0 (Java 2 Micro Edition) and contains 4 class files: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • BugCheckerMidlet.class — main MIDlet with helper functions: xorshift32, ror32, rol16
  • BugCheckerMidlet$CheckerCanvas.class — UI canvas with PIN entry and flag checker screens
  • BugCheckerMidlet$StageVM.class — core verification VM with encrypted bytecode
  • BugCheckerMidlet$1.class — synthetic inner class (empty)

Verification Flow

  1. PIN entry: User enters a 4-digit PIN → derivePinKey(pin) produces a 32-bit pinKey via heavy computation:

    • Initializes a 16384-element table with xorshift32
    • Performs 8,388,608 mixing iterations
    • Final fold over every 97th element ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
  2. Flag verification via StageVM.verify(String input, int pinCode, int pinKey):

    • Decrypts ENC_STAGE (164 bytes) using STATIC_KEY = "dynaVM42" with position-dependent XOR mixing
    • Validates magic bytes BG2\x06 (66, 71, 50, 6)
    • Validates a 16-bit checksum over the bytecode body
    • Interprets VM opcodes sequentially

Bytecode Decryption

The decryption formula for each byte at position i:

key_byte = STATIC_KEY[(i + 5) % 8]
mix = ((key_byte + i * 29) & 0xFF + 55) & 0xFF
decrypted[i] = encrypted[i] ^ mix

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

VM Opcodes

After decryption, the bytecode contains these opcodes:

OpcodeNameArgsDescription
0x10REQUIRE_UNLOCKu16Checks ((pinKey ^ ror32(pinCode * 40503, 7)) + 15706) & 0xFFFF == 37403
0x11LEN_EQu8Checks input.length() == 30
0x12CONTAINSu8Skips arg bytes (data section, length 0)
0x30CHAR_DYNu8 charIdx, u8 param, u16 expectedPer-character verification via slowCharMix
0x7FEND—Returns true if all checks passed
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

CHAR_DYN Verification

30 CHAR_DYN instructions check all 30 flag characters in scrambled order (not sequential): positions 10, 5, 29, 16, 14, 4, 23, 24, 6, 9, 7, 21, 13, 27, 26, 20, 3, 8, 12, 18, 1, 17, 11, 25, 19, 15, 28, 22, 0, 2.

For each instruction, the VM:

  1. Updates state s9 = xorshift32(s9 + param*40503 + charIdx*73244475 + (pinKey & 32767) + (s10 & 65535))
  2. Computes a target value from the expected u16 using v20, v21, v22 derived from param, charIdx, pinCode, pinKey, s10
  3. Calls slowCharMix(charVal, charIdx, param, pinCode, pinKey, s9, s10) — a heavy mixing function
  4. Compares (mix ^ (mix >>> 16)) & 0xFFFF against the target
  5. Updates s10 = xorshift32(s10 ^ mix ^ (charVal * 16843009) ^ (charIdx * 40503)) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

slowCharMix Function

The core mixing function performs ~1500-2000 xorshift32 loop iterations per character:

s8 = xorshift32(s9 ^ (charVal*16843009) ^ (param*40503) ^ s10 ^ (pinKey*521288629))
loop_count = 1536 + ((pinCode + param + charIdx) & 511)
for j in range(loop_count):
    s11 = xorshift32(s8 + (pinKey ^ (j*73244475)) + ((pinCode+17)*522133279))
    s8 = xorshift32((s8 ^ ror32(s11, s8&31)) + s10 + j*40503 + param*131)
    s8 = s8 ^ ror32(s11 ^ s10, (charIdx + j) & 31)
return xorshift32(s8 ^ ror32(s10, (param + charIdx) & 31))

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Critical gotchas discovered during bytecode analysis:

  • Loop count uses pinCode (not charVal): 1536 + ((pinCode + param + charIdx) & 511)
  • Final rotation uses charIdx (not charVal): ror32(s10, (param + charIdx) & 31)

PIN Discovery

The challenge title "Вернуть 2007й" ("Return 2007") directly hints at PIN = 2007. Confirmed by computing derivePinKey(2007) = 1061938179 and verifying the REQUIRE_UNLOCK check passes: ((1061938179 ^ ror32(2007 * 40503, 7)) + 15706) & 0xFFFF == 37403. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Extract and decompile

bash
unzip FlagChecker.jar -d extracted/
javap -c -p extracted/BugCheckerMidlet.class
javap -c -p 'extracted/BugCheckerMidlet$StageVM.class'
javap -c -p 'extracted/BugCheckerMidlet$CheckerCanvas.class'

Step 2: Identify PIN from title

The title "Вернуть 2007й" = "Return 2007" → PIN = 2007.

Step 3: Solve with brute-force

For each of the 30 CHAR_DYN instructions (in execution order), brute-force the character value over the alphabet ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_ (65 chars). The evolving state variables s9 and s10 must be updated correctly after each character is found. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Complete solve script

python
#!/usr/bin/env python3
"""
Full solver for FlagChecker J2ME challenge - custom StageVM bytecode interpreter.
"""
import sys
import time

def to_int32(x):
    x = x & 0xFFFFFFFF
    if x >= 0x80000000:
        return x - 0x100000000
    return x

def to_uint32(x):
    return x & 0xFFFFFFFF

def xorshift32(state):
    s = to_uint32(state)
    s ^= to_uint32(s << 13) & 0xFFFFFFFF
    s ^= (s >> 17)
    s ^= to_uint32(s << 5) & 0xFFFFFFFF
    return to_int32(s)

def ror32(val, shift):
    shift = shift & 31
    val = to_uint32(val)
    return to_int32((val >> shift) | to_uint32(val << (32 - shift)))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def rol16(val, shift):
    shift = shift & 15
    v = val & 0xFFFF
    if shift == 0:
        return v
    return ((v << shift) | (v >> (16 - shift))) & 0xFFFF

STATIC_KEY = bytes([100, 121, 110, 97, 86, 77, 52, 50])  # "dynaVM42"

enc_stage_bytes = []
for b in [-58, -49, -111, -12, 52, -92, 93, 73,
          114, 98, -117, -22, 6, -103, 86, 99,
          100, 93, -31, -5, -117, 54, 11, -102,
          108, -46, 107, -70, -33, 97, 17, 32,
          42, -68, -23, -68, -12, -46, -75, 108,
          91, 32, 60, 74, -114, 51, -2, -8,
          -52, 88, -51, 82, -110, 84, 39, -51,
          -20, -23, 0, 77, -14, -66, -103, 103,
          57, -2, -45, 39, 55, 49, -78, -88,
          -95, -112, 109, -78, 124, 69, 36, -119,
          -40, -88, -87, -32, 59, 17, 102, 124,
          14, -42, -119, -38, 31, 94, -127, 88,
          84, 96, 7, 15, 115, 38, 42, 8,
          -45, -33, 91, -88, -26, 62, -32, 16,
          53, -34, 99, -45, -28, -9, -103, 72,
          96, 16, 48, 54, 50, -47, -18, -23,
          101, 11, -32, 66, -73, -52, 3, 85,
          -36, -1, -69, 73, -43, -82, -78, 12,
          61, -51, -61, 84, 87, -14, 101, -104,
          -68, 40, 99, 17, 108, 108, 109, 65,
          -55, -41, -15, 108]:
    enc_stage_bytes.append(b & 0xFF)
ENC_STAGE = bytes(enc_stage_bytes)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def decrypt_stage():
    result = bytearray(len(ENC_STAGE))
    for i in range(len(ENC_STAGE)):
        key_byte = STATIC_KEY[(i + 5) % len(STATIC_KEY)] & 0xFF
        mix = ((key_byte + (i * 29) & 0xFF) + 55) & 0xFF
        result[i] = (ENC_STAGE[i] ^ mix) & 0xFF
    return bytes(result)

def derive_pin_key(pin):
    table = [0] * 16384
    s = to_int32(to_int32(pin * 40503) ^ 2135587861)
    s = xorshift32(s)
    for i in range(16384):
        s = xorshift32(to_int32(s + to_int32(i * 73244475) + to_int32(-1515870811)))
        table[i] = to_int32(s ^ ror32(s, i & 31))
    s2 = xorshift32(to_int32(s ^ 1831565813))
    for i in range(8388608):
        idx = to_uint32(to_int32(s2 ^ to_int32(i * 40503))) & 16383
        val = table[idx]
        s3 = xorshift32(to_int32(s2 + val + to_int32(pin * 521288629) + i))
        idx2 = to_uint32(to_int32((to_uint32(val) >> 9) ^ s3 ^ i)) & 16383
        table[idx] = to_int32(val ^ ror32(s3, val & 31) ^ table[idx2])
        s2 = to_int32(s3 ^ table[to_uint32(to_int32(idx + (s3 & 31))) & 16383])
    for i in range(0, 16384, 97):
        s2 = xorshift32(to_int32(s2 ^ table[i] ^ to_int32(i * 17881)))
    return s2
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def slow_char_mix(char_val, char_idx, param, pin_code, pin_key, s9_val, s10_val):
    s8 = xorshift32(to_int32(
        s9_val ^ to_int32(char_val * 16843009) ^ to_int32(param * 40503) ^
        s10_val ^ to_int32(pin_key * 521288629)
    ))
    loop_count = 1536 + ((pin_code + param + char_idx) & 511)
    for j in range(loop_count):
        s11 = xorshift32(to_int32(
            s8 + to_int32(pin_key ^ to_int32(j * 73244475)) +
            to_int32(to_int32(pin_code + 17) * 522133279)
        ))
        s8 = xorshift32(to_int32(
            to_int32(s8 ^ ror32(s11, s8 & 31)) + s10_val + to_int32(j * 40503) +
            to_int32(param * 131)
        ))
        s8 = to_int32(s8 ^ ror32(to_int32(s11 ^ s10_val), (char_idx + j) & 31))
    return xorshift32(to_int32(s8 ^ ror32(s10_val, (param + char_idx) & 31)))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Decrypt and verify bytecode
stage = decrypt_stage()
assert stage[0] == 66 and stage[1] == 71 and stage[2] == 50 and stage[3] == 6

# PIN = 2007 (from title "Вернуть 2007й")
pinCode = 2007
pinKey = derive_pin_key(pinCode)  # = 1061938179

# Verify REQUIRE_UNLOCK
val = to_int32(pinKey ^ ror32(to_int32(pinCode * 40503), 7))
val = (val + 15706) & 0xFFFF
assert val == 37403

# CHAR_DYN instructions: (charIdx, param, expected_u16)
char_dyn_instructions = [
    (10, 135, 30755), (5, 146, 14719), (29, 178, 20626), (16, 3, 36847),
    (14, 148, 43566), (4, 83, 37975), (23, 48, 8845), (24, 56, 41182),
    (6, 242, 37125), (9, 251, 1934), (7, 215, 64822), (21, 83, 18228),
    (13, 32, 42664), (27, 74, 2380), (26, 226, 3927), (20, 114, 22034),
    (3, 112, 48904), (8, 132, 56695), (12, 48, 40847), (18, 10, 49390),
    (1, 230, 12401), (17, 111, 16508), (11, 188, 36383), (25, 97, 963),
    (19, 122, 50573), (15, 176, 4953), (28, 204, 59669), (22, 35, 29939),
    (0, 232, 47163), (2, 19, 53613),
]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_"

# Initial state
s9 = xorshift32(to_int32(pinKey ^ 1831565813))
s10 = xorshift32(to_int32(to_int32(pinKey * 73244475) ^ to_int32(pinCode * 40503) ^ 331804469))

flag = ['?'] * 30

for inst_idx, (char_idx, param, expected_u16) in enumerate(char_dyn_instructions):
    s9 = xorshift32(to_int32(
        s9 + to_int32(param * 40503) + to_int32(char_idx * 73244475) +
        (to_uint32(pinKey) & 32767) + (to_uint32(s10) & 65535)
    ))
    v20 = (param*145 + char_idx*311 + ((to_uint32(s10)>>11)&0xFFFF) + (to_uint32(pinKey)&0xFFFF)) & 0xFFFF
    v21 = ((pinCode + param + char_idx + (to_uint32(s10) & 7)) & 7) + 1
    v22 = (pinCode*29 + param*41 + char_idx*17 + ((to_uint32(s10)>>3)&0xFFFF)) & 0xFFFF
    target = (rol16(expected_u16 ^ v20, v21) + v22) & 0xFFFF
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    for c in ALPHABET:
        char_val = ord(c) & 0xFF
        mix = slow_char_mix(char_val, char_idx, param, pinCode, pinKey, s9, s10)
        computed = (to_uint32(mix) ^ (to_uint32(mix) >> 16)) & 0xFFFF
        if computed == target:
            flag[char_idx] = c
            s10 = xorshift32(to_int32(s10 ^ mix ^ to_int32(char_val*16843009) ^ to_int32(char_idx*40503)))
            break

print(f"Flag: {''.join(flag)}")
# BugCTF{REDACTED}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍