关于Base64隐写的简要分析

前言

最近在入门CTF时遇见了一道Misc题是关于Base64隐写的(文章后半部分会提到),刷新了我对Base64的认知,因此写下这篇文章来介绍一下关于Base64隐写的知识。

在介绍Base64隐写之前,我们得先了解Base64编码的原理。

Base64编码

Base64编码就是用64个字符(2的6次方),对二进制数据进行编码的方式。这64个字符包括大写字母(A-Z)、小写字母(a-z)、数字(0-9)以及+和/这两个符号。由于Base64编码只用到了64个字符,所以使用6个二进制位就可以把所有的字符表示出来,于是原来的1个字节对应8个二进制位在Base64编码中就变成了1个字节对应6个二进制位。

每个字符对应的数值见下表:
base64编码表

加密原理:加密时先将原来的每个字符对应的ASCII码值转化为二进制数,再把该二进制数按照6位一组划分可得到一个新的二进制数(对应着十进制中的0-63),这时再对照Base64编码表将新的数值转化为字符,如果位数不足则补0,用=符号填充到字符串末尾(一个=符号填充两个0),即可完成Base64编码。

补0这部分需要更深入一些。我们可以算出8和6的最小公倍数是24,所以当原文的字节数恰好是(24÷8=3)的倍数时,是能完整编码而不需要进行补0操作的;但在实际情况中总会出现原文的字节数不为3的倍数的情况,这时编码会出现位数不足的情况。

如下图,这是位数恰好的情况。

位数恰好的情况

再如下图,这是位数不足的情况,最后两位11无法对应一个字符。

位数不足的情况

这时就需要在末尾补上0,直到和剩余的那些位数拼在一起凑够6位,以便能够对应着一个字符。Base64编码中,每补两个0就要在编码后文本的末尾加上一个=符号。如下图,由于在11后补上了4个0,所以需要添加2个=符号。

补0

而解密过程是将=符号前的每个字符对照Base64编码表转化为二进制数连接在一起,判断编码后文本末尾有x个=符号,然后对应地在二进制数末尾删去2*x个0,接着将处理后的二进制数按8位划分,将每组数值对照ASCII表转化为字符并连接在一起即可得到原来的字符串。

知道了Base64编码的原理之后,我们就可以很容易地理解Base64隐写了。

Base64隐写

我们已经知道补0的操作,那么补的必须是0吗,换成1可不可以?当然可以,因为在解码时无论最后补的这些位上的数是0或是1,都会被删去,修改这些位不会影响解码结果,这便是Base64隐写的精髓所在。

修改补位

我们可以通过修改补0位的数据来写入任何我们自定义的数据,只不过写入的数据越长,需要的Base64编码文本条数就越多。一个=符号代表着我们可以写入两位二进制数据。

隐写过程:把我们想要隐写的文本转化为二进制数,然后找一段足够长的文本按行切分并进行Base64编码,如果出现了=符号则代表这行文本能被我们用于隐写,能隐写几位,我们就从刚才得到的二进制数开头起取几位填入。经过批量处理后,我们就能完美地将数据隐写在这一行行Base64编码文本之中了。

那么,我们如何判断Base64编码文本中是否被隐写了某些信息呢?

正常情况下,解Base64得到的文本再次Base64编码,得到的值应该是和原Base64编码一样的。如果不一样,则证明这段Base64编码文本被隐写了。

一般在做CTF题目时遇到大量Base64编码的文本时,就要考虑Base64隐写。

以下是我编写的用于解Base64隐写的脚本代码:

import base64

def Base64Stego_Decrypt(LineList):
    Base64Char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"     #Base64字符集 已按照规范排列
    BinaryText = ""
    for line in LineList:
        if line.find("==") > 0:     #如果文本中有2个=符号
            temp = bin(Base64Char.find(line[-3]) & 15)[2:]      #通过按位与&15运算取出二进制数后4位 [2:]的作用是将0b过滤掉
            BinaryText = BinaryText+"0"*(4-len(temp))+temp      #高位补0
        elif line.find("=") > 0:        #如果文本中有1个=符号
            temp = bin(Base64Char.find(line[-2]) & 3)[2:]       #通过按位与&3运算取出二进制数后2位
            BinaryText = BinaryText+"0"*(2-len(temp))+temp      #高位补0
    Text = ""
    if(len(BinaryText) % 8 != 0):       #最终得到的隐写数据二进制位数不一定都是8的倍数,为了避免数组越界,加上一个判断
        print("警告:二进制文本位数有误,将进行不完整解析。")
        for i in range(0, len(BinaryText), 8):
            if(i+8 > len(BinaryText)):
                Text = Text+"-"+BinaryText[i:]
                return Text
            else:
                Text = Text+chr(int(BinaryText[i:i+8], 2))
    else:
        for i in range(0, len(BinaryText), 8):
            Text = Text+chr(int(BinaryText[i:i+8], 2))      #将得到的二进制数每8位一组对照ASCII码转化字符
        return Text

def Base64_ForString_Decrypt(Text):     #Base64解密
    try:
        DecryptedText = str(Text).encode("utf-8")
        DecryptedText = base64.b64decode(DecryptedText)
        DecryptedText = DecryptedText.decode("utf-8")
    except:
        return 0
    return DecryptedText

if __name__ == "__main__":
    Course = input("文件名:")
    File = open(Course, "r")
    LineList = File.read().splitlines()
    print("显式内容为:")
    for line in LineList:
        print(Base64_ForString_Decrypt(line),end="")
    print("隐写内容为:")
    print(Base64Stego_Decrypt(LineList))

WriteUp

下面是攻防世界Misc新手区题目 base64stego 的WriteUp:

1.下载题目附件查看内容,看到是一大堆的Base64编码文本,将其解码,得到一些关于隐写术的科普知识。

题目文件文本

解密内容

2.结合我们所学知识,判断出该题考察的应该是Base64隐写,使用脚本进行解密。

脚本处理结果

3.得到最终flag为:flag{Base_sixty_four_point_five}


暂无留言

发表留言