用PHP编写图像的隐写术
隐秘术是将秘密消息放在另一条消息中的一种做法,只有通过以不同的方式查看原始消息,才能看到隐藏的消息。这可能像平时写一页文字,然后使用每个句子的首字母在文字中隐藏一条消息一样平凡。只有将每个句子的第一个字母收集在一起,才能看到和阅读隐藏的消息。
现代隐写术的定义是将消息(或文件)隐藏在文件内。例如,您可以查看图像并查看图像,但是如果隐藏了一条消息,并且您知道要查找的位置,则还可以将其提取。
那里有一些教程,通过在其他文件的标头之前插入文件来查看如何将文件隐藏在其他文件中。例如,您可以通过在图像标题之前插入文本来将文本文档注入到图像文件中。如果使用文本编辑器打开文件,则将看到文本,如果使用图片查看器打开文件,则将看到图像。虽然这种标题方法在技术上是隐秘术,因为您只知道要在此处找到文本文件,所以这不是我在这里要讨论的内容。
图像隐写术是一种稍微改变图像颜色的做法,作为在图像中隐藏数据的一种机制。通常,这是通过更改像素颜色的最低有效位来非常轻微地更改颜色来完成的。图像中的像素存储为由红色,绿色和蓝色通道组成的24位二进制数。如果是Alpha(或添加了透明度值),则有时可能是32位。
让我们来看一个例子。当我们提取随机像素的颜色时,我们得到的数字为9,613,560。该编号包含有关红色,绿色和蓝色通道的信息。如果我们将其提取为24位二进制数,则可以看到不同的通道。
RED GREEN BLUE 10010010 10110000 11111000 146 176 248
该数字中的最低有效位是每个颜色通道的最右边的位(在这种情况下为0)。如果将任何通道的最低有效位更改为1,则颜色变化不会很大。相反,如果我们更改了最重要的位,即最左侧的位,则颜色将发生很大变化。为了演示,让我们更改蓝色通道的最低有效数字并查看结果。
Original Colour RED GREEN BLUE 146 176 248 10010010 10110000 11111000 New Colour RED GREEN BLUE 146 176 249 10010010 10110000 11111001
原始颜色为浅蓝色,十六进制值为#92B1F8,新颜色仍为浅蓝色,十六进制值为#92B1F9。因此,尽管我们更改了颜色,但实际上并没有太大改变。实际上,要分辨出这种差异需要非常好的眼光。也可以通过这种方式更改红色和绿色通道的颜色以隐藏数据。一些隐写术还着眼于更改颜色通道的最后两个有效位,虽然这改变了值而不是改变一位,但仍然使更改难以检测。通过为每个像素存储两位,我们还可以为整个图像添加更多数据。
到现在为止,我们只对单个位进行编码,所以如果我们想对图像进行更多编码,会发生什么。如果我们采用二进制数101,则需要将其编码为图像中的三个不同像素,因为更改所有三个颜色通道可能会使图像变化到足以引起注意的程度。当我们遍历图像中的每个像素时,如果位不匹配值,我们将更改单个通道中的最低有效位。这样,我们可以非常快速地对数据进行编码。
10010010 10110000 11111000 10010010 10110000 11111001 10000010 10000010 11100010 -> 10000010 10000010 11100010 10010010 10110110 11111000 10010010 10110110 11111001
为了使二进制数据能够编码,我们首先需要创建数据。假设我们要在图像中编码字母“a”。第一步是将该字母转换为其等效的ASCII二进制数字。在Wikipedia上查找ASCII表可知,“a”的二进制数如下。
a=01100001
为了使用PHP将字符串转换为等效的ASCII码,我们需要通过ord()函数运行每个字符,该函数将为我们提供表示该字符的ASCII码的整数。然后,我们需要使用该整数将其转换为等效的二进制数,以便可以将其编码为图像。要将PHP中的十进制数转换为二进制数,我们使用decbin()函数,该函数将返回表示二进制数的字符串。该函数的问题在于它将二进制数返回到最高有效位。换句话说,我们将需要表示“a”的8位数字转换为7位数字,因为最初的0将被剥离。由于此函数的返回值为字符串,因此我们将其传递给该str_pad()函数,以强制显示所有8位数字。
这是将获取一些文本并将其转换为等效二进制代码的代码。
$message = 'text'; $binaryMessage = ''; for ($i = 0; $i < mb_strlen($message); ++$i) { $character = ord($message[$i]); $binaryMessage .= str_pad(decbin($character), 8, '0', STR_PAD_LEFT); } echo $binaryMessage;
这将产生以下输出。
01110100011001010111001101110100
有了这个数字之后,我们需要将其注入到我们的图像中。为此,我将看下几个月前我walking狗时拍摄的以下图像。这是一张250x188像素的jpeg图像,因此对于隐写术来说非常小,但是由于这只是一个演示,因此我不希望在此屏幕上显示大量图像。
下面是将消息注入此图像的代码。在这里,我仅通过使用每个像素中蓝色通道的最低有效位来使用隐写术的简单版本。我从图像的左上角开始,并一次处理每一行像素,直到消息被编码为止。下面的方法将接收一条消息,将其转换为二进制字符串,然后将其注入从图像左上方开始运行的像素的蓝色中。我们注入到图像中的字符串包含ASCII字符“文本结尾”,以二进制形式表示为00000011。提取消息时,我们将使用它来检测消息的结尾。
function steganize($file, $message) { //将消息编码为二进制字符串。 $binaryMessage = ''; for ($i = 0; $i < mb_strlen($message); ++$i) { $character = ord($message[$i]); $binaryMessage .= str_pad(decbin($character), 8, '0', STR_PAD_LEFT); } //在字符串中插入“文本结尾”字符。 $binaryMessage .= '00000011'; //将图像加载到内存中。 $img = imagecreatefromjpeg($file); //获取图像尺寸。 $width = imagesx($img); $height = imagesy($img); $messagePosition = 0; for ($y = 0; $y < $height; $y++) { for ($x = 0; $x < $width; $x++) { if (!isset($binaryMessage[$messagePosition])) { //无需在消息末尾继续进行处理。 break 2; } //提取颜色。 $rgb = imagecolorat($img, $x, $y); $colors = imagecolorsforindex($img, $rgb); $red = $colors['red']; $green = $colors['green']; $blue = $colors['blue']; $alpha = $colors['alpha']; //将蓝色转换为二进制。 $binaryBlue = str_pad(decbin($blue), 8, '0', STR_PAD_LEFT); //用我们的信息替换蓝色的最后一点。 $binaryBlue[strlen($binaryBlue) - 1] = $binaryMessage[$messagePosition]; $newBlue = bindec($binaryBlue); //将该新颜色注入到图像中。 $newColor = imagecolorallocatealpha($img, $red, $green, $newBlue, $alpha); imagesetpixel($img, $x, $y, $newColor); //前进消息位置。 $messagePosition++; } } //将图像保存到文件。 $newImage = 'secret.png'; imagepng($img, $newImage, 9); //销毁图像处理程序。 imagedestroy($img); }
您可能想知道为什么我没有对此进行按位运算,因为我手头有二进制数。好吧,因为PHP将所有数字存储为基本上相同的数据类型,所以如果将其转换为PHP二进制数字,则很可能会失去精度。因此,我们首先将数字转换为字符串,然后执行更改,然后再次将其转换回整数值。这也很清楚发生了什么,并允许我们扩展此代码以例如更改最后两个有效位或更改更多颜色通道。
要运行此代码,我们提供图像的文件名和消息。
$file = 'mushroom.jpg'; $message = 'Message hidden in plain sight.'; steganize($file, $message);
这将采用我们给出的信息,并将其一次注入一个图像到图像中。例如,消息的第一个字符是M,它转换为二进制数字01001101。它以下面的方式注入到图像中。
此功能的最终输出如下图。与上图相比,这看上去几乎没有变化,但是我向您保证,它是包含该消息的另一张图像。实际上,它看起来是如此相似,以至于需要对两个图像进行直接比较才能发现差异。
该图像具有与原始图像相同的尺寸。唯一的区别是图像现在是png,并且其颜色略有变化。使用上述技术,我们可以存储的文本量有一个上限。让我们做一些数学运算来算出我们可以在该图像中存储多少个字符。
我们的原始图像是250x188=47,000像素。每个字符47,000个像素/8位(假设我们使用ASCII)意味着我们可以存储5875个字符。因此,我们的消息最多可以包含5874个字符,因为我们需要为空格保留一个空格。
要逆转该过程,我们需要获取机密文件并从蓝色像素中提取最低有效位。我们使用这些位并建立我们以二进制字符串形式注入到图像中的原始消息。查看消息中的第一个字符,我们将其拼凑成这样。
01110100 0 01110001 1 01100010 0 01001000 0 = 01001101 = "M" 01010001 1 01100101 1 01101010 0 01101101 1
找到文本字符的结尾后,我们将停止该过程并将二进制消息转换为原始文本。这是一个将提取我们创建的png文件并从中提取隐藏消息的函数。我们仅以与编码器相同的顺序遍历像素,然后从蓝色通道中提取最低有效像素。
function desteganize($file) { //将文件读入内存。 $img = imagecreatefrompng($file); //阅读消息的尺寸。 $width = imagesx($img); $height = imagesy($img); //设置信息。 $binaryMessage = ''; //初始化消息缓冲区。 $binaryMessageCharacterParts = []; for ($y = 0; $y < $height; $y++) { for ($x = 0; $x < $width; $x++) { //提取颜色。 $rgb = imagecolorat($img, $x, $y); $colors = imagecolorsforindex($img, $rgb); $blue = $colors['blue']; //将蓝色转换为二进制。 $binaryBlue = decbin($blue); //将最低有效位提取到out消息缓冲区中。 $binaryMessageCharacterParts[] = $binaryBlue[strlen($binaryBlue) - 1]; if (count($binaryMessageCharacterParts) == 8) { //如果消息缓冲区有8个部分,则可以更新消息字符串。 $binaryCharacter = implode('', $binaryMessageCharacterParts); $binaryMessageCharacterParts = []; if ($binaryCharacter == '00000011') { //如果找到“文本结尾”字符,则停止查找消息。 break 2; } else { //将我们找到的字符添加到消息中。 $binaryMessage .= $binaryCharacter; } } } } //将我们发现的二进制消息转换为文本。 $message = ''; for ($i = 0; $i < strlen($binaryMessage); $i += 8) { $character = mb_substr($binaryMessage, $i, 8); $message .= chr(bindec($character)); } return $message; }
要运行上面的代码,我们只需要输入我们的秘密文件并打印出结果消息即可。
$secretfile = 'secret.png'; $message = desteganize($secretfile); echo $message;
打印出以下内容,这是我们的原始消息。
Messagehiddeninplainsight.
正如我之前提到的,这是隐写术的最简单形式。通过将消息编码为不同的颜色和跨图像中的不同像素,我们可以轻松地走得更远。将消息静态地跨像素静态编码并每次使用相同的颜色实际上是一个非常糟糕的主意,因为这意味着可以轻松提取消息。现代隐写术倾向于使用更复杂的数学来在图像上分布像素,因此很难猜测。斐波那契数列或像素的多项式分布之类的技术也可以用来挑选要编码的像素。
您可能会注意到,我在这里交换了图像格式,从jpeg开始并将其转换为png文件。这是因为,由于图像中使用了压缩算法,因此jpeg不能很好地存储隐写术。如果您看一下jpeg的构造方式,那么您会发现它使用遮罩算法会生成较小的像素块。由于这个原因,最低有效位经常被丢弃以节省空间。我曾尝试过在开始时生成jpeg图像,但是我的信息一直丢失,我认为这种压缩机制是原因。
充分利用隐写术的一些技巧:
图像越大,越容易在其中隐藏数据。我在这里使用了一个小图像,但是没有什么可以阻止您使用大图像并在其中隐藏更多数据。
具有均匀颜色的图像很难隐藏数据,因为容易发现颜色略有不同的胭脂像素。
将您编码的消息加密成图像也是一个好主意。这意味着,如果有人能够提取您的消息,那么他们必须先解密才能读取消息。为邮件增加了一层保护,尽管这确实意味着收件人必须对邮件进行双重解码。
由于可以通过查看原始邮件将邮件从图像中提取出来,因此最好不要释放原始图像或使用已有的图像(如股票图像)。
随意使用上面的代码尝试自己的图像。查看您是否可以从我在此处上传的图像中提取消息,或者对自己的消息进行编码。我应该注意,这是很有趣,而不是作为一种完整的加密算法。