Magic or Not

645 Words · 2 Minutes, 55 Seconds

Challenge Name: misc_magic-or-not

Category: Misc

Difficulty: Easy

Author: H4N5

Challenge Description

As an intern at Brunner Corporation, I developed a cutting-edge image obfuscation algorithm, designed to hide sensitive image data.

I’m confident it’s secure, but the security team isn’t convinced. They believe custom cryptography always hides a flaw.

Can you prove them wrong? Analyze the implementation, recover the original image, and find the flag.

We get four files with image extensions — Brunner1.jpg, Brunner2.gif, Brunner3.png, Brunner4.bmp — but file reports all four as raw data: their magic bytes are wrong. The name “magic-or-not” is the whole hint.

Approach

Step 1 — The headers are corrupted, and there’s a tell

$ for f in Brunner*; do file "$f"; done
Brunner1.jpg: data
Brunner2.gif: data
Brunner3.png: data
Brunner4.bmp: data

Hexdumping each file shows enormous runs of a single repeated byte — a different one per file:

Brunner1.jpg  ... 58 58 58 58 ...   (0x58 'X')
Brunner2.gif  ... 57 57 57 57 ...   (0x57 'W')
Brunner3.png  ... 59 59 59 59 ...   (0x59 'Y')
Brunner4.bmp  ... 5a 5a 5a 5a ...   (0x5a 'Z')

Real images have large flat regions (a solid background), which are runs of 0x00. A run of a constant non-zero byte where zeros should be is the classic fingerprint of a single-byte XOR: 0x00 ^ key == key. So each file’s repeated byte is its XOR key.

Step 2 — Confirm against known magic bytes

Each format has a fixed file signature; XORing the file’s first bytes against it must yield a constant. For Brunner3.png (should start 89 50 4E 47 0D 0A 1A 0A):

d0^89 = 59   09^50 = 59   17^4e = 59   1e^47 = 59
54^0d = 59   53^0a = 59   43^1a = 59   53^0a = 59

Every byte gives 0x59 — matching the filler. The same holds for the others:

FileExpected magicKey(filler char)
Brunner1.jpgFF D8 FF0x58X
Brunner2.gif47 49 46 38 (GIF8)0x57W
Brunner3.png89 50 4E 47 …0x59Y
Brunner4.bmp42 4D (BM)0x5aZ

The keys are W X Y Z — a cute touch, and confirmation the “cutting-edge algorithm” is just XOR with one byte.

Step 3 — Decrypt and validate

keys = {"Brunner1.jpg":0x58, "Brunner2.gif":0x57, "Brunner3.png":0x59, "Brunner4.bmp":0x5a}
for fn, k in keys.items():
    data = bytes(b ^ k for b in open(fn,"rb").read())
    open("out/dec_"+fn, "wb").write(data)
$ file out/dec_*
dec_Brunner1.jpg: JPEG image data, ... 642x896
dec_Brunner2.gif: GIF image data, version 89a, 190 x 896
dec_Brunner3.png: PNG image data, 68 x 896, 8-bit RGBA
dec_Brunner4.bmp: PC bitmap, ... 321 x 896 x 32

All four decode to valid images — and all are 896 px tall. They are vertical strips of one picture (widths 642 + 190 + 68 + 321 = 1221).

Step 4 — Reassemble

Stitching the strips left-to-right in filename order (1, 2, 3, 4) with ImageMagick:

convert dec_Brunner1.jpg dec_Brunner2.gif dec_Brunner3.png dec_Brunner4.bmp +append stitched.png

produces a clean 1221×896 photo of a pie-textured flag on a flagpole, with the flag text spelling out the answer. The seams line up perfectly and the text is continuous, confirming the order is correct.

Flag

brunner{ctf2026}

Reflections and Learnings


miscxorfile-signaturesimagemagick

Misc