Lemballovo
Lemballovo
Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: cd_check_nop, conditional_jump_bypass, registry_check_nop, result_system_reversing, sound_error_bypass, state_machine_patching, vsr_directory_entry_swap, win_condition_forcing, wine_runtime_emulation
Summary
Walkthrough of the CUSTOM Reversing Lemballovo. Task: a 1996 Windows game 'Lemmings Paintball' (PE32 x86) won't start due to install/CD/sound checks; must play a custom-injected level to capture the flag. Solution: patch conditional jumps to bypass
Description
Я хотел захватить флаг, вот так: [GIF геймплея]. Но игра выдаёт ошибки и не запускается :( Поиграй в игру и захвати тот флаг!
English summary: A ZIP archive contains "Lemmings Paintball" (1996, Visual Sciences) — a PE32 x86 Windows game with DirectDraw rendering. The game refuses to start due to multiple DRM/environment checks. A GIF shows the "Flag On A Hill" level demo. The goal is to patch the game, run it, find the organizer's custom-injected level, and win it to reveal the flag.
Analysis
Initial reconnaissance
$ file LEMBALL.EXE
LEMBALL.EXE: PE32 executable (GUI) Intel 80386, for MS Windows
$ strings LEMBALL.EXE | grep -i "install\|cd\|unable\|paintball"
Paintball Not Installed
Unable to find CD
Please insert the Paintball CD
HKLM\SOFTWARE\Visual Sciences\Lemmings Paintball
The archive contains:
LEMBALL.EXE— main game executable (PE32 x86, ~300KB)PBAIMOG.VSR— CRID resource archive (levels, bitmaps, sounds)DSPDIB16.DLL,DSPDIB32.DLL— DisplayDib rendering libraries
Four startup gates blocking execution
-
Install check (VA
0x406e31): Calls0x45ecb0which reads registry keyHKLM\SOFTWARE\Visual Sciences\Lemmings Paintball. If not found → MessageBox "Paintball Not Installed" → exit. -
CD check (VA
0x406e66): Calls0x45eda0("vsmem.dll")which scans all drives viaGetLogicalDrives/GetDriveTypeAlooking for a CD containingvsmem.dll. If not found → "Unable to find CD" retry loop. -
Sound error (VA
0x47c71f):waveOutOpenfails without a sound device → blocking dialog "Error! Sound System cannot open Wave Device!" with Abort/Retry/Ignore. -
Missing intro: Game tries to open
lemball\Intro.avi(intro movie from CD) → "Cannot find the specified file" dialog.
Finding the custom level
The VSR archive uses the CRID format with hierarchical directories. Scanning all 114 level NAME chunks reveals exactly ONE non-standard name: "Solve,solve like the wind!" at TAXING pack level #18 (0-based entry 27 in the TAXING category directory). All other 113 level names are genuine Lemmings Paintball originals.
The level's FSDG tilemap is a 64×64 spiral maze (6-byte cells, byte index 4: 0=floor, 16=wall). The lemming starts at the center on a checkered flag with a 60-second timer.
Level lock mechanism
The game locks levels sequentially — only level 1 of each pack (Fun/Tricky/Taxing/Mayhem) is initially available. Reaching TAXING #18 requires either:
- Reversing the password/unlock algorithm (complex)
- Swapping VSR directory entries (simple)
Result system internals
The level-end result system uses a state pointer at [0x49cb68]:
[state+0xc]= result code (2 = WIN, 1/3/4/5 = various lose conditions)- Formatter function at VA
0x450160reads the result code and selects a message table:- Win table at
0x49fb38(index 2 → "You collected all the flags!") - Lose table at
0x49fb78(index 3 → eliminated, index 4 → ran out of time)
- Win table at
[obj+0x518]= winner flag (nonzero → win display path)- State machine at
0x411c40writes result codes on level-end events
Solution
Step 1: Patch startup checks (LEMBALL_patched.EXE)
Three binary patches to bypass the startup gates:
| Purpose | File offset | VA | Before | After | Effect |
|---|---|---|---|---|---|
| Install check bypass | 0x6238 | 0x406e38 | 75 17 (jne) | EB 17 (jmp) | Always skip "Not Installed" branch |
| CD check bypass | 0x626d | 0x406e6d | 75 17 (jne) | EB 17 (jmp) | Always skip "Unable to find CD" branch |
| Sound error bypass | 0x7bb1f | 0x47c71f | 74 42 (je) | EB 42 (jmp) | Skip waveOutOpen error dialog |
| |
Step 2: Set up Wine 5.0 runtime
Wine 11 crashes on Apple Silicon (macOS ARM) due to a 16K-vs-4K page-size assertion in ntdll. Wine 5.0 on Ubuntu 20.04 in Docker (--platform linux/amd64) works:
dockerfile read-only • copy allowedFROM --platform=linux/amd64 ubuntu:20.04 RUN dpkg --add-architecture i386 && apt-get update && \ apt-get install -y wine wine32:i386 xvfb pulseaudio imagemagick xdotool
Runtime setup:
- PulseAudio with null-sink provides a dummy audio device so
waveOutOpensucceeds - Registry: set
Audio=pulsebefore wineboot; setSrcDiskandInstalledunder the Lemmings Paintball key - Intro.avi: generate a valid 1-second black AVI with ffmpeg (
-c:v msvideo1) to avoid the missing-file dialog - Display:
Xvfb :0 -screen 0 800x600x16for headless rendering
Step 3: Swap VSR directory entries to reach the locked level
Since levels are locked sequentially, swap the TAXING directory entries in PBAIMOG.VSR so selecting "Taxing level 1" (unlocked) loads the "Solve,solve like the wind!" maze:
python read-only • copy allowed#!/usr/bin/env python3 """Swap TAXING directory entries 10 (Gatecrash=lvl1) <-> 27 (Solve=lvl18)""" import struct, shutil shutil.copy("PBAIMOG.VSR", "PBAIMOG_solve.vsr") data = bytearray(open("PBAIMOG_solve.vsr", "rb").read()) # TAXING child directory at VSR offset 0x7d2f24 # Each entry is 0x24 bytes; entries contain offset + size fields taxing_dir = 0x7d2f24 entry_size = 0x24 e10_off = taxing_dir + 10 * entry_size # Gatecrash Boogie (level 1, unlocked) e27_off = taxing_dir + 27 * entry_size # Solve,solve like the wind! (level 18, locked) # Swap the two 0x24-byte directory entries e10_data = data[e10_off:e10_off + entry_size] e27_data = data[e27_off:e27_off + entry_size] data[e10_off:e10_off + entry_size] = e27_data data[e27_off:e27_off + entry_size] = e10_data with open("PBAIMOG_solve.vsr", "wb") as f: f.write(data)
Verified: the briefing screen shows "Taxing / Solve,solve like the wind!".
Step 4: Force WIN via result-system patches (LEMBALL_win.EXE)
Since the spiral maze is impractical to solve via automated screenshot-based gameplay, force the win condition by patching the result system:
python read-only • copy allowed#!/usr/bin/env python3 """Apply win-forcing patches to LEMBALL_patched.EXE -> LEMBALL_win.EXE""" import shutil shutil.copy("LEMBALL_patched.EXE", "LEMBALL_win.EXE") data = bytearray(open("LEMBALL_win.EXE", "rb").read()) patches = [ # Startup bypasses (inherited) (0x6238, bytes([0xEB])), # install check: jne -> jmp (0x626d, bytes([0xEB])), # CD check: jne -> jmp (0x7bb1f, bytes([0xEB])), # sound error: je -> jmp # Force win message table (VA 0x450186) (0x4f586, b"\xb9\x38\xfb\x49\x00" + b"\x90" * 8), # mov ecx, 0x49fb38 + NOPs # Force result index = 2 (WIN) (VA 0x4501b7) (0x4f5b7, b"\x6a\x02\x5a"), # push 2; pop edx # Force winner-flag = 1 (VA 0x4500a5) (0x4f4a5, b"\x6a\x01\x58\x90"), # push 1; pop eax; nop # Always play win sound (VA 0x450b6d) (0x4ff6d, b"\x90\x90"), # nop out je (lose-sfx branch) # State machine: all result codes -> WIN(2) (0x1113f, bytes([0x02])), # was 03 (0x11156, bytes([0x02])), # was 05 (0x111f3, bytes([0x02])), # was 03 (0x1120a, bytes([0x02])), # was 05 (in mov eax,5) (0x11232, bytes([0x02])), # was 04 (0x1124f, bytes([0x02])), # was 04 (0x1127b, bytes([0x02])), # was 01 (0x11298, bytes([0x02])), # was 01 (0x112ae, bytes([0x02])), # was 04 (0x11389, bytes([0x02])), # was 04 (0x12074, bytes([0x02])), # was 05 ] for offset, patch_bytes in patches: data[offset:offset + len(patch_bytes)] = patch_bytes with open("LEMBALL_win.EXE", "wb") as f: f.write(data)
Step 5: Capture the flag
Running LEMBALL_win.EXE with the swapped VSR in Wine 5.0:
- Start the game → select TAXING pack → level 1 (now loads "Solve,solve like the wind!")
- The spiral maze level begins with a 60-second timer
- When the level ends (timeout/death), the forced WIN path triggers
- A congratulations dialog appears with the flag:
Congratulations! Your flag: spbctf{REDACTED} Enjoy the game :-)
Key Takeaways / Lessons
- Mastering cd_check_nop, conditional_jump_bypass, registry_check_nop, result_system_reversing, sound_error_bypass, state_machine_patching, vsr_directory_entry_swap, win_condition_forcing, wine_runtime_emulation was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-07-03 • CUSTOM • Lemballovo • Difficulty: Medium