Недавно нас окатило волной громких скандалов, разразившихся словно гром среди ясного неба, одним из самых громких среди них было раскрытие программы PRISM. Итак, давайте представим такую ситуацию: есть два человека, которые хотят обмениваться сообщениями с секретной информацией, есть люди, желающие заполучить эту информацию. Одним из самых разумных решений данной проблемы может стать обмен сообщениями через открытые источники с использованием стеганографии. Например, пользователь1 оставляет на форуме смищную гифку с милым котэ, в недрах которой скрывается какое-то тайное послание. Для человеческого глаза, скорей всего, будет незаметно легкое искажение цветов, которое вполне можно списать на низкое качество картинки, а вот пользователь2, обладающий специальной программой и ключем без проблем получит из картинки информацию. [Intro] Для начала давайте поговорим о теории. Стеганография, которой посвящена данная статья, это, по сути, искуство сокрытия информации от посторонних глаз. Существует множество разных способов спрятать данные на виду, мы же прибегнем к модифицированному LSB(Least Significant Bit, наименьший значащий бит, подробнее о нем можно почитать в Википедии, скажу лишь, что данные при таком подходе прячутся в последние значащие биты, таким образом очень слабо влияя на значения байтов): будем менять 4 наименее значащих бита. Да, это намного более значительные изменения, но как показывает практика, изменение в значении цвета на 15(из 255 возможных) пунктов легко пропустить, не имея оригинала, что бы сравнить. Вот кстати пример: одно из этих изображений оригинал, другое копия с небольшим посланием. [Кодим] Я буду писать на питоне, этот язык понятен почти всем, даже тем, кто его не знает. Код писался и тестировался под python3, как будет работать под второй веткой неизвестно. Для начала можно ознакомиться с описанием формата вот тут: http://www.onicos.com/staff/iz/formats/gif.html Там все очень кратко, но достаточно полезно для понимания кода. Суть нашего кода будет предельно простой: зашифровать сообщение(максимальная длинна 384 байта) и положить его в главную палитру файла. В формате используются битовые поля, но в питоне, как оказалось, нет стандартной из реализации, поэтому пришлось городить свой костыль: PHP: class BitArray: def __init__(self, val=None, length=0): bits=[] if type(val) is int: while(val!=0): if val&1: bits+=[1] else: bits+=[0] val>>=1 elif type(val) is list: bits=list(val) if bits==[]: bits=[0] if length!=0: if length>=len(bits): bits=bits+[0]*(length-len(bits)) else: raise Exception("val too long") self._val=bits #индексатор, возвращающий объект BitArray def __getitem__(self, item): return self.__class__(self._val[item]) #длинна в битах def __len__(self): return len(self._val) #исключительно для отладки, наглядное представление объекта для интерактивного режима def __repr__(self): bits=self._val bits.reverse() return str(self.__class__)+" Bits: "+"".join(str(x) for x in bits)+"; val: "+str(int(self)) #приведение к int def __int__(self): res=0 for i in range(len(self._val)): res|=self._val[i]<<i return res def __add__(self, ba): self._val+=ba._val return self Поскольку писать универсальный массив не хотелось, в классе только необходимый для конкретно этого проекта функционал, ничего лишнего. Теперь давайте опишем такой же минималистичный класс для работы с gif-файлом: PHP: class Gif: #http://www.onicos.com/staff/iz/formats/gif.html def __init__(self, data): self._header=data[:6] self._info=data[6:12] info=BitArray(self._info[4]) if int(info[0])!=1: raise Exception("This file haven't global pallet") tblsize=2**(1+int(info[5:])) pallet=data[12:12+tblsize*3] self._data=data[12+tblsize*3:]#все, с чем не умеем работать храним в неизменном виде self._pallet=pallet def putData(self, data): data=list(data) maxlen=len(self._pallet)*.5 #максимальная длинна данных, которые вместит файл data=list(len(data).to_bytes(2,"little"))+data #добвляем в начало длинну данных if maxlen>=len(data): data+=[0]*int(maxlen-len(data)) #при необходимости дополняем нулями наше смообщение else: raise Exception("Can't insert {0} bytes of data in {1}".format(len(data),maxlen)) rdata=[] for d in data: #разбиваем каждый байт на две части ba=BitArray(d) rdata+=[int(ba[:4]),int(ba[4:])] data=rdata self._pallet=[(int(x) & 0b11110000) for x in list(self._pallet)] #очищаем младшие биты каждого байта в палитре self._pallet=[(self._pallet[i]|data[i]) for i in range(len(data))] #ложим данные в палитру def getData(self): data=[(self._pallet[i]&0xf) for i in range(len(self._pallet))] #удаляем ненужные нам биты data=[int(BitArray(data[i],4)+BitArray(data[i+1],4)) for i in range(0,len(data),2)] #складываем байты из двух частей l=int.from_bytes(data[:2],"little") return data[2:2+l] def construct(self): #собираем файл из частей return self._header+self._info+bytes(self._pallet)+self._data Надеюсь, коментов в коде достаточно для его понимания, но если нет -- задавайте вопросы, на все отвечу. [Шифруемся] Поскольку одной из целей было создать код, не зависящий от сторонних библиотек, а в питоне нет ничего кроме хешфункций, было принято решение просто гаммировать данные хитрым хешем пароля: PHP: import hashlib #генератор гаммы для шифрования #не лучший вариант, но и не худший:) def GammaString(secret): if type(secret) is str: secret=secret.encode() md5=hashlib.md5() sha=hashlib.sha512() md5.update(secret) sha.update(secret) while True: digest=md5.digest()+sha.digest() for d in digest: yield d md5.update(digest) sha.update(digest) #опять таки не лучший вариант def xor(data, secret): if type(data) is str: data=data.encode() gamma=GammaString(secret) return bytes(d^next(gamma) for d in data) Коротко о сути работы GammaString: этот генератор сначала берет md5 и sha512 хэши пароля, после чего складывает их в один digest и посимвольно отдает в функцию гаммирования, когда этот digest заканчивается, берется его md5 и sha512 хеши и складываются в новый digest и все начинается с начала, таким образом мы получаем почти бесконечную довольно качественную гамму. [Интерфейс]Поскольку это просто заготовка, интерфейс будет консольным, за него отвечает следующий код: PHP: import chipher from giffile import Gif import sys usage=""" Using {0}: {0} <put|get> <-s"password"|-sf"pwdfile"> [<-d"data string"|-df"datafile">] <-g"gif container"> [<-o"out file name">] Parameters: put \t put data to container get \t get data from container -s"password" \t set chiping password -sf"pwdfile" \t using as pwd content of file -d"data string" \t set chiping text, must be ignored when get -df"datafile" \t using text of file as chiping text, must be ignored when get <-g"gif container"> \t set gif container <-o"out file name"> \t set output, if ignored put out to console """ def main(argv): gif="" data="" secret="" out="" action="put" for arg in argv: if arg.startswith("-s"): secret=arg[3:-1] elif arg.startswith("-d"): data=arg[3:-1] elif arg.startswith("-sf"): secret=open(arg[3:-1],"rb").read() elif arg.startswith("-df"): data=open(arg[3:-1],"rb").read() elif arg.startswith("-g"): gif=open(arg[3:-1],"rb").read() elif arg.startswith("-o"): out=arg[3:-1] elif arg=="put": action="put" elif arg=="get": action="get" if gif=="" or secret=="": print(usage) return gif=Gif(gif) res=b"" if action=="put": if data=="": print(usage) return data=chipher.xor(data,secret) gif.putData(data) res=gif.construct() elif action=="get": res=chipher.xor(gif.getData(),secret) if out=="": try: print(res.decode()) except: print("Can't decode content as string, use -o parameter") print(res) return else: with open(out,"wb") as f: f.write(res) if __name__=="main": main(sys.argv) Как видите, все весьма просто. Останавливаться на нем, я пожалуй не буду, код кривой, но вроде рабочий. [В заключение] Определить наличие в файле зашифрованных скрытых данных может быть весьма сложно, особенно не зная пароля. Попробуйте угадать в какой из картинок скрыто сообщение(просто) и какое(сложнее). Подсказка: там секретные данные.
в статье забыл указать что у меня код распихан по 3 файлам, если все скинуть в один -- ничего не получится, вот готовый код: http://yadi.sk/d/Cq4jEwKQ7iT5g запускать steg.py
нууу, например если грузить на чужой сервер вполне может произойти перекодирование в другой формат с потерей информации. да и конкретно этот алгоритм не позволяет передавать даже средние по размеру сообщения.
хотите сказать что по факту способ не то что небезопасен, так ещё и не надёжен, может аукнутся потерей информации?