Marlboro

Bitskrieg CTFby smothy

Marlboro - Forensics

Points: 499 | Flag: BITSCTF{d4mn_y0ur_r34lly_w3n7_7h47_d33p} | Solved by: Smothy @ 0xN1umb

enhance

what we got

a zip called SaveMeFromThisHell.zip with one file inside: Marlboro.jpg. challenge description talking about smoke, fire, secrets, and some "programming language from hell where code mutates and sanity is optional". 500 points. aight lets go.

marlboro

sick VW Golf with the Marlboro livery ngl. but we're not here for the car.

the solve

layer 1: the image has a secret

first thing i always do - check for appended data after the JPEG:

python
data = open('Marlboro.jpg', 'rb').read()
idx = data.rfind(b'\xff\xd9')  # JPEG end marker
print(f'Data after JPEG: {len(data) - idx - 2} bytes')
# Data after JPEG: 17082 bytes

17KB hidden after the JPEG. classic. file says its a zip with extra data prepended - thats our polyglot JPEG/ZIP.

bash
$ unzip Marlboro.jpg
  inflating: extracted/smoke.png
 extracting: extracted/encrypted.bin

two files: a dark smoke.png and a mysterious encrypted.bin.

layer 2: steganography in the smoke

checked exiftool on smoke.png and immediately spotted something:

Author: aHR0cHM6Ly96YjMubWUvbWFsYm9sZ2UtdG9vbHMv

base64 decode:

https://zb3.me/malbolge-tools/

Malbolge. the programming language literally designed to be as difficult to use as possible. named after the eighth circle of hell in Dante's Inferno. thats what the challenge description meant by "programming language from hell."

then ran zsteg on the png:

bash
$ zsteg smoke.png
b1,rgb,lsb,xy  .. text: "# Marlboro Decryption Key\n# Format: 32-byte XOR
key in hexadecimal\nKEY=c7027f5fdeb20dc7308ad4a6999a8a3e069cb5c8111d5690
4641cd344593b657\n# Usage: XOR each byte of encrypted.bin with key[i % 32]"

LSB steganography hiding a decryption key in the RGB channels. we got the key and the instructions.

layer 3: decrypting the transmission

python
key_hex = 'c7027f5fdeb20dc7308ad4a6999a8a3e069cb5c8111d56904641cd344593b657'
key = bytes.fromhex(key_hex)
data = open('encrypted.bin', 'rb').read()
decrypted = bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])

output:

Content-Type: text/x-malbolge Interpreter: https://zb3.me/malbolge-tools/#interpreter Language: Malbolge D'`$##\!<5|jWVT/eRcONp_oJJHHF!3gfBc!?a_{)(xwYon4rqpi/mlkdibaf_%]ba `_XWVz=YRQPOsSRQ3ONGkKD,BAe(DCBA@?>7[5432V6v.3,P*).-&Jk)"!&}C#cy~wv <zyxZpotm3qSi/gOkd*KJ`ed]\"`Y^]\[TxRQVOTSLpPONMLEiIH*)?>bB$@987[Z: 38765.Rsr*/.-&%$#G'~f$#z@~}|{t:[wvonm3kSonmlkdihg`&GFbaZ~^@VUTYQRQP tNMLKPImGFKJCBf)E>bB;@?>7[5432Vw/43,POp.'&+*#G!~D1

Malbolge source code. with an explicit pointer to the zb3.me interpreter. this is gonna be important.

deeper

layer 4: the malbolge interpreter rabbit hole (the hard part)

so this is where it got absolutely insane. i wrote a standard Malbolge interpreter based on the Ben Olmstead reference implementation and ran the code. it produced 40 bytes of complete garbage:

00614a4b0c660c7f90185f573a36626f7b73731d0f64646c343d1f...

not a flag. not anything. tried multiple implementations, all same garbage output.

spent way too long debugging this until i realized - the challenge SPECIFICALLY points to zb3.me/malbolge-tools/. so i pulled the actual JavaScript VM source from that site and compared it to the standard reference implementation.

the critical difference: the zb3 interpreter uses a different encryption table (xlat2) for the self-modifying code step, while the standard reference implementation uses xlat1.

standard Malbolge:

js
// after executing an instruction, encrypt it with xlat1:
mem[c] = xlat1[mem[c] - 33];

zb3 implementation:

js
// after executing, encrypt with xlat2 (DIFFERENT table!):
vm.mem[vm.c] = xlat2.charCodeAt(vm.mem[vm.c] - 33);

xlat2 from zb3:

5z]&gqtyfr$(we4{WP)H-Zn,[%\3dL+Q;>U!pJS72FhOA1CB6v^=I_0/8|jsb9m<.TVac`uY*MK'X~xDl}REokN:#?G"i@

there's also a difference in xlat1 itself (position 75: Y vs H) and the code pointer advancement behavior after jumps. the zb3 interpreter always advances the code pointer, even after a jmp instruction.

once i used the EXACT zb3 VM implementation:

js
var xlat1 = '+b(29e*j1VMEKLyC})8&m#~W>qxdRp0wkrUo[D7,XTcA"lI.v%{gJh4G\\-=O@5`_3i<?Z\';FNQuY]szf$!BS/|t:Pn6^Ha';
var xlat2 = '5z]&gqtyfr$(we4{WP)H-Zn,[%\\3dL+Q;>U!pJS72FhOA1CB6v^=I_0/8|jsb9m<.TVac`uY*MK\'X~xDl}REokN:#?G"i@';

// ... exact zb3 step function with xlat2 encryption ...

output:

BITSCTF{d4mn_y0ur_r34lly_w3n7_7h47_d33p}

hell

the full chain

Marlboro.jpg └─ appended ZIP (polyglot) ├─ smoke.png │ ├─ Author metadata → base64 → https://zb3.me/malbolge-tools/ │ └─ LSB steganography → XOR decryption key └─ encrypted.bin └─ XOR decrypt → Malbolge source code └─ run with zb3 interpreter (NOT standard!) → flag

what i learned

  • Malbolge has multiple interpreter implementations and they are NOT all equivalent. the encryption table used after instruction execution (xlat1 vs xlat2) completely changes the program output
  • when a challenge explicitly points to a specific interpreter, USE THAT EXACT ONE
  • "programming language from hell" is not just flavor text lmao
  • the flag says it all: d4mn_y0ur_r34lly_w3n7_7h47_d33p - yeah we really did go that deep

flag

BITSCTF{d4mn_y0ur_r34lly_w3n7_7h47_d33p}

victory


4 layers deep into a forensics challenge at 3am debugging differences between Malbolge interpreter implementations. this is what peak CTF performance looks like fr

smothy out ✌️