base64编码原理及其隐写术详解
base64编码原理及其隐写术详解
本文深入讲解了base64编码的原理及其在CTF比赛中的应用,包括基础概念、编码原理、隐写术等内容。文章适合有一定计算机基础的读者学习,能够帮助读者掌握base64编码的相关知识和技能。
一、base64基础
1.base家族特征及识别方法
初步了解常用base
在我们学习 base编码的时候,首先会观察一种编码的特征,我们可以在一些编码网站中对一些字符串编码以观察编码特征,"ordinary"在不同base下的编码如下:
- base64: b3JkaW5hcnk=
- base32: N5ZGI2LOMFZHS===
- base16: 6F7264696E617279
不难发现,对于base64和base32来讲,最显著的特征莫过于结尾耀眼的"="。那么对于前两者来讲,所有的编码结尾一定有"="吗?答案是否定的,具体原因会在后边将base64编码原理时说清。同时我们会发现base16编码貌似有点格格不入,一个是编码后的字符串和前两者来说相差悬殊,另一个是base16编码后的文本结尾没有"=",这是必然还是巧合?答案是必然,同样,具体原因会在介绍原理时讲到。
base编码的字符组成
对于不同的base编码,差别主要在于构成不同编码的字符种类不同。例如:
- base64编码字符构成:"a
z"、"AZ"、"0~9"、"+"、"/"一共正好64位字符组 - base32编码字符构成:"A
Z"、"27"一共正好32位字符 - base16编码字符构成:"0
9"、"AF"一共正好16位字符
所以base n中的n就是用来编码的字符的种类
识别方式
学习到这,我们应该不难发现,不同的base编码之间的编码字符构成具有明显的特殊性,总结:
- 当我们看到结尾处存在"=",首先要考虑到base编码
- 编码中同时存在大小写字母 --> base64
- 编码中只存在大写字母,且没有超过7的数字 --> base32
- 编码中数字居多,只存在大写字母,且字母不超过F --> base16
以上编码识别方式只能辅助做一个大概判断,题目可能出现以base作为载体的一些加密方式(如AES加密)会导给出base编码无法通过解码得到答案
二、base64进阶
1.base64编码基本原理
编码过程
例如我们要对ordinary进行编码
- 先将每一个字符对应转化成ascii码
- 再将ascii码转化成八位二进制数
- 从左到右依次划分为六个一组
- 最后一组如果不足6位,用“0”补齐,填充零的个数除以二就是末尾等号的个数
- 将划分好的八位二进制数转化成十进制
- 对应base64索引替换成字符编码,并根据补充0的个数填充"="
o r d i n a r y
ascii码 111 114 100 105 110 97 114 121
八位二进制数 01101111 01110010 01100100 01101001 01101110 01100001 01110010 01111001
六位二进制数 011011 110111 001001 100100 011010
十进制数 27 55 9 36 26
base64编码 b 3 J k a
010110 111001 100001 011100 100111 100100
22 57 33 28 39 36
W 5 h c n k
最后我们得到ordinary经过base64编码:b3JkaW5hcnk=
2.其他base编码原理
剩下两种编码过程和base64大差不差,重点说一下区别
base编码过程的深入理解
base32编码在上述地3步骤划分二进制数时以5个一组进行划分,同时索引表不同。以5个一组划分原因也很简单,简单解释一下:
在base64中为什么要以6个为一组进行划分呢?理由是6个二进制数最大能表示的十进制数是多少?答案是2的6次方-1就是63,例如最大的六位二进制数是111111,它表示63,对应索引表是"",而计算机是从0开始计数的,所以六位二进制数恰好能表示64个符号。同理五位二进制数最大能表示的十进制数是2的5次方-1,也就能对应32个字符。哎?你发现了吗,base 2的n次方在第3步划分二进制位数时,划分的就是n位。那么回到我们最开始的那个问题,“为什么base16编码后结尾不会出现'='呢?”,聪明的你应该发现了,我们在编码的第2步是将每个待编码字符变成八位二进制数,根据上述原理base16第三步时应该划分4位一组,8是4的倍数,结尾不会出现填充0的情况,最后也就不会出现"="
三、base64难点
1.base64隐写原理
我们知道在编码时第三步6位一组进行划分时,最后一组六位二进制数不足6位会默认用"0"补齐,就会出现三种情况分别是:需要补两个"0",需要补四个"0",不需要补"0"。那么在我们解码的时候就会根据"="的数量丢掉填充的"0"。那么我们就不难发现,在填充时无论填充的是"0"还是"1"对于明文来说不会产生任何影响,但是会更改编码后除等号外的最后一位字符,所以我们可以人为的把想要隐藏的二进制信息隐藏到base64编码的字符串中
2.过程演示
假如我想藏一个10进去
六位二进制数 011011 110111 001001 100100 011010
十进制数 27 55 9 36 26
base64编码 b 3 J k a
010110 111001 100001 011100 100111 100110
22 57 33 28 39 36
W 5 1 c n m
最后得到的编码就是:b3JkaW5hcnm=
额,虽然现在有点像是在骂人,不过问题不大,我们用网站解码一下,看看会不会对明文产生影响
事实证明不会产生影响。
3.隐写、提取二进制信息脚本
隐写脚本:
import base64
PAD_CHAR = '='
SIX = 6
code_to_char = {
i: char for i, char in enumerate(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
)
}
hide_all_data = "001001000111000001000001011001101011001101100011001101101100101000110101100100101011111000110111110011011010100101010100" # 隐藏的二进制信息
def get_padding_length(data):
all_data = ''.join(format(ord(char), '08b') for char in data)
return SIX - (len(all_data) % SIX) if len(all_data) % SIX != 0 else 0
def hide_write(data, hide_data):
all_data = ''.join(format(ord(char), '08b') for char in data)
padding_length = get_padding_length(data)
if padding_length == 0:
return base64.b64encode(data.encode("utf-8")).decode("utf-8")
else:
chunks = [all_data[i:i + SIX] for i in range(0, len(all_data), SIX)]
end_chunk = chunks[-1] + hide_data
hide_chunks = chunks[:-1] + [end_chunk]
hide_str = ''.join(code_to_char[int(chunk, 2)] for chunk in hide_chunks)
return hide_str + PAD_CHAR * (padding_length // 2)
def process_file(input_file, output_file):
with open(input_file, "r") as f_in, open(output_file, "w") as f_out:
lines = f_in.readlines()
flag = 0
for line in lines:
line = line.strip()
padding_length = get_padding_length(line)
if padding_length != 0:
hide_data = hide_all_data[flag:flag + padding_length]
flag += padding_length
else:
hide_data = ''
f_out.write(hide_write(line, hide_data) + '\n')
process_file("carrier.txt", "output.txt")
提取二进制信息脚本:
提取的二进制信息可能不是用于转化ascii码,可能是莫斯密码的密文,也有可能是其他加密方式的密文(可以用二进制信息表示,具体看题目提示)
import string
base64chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/"
bins = ""
def getSecret(s):
global bins
six_bin = ""
countZero = 0
for item in s:
if item == "=":
countZero += 1
else:
six_bin += bin(int(base64chars.index(item)))[2:].zfill(6)
bins += six_bin[-(countZero * 2):]
with open("c.txt", "r") as f:
lines = f.readlines()
for line in lines:
line = line.strip()
if line.count("="):
getSecret(line)
decrypt_bindata = bins
with open("output.txt", "w") as file:
file.write(decrypt_bindata)
提取解密脚本:
将所有二进制数提取后汇总到一起八位一组转化成ascii码进而转化成字符
import string
base64chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/"
bins = ""
def getSecret(s):
global bins
six_bin = ""
countZero = 0
for item in s:
if item == "=":
countZero += 1
else:
six_bin += bin(int(base64chars.index(item)))[2:].zfill(6)
bins += six_bin[-(countZero * 2):]
with open("flag.txt", "r") as f:
lines = f.readlines()
for line in lines:
line = line.strip()
if line.count("="):
getSecret(line)
s = ""
for i in range(0, len(bins), 8):
s += chr(int(bins[i:i + 8], 2))
print(s)
总结:
base64隐写是比较好判断的,隐写一定需要大量的base64编码作为载体,所以一般会很长且分为多段,如果其中两段编码大体相同,只有最后一个除号外的字符不同,别犹豫,就是用了隐写。