Mirage

Contents

Mirage#

Helpers#

import numpy as np
import os
import requests
from dotenv import load_dotenv
from PIL import Image
from string import ascii_lowercase as ASCII

load_dotenv()
API_KEY = os.environ["API_KEY"]
URL_QUERY = "https://%s.crucible.dreadnode.io/score"
URL_FLAG = "https://crucible.dreadnode.io/api/challenges/%s/submit-flag"
CHALLENGE = "mirage"

def query(prompt):
    response = requests.post(
        URL_QUERY % CHALLENGE,
        headers={ "X-API-Key": API_KEY },
        json={ "data": prompt }
    )
    return response.json()

def submit(flag):
    response = requests.post(
        URL_FLAG % CHALLENGE,
        headers={ "X-API-Key": API_KEY },
        json={ "challenge": CHALLENGE, "flag": flag }
    )
    return False if response.status_code != 200 else response.json().get("correct")

Solution#

For this challenge we’re given a fairly innocuous image:

mirage

By running ExifTool against it we can find a hint that indicates we need to use a Caesar Cipher with a rotation of 3 (or -3).

!exiftool ./data/mirage.png
ExifTool Version Number         : 11.88
File Name                       : mirage.png
Directory                       : ./data
File Size                       : 1494 kB
File Modification Date/Time     : 2024:11:05 07:21:07+00:00
File Access Date/Time           : 2024:11:25 18:19:11+00:00
File Inode Change Date/Time     : 2024:11:25 18:17:42+00:00
File Permissions                : rw-rw-r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 1024
Image Height                    : 1024
Bit Depth                       : 8
Color Type                      : RGB
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Author                          : Captain Codebeard
Hint                            : Thar be secrets in the pixels that must be decoded!
Key                             : Caesar Cipher 3
Image Size                      : 1024x1024
Megapixels                      : 1.0

We can extract of the least significant bit of each of the three colour channels for the first chunk of pixels and convert them to a binary string. We can then convert the bytes in that binary string into ASCII characters.

# Extract LSB of the first 200 pixel values and convert them to a binary string
str_binary = "".join([
    str(value & 1)
    for value in np.array(Image.open("./data/mirage.png")).flatten()[:200]
])

# Convert the binary string to ASCII characters
str_ascii = "".join([
    chr(int(str_binary[i:i+8], 2))
    for i in range(0, len(str_binary), 8)
])

print(str_ascii)
{Q0w_Zk@w_Lw_Vh3pv}¶Ûm¶Û¶

The above string includes angle brackets which probably enclose the flag. We can decode the characters using a Caesar Cipher with a rotation of -3.

# Extract the flag using a Caesar cipher (accounting for upper/lowercase)
flag = ""
for char_in in str_ascii:
    char_lower = char_in.lower()
    if char_lower in ASCII:
        char_out = ASCII[(ASCII.index(char_lower) - 3) % 26]
        flag += char_out if char_in.islower() else char_out.upper()
    else:
        flag += char_in
    if char_in == "}":
        break

print(flag)
print("Flag accepted:", submit(query(flag)["flag"]))
{N0t_Wh@t_It_Se3ms}
Flag accepted: True