隐写术-LSB算法实现

隐写术


隐写术是一门关于信息隐藏的技巧与科学,所谓信息隐藏指的是不让除预期的接收者之外的任何人知晓信息的传递事件或者信息的内容。隐写术的英文叫做Steganography,来源于特里特米乌斯的一本讲述密码学与隐写术的著作Steganographia,该书书名源于希腊语,意为“隐秘书写”。比如电视中出现频率较高的天书,经过水泡、火烤才能看见其中内容,这就是一种隐写术。


RGB色彩模式


RGB色彩就是常说的光学三原色,R代表Red(红色),G代表Green(绿色),B代表Blue(蓝色)。自然界中肉眼所能看到的任何色彩都可以由这三种色彩混合叠加而成,因此也称为加色模式。

RGB模式又称RGB色空间。它是一种色光表色模式,它广泛用于我们的生活中,如电视机、计算机显示屏、幻灯片等都是利用光来呈色。印刷出版中常需扫描图像,扫描仪在扫描时首先提取的就是原稿图像上的RGB色光信息。RGB模式是一种加色法模式,通过R、G、B的辐射量,可描述出任一颜色。计算机定义颜色时R、G、
B三种成分的取值范围是0-255,0表示没有刺激量,255表示刺激量达最大值。R、G、B均为255时就合成了白光,R、G、B均为0时就形成了黑色。在显示屏上显示颜色定义时,往往采用这种模式。图像如用于电视、幻灯片、网络、多媒体,一般使用RGB模式。


LSB算法


全称为Least Significant Bit,在二进制数中意为最低有效位,一般来说,MSB(最高有效位)位于二进制数的最左侧,LSB位于二进制数的最右侧。比如一种颜色,用8位的二进制表示为:00100011,那么最左侧的0所在的位置,就是最高有效位MSB,最右侧的1所在的位置,就是最低有效位LSB。

