Last active
January 5, 2025 18:42
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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).