前言
最近在入门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个二进制位。
每个字符对应的数值见下表:
加密原理:加密时先将原来的每个字符对应的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个=符号。
而解密过程是将=符号前的每个字符对照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}