隐写术:在其他事物中隐藏事物的艺术与科学——第四部分
作者:Dakota Nelson
第四部分:抗干扰隐写术
这是四部分图像隐写术系列的最后篇章。您可以从第一部分、第二部分或第三部分开始,或直接阅读下文:
我们已经涵盖了在图像中隐藏数据的基础知识、数学原理,以及使用汉明码的错误检测和校正。文中出现过维恩图、小狗图案和秘密信息,整个过程充满趣味。
我们如何收尾?当然是用代码!我们将整合所有内容,进行完善,编写文档,并宣布我们的隐写项目完成。在第三部分结束时,我留下了关于纠错码的数学内容,并承诺将这些数学付诸实践——这正是我们要做的:对经过汉明编码器处理的数据进行隐写隐藏,以便校正由压缩(或其他原因)引起的错误。
本文所有代码可在Github找到——下面我会逐步讲解,但您随时可以自行查看。
让我们开始吧。
在阅读第二部分时,我们介绍了用于将数据隐写入图像的Python代码。最终的Python隐写软件与此前相同(仅稍作清理),因此我将跳过完整解释。作为回顾——图像中每个像素的最低有效位基本上是随机的,因此我们可以将它们更改为任何内容,图像在人眼看来保持不变。这段代码只是翻转正确的位,以输出包含我们信息的图像。
但是,如果图像随后被压缩或以某种方式损坏,会发生什么?在上一篇文章中,我们深入探讨了汉明码背后的数学原理,这是一种纠错码,允许我们修复消息中的错误。以下是将该数学原理付诸实践的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
def encode(msg):
""" 传入比特列表(整数,1或0),返回汉明(8,4)编码的比特列表 """
while len(msg) % 4 != 0:
# 填充消息至合适长度
msg.append(0)
msg = np.reshape(np.array(msg), (-1, 4))
# 使用转换矩阵创建奇偶校验位
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)
# 对矩阵乘法取模2
return np.mod(result, 2)
|
这段代码相当密集,让我们逐步分解。首先,我们在消息末尾添加零,直到其长度合适,以便矩阵乘法正确运行(消息中的比特数必须是4的倍数)。
一旦消息长度正确,我们创建一个n×4的消息比特数组(其中n是适应整个消息所需的任何值)。然后将该数组与转换矩阵相乘。
等等。
“矩阵,”您说,“它们从哪来的?我们没谈过任何臭矩阵。”
细心的读者,您抓住了我。我没有提到矩阵,这里也基本忽略它们,只说明一点:还记得我们必须计算维恩图中每个圆圈中的比特数,然后根据数据位为每个组创建奇偶校验位吗?这就是这个矩阵的作用。我们将消息乘以这个汉明码矩阵,然后对结果取模2(即取结果数组中每个条目除以2的余数),我们就得到了一个汉明编码的消息。
(如果您仍然不喜欢我不解释矩阵乘法,这里是数学版本:矩阵的左半部分是单位矩阵(保留原始消息),而右半部分的列完全线性独立,使得每个生成的奇偶校验位基于3个数据位,且奇偶校验无冗余。给定4位数据,该矩阵输出8位“数据加奇偶校验”,纠错码人员称之为码字。)
现在我们有了一个能容忍一些错误的汉明编码消息,我们像往常一样将其插入图像中, exactly how we’ve discussed in previous posts。然后在另一端提取—— again, as usual。实际隐写术的机制现在应该很熟悉了。(如果不熟悉,请回去阅读第二部分,了解图像隐写技术的讨论。)
一旦我们从隐写算法放入的图像中检索到消息,如何修复错误?这毕竟是汉明纠错码的全部意义。
答案居然是更多矩阵。下一段代码作为汉明码解码器,分为三部分。我们将分别分解每一部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
def syndrome(msg):
""" 传入汉明(8,4)编码的比特列表(整数,1或0),返回该列表的错误综合征 """
msg = np.reshape(np.array(msg), (-1, 8)).T
# 综合征生成矩阵
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)
# 对矩阵乘法取模2
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
|
def correct(msg, syndrome):
""" 传入综合征和消息(按收到状态,假定有某些错误),将使用综合征尽可能校正消息 """
# 任何错误位的综合征将与综合征生成矩阵中对应错误位的列匹配;
# 综合征(1,1,0,1)表示第三位已被翻转,因为它对应矩阵的第三列
# 综合征生成矩阵(从上面复制粘贴)
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]):
# 全零——无错误!
continue
# 否则我们有一个错误综合征
for col in range(transition.shape[1]):
# 不是非常Python式的迭代,但我们需要索引
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)
# 转换为常规Python列表,这很麻烦
return res.T.reshape((1,-1)).tolist()[0]
|
现在我们已经校正了消息,我们可以解码它!使用上面的解码矩阵,我们进行同样的矩阵乘法以获取最终消息。
那么,这一切的结果是什么?继续我们在第三部分中的图像隐写示例,这是最终结果:
这是我们的原始消息,右侧是文本,左侧是对应的十六进制值:
1
2
3
4
|
[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
|
[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
|
[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找到更多他的文章。