Link to archived challenge
Category |
Difficulty |
Solves |
Author |
forensics |
medium |
6 |
sudoBash418 |
Description#
We’ve managed to capture some data from an adversary - but they seem to have encoded the data in an image?
Players are given an archive containing two files: generate.py
and result.png
.
Analysis#
The player is meant to deduce that the generate.py
script was used to encode some input into the given result.png
. In this case, that input was the flag.
I’ve added a few comments to generate.py
here for clarity:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
#!/usr/bin/env python3
from PIL import Image # requires Pillow: `pip install pillow`
def encode(data: bytes) -> Image.Image:
"""Encode a bytestring into a new Pillow image."""
img = Image.new("RGB", (len(data), 1))
buf = bytearray(len(data) * 3) # contains the raw RGB values
for i in range(len(data)):
# split the byte into 3 parts using separate bitmasks
# each part is assigned to a different channel of the pixel
buf[i*3+1] = data[i] & 0x70 # 0b01110000
buf[i*3] = data[i] & 0x8a # 0b10001010
buf[i*3+2] = data[i] & 0x05 # 0b00000101
img.frombytes(bytes(buf)) # load raw RGB values into Image
return img
if __name__ == "__main__":
import argparse
# `generate.py "clubeh{...}" result.png`
parser = argparse.ArgumentParser()
parser.add_argument("flag")
parser.add_argument("filename")
args = parser.parse_args()
encode(args.flag.encode()).save(args.filename)
|
Each output pixel maps to a single byte of the flag, split across the three RGB channels.
The bitmasks were chosen to ensure that the original bytes can be recovered without any loss of information.
Solution#
Here’s my solve script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#!/usr/bin/env python3
from PIL import Image
def decode(img: Image.Image) -> bytes:
buf = bytearray()
for pixel in img.getdata():
# works because 0x70 | 0x8a | 0x05 == 0xff
buf.append(pixel[0] | pixel[1] | pixel[2])
return bytes(buf)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("filepath", help="Path to the image containing encoded data.")
args = parser.parse_args()
with Image.open(args.filepath) as img:
flag = decode(img)
print(f"Recovered flag: {flag!r}")
|
Run solve.py result.png
to obtain the flag.