当前位置: 首页 > 技术干货 > 浅谈LSB隐写解题与出题

浅谈LSB隐写解题与出题

发表于:2022-01-25 16:03 作者: zoeyctf 阅读数(1437人)

前言:LSB隐写在CTF中属于出现得比较多的类型。这篇文章对LSB隐写的原理,解题方法,出题脚本,以及LSB隐写特性进行研究。

LSB隐写原理

LSB即为最低有效位(Least Significant Bit,lsb),我们知道,图片中的图像像素一般是由RGB三原色(红绿蓝)组成,每一种颜色占用8位,取值范围为0x00~0xFF,即有256种颜色,一共包含了256的3次方的颜色,即16777216种颜色。而人类的眼睛可以区分约1000万种不同的颜色,这就意味着人类的眼睛无法区分余下的颜色大约有6777216种。 

image.png



LSB隐写就是修改RGB颜色分量的最低二进制位也就是最低有效位(LSB),而人类的眼睛不会注意到这前后的变化,每个像数可以携带3比特的信息。 

image.png


上图我们可以看到,十进制的235表示的是绿色,我们修改了在二进制中的最低位,但是颜色看起来依旧没有变化。我们就可以修改最低位中的信息,实现信息的隐写。


StegSolve工具

这里推荐使用一款功能很强大的lsb隐写分析工具---StegSolve图片通道查看器。下载地址:http://www.caesum.com/handbook/Stegsolve.jar,使用stegsolve打开图片,按右方向键查看各通道显示的图像。

图像处理主要是analyse这个模块,主要有这四个功能:

File Format: 文件格式,查看图片的具体信息

Data Extract: 数据抽取,提取图片中隐藏数据

Frame Browser: 帧浏览器,主要是对GIF之类的动图进行分解,动图变成一张张图片

Image Combiner: 拼图,图片拼接

对于LSB隐写的图片,我们用StegSolve打开模块,由于是RGB三原色的最低位隐写,所以在Data Extract模,提取Red,Green,和Blue的0通道信息,在这三个颜色的0通道上打勾,并按下Preview键,当隐写的内容为文本文件时如下所示:

image.png

 

 

当隐写的内容为图片时如下所示:

image.png 

 

PNG文件头可以看出隐写内容为PNG文件,按save Bin键保存为PNG文件

LSB隐写脚本

我在github上参考了这位大佬的脚本,https://github.com/librauee/Steganalysis/tree/master/LSB,但感觉用起来不是很方便,稍微修改了一下,如下所示:

from PIL import Image
import sys

def toasc(strr):
    return int(strr, 2)       
         
#str1为所要提取的信息的长度(根据需要修改),str2为加密载体图片的路径,str3为提取文件的保存路径
def decode(str1,str2,str3):
    b="" 
    im = Image.open(str2)
    lenth = int(str1)*8  
    width,height = im.size[0],im.size[1]
    count = 0
    for h in range(height):
        for w in range(width):
            #获得(w,h)点像素的值
            pixel = im.getpixel((w, h))
            #此处余3,依次从RGB三个颜色通道获得最低位的隐藏信息
            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(str3,"w",encoding='utf-8') as f:
        for i in range(0,len(b),8):
            #以每8位为一组二进制,转换为十进制            
            stra = toasc(b[i:i+8])
            #将转换后的十进制数视为ascii码,再转换为字符串写入到文件中
            #print((stra))
            f.write(chr(stra))
    print("sussess")

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

def mod(x,y):
    return x%y

#str1为载体图片路径,str2为隐写文件,str3为加密图片保存的路径
def encode(str1,str2,str3):
    im = Image.open(str1)
    #获取图片的宽和高
    width,height= im.size[0],im.size[1]
    print("width:"+str(width))
    print("height:"+str(height))
    count = 0
    #获取需要隐藏的信息
    key = get_key(str2)
    keylen = len(key)
    for h in range(height):
        for w in range(width):
            pixel = im.getpixel((w,h))
            a=pixel[0]
            b=pixel[1]
            c=pixel[2]
            if count == keylen:
                break
            #下面的操作是将信息隐藏进去
            #分别将每个像素点的RGB值余2,这样可以去掉最低位的值
            #再从需要隐藏的信息中取出一位,转换为整型
            #两值相加,就把信息隐藏起来了
            a= a-mod(a,2)+int(key[count])
            count+=1
            if count == keylen:
                im.putpixel((w,h),(a,b,c))
                break
            b =b-mod(b,2)+int(key[count])
            count+=1 
            if count == keylen:
                im.putpixel((w,h),(a,b,c))
                break
            c= c-mod(c,2)+int(key[count])
            count+=1
            if count == keylen:
                im.putpixel((w,h),(a,b,c))
                break
            if count % 3 == 0:
                im.putpixel((w,h),(a,b,c))
    im.save(str3)


if __name__ == '__main__':
    if '-h' in sys.argv or '--help' in sys.argv or len(sys.argv) < 2:
        print ('Usage: python test.py <cmd> [arg...] [opts...]')
        print ('  cmds:')
        print ('    encode image + flag -> image(encoded)')
        print ('    decode length + image(encoded) -> flag')
        sys.exit(1)
    cmd = sys.argv[1]
    if cmd != 'encode' and cmd != 'decode':
        print('wrong input')
        sys.exit(1)
    str1 = sys.argv[2]
    str2 = sys.argv[3]
    str3 = sys.argv[4]
    if cmd != 'encode' and cmd != 'decode':
        print ('Wrong cmd %s' % cmd)
        sys.exit(1)
    elif cmd=='encode':
        encode(str1,str2,str3)
    elif cmd=='decode':
        decode(str1,str2,str3)

LSB隐写出题

这里以合天的图标为例子,对其进行lsb隐写,以文件和图片两种方式。

image.png

 

 

如图所示

image.png 

 

得到两张lsb隐写的图片

 image.png

 

我们可以用脚本或者使用StegSolve工具获取flag,工具如前文所示,这里演示用脚本获取flag。这里我们先查看flag.txt的大小,当然在不知道大小的情况下也是可以的,要得到完整的flag可以把文件大小设置大一些。

image.png 

 

测试结果如下:

image.png

 

 

如果把文件大小设置太小得到的flag不完整,设置太大会产生一些额外的字符,为得到完整的flag,可以把大小设置稍大一些。

不同于文本文件的大小可以任意调整,不影响文件的阅读,图片调整大小对图片的影响较大,而且在一般情况下,我们是不知道被隐写的文件的大小,所以并不推荐用脚本来获取flag,推荐使用工具来解题,脚本可以使用在出题中。

LSB隐写特性

加密性

由于LSB隐写在只知道加密图片的情况下就可以知道隐写的内容,可见加密性是比较差的。

鲁棒性

我们通过测试来检测LSB隐写的鲁棒性,首先我们对合天_txt.png图片进行以下操作。

 image.png

 

因为文本隐写隐藏在图片最开始的地方,所以我们对图片最开始的部分进行攻击,结果如下:

image.png 

 

可以看到内容完全被破坏。当然如果不在图片最开始的地方攻击图片,则对内容恢复完全没有问题。

image.png 

 

再测试一下图片隐写的鲁棒性,对合天_jpg.png图片进行处理,结果如图所示:

image.png 

 

可以看出,图片隐写的鲁棒性更差,这是因为图片的大小比文本大得多,对图片的完整性要求就更高。总的来说,lsb隐写的鲁棒性是很差的。

参考文章

LSB图片隐写 https://segmentfault.com/a/1190000016223897

隐写脚本 https://github.com/librauee/Steganalysis/tree/master/LSB