Skip to content

Instantly share code, notes, and snippets.

@DDoop
Last active January 5, 2025 18:42
Show Gist options
  • Save DDoop/ccbd02b695a04c73bb75c982e976f9f6 to your computer and use it in GitHub Desktop.
Save DDoop/ccbd02b695a04c73bb75c982e976f9f6 to your computer and use it in GitHub Desktop.
Hides a signature at a random offset inside the RGB data of an image file. Relies on CRC32. Written & tested on Linux
import subprocess
from PIL import Image
from random import randrange
class ByteStreamReader:
chunk_length = 32
hash_length = 8
def __init__(self, bytearr):
self.bytearr = bytearr
def hash_value(self, v):
command = f"crc32 <(echo {v})"
res = subprocess.run(
["bash", "-c", command],
capture_output=True,
)
crc_hash = res.stdout.strip()
return str(crc_hash)[2:-1]
def split_chunk(self, offset):
target_chunk = self.bytearr[offset:offset + self.chunk_length]
arr1 = target_chunk[:self.hash_length]
arr2 = target_chunk[self.hash_length:self.chunk_length]
return arr1, arr2
class ImageDeserializer:
def __init__(self, image_path):
self.image_path = image_path
def deserialize(self):
with Image.open(self.image_path) as img:
img = img.convert('RGB')
width, height = img.size
pixel_data = bytearray()
for y in range(height):
for x in range(width):
r, g, b = img.getpixel((x, y))
pixel_data.extend([r, g, b])
return pixel_data.hex()
class ImageSerializer:
def __init__(self, output_path):
self.output_path = output_path
def serialize(self, raw_pixel_data, width, height):
pixel_data = bytearray.fromhex(raw_pixel_data)
img = Image.new('RGB', (width, height))
img.putdata([(pixel_data[i], pixel_data[i + 1], pixel_data[i + 2])
for i in range(0, len(pixel_data), 3)])
img.save(self.output_path, format='PNG')
class ImageTamper(ByteStreamReader):
def sign_image_data(self):
alen = len(self.bytearr)
chunks = alen / self.chunk_length
random_offset = randrange(int(0.15 * chunks), int(chunks - 0.15 * chunks))
arr1, arr2 = self.split_chunk(random_offset)
h = self.hash_value(arr2)
print('===' * 10)
print("offset: ", random_offset)
print("byte: ", random_offset * self.chunk_length)
print(arr1, '|', arr2)
print("hash: ", h)
self.new_pixel_data = self.bytearr
self.new_pixel_data = self.bytearr[:self.chunk_length * random_offset]
self.new_pixel_data += h + arr2
self.new_pixel_data += self.bytearr[self.chunk_length * random_offset + self.chunk_length:]
class ImageReader(ByteStreamReader):
def find_magic_offset(self):
res = []
alen = len(self.bytearr)
chunks = int(alen / self.chunk_length)
for i in range(chunks):
offset = i * self.chunk_length
arr1, arr2 = self.split_chunk(offset)
h = self.hash_value(arr2)
if arr1 == h:
res.append(offset)
if len(res) > 1:
print("found more than 1 match")
print(res)
elif len(res) < 1:
print("found no match")
elif len(res) == 1:
print("found match")
print(res)
if __name__ == "__main__":
filename = r'img/test.png'
width, height = [0, 0]
with Image.open(filename) as img:
width, height = img.size
deserializer = ImageDeserializer(filename)
pixel_data = deserializer.deserialize()
it = ImageTamper(pixel_data)
it.sign_image_data()
serializer = ImageSerializer('output_image.png')
serializer.serialize(it.new_pixel_data, width, height)
deserializer = ImageDeserializer('output_image.png')
pixel_data = deserializer.deserialize()
im = ImageReader(pixel_data)
im.find_magic_offset()
@DDoop
Copy link
Author

DDoop commented Oct 25, 2024

This code is not malicious but was inspired by malicious software design. Recent reporting on Ghostpulse provoked me to write this proof of concept. Initially it was a bash script but I heard that can have trouble working with empty bytes and I grew up reading/writing Python. This script only picks a random 16 byte chunk and sets the first 4 bytes to equal the CRC32 hash of the 2nd half (last 12 bytes).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment