Base64编码和隐写
1. Base64 基本介绍
Base64 (基底64)这个术语源自 MIME 的 Content-Transfer-Encoding1,它是一种基于 64 个可打印字符来表示二进制数据的方法。
因为 $\log_2{64}=6$,所以 Base64 编码每 6 个比特位为 1 个单元,换句话说,Base64 编码将每 3 字节(24 位)转换为 4 个 6 位的字符。
Base64 只是进行了编码,方便数据的传输,并不是加密。
Base64 可打印字符包括大写字母 A-Z
、小写字母 a-z
、数字 0-9
,这样就有 62 个字符了,另外两个字符可能略有不同,通常采用 MIME 中的方式,也就是选择斜杆 /
和加号 +
,这样共 64 个字符,并将等号 =
作为后缀填充。
在 URL 中使用标准 Base64 编码时1会将
+
、/
和=
字符进行 URL 编码,导致+
变为%2B
、/
变为%2F
和=
变为%3D
,使得字符串变得冗长。为此,有专门针对 URL 的 Base64编码,它将标准 Base64 编码的+
和/
字符分别替换为-
和_
,从而避免对一些特殊字符进行 URL 编码/解码。
下表为标准 Base64 编码的字符集
Index | Binary | Char | Index | Binary | Char | Index | Binary | Char | Index | Binary | Char | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 000000 | A |
16 | 010000 | Q |
32 | 100000 | g |
48 | 110000 | w |
|||
1 | 000001 | B |
17 | 010001 | R |
33 | 100001 | h |
49 | 110001 | x |
|||
2 | 000010 | C |
18 | 010010 | S |
34 | 100010 | i |
50 | 110010 | y |
|||
3 | 000011 | D |
19 | 010011 | T |
35 | 100011 | j |
51 | 110011 | z |
|||
4 | 000100 | E |
20 | 010100 | U |
36 | 100100 | k |
52 | 110100 | 0 |
|||
5 | 000101 | F |
21 | 010101 | V |
37 | 100101 | l |
53 | 110101 | 1 |
|||
6 | 000110 | G |
22 | 010110 | W |
38 | 100110 | m |
54 | 110110 | 2 |
|||
7 | 000111 | H |
23 | 010111 | X |
39 | 100111 | n |
55 | 110111 | 3 |
|||
8 | 001000 | I |
24 | 011000 | Y |
40 | 101000 | o |
56 | 111000 | 4 |
|||
9 | 001001 | J |
25 | 011001 | Z |
41 | 101001 | p |
57 | 111001 | 5 |
|||
10 | 001010 | K |
26 | 011010 | a |
42 | 101010 | q |
58 | 111010 | 6 |
|||
11 | 001011 | L |
27 | 011011 | b |
43 | 101011 | r |
59 | 111011 | 7 |
|||
12 | 001100 | M |
28 | 011100 | c |
44 | 101100 | s |
60 | 111100 | 8 |
|||
13 | 001101 | N |
29 | 011101 | d |
45 | 101101 | t |
61 | 111101 | 9 |
|||
14 | 001110 | O |
30 | 011110 | e |
46 | 101110 | u |
62 | 111110 | + |
|||
15 | 001111 | P |
31 | 011111 | f |
47 | 101111 | v |
63 | 111111 | / |
|||
Padding | = |
2. Base64 编码过程
Base64 编码的步骤大致如下:
(1)将每个字符转换成二进制,合并得到一个二进制串,如果长度不能被 6 整除,需要进行填充;
(2)将二进制串按 6 位一组划分,根据 Base64 字符集进行编码;
(3)Base64 编码后的字符长度不是 4 的倍数时,用一个或两个 =
进行填充。
以 Man
的 Base64 编码结果 TWFu
为例,其转换过程如下所示
在将每 3 个字节转换为 4 个 Base64 字符时,如果要转换的字节数不能被 3 整除,最后会多出 1 个或 2 个字节,这时需要用 0 在末尾补足,使二进制串的长度能够被 6 整除,编码后字符长度不是 4 的倍数的,需要在 Base64 编码后的需要加上一个或两个 =
号,使编码后的长度能被 4 整除。
换句话说,一个
=
表示最后 4 个 Base64 字符(包括=
在内)解码后可以得到 2 个 8 位的字符,两个=
表示最后 4 个 Base64 字符(包括=
在内)解码后可以得到 1 个 8 位的字符。不存在 3个
=
的情况,因为这种情况下 4 个 Base64 字符中只有 1 个非填充字符,有效位仅有 6 位,不可能由 8 位字符编码而成。
比如下面的例子,二进制串长度为 16,补上 2 位后的长度才能被 6 整除:
再比如下面的例子,二进制串长度为 8,需要补上 4 位后长度才能被 6 整除:
填充不是必须的,因为无需填充也可以通过编码后的内容计算出缺失的字节。所以在一些实现中填充是必须的,有些却不是。一种必须使用填充的场合是当需要将多个 Base64 编码文件合并为一个文件的时候2。
3. Base64 解码过程
解码过程如下:
(1)根据 Base64 字符集转变成相应的二进制串,如果存在填充字符 =
需要先去掉;
(2)将二进制串按 8 位一组划分,剩余长度如果不足 8 位,则直接去除;
(3)将每 8 位二进制串转换为相应的 ASCII 字符。
注:标准 ASCII 码使用 7 位二进制数组合来表示 128 个字符。现在许多基于 x86 的系统都支持使用扩展 ASCII。扩展 ASCII 码使用 8 位二进制数表示 256 个字符。
有无填充字符 =
其实都可以进行 Base64 解压。当只有一个填充字符 =
时,下图分别展示了有无填充字符的解码过程:
当只有两个填充字符 =
时,下图分别展示了有无填充字符的解码过程:
4. Base64 隐写
我们已经知道补 Base64 编码过程中可能存在补 0 的操作,那么补的必须是 0 吗,换成1可不可以?当然可以3。
因为解码时无论最后补的数是 0 还是 1,都会被删去,修改这些位不会影响解码结果,这便是Base64隐写的原理。
从上文知道,Base64 解压时 8 位一组,不足 8 位的直接去除,所以补的位不影响结果。
我们可以通过修改补 0 位的数据来写入我们想隐写的内容,只不过写入的数据越长,需要的 Base64 编码文本条数就越多。
一个 =
符号代表着我们可以写入 2 位二进制数据,两个 =
可以写入 4 位二进制数据。
在本文
Base64 编码过程
、Base64 解码过程
这两部分可以体现,编码过程中分别补上了 2 个 0 和 4 个 0,解码时中都去掉了。
正常情况下,将 Base64 字符串解码后得到的文本再次进行 Base64 编码,得到的结果应该是和原 Base64 编码一样的,如果不一样说明证明这段 Base64 编码文本有隐写的内容。
- Base64 隐写加密脚本如下
1 | import base64 |
隐写的信息越长,用来隐写的文本文件也就要越长,上面的代码展示了隐写字符串 abc
的例子。
经 Base64 隐写后,最终输出的 stego.txt
的内容如下所示
1 | dGhpcyBpcyBsaW5lIDEu |
- Base64 隐写解密脚本如下
1 | import base64 |
参考资料
1. https://en.wikipedia.org/wiki/Base64 ↩