Недавно нас окатило волной громких скандалов, разразившихся словно гром среди ясного неба, одним из самых громких среди них было раскрытие программы PRISM.
Итак, давайте представим такую ситуацию: есть два человека, которые хотят обмениваться сообщениями с секретной информацией, есть люди, желающие заполучить эту информацию. Одним из самых разумных решений данной проблемы может стать обмен сообщениями через открытые источники с использованием стеганографии.
Например, пользователь1 оставляет на форуме смищную гифку с милым котэ, в недрах которой скрывается какое-то тайное послание. Для человеческого глаза, скорей всего, будет незаметно легкое искажение цветов, которое вполне можно списать на низкое качество картинки, а вот пользователь2, обладающий специальной программой и ключем без проблем получит из картинки информацию.
[Intro]
Для начала давайте поговорим о теории.
Стеганография, которой посвящена данная статья, это, по сути, искуство сокрытия информации от посторонних глаз.
Существует множество разных способов спрятать данные на виду, мы же прибегнем к модифицированному LSB(Least Significant Bit, наименьший значащий бит, подробнее о нем можно почитать в Википедии, скажу лишь, что данные при таком подходе прячутся в последние значащие биты, таким образом очень слабо влияя на значения байтов): будем менять 4 наименее значащих бита. Да, это намного более значительные изменения, но как показывает практика, изменение в значении цвета на 15(из 255 возможных) пунктов легко пропустить, не имея оригинала, что бы сравнить.
Вот кстати пример:
--
одно из этих изображений оригинал, другое копия с небольшим посланием.
[Кодим]
Я буду писать на питоне, этот язык понятен почти всем, даже тем, кто его не знает. Код писался и тестировался под python3, как будет работать под второй веткой неизвестно.
Для начала можно ознакомиться с описанием формата вот тут Там все очень кратко, но достаточно полезно для понимания кода. Суть нашего кода будет предельно простой: зашифровать сообщение(максимальная длинна 384 байта) и положить его в главную палитру файла.
В формате используются битовые поля, но в питоне, как оказалось, нет стандартной из реализации, поэтому пришлось городить свой костыль:
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-файлом:
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
Надеюсь, коментов в коде достаточно для его понимания, но если нет -- задавайте вопросы, на все отвечу.
[Шифруемся]
Поскольку одной из целей было создать код, не зависящий от сторонних библиотек, а в питоне нет ничего кроме хешфункций, было принято решение просто гаммировать данные хитрым хешем пароля:
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 и все начинается с начала, таким образом мы получаем почти бесконечную довольно качественную гамму.
[Интерфейс]
Поскольку это просто заготовка, интерфейс будет консольным, за него отвечает следующий код:
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)
Как видите, все весьма просто. Останавливаться на нем, я пожалуй не буду, код кривой, но вроде рабочий.
[В заключение]
Определить наличие в файле зашифрованных скрытых данных может быть весьма сложно, особенно не зная пароля. Попробуйте угадать в какой из картинок скрыто сообщение(просто) и какое(сложнее). Подсказка: там секретные данные.