由于图像的每一个像素点都是由RGB(红、绿、蓝)三原色组成,可以暂时称其为颜色通道,如果每个颜色通道的值占8位,那么RGB色彩模式的图片中,每个像素点的颜色就可以用6位长度的十六进制数来(如#FFFFFF),LSB隐写即是修改每个颜色通道的最低一位,将其替换为我们想要嵌入的信息中的内容,以此来实现数据隐藏。因为是最低有效位,所以对实际的颜色影响不大,肉眼几乎分辨不出区别。
一个像素点包含三种颜色,每个颜色修改最后1位,这样一个像素点就可以携带3位信息
应用LSB算法的图像格式需为位图形式,即图像不能经过压缩,如LSB算法多应用于png、bmp等格式,而jpg格式较少。

详细参考:https://wenku.baidu.com/view/ff590e9d5f0e7cd1842536d7.html


Python Imaging Library


Python Imaging Library(简称PIL)为Python解释器提供了图像处理的功能,PIL提供了广泛的文件格式支持、高效的内部表示以及相当强大的图像处理功能。PIL图像处理库的核心被设计成为能够快速访问以几种基本像素类型表示的图像数据,它为通用图像处理工具提供了一个坚实基础。结合PIL可以方便的编写Python脚本处理图片隐写问题。


StegSolve


StegSolve是一款基于Java开发的流行图片隐写分析软件,其支持常见的图片文件格式,可以对不同的文件进行结合(包括XOR、ADD、SUB等操作),可以对图片文件格式进行分析,可以提取GIF文件中的帧等,覆盖了基本的图片隐写分析需求。


LSB隐写题目


以前的时候,CTF中遇到类似的题目,还会有“最低”,“最下面”等程度不一的提示,但随着CTF的难度越来越大,像最低位隐写这样的隐写术,也几乎没有什么难度了。所以对应的题目几乎也没有什么提示,解题者能做的就是尝试了。

假设我们现在得到了一张图片,我们先使用Stegsolve来对他进行分析。先打开需要分析的图片:
image
image

勾选如下的选项。
image

然后勾选RGB三个颜色通道的最低位(我们所说的RGB即是Red,Green,Blue三个颜色,而下图中的7,6,…1,0即是颜色用二进制表示时的高位到低位,我们是LSB最低有效位,所以自然选择0),并且旁边的“Bit Order”选择“LSB First”(其实这个选不选,影响不大)。然后点击“Preview”就可以看到最低位的隐藏信息了。
image


python实现LSB隐写


python安装PIL、Pillow模块的教程:https://www.cnblogs.com/pcat/p/6790058.html

我们可以整理一下进行最低位隐写的逻辑,然后用python写出一个脚本,来实现将指定内容写入指定图片的最低位。要想把数据写入图片的最低位,我们就需要考虑整体的流程:

1. 获取要写入的内容,并将其转换为二进制字符串
2. 依次读取转换后的二进制字符串,并按照顺序替换掉图片某个像素的三个颜色通道的最低位数值
3. 保存替换后的图片

我们跟着这个顺序来,首先,获取要写入的内容,并将其转换为二进制字符串:

def getHideString(hide_string):
  #获取要隐藏的文件内容
  tmp = hide_string
  f = file(tmp,"rb")
  str_bin = ""
  s = f.read()
  for i in range(len(s)):
  	#逐个字节将要隐藏的文件内容转换为二进制,并拼接起来
  	#1.先用ord()函数将s的内容逐个转换为ascii码
  	#2.使用bin()函数将十进制的ascii码转换为二进制
  	#3.由于bin()函数转换二进制后,二进制字符串的前面会有"0b"来表示这个字符串是二进制形式,所以用replace()替换为空
  	#4.又由于ascii码转换二进制后是七位,而正常情况下每个字符由8位二进制组成,所以使用zfill() 方法返回指定长度的字符串,原字符串右对齐,前面填充0
    str_bin = str_bin + str(bin(ord(s[i])).replace('b','')).zfill(8)
    #print str
  f.closed
  return str_bin

然后是将内容写入到图片中,先获取图片的宽高,初始化计数器count,

#original_file为载体图片路径,hide_string为隐写文件,new_file为加密图片保存的路径
def encode(original_file,hide_string,new_file):  
  im = Image.open(original_file)
  #获取图片的宽和高
  width = im.size[0]
  print "width:"+str(width)+"\n"
  height = im.size[1]
  print "height:"+str(height)+"\n"
  count = 0

调用前面的getHiderString函数来获取需要隐藏的信息,并计算信息的长度

 #获取需要隐藏的信息
  key = getHideString(hide_string)
  keylen = len(key)

进行循环,横向挨个读取像素点的三个颜色通道的值,

   for h in range(0,height):
       for w in range(0,width):
           pixel = im.getpixel((w,h))
           R = pixel[0]
           G = pixel[1]
           B = pixel[2]

然后进行数据的写入,每做一步都需要判断一下信息是否写完:

      if count == keylen:
        break
      #分别将每个像素点的RGB值余2,这样可以获得最低位的值,然后用原来的值减去最低位的值
      #再从需要隐藏的信息中取出一位,转换为整型
      #两值相加,需要隐藏的信息就将原来的最低位信息替换掉了
      R= R-mod(R,2)+int(key[count])
      count+=1
      if count == keylen:
        im.putpixel((w,h),(R,G,B))
        break
      G =G-mod(G,2)+int(key[count])
      count+=1
      if count == keylen:
        im.putpixel((w,h),(R,G,B))
        break
      B= B-mod(B,2)+int(key[count])
      count+=1
      if count == keylen:
        im.putpixel((w,h),(R,G,B))
        break

循环完一次(一个像素点)后,还需要判断一个像素点的是否都替换了,如果替换了,就真正的写进图片中

      if count % 3 == 0:
        im.putpixel((w,h),(R,G,B))

最后保存图片

  im.save(new_file)

完整代码如下:

# -*- coding: UTF-8 -*-
from PIL import Image

def getHideString(hide_string):
  #获取要隐藏的文件内容
  tmp = hide_string
  f = file(tmp,"rb")
  str_bin = ""
  s = f.read()
  for i in range(len(s)):
  	#逐个字节将要隐藏的文件内容转换为二进制,并拼接起来
  	#1.先用ord()函数将s的内容逐个转换为ascii码
  	#2.使用bin()函数将十进制的ascii码转换为二进制
  	#3.由于bin()函数转换二进制后,二进制字符串的前面会有"0b"来表示这个字符串是二进制形式,所以用replace()替换为空
  	#4.又由于ascii码转换二进制后是七位,而正常情况下每个字符由8位二进制组成,所以使用zfill() 方法返回指定长度的字符串,原字符串右对齐,前面填充0
    str_bin = str_bin + str(bin(ord(s[i])).replace('b','')).zfill(8)
    #print str
  f.closed
  return str_bin

def mod(x,y):
  return x%y;
#original_file为载体图片路径,hide_string为隐写文件,new_file为加密图片保存的路径
def encode(original_file,hide_string,new_file):  
  im = Image.open(original_file)
  #获取图片的宽和高
  width = im.size[0]
  print "width:"+str(width)+"\n"
  height = im.size[1]
  print "height:"+str(height)+"\n"
  count = 0
  #获取需要隐藏的信息
  key = getHideString(hide_string)
  keylen = len(key)
  for h in range(0,height):
    for w in range(0,width):
      pixel = im.getpixel((w,h))
      R = pixel[0]
      G = pixel[1]
      B = pixel[2]
      if count == keylen:
        break
      #分别将每个像素点的RGB值余2,这样可以获得最低位的值,然后用原来的值减去最低位的值
      #再从需要隐藏的信息中取出一位,转换为整型
      #两值相加,需要隐藏的信息就将原来的最低位信息替换掉了
      R= R-mod(R,2)+int(key[count])
      count+=1
      if count == keylen:
        im.putpixel((w,h),(R,G,B))
        break
      G =G-mod(G,2)+int(key[count])
      count+=1
      if count == keylen:
        im.putpixel((w,h),(R,G,B))
        break
      B= B-mod(B,2)+int(key[count])
      count+=1
      if count == keylen:
        im.putpixel((w,h),(R,G,B))
        break
      if count % 3 == 0:
        im.putpixel((w,h),(R,G,B))
  im.save(new_file)

#原图
original_file = "flag.png"
#处理后输出的图片路径
new_file = "out.png"
#需要隐藏的信息
hide_string = "flag.txt"
encode(original_file,hide_string,new_file)

接下来测试脚本是否真的可以隐藏信息,先创建文件flag.txt,并将字符串写入其中。
image

创建好后运行脚本:
image

查看文件夹下,确实有新的图片生成。
image

用Stegsolve对生成的图片进行验证,确认字符串是否写入成功:
image


Python提取LSB隐写信息


实现脚本代码如下:

# -*- coding:UTF-8 -*-
from PIL import Image
 
def mod(x,y):
    return x%y;
 
def toasc(strr):
    return int(strr, 2)
#le为所要提取的信息的长度,str1为加密载体图片的路径,str2为提取文件的保存路径
def func(le,str1,str2):
    a=""
    b=""
    im = Image.open(str1)
    lenth = le*8
    width = im.size[0]
    height = im.size[1]
    count = 0
    for h in range(0, height):
        for w in range(0, width):
        #获得(w,h)点像素的值
            pixel = im.getpixel((w, h))
            #此处余3,依次从R、G、B三个颜色通道获得最低位的隐藏信息
            if count%3==0:
                count+=1
                b=b+str((mod(int(pixel[0]),2)))
                if count ==lenth:
                    break
            if count%3==1:
                count+=1
                b=b+str((mod(int(pixel[1]),2)))
                if count ==lenth:
                    break
            if count%3==2:
                count+=1
                b=b+str((mod(int(pixel[2]),2)))
                if count ==lenth:
                    break
        if count == lenth:
            break
    with open(str2,"wb") as f:
        for i in range(0,len(b),8):
        #以每8位为一组二进制,转换为十进制
            stra = toasc(b[i:i+8])
            #将转换后的十进制数视为ascii码,再转换为字符串写入到文件中
            f.write(chr(stra))
            stra =""
    f.closed
#文件长度
le = 30
#含有隐藏信息的图片
new = "step2_out.png"
#信息提取出后所存放的文件
tiqu = "get_flag.txt"
func(le,new,tiqu)

运行脚本,提取到指定长度的最低位信息,保存在get_flag.txt文件夹内。
image


有关LSB隐写的思考


  1. 其实LSB现在很少以单独的考点出现在CTF比赛中了,一般都是很与其他的信息隐藏技术一起出现的。
  2. 文中只是介绍了文本的写入方式,如何将一个文件写入到其中?其实也很简单,直接将文件的内容当作文本来处理,写入到目标图片的最低位。但是如何将写入的文件提取出来,这就需要平时的积累,能够在看到文件的十六进制的头部时快速分辨其文件类型。当然,有这种功能的工具也早就有了,比如binwalk。
  3. 了解了最低位隐写的原理之后,我们就很容易知道,他还可以引申出其他的一些利用方式,比如在更高的位数中隐藏数据、同时用两个位数来隐藏数据等(当然这样的隐藏数据,可能会对原图片的影响比较大,甚至用肉眼即可分辨出与原图片的差别);又或者不使用RGB的顺序来隐藏数据,而是使用GBR、BRG等顺序来隐藏数据。
posted @ 2021-12-30 16:41  luogi  阅读(3457)  评论(1编辑  收藏  举报