隐写术:在其他事物中隐藏事物的艺术与科学 – 第四部分
Dakota Nelson //
第四部分:弹性隐写术
这是四部分系列关于图像隐写术的最后一篇。你可以从第一部分、第二部分或第三部分开始,或者直接阅读下文:
我们已经介绍了在图像中隐藏数据的基础知识,涵盖了数学原理,以及使用汉明码进行错误检测和纠正。有过维恩图、小狗和秘密消息,一切都非常令人兴奋。
我们如何结束?
当然是代码!我们将把所有内容整合在一起,打上一个漂亮的蝴蝶结,编写一些文档,并宣布我们的隐写项目完成。在第三部分之后,我留下了一堆关于纠错码的数学知识,并承诺将这些数学付诸实践,这正是我们要做的——隐写地隐藏数据,这些数据我们已经通过汉明编码器运行,以便我们可以纠正由压缩(或其他任何原因)引起的错误。
本文的所有代码都可以在Github上找到——我将在下面带你逐步了解,但你也可以随时自己去看看。
让我们开始吧。
当你阅读第二部分时,我们介绍了用于隐写插入数据到图像中的Python代码。最终的隐写软件,在Python中,与之前相同(除了稍微清理了一下),所以我将跳过对其的完整解释。作为复习——图像中每个像素的最低有效位基本上是随机的,所以我们可以将它们更改为我们想要的任何内容,图像在人类观察者看来是一样的。这段代码只是翻转正确的位,以输出包含我们消息的图像。
但是,如果图像随后被压缩或以某种方式损坏,会发生什么?在上一篇文章中,我们逐步了解了汉明码背后的数学原理,这是一种纠错码,允许我们修复消息中的错误。以下是将该数学付诸实践的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
def encode(msg):
""" passed a list of bits (integers, 1 or 0),
returns a hamming(8,4)-coded list of bits """
while len(msg) % 4 != 0:
# pad the message to length
msg.append(0)
msg = np.reshape(np.array(msg), (-1, 4))
# create parity bits using transition matrix
transition = np.mat('1,0,0,0,0,1,1,1;\
0,1,0,0,1,0,1,1;\
0,0,1,0,1,1,0,1;\
0,0,0,1,1,1,1,0')
result = np.dot(msg, transition)
# mod 2 the matrix multiplication
return np.mod(result, 2)
|
这是一段相当密集的代码,所以让我们一步一步地来。首先,我们在消息末尾添加零,直到其长度合适,以便矩阵乘法正确工作(消息中的位数必须是4的倍数)。
一旦消息长度正确,我们创建一个消息位的nx4数组(其中n是适应整个消息所需的任何值)。然后将此数组乘以一个转移矩阵。
等一下。
“矩阵,”你说,“它们从哪里来?我们没谈过任何臭矩阵。”
嗯,敏锐的读者,你抓住了我。我没有提到矩阵,我在这里基本上会忽略它们,除了说这一点:还记得我们必须计算维恩图中每个圆中的位数,然后根据数据位为每个组创建一个奇偶校验位吗?这就是这个矩阵所做的。我们将消息乘以这个汉明码矩阵,然后将结果模2(即,取结果数组中每个条目除以2的余数),我们就得到了一个汉明编码的消息。
(如果你仍然不喜欢我不解释矩阵乘法,这里是数学版本:矩阵的左半部分是单位矩阵(保留我们的原始消息),而右半部分的列彼此完全线性独立,使得每个生成的奇偶校验位基于3个数据位,没有奇偶校验冗余。给定4位数据,此矩阵输出8位“数据加奇偶校验”,纠错码人员称之为码字。)
现在我们有了一个可以容忍一些错误的汉明编码消息,我们将其插入到图像中,就像往常一样,正如我们在之前的文章中讨论的那样。然后我们在另一端提取它——再次,像往常一样。实际隐写术的机制现在应该对你来说很熟悉了。(如果不是,回去阅读第二部分,了解图像隐写技术的讨论。)
一旦我们从隐写算法放入的图像中检索到我们的消息,我们如何修复错误?毕竟,这就是汉明纠错码的全部意义。
事实证明,答案是更多的矩阵。下一段代码作为汉明码解码器分为三部分。我们将分别分解每一部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
def syndrome(msg):
""" passed a list of hamming(8,4)-encoded bits (integers, 1 or 0),
returns an error syndrome for that list """
msg = np.reshape(np.array(msg), (-1, 8)).T
# syndrome generation matrix
transition = np.mat('0,1,1,1,1,0,0,0;\
1,0,1,1,0,1,0,0;\
1,1,0,1,0,0,1,0;\
1,1,1,0,0,0,0,1')
result = np.dot(transition, msg)
# mod 2 the matrix multiplication
return np.mod(result, 2)
|
第一个任务是计算这个汉明码的综合征。这个错误综合征,正如其名,基本上是记录消息中出了什么问题的记录——很像疾病的综合征,它可以告诉我们消息中出了什么问题,这样我们就可以知道如何应用我们的错误纠正来修复它。这里的机制与汉明编码相同——将数组调整为正确的形状,与适当的汉明码综合征矩阵相乘,然后将所有内容模2。
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
|
def correct(msg, syndrome):
""" passed a syndrome and a message (as received, presumably with some
errors), will use the syndrome to correct the message as best possible
"""
# the syndrome for any incorrect bit will match the column of the syndrome
# generation matrix that corresponds to the incorrect bit; a syndrome of
# (1, 1, 0, 1) would indicate that the third bit has been flipped, since it
# corresponds to the third column of the matrix
# syndrome generation matrix (copy/pasted from above)
transition = np.mat('0,1,1,1,1,0,0,0;\
1,0,1,1,0,1,0,0;\
1,1,0,1,0,0,1,0;\
1,1,1,0,0,0,0,1')
for synd in range(syndrome.shape[1]):
if not np.any(syndrome[:,synd]):
# all zeros - no error!
continue
# otherwise we have an error syndrome
for col in range(transition.shape[1]):
# not very pythonic iteration, but we need the index
if np.array_equal(transition[:,col], syndrome[:,synd]):
current_val = msg[synd,col]
new_val = (current_val + 1) % 2
msg.itemset((synd,col), new_val)
return msg
|
一旦我们有了一个综合征,我们就知道如何纠正消息。上面的代码通过将每个错误综合征与上面的综合征生成矩阵进行比较,来匹配每个综合征与需要翻转的位。如果我们找到匹配项,我们翻转相应的位——如果不匹配,我们使用continue
关键字跳到循环的下一次迭代。
1
2
3
4
5
6
7
8
9
10
|
def decode(msg):
r = np.mat('1,0,0,0,0,0,0,0;\
0,1,0,0,0,0,0,0;\
0,0,1,0,0,0,0,0;\
0,0,0,1,0,0,0,0')
res = np.dot(r, msg.T)
# convert to a regular python list, which is a pain
return res.T.reshape((1,-1)).tolist()[0]
|
现在我们已经纠正了我们的消息,我们可以解码它!使用上面的解码矩阵,我们进行相同的老矩阵乘法,以得到我们的最终消息。
那么,所有这些的结果是什么?继续我们第三部分的图像隐写示例,这是最终结果:
这是我们的原始消息,右边是文本,左边是对应的十六进制值:
1
2
3
4
5
|
[dnelson@blueharvest hamming-stego]$ xxd -s 7 -l 45 output.txt
00000007: 7765 2772 6520 676f 696e 6720 746f 2068 we're going to h
00000017: 6964 6520 7468 6973 206d 6573 7361 6765 ide this message
00000027: 2069 6e20 616e 2069 6d61 6765 21 in an image!
|
这是在插入一些错误并稍微填充之后的数据:
1
2
3
4
5
6
7
8
9
|
[dnelson@blueharvest hamming-stego]$ xxd -s 70 -l 100 output.txt
00000046: 7765 2772 6520 656b 696e 6720 746f 2068 we're eking to h
00000056: 6964 6520 7468 6973 206d 6573 7361 6765 ide this message
00000066: 2069 6e20 616e 2069 6d61 6765 0100 0400 in an image....
00000076: 0000 0000 0000 0000 0002 0000 0000 0000 ................
00000086: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000096: 0000 0800 0000 0000 0000 1000 0000 0000 ................
000000a6: 0000 0000 ....
|
这是我们最终的、纠正后的输出:
1
2
3
4
5
6
7
8
9
|
[dnelson@blueharvest hamming-stego]$ xxd -s 54765 -l 100 output.txt
0000d5ed: 7765 2772 6520 676f 696e 6720 746f 2068 we're going to h
0000d5fd: 6964 6520 7468 6973 206d 6573 7361 6765 ide this message
0000d60d: 2069 6e20 616e 2069 6d61 6765 2100 0000 in an image!...
0000d61d: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000d62d: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000d63d: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000d64d: 0000 0000 ....
|
就是这样——我们使用汉明码纠正了一条消息!而之前我们必须将每个字符重复9次,这个汉明码将4字节的数据放入每个8字节的码字中——数据与总数的比例为1:2,而不是之前的1:9。一个相当可观的改进!
但请注意,汉明码的纠错能力只有这么好。上面“损坏”的消息对人类来说仍然相当可读——比这更多,错误就会开始渗透到最后。也许那没问题……但也许不是。一如既往,存在权衡。
想看看完整的图像隐写示例吗?访问https://github.com/DakotaNelson/hamming-stego查看!该链接直接指向Github,在那里你会找到一个免费的Python隐写工具,可在Linux、Mac、Windows或任何可以运行Python的地方使用。
如果你想要这整个四部分系列的单个PDF,你可以前往Striker Security Blog下载一个。
*Dakota运行Striker Security——你可以在https://strikersecurity.com/blog找到更多他的写作。