← Back to Writeups
CUSTOMMediumReversing

Artistic Warmup

XESXOR9/10/20263 min read
#reversing#custom#medium#api_hash_resolution#bitmap_extraction#gdi_bitmap_rendering#xor_decryption#windows

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
$ 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:

HashAPI
0x5fbff0fbkernel32!LoadLibraryA
0xa05cbae0gdi32!CreateCompatibleDC
0xfff5b73dgdi32!CreateDIBSection
0xeb9a1ab1gdi32!CreateFontA
0x7cf4fd7cgdi32!SelectObject
0x5f1ebf5dgdi32!SetBkColor
0x41936715gdi32!SetTextColor
0x805294c3gdi32!TextOutA
0x0dd2a6fbgdi32!GdiFlush
0xcc68186fgdi32!DeleteObject
0x9f3bef5fgdi32!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:

  1. Bitmap creation: CreateCompatibleDC(0) + CreateDIBSection create a 450×50 pixel, 32bpp (top-down DIB) — totaling 450 × 50 × 4 = 90,000 bytes
  2. Font setup: CreateFontA creates Consolas 24pt font
  3. Color setup: black background (SetBkColor(0)), white text (SetTextColor(0xFFFFFF))
  4. Rendering: TextOutA draws user input onto the bitmap at position (0, 0)
  5. Obfuscation: each byte of the bitmap is XOR'd with 0xAA
  6. Comparison: the result is compared byte-by-byte with 90,000 bytes of reference data embedded in the binary ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Addresses

ElementAddress
Validation function0x1400bfb00
API resolver0x140001510
Reference bitmap data0x1400c5020 (file offset 0xc3620)
Data size0x15f90 (90,000 bytes)
Comparison loop0x1400bfe0c — 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
#!/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}

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