问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

base64编码原理及其隐写术详解

创作时间:
作者:
@小白创作中心

base64编码原理及其隐写术详解

引用
CSDN
1.
https://m.blog.csdn.net/2402_88262533/article/details/145220418

本文深入讲解了base64编码的原理及其在CTF比赛中的应用,包括基础概念、编码原理、隐写术等内容。文章适合有一定计算机基础的读者学习,能够帮助读者掌握base64编码的相关知识和技能。

一、base64基础

1.base家族特征及识别方法

初步了解常用base

在我们学习 base编码的时候,首先会观察一种编码的特征,我们可以在一些编码网站中对一些字符串编码以观察编码特征,"ordinary"在不同base下的编码如下:

  • base64: b3JkaW5hcnk=
  • base32: N5ZGI2LOMFZHS===
  • base16: 6F7264696E617279

不难发现,对于base64和base32来讲,最显著的特征莫过于结尾耀眼的"="。那么对于前两者来讲,所有的编码结尾一定有"="吗?答案是否定的,具体原因会在后边将base64编码原理时说清。同时我们会发现base16编码貌似有点格格不入,一个是编码后的字符串和前两者来说相差悬殊,另一个是base16编码后的文本结尾没有"=",这是必然还是巧合?答案是必然,同样,具体原因会在介绍原理时讲到。

base编码的字符组成

对于不同的base编码,差别主要在于构成不同编码的字符种类不同。例如:

  • base64编码字符构成:"az"、"AZ"、"0~9"、"+"、"/"一共正好64位字符组
  • base32编码字符构成:"AZ"、"27"一共正好32位字符
  • base16编码字符构成:"09"、"AF"一共正好16位字符

所以base n中的n就是用来编码的字符的种类

识别方式

学习到这,我们应该不难发现,不同的base编码之间的编码字符构成具有明显的特殊性,总结:

  • 当我们看到结尾处存在"=",首先要考虑到base编码
  • 编码中同时存在大小写字母 --> base64
  • 编码中只存在大写字母,且没有超过7的数字 --> base32
  • 编码中数字居多,只存在大写字母,且字母不超过F --> base16

以上编码识别方式只能辅助做一个大概判断,题目可能出现以base作为载体的一些加密方式(如AES加密)会导给出base编码无法通过解码得到答案

二、base64进阶

1.base64编码基本原理

编码过程

例如我们要对ordinary进行编码

  1. 先将每一个字符对应转化成ascii码
  2. 再将ascii码转化成八位二进制数
  3. 从左到右依次划分为六个一组
  4. 最后一组如果不足6位,用“0”补齐,填充零的个数除以二就是末尾等号的个数
  5. 将划分好的八位二进制数转化成十进制
  6. 对应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编码作为载体,所以一般会很长且分为多段,如果其中两段编码大体相同,只有最后一个除号外的字符不同,别犹豫,就是用了隐写。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号