Skip to content

Instantly share code, notes, and snippets.

@MidnightLightning
Created July 27, 2012 01:50
Show Gist options
  • Save MidnightLightning/3185747 to your computer and use it in GitHub Desktop.
Save MidnightLightning/3185747 to your computer and use it in GitHub Desktop.
Steganography tools

Steganography: the art of hiding in plain sight. Messages specifically. These are a series of tools that aid in embedding messages in digital files.

While steganography provides obscurity, it does not strictly provide security. Before hiding your message using any of these scripts, it's suggested you encode your message (try PGP/GnuPG encryption or put it in a TrueCrypt container if you're at a loss).

pngload.py

The PNG file format divides the image data into 'chunks', and allows for additional, private chunks to be added by image editors. This script takes the message you wish to embed and saves it as binary data in such an ancillary chunk.

The files being embedded are compressed with bzip2 compression if they're not already a bzip2 archive. This is different from the deflate compression used in the rest of the PNG file format, but gives better compression. Run pngload.py --help for usage information.

colorbits.py

8-bit image files encode color data in red, green, and blue values from 0-255 (0x00 to 0xFF) each. This script uses the lower 4 bits of each color channel to save a separate binary message. Thus every two pixels can store three bytes worth of data (0xRG, 0xBR, 0xGB), and each pixel's color channel get shifted at max ± 16 of 255. The script adds a four-byte header on the front of the binary data, which indicates its length. The output has to be a PNG file, since JPEG compressing can ruin the lower bits, and palette-based images (PNG8 or GIF) don't have enough color options for this purpose.

Future enhancements: If the image is large enough and the message is small enough, the script could use less bits/channel to store the message (4 = 3 bytes/2 pixels, 2 = 3 bytes/4 pixels or 1 = 3 bytes/8 pixels). This would produce less visible color shift in the image overall, but would take up more of the image. In addition, being able to define a rectangle in the image where the message is stored would allow the user to pick a more noisy area of the image to make it less noticeable that the colors have been messed with.

import argparse, Image, math, os.path
from struct import Struct
parser = argparse.ArgumentParser(description="Add a binary message to the low color bits of an image")
parser.add_argument('target', help='The image file to stuff')
parser.add_argument('-d', '--decode', help='Extract the payload from target to outfile', metavar='outfile')
parser.add_argument('-e', '--encode', help='Encode infile into target', metavar='infile')
args = parser.parse_args()
def lowbitsmerge(hi, lo):
return (hi & 0xF0) | (lo & 0xF)
def bitsmerge(hi, lo):
return ((hi & 0xF)<< 4) | (lo & 0xF)
def encodeFile(args):
im = Image.open(args.target)
# Every two RGB triplets can hold three bytes of data
# Assuming big endian, and a means to determine length of the stream
# RG|B R|GB
# RG B|R GB
maxsize = im.size[0]*im.size[1] # number of pixels
maxsize = int(maxsize*3/2) # two bytes per three pixels
maxsize = maxsize-4 # reserve a length header
print args.target, 'can hold', maxsize, 'bytes of data'
infile = open(args.encode, 'r')
insize = os.path.getsize(args.encode)
print args.encode, 'is', insize, 'bytes in size'
if (insize > maxsize):
raise Exception('Input file is too large to fit in target file')
if (im.mode != 'RGB' and im.mode != 'RGBA'): # If not an RGB image, make it one
im.convert('RGB')
bin = Struct('>I').pack(insize)+infile.read() # The binary message to encode
i = 0
curx = 0
cury = 0
pix = im.load()
while True:
data = []
for x in range(0,3): # get three bytes worth of data
if (i+x < len(bin)):
tmp = ord(bin[i+x:i+x+1])
data.append(tmp >> 4) # high bits
data.append(tmp & 0xF) # low bits
else:
data.append(0)
data.append(0)
#print curx, cury, data
# Put these three bytes in the image
if (len(pix[curx, cury]) == 4):
pix[curx, cury] = (lowbitsmerge(pix[curx, cury][0], data[0]), lowbitsmerge(pix[curx, cury][1], data[1]), lowbitsmerge(pix[curx, cury][2], data[2]), pix[curx, cury][3])
else:
pix[curx, cury] = (lowbitsmerge(pix[curx, cury][0], data[0]), lowbitsmerge(pix[curx, cury][1], data[1]), lowbitsmerge(pix[curx, cury][2], data[2]))
curx = curx+1
if (curx >= im.size[0]):
curx = 0
cury = cury+1
if (len(pix[curx, cury]) == 4):
pix[curx, cury] = (lowbitsmerge(pix[curx, cury][0], data[3]), lowbitsmerge(pix[curx, cury][1], data[4]), lowbitsmerge(pix[curx, cury][2], data[5]), pix[curx, cury][3])
else:
pix[curx, cury] = (lowbitsmerge(pix[curx, cury][0], data[3]), lowbitsmerge(pix[curx, cury][1], data[4]), lowbitsmerge(pix[curx, cury][2], data[5]))
curx = curx+1
if (curx >= im.size[0]):
curx = 0
cury = cury+1
i = i+3
if (i >= len(bin)):
break;
im.save('out.png', 'PNG')
print "Created out.png"
def decodeFile(args):
im = Image.open(args.target)
pix = im.load()
data = ''
held = -1
datalen = False
byteStruct = Struct('B')
for y in range(im.size[1]):
for x in range(im.size[0]):
rgb = pix[x,y]
if held < 0:
data = data + chr(bitsmerge(rgb[0], rgb[1]))
held = rgb[2]
else:
data = data + chr(bitsmerge(held, rgb[0]))
data = data + chr(bitsmerge(rgb[1], rgb[2]))
held = -1
if datalen == False and len(data) >= 4:
datalen = Struct('>I').unpack(data[0:4])[0]
print 'Payload is',datalen,'bytes'
data = data[4:] # Slice off header
if datalen != False and len(data) >= datalen:
# Save and exit
outhandle = open(args.decode, 'w')
outhandle.write(data)
outhandle.close()
print 'Payload saved to',args.decode
return
if (args.encode):
encodeFile(args)
else:
decodeFile(args)
import argparse, bz2, os.path, sys
from struct import Struct
from binascii import crc32
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description="Add files to a PNG image as additional chunks in the file structure\n\nAdd files: "+sys.argv[0]+" target [payload [payload ...]]\nExtract files: "+sys.argv[0]+" target -e")
parser.add_argument("-e", "--extract", action='store_true', help="Extract the payload files from the given target")
parser.add_argument("target", help="The PNG file to stuff with payload files")
parser.add_argument("payload", help="The file(s) to add to the target", nargs='*')
args = parser.parse_args()
def rotName(str):
offset = 10
out = ''
for i in range(len(str)):
out = out + chr((ord(str[i:i+1]) + offset*(i+1)) % 255)
return out
def unRotName(str):
offset = 10
out = ''
for i in range(len(str)):
tmp = ord(str[i:i+1]) - offset*(i+1)
while (tmp < 0):
tmp += 255
out = out + chr(tmp)
return out
def verifyPNG(handle):
cur = handle.tell()
handle.seek(0)
if (handle.read(8) != Struct('BBBBBBBB').pack(137, 80, 78, 71, 13, 10, 26, 10)):
handle.seek(cur)
return False
handle.seek(cur)
return True
def loadPNG(args):
# Step through the file, scanning the chunks
target_handle = open(args.target, 'rb')
if not verifyPNG(target_handle): raise argparse.ArgumentTypeError('Target is not a PNG file')
insertion_point = False
target_handle.seek(8)
while True:
try:
tmp = target_handle.read(4)
if len(tmp) == 0:
break;
length = Struct('>I').unpack(tmp)[0]
name = "".join(Struct('cccc').unpack(target_handle.read(4)))
except EOFError:
break
if (name == 'IDAT'):
# Here's the image data; insert right before this
insertion_point = target_handle.tell()-8 # Jump back 8 bytes to beginning of IDAT block
break;
target_handle.seek(length+4, 1)
if (insertion_point == False):
raise TypeError('Target PNG file has no IDAT chunk')
payload_binary = ''
for input in args.payload:
handle = open(input, 'rb')
data = handle.read() # Get whole file
if (data[:2] != 'BZ'):
# Copmress this file
data = bz2.compress(data, 9)
data = rotName(input)+chr(255)+data
name = 'tcDt'
crc = crc32(name + data) & 0xffffffff
print crc
payload_binary = payload_binary + Struct('>I').pack(len(data)) + name + data + Struct('>I').pack(crc) # The full chunk
handle.close()
out_handle = open('out.png', 'w')
target_handle.seek(0)
out_handle.write(target_handle.read(insertion_point))
out_handle.write(payload_binary)
out_handle.write(target_handle.read())
target_handle.close()
out_handle.close()
def extractPNG(args):
# Step through the file, scanning the chunks
target_handle = open(args.target, 'rb')
if not verifyPNG(target_handle): raise argparse.ArgumentTypeError('Target is not a PNG file')
payloads = []
target_handle.seek(8)
while True:
try:
tmp = target_handle.read(4)
if len(tmp) == 0:
break;
length = Struct('>I').unpack(tmp)[0]
name = "".join(Struct('cccc').unpack(target_handle.read(4)))
except EOFError:
break
if (name == 'tcDt'):
# Here's some payload data
payloads.append(target_handle.read(length))
target_handle.seek(4, 1)
else:
target_handle.seek(length+4, 1)
for payload in payloads:
pos = payload.index(chr(255))
name = unRotName(payload[0:pos])
data = payload[pos+1:]
if (name[-2:] != 'bz'):
data = bz2.decompress(data)
if (os.path.exists(name)):
print name+" already exists; skipping"
else:
f = open(name, 'w')
f.write(data)
f.close()
if args.extract == True:
extractPNG(args)
else:
loadPNG(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment