Artistic Warmup
Artistic Warmup
Platform: srdnlen | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-02-28 | Status: Solved Techniques: api_hash_resolution, bitmap_extraction, gdi_bitmap_rendering, xor_decryption
Summary
Task: PE32+ Windows flag checker using GDI bitmap comparison instead of string comparison. Solution: Extract embedded XOR-encrypted bitmap data, decrypt with 0xAA key, render as image to reveal flag text.
Artistic Warmup — srdnlen CTF
Description
just an easy warmup
PE32+ executable (console) x86-64 for Windows, compiled with MinGW GCC, stripped. Classic flag checker — accepts input and outputs "Valid flag!" or "Invalid flag.".
Analysis
Initial Reconnaissance
bash read-only • copy allowed$ file rev_artistic_warmup.exe rev_artistic_warmup.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows $ strings rev_artistic_warmup.exe | grep -i flag Invalid flag. Valid flag!
The binary contains 13,523 strings (due to MinGW/C++ runtime), but no flag or base64 data in plain text. Key strings "Valid flag!" and "Invalid flag." confirm — this is a flag checker.
Identifying the Validation Function
By cross-referencing the strings "Valid flag!" and "Invalid flag.", the main validation function was found at address fcn.1400bfb00 (0x1400bfb00).
API Resolution via DJB2 Hashing
The binary uses an anti-analysis technique — dynamic resolution of Windows API through function name hashing (DJB2 variant). The resolver function is located at address fcn.140001510. All 11 API calls are hidden from static analysis:
| Hash | API |
|---|---|
| 0x5fbff0fb | kernel32!LoadLibraryA |
| 0xa05cbae0 | gdi32!CreateCompatibleDC |
| 0xfff5b73d | gdi32!CreateDIBSection |
| 0xeb9a1ab1 | gdi32!CreateFontA |
| 0x7cf4fd7c | gdi32!SelectObject |
| 0x5f1ebf5d | gdi32!SetBkColor |
| 0x41936715 | gdi32!SetTextColor |
| 0x805294c3 | gdi32!TextOutA |
| 0x0dd2a6fb | gdi32!GdiFlush |
| 0xcc68186f | gdi32!DeleteObject |
| 0x9f3bef5f | gdi32!DeleteDC |
| |
All resolved functions are from Windows GDI (Graphics Device Interface), which explains the name "Artistic".
Validation Logic — "Artistic" Comparison
Instead of traditional character-by-character string comparison, the program uses bitmap comparison:
- Bitmap creation:
CreateCompatibleDC(0)+CreateDIBSectioncreate a 450×50 pixel, 32bpp (top-down DIB) — totaling 450 × 50 × 4 = 90,000 bytes - Font setup:
CreateFontAcreates Consolas 24pt font - Color setup: black background (
SetBkColor(0)), white text (SetTextColor(0xFFFFFF)) - Rendering:
TextOutAdraws user input onto the bitmap at position (0, 0) - Obfuscation: each byte of the bitmap is XOR'd with
0xAA - Comparison: the result is compared byte-by-byte with 90,000 bytes of reference data embedded in the binary
Key Addresses
| Element | Address |
|---|---|
| Validation function | 0x1400bfb00 |
| API resolver | 0x140001510 |
| Reference bitmap data | 0x1400c5020 (file offset 0xc3620) |
| Data size | 0x15f90 (90,000 bytes) |
| Comparison loop | 0x1400bfe0c — 0x1400bfe17 |
Solution
Step 1: Extract Reference Bitmap
Extract 90,000 bytes of reference data from the binary at offset 0xc3620.
Step 2: XOR Decryption
Each byte is XOR'd with 0xAA to reverse the obfuscation — yielding raw bitmap pixel data.
Step 3: Render as Image
The raw data is interpreted as 450×50 pixels, 32bpp (BGRA). The result is an image with white text on black background containing the flag.
python read-only • copy allowed#!/usr/bin/env python3 """Artistic Warmup solver — extract flag from embedded XOR'd bitmap.""" from PIL import Image import struct BINARY = "rev_artistic_warmup.exe" BITMAP_OFFSET = 0xc3620 # File offset of reference bitmap data BITMAP_SIZE = 90000 # 450 * 50 * 4 bytes (32bpp) WIDTH = 450 HEIGHT = 50 XOR_KEY = 0xAA # Extract reference data from binary with open(BINARY, "rb") as f: f.seek(BITMAP_OFFSET) data = bytearray(f.read(BITMAP_SIZE)) # XOR decrypt for i in range(len(data)): data[i] ^= XOR_KEY # Create image from raw BGRA pixel data img = Image.new("RGBA", (WIDTH, HEIGHT)) pixels = img.load() for y in range(HEIGHT): for x in range(WIDTH): offset = (y * WIDTH + x) * 4 b, g, r, a = data[offset:offset + 4] pixels[x, y] = (r, g, b, 255) img.save("reference_bitmap.png") print(f"Saved reference_bitmap.png ({WIDTH}x{HEIGHT})") print("Flag is visible as white text on black background in the image") # Flag: srdnlen{REDACTED}
Result
The resulting image contains white text on black background — the flag is readable directly:
srdnlen{REDACTED}