Merikanto

一簫一劍平生意,負盡狂名十五年

Steganography with Python

LSB-Steganography is a technique in which we hide messages inside an image by replacing Least Significant Bit (LSB) of an image with the bits of messages to be hidden.

The Least Significant Bit (LSB) is the lowest bit of a binary number. For example, in the binary number 10010010, “0”is the least significant bit (as shown in the image above).

LSB

By modifying only the first most right bit of an image, we can insert our secret message almost unnoticeably, but if our message is too large, we will need to start modifying the second rightmost bit and so on.


Implementation in Python

We only need one module —— the PIL module. We first use the constLenBin() function to transform the strings of characters to binaries, making sure that the length is always 8. Then we use the encodeDataInImage() function to encode the binaries into images.

The decodeImage() is to return the hidden texts after the image is being decoded, and the binaryToString() function converts the binaries to strings of characters. If the code in the function binaryToString() seems confusing, please read about UTF-8 and code points here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from PIL import Image

# 内置函数bin()的替代,返回固定长度的二进制字符串
def constLenBin(int):

# 去掉bin()返回的二进制字符串中的'0b',并在左边补足'0'直到字符串长度为8
binary = "0"*(8-(len(bin(int))-2))+bin(int).replace('0b','')
return binary


# 将字符串编码到图片中
def encodeDataInImage(image, data):

# 获得最低有效位为 0 的图片副本
evenImage = makeImageEven(image)

# 将需要被隐藏的字符串转换成二进制字符串
binary = ''.join(map(constLenBin, bytearray(data, 'utf-8')))

if len(binary) > len(image.getdata()) * 4:
# 如果不可能编码全部数据,跑出异常
raise Exception("Error: Can't encode more than" + len(evenImage.getdata()) * 4 + " bits in this image. ")

# 将binary中的二进制字符串信息编码进像素里
encodedPixels = [(r+int(binary[index*4+0]), g+int(binary[index*4+1]),
b+int(binary[index*4+2]), t+int(binary[index*4+3]))
if index*4 < len(binary) else (r,g,b,t)
for index,(r,g,b,t) in enumerate(list(evenImage.getdata()))]

# 创建新图片以存放编码后的像素
encodedImage = Image.new(evenImage.mode, evenImage.size)
# 添加编码后的数据
encodedImage.putdata(encodedPixels)
return encodedImage


# 取得一个 PIL 图像并且更改所有值为偶数(使最低有效位为0)
def makeImageEven(image):

# 得到一个这样的列表:[(r,g,b,t),(r,g,b,t)...]
pixels = list(image.getdata())

# 更改值为偶数(魔法般的移位)
evenPixels = [(r>>1<<1,g>>1<<1,b>>1<<1,t>>1<<1) for [r,g,b,t] in pixels]

# 创建一个相同大小的图片副本
evenImage = Image.new(image.mode, image.size)

# 把上面的像素放入到图片副本
evenImage.putdata(evenPixels)
return evenImage


# 解码隐藏数据
def decodeImage(image):

# 获得像素列表
pixels = list(image.getdata())

# 提取图片中所有最低有效位中的数据
binary = ''.join([str(int(r>>1<<1!=r))+str(int(g>>1<<1!=g))
+str(int(b>>1<<1!=b))+str(int(t>>1<<1!=t)) for (r,g,b,t) in pixels])

# 找到数据截止处的索引
locationDoubleNull = binary.find('0000000000000000')

endIndex = locationDoubleNull+(8-(locationDoubleNull %8))
if locationDoubleNull%8 != 0 else locationDoubleNull

data = binaryToString(binary[0:endIndex])
return data


# 从二进制字符串转为 UTF-8 字符串
def binaryToString(binary):

index = 0
string = []
rec = lambda x, i: x[2:8] + (rec(x[8:], i-1) if i > 1 else '') if x else ''
fun = lambda x, i: x[i+1:8] + rec(x[8:], i-1)

while index + 1 < len(binary):

# 存放字符所占字节数,一个字节的字符会存为0
chartype = binary[index:].index('0')
length = chartype*8 if chartype else 8
string.append(chr(int(fun(binary[index:index+length],chartype),2)))
index += length
return ''.join(string)


if __name__ == "__main__":
encodeDataInImage(Image.open("coffee.png"), 'Hello World!').save('stega.png')
print(decodeImage(Image.open("stega.png")))


The output will be a line of words – in our case, Hello World! – hidden in the new image stega.png.