Вернуть 2007й (Return 2007)
Вернуть 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,rol16BugCheckerMidlet$CheckerCanvas.class— UI canvas with PIN entry and flag checker screensBugCheckerMidlet$StageVM.class— core verification VM with encrypted bytecodeBugCheckerMidlet$1.class— synthetic inner class (empty)
Verification Flow
-
PIN entry: User enters a 4-digit PIN →
derivePinKey(pin)produces a 32-bitpinKeyvia heavy computation:- Initializes a 16384-element table with xorshift32
- Performs 8,388,608 mixing iterations
- Final fold over every 97th element
-
Flag verification via
StageVM.verify(String input, int pinCode, int pinKey):- Decrypts
ENC_STAGE(164 bytes) usingSTATIC_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
- Decrypts
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:
| Opcode | Name | Args | Description |
|---|---|---|---|
0x10 | REQUIRE_UNLOCK | u16 | Checks ((pinKey ^ ror32(pinCode * 40503, 7)) + 15706) & 0xFFFF == 37403 |
0x11 | LEN_EQ | u8 | Checks input.length() == 30 |
0x12 | CONTAINS | u8 | Skips arg bytes (data section, length 0) |
0x30 | CHAR_DYN | u8 charIdx, u8 param, u16 expected | Per-character verification via slowCharMix |
0x7F | END | — | 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:
- Updates state
s9 = xorshift32(s9 + param*40503 + charIdx*73244475 + (pinKey & 32767) + (s10 & 65535)) - Computes a target value from the expected u16 using
v20,v21,v22derived fromparam,charIdx,pinCode,pinKey,s10 - Calls
slowCharMix(charVal, charIdx, param, pinCode, pinKey, s9, s10)— a heavy mixing function - Compares
(mix ^ (mix >>> 16)) & 0xFFFFagainst the target - 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(notcharVal):1536 + ((pinCode + param + charIdx) & 511) - Final rotation uses
charIdx(notcharVal):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 read-only • copy allowedunzip 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 read-only • copy allowed#!/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}