0x00 序
真的是颓了很久,最近突然很好奇base64的编码方式,于是在查阅了大量相关资料后逐渐对Base系列的编码方式有了一个大概的了解,但对于部分的Base成员编码方式,我在网上找到的资料也仅仅只提到索引表,本文基于网上查阅的部分资料,外加我的理解和研究,对Base系列编码做了一个汇总和整理。
部分内容基于我自己的研究和理解,如果描述有错误或理解有误区,请各位师傅及时指正批评,在此谢过。
0x01 起源
在计算机世界的混沌时代,为了方便计算机更好地识别人类语言,更友好地与用户进行交互,编码应运而生,此时ASCII编码横空出世。
ASCII码使用两个字节来表示一个字符,既8位二进制数,所以ASCII有0~255共256个字符,其中常用的字符是0~127,而128~255是后来扩充的ASCII表。
而在ASCII码表中的前128个字符中,0~32和127是计算机无法打印的不可见字符,同时,ASCII码的128~255之间的值是不可见字符。而在网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。所以就先把数据先做一个Base64编码,统统变成可见字符,这样出错的可能性就大降低了。
base64 最早就是用来邮件传输协议中的,原因是邮件传输协议只支持 ASCII字符传递,因此如果要传输二进制文件,如:图片、视频是无法实现的。因此 base64 就可以用来将二进制文件内容编码为只包含 ASCII字符的内容,这样就可以传输了。
以前的交换机只能处理标准ascii码。也就是说最高位是0。那么base64就是一种编码方法。保证一般数据最高位为0。怎么做?现在假设我们有3字节24位数据流。我们现在每6位切一刀。在这6位前面补00这样三字节变成四字节。而且保证最高位为0 这样就能在网络上传输了。
0x02 庞大的Base编码家族
尽管最早的Base编码只有Base64,但随着技术的发展和网络世界的需求,现在的Base系列编码俨然已发展成一个庞大的编码家族,除了我们耳熟能详的Base64之外,还有Base16、Base32、Base36、Base58、Base62、Base85、Base91、Base92。
0x03 Base系列编码概述
基于我个人的理解,我将Base系列编码大概分为两个分支,一个分支是以Base64编码规则为基准,囊括了Base16、Base32、Base64的Old School体系,另一个分支则是除此之外剩下所有的Base成员。
0x04 OldSchool系列编码体系
这一个分支编码基本遵循同一规则,每一位成员都拥有自己的编码索引表,索引表的规模取决去成员采用多少比特位对源数据进行切割编码,例如base64就是使用6个比特位对源数据进行切割编码,而2的6次方等于64,于是base64的编码索引表由64个可见字符组成。那么编码是如何完成的?
首先会将源数据转换为8比特位的二进制数据(ASCII码由8个比特位表示),然后对二进制数据进行对应的比特位进行且切割,然后再将切割重组后的数据还原为十进制数,对照索引表进行编码。
在切割重组的过程中,如果遇到位数不足时,需要使用0x00进行填充补齐6位,并在编码完成之后使用字符“=”进行说明,填充了多少位,在编码完成之后就是用多少个“=”字符进行说明,同时“=”在Base系列编码中是通用的填充说明字符。
接下来,我将对所有Base成员的编码规则进行一个详解,并使用Python简单实现Base64编码和解码的方法。
0x05 Base64
Base64,采用6个比特位进行切割编码,比编码索引表组成由A~Z,a~z,0~9,“+”,“/“依次排列组成,共计64个可见字符。
base64索引表
序号 | 字符 | 序号 | 字符 | 序号 | 字符 | 序号 | 字符 |
---|---|---|---|---|---|---|---|
0 | A | 16 | Q | 32 | g | 48 | w |
1 | B | 17 | R | 33 | h | 49 | x |
2 | C | 18 | S | 34 | i | 50 | y |
3 | D | 19 | T | 35 | j | 51 | z |
4 | E | 20 | U | 36 | k | 52 | 0 |
5 | F | 21 | V | 37 | l | 53 | 1 |
6 | G | 22 | W | 38 | m | 54 | 2 |
7 | H | 23 | X | 39 | n | 55 | 3 |
8 | I | 24 | Y | 40 | o | 56 | 4 |
9 | J | 25 | Z | 41 | p | 57 | 5 |
10 | K | 26 | a | 42 | q | 58 | 6 |
11 | L | 27 | b | 43 | r | 59 | 7 |
12 | M | 28 | c | 44 | s | 60 | 8 |
13 | N | 29 | d | 45 | t | 61 | 9 |
14 | O | 30 | e | 46 | u | 62 | + |
15 | P | 31 | f | 47 | v | 63 | / |
源数据为字符串”Utopia“,对应ASCII码为0x55 0x74 0x6f 0x70 0x69 0x61,转换为二进制数据为01010101 01110100 01101111 01110000 01101001 01100001,对其进行6比特位的切割重组后,新的排列方式为010101 010111 010001 101111 011100 000110 100101 100001,对应的十进制为
21 23 17 47 28 6 37 33,根据base64的索引表进行编码之后为VXRvcGlh,各位师傅可以自行进行验证。
我们再举一个需要填充的例子:
源数据为字符串”Li1W“,对应的ASCII码为0x4c 0x69 0x31 0x57,转换为二进制数据为01001100 01101001 00110001 01010111,对其进行6比特位的切割重组后,新的排列方式为010011 000110 100100 110001 010101 11,最后需要使用两个0x00进行填充补齐,填充完的二进制数据为010011 000110 100100 110001 010101 110000,转换为十进制为19 6 36 49 21 48,根据base64的索引表进行编码之后为TGkxVw,使用了两个0x00填充,还需要两个”=“字符进行说明,最后编码的结果为:TGkxVw==。
Python实现编码过程:
-
构建编码索引表
-
将字符串转换为8位对齐的二进制数据。
-
将8位对齐的二进制数据转换为6位对齐的二进制数据,如不够6位的在后面使用0x00补齐。
-
将6位对齐的二进制数据转换为十进制数据。
-
参照索引表完成编码。
-
根据填充情况添加”=“进行说明。
import re
def b64encode(source):
#索引表
b64_list = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9","+","/"]
b64Bin_list = [] #临时存储源数据的二进制数据
#遍历获取源数据的二进制数据
for index in range(len(source)):
char = source[index]
b64Hex = ord(char)
b64Bin = bin(b64Hex).replace("0b","0").zfill(8)
b64Bin_list.append(b64Bin)
temp = "" #临时存储源数据的二进制数据
for i in b64Bin_list:
temp = temp + i
fillCount = 0 #记录填充次数
if len(temp)%6 != 0:
fillCount = 6 - len(temp)%6
temp = temp + "0"*fillCount
#按6比特位对二进制数据进行切割
encodeList = re.findall(r".{6}",temp)
b64Encoded = ""
#进行编码
for encodeBin in encodeList:
index = int(encodeBin,2)
b64Encoded = b64Encoded + b64_list[index]
#判断是否进行填充,并添加"="说明
if fillCount != 0:
b64Encoded = b64Encoded + "="*int(fillCount/2)
return b64Encoded
Python实现解码解码过程:
-
识别编码后字符中的"="个数
-
将编码后的字符转换为二进制数据
-
根据识别的"="格式对二进制数据填充的0x00进行去除
-
将二进制数据进行8比特位的分割重组
-
按照ASCII码表进行还原
因为懒,解码过程我就不写,参考编码过程即可实现。
0x06 Base16和Base32
因为这个分支编码基本都遵从同一套体系和编码规则,不同的编码方式无非就是两点不同:一.索引表不同;二.切割比特位方式不同。
所以后续的成员,我只对索引表和切割方式进行说明。
Base16,以4比特位进行分割重组,所以索引表由16个可见字符构成,按顺序”0“~”F"进行排列。由于ASCII表有8位表示一个字符,所以Base16对源数据进行分割重组时,永远不会需要进行对位数进行填充,既Base16编码是不会出现“=”的。
Base32,以5比特位进行分割重组,所以索引表由32个可见字符构成,由“A"~"Z","2"~"7"进行顺序排列。
0x07 其他Base成员
为何要将Base16、Base32和Base64之外的成员归为其他分支呢?因为其他成员无法对源数据的二进制数进行整数倍比特位分割重组,所以在编码规则上就只能另辟蹊径了,但万变不离其宗,第二分支的成员也是依托索引表进行编码的,我们还是需要先对索引表下手,以下是对第二分支各成员的详解。
0x08 Base36
Base36的索引表由数字0~9加上26个英文字母组成,英文字母要么全大写,要么全小写,在此我们参考的索引表全部使用小写。
Base36索引表
序号 | 字符 | 序号 | 字符 | 序号 | 字符 | 序号 | 字符 |
---|---|---|---|---|---|---|---|
0 | 0 | 9 | 9 | 18 | i | 27 | r |
1 | 1 | 10 | a | 19 | j | 28 | s |
2 | 2 | 11 | b | 20 | k | 29 | t |
3 | 3 | 12 | c | 21 | l | 30 | u |
4 | 4 | 13 | d | 22 | m | 31 | v |
5 | 5 | 14 | e | 23 | n | 32 | w |
6 | 6 | 15 | f | 24 | o | 33 | x |
7 | 7 | 16 | g | 25 | p | 34 | y |
8 | 8 | 17 | h | 26 | q | 35 | z |
由于Base36已无法再进行比特位的分割重组,所以Base36采用了进制转换的方式进行编码,实际上Base36的索引表也就是三十六进制的映射表,那么Base36是如何完成编码的呢?
编码方式:
-
将每一个字符转换为ASCII码对应的序号。
-
将每个字符转换为二进制数。
-
将每个二进制数使用0填充高位,使其8位对齐。
-
将二进制数拼接后转换为十进制数。
-
将十进制数转换为三十六进制即可完成编码。
十进制转换三十六进制,我们可以采用求商取余再参照索引表来得出最后的结果,这里举一个例子。
依然使用字符串“li1w”作为源数据,将字符串转换为ASCII码为0x6c 0x69 0x31 0x77,将ASCII码转换为二进制数为1101100 1101001 110001 1110111。再将不满8位的数据补齐8位,结果为:01101100 01101001 00110001 01110111,拼接之后的结果为:01101100011010010011000101110111。
转换为十进制数为:1818833271。
使用求商取余的方式计算36进制数:
-
1818833271除36,商50523146,余15,查询索引表结果为f,既第一位f。
-
50523146除36,商1403420,余26,查询索引表结果为q,既第二位q。
-
1403420除36,商38983,余32,查询索引表结果为w,既第三位w。
-
38983除36,商1082,余31,查询索引表结果为v,既第三位v。
-
1082除36,商30,余2,查询索引表结果为2,既第四位2。
-
30除36,商0,余30,查询索引表结果为u,既第五位u。
-
最终的编码结果为u2vwqf。
为验证最终结果,我使用工具也进行了编码测试,最终结果与我们得到的结果一致。
0x09 Base58
Base58是Bitcoin中使用的一种独特编码方式,主要用于Bitcoin钱包的产生。
Base58的索引表在Base64索引表的基础上,去掉了4个容易混淆的字符和两个符号,分别是数字中的“0”,大写字母中的“O”和“I”,小写字母中的“l”,以及符号“+”和“/”,最终由58个字符构成了Base58的索引表。
Base58索引表
序号 | 字符 | 序号 | 字符 | 序号 | 字符 | 序号 | 字符 |
---|---|---|---|---|---|---|---|
0 | 1 | 16 | H | 32 | Z | 48 | q |
1 | 2 | 17 | J | 33 | a | 49 | r |
2 | 3 | 18 | K | 34 | b | 50 | s |
3 | 4 | 19 | L | 35 | c | 51 | t |
4 | 5 | 20 | M | 36 | d | 52 | u |
5 | 6 | 21 | N | 37 | e | 53 | v |
6 | 7 | 22 | P | 38 | f | 54 | w |
7 | 8 | 23 | Q | 39 | g | 55 | x |
8 | 9 | 24 | R | 40 | h | 56 | y |
9 | A | 25 | S | 41 | i | 57 | z |
10 | B | 26 | T | 42 | j | ||
11 | C | 27 | U | 43 | k | ||
12 | D | 28 | V | 44 | m | ||
13 | E | 29 | W | 45 | n | ||
14 | F | 30 | X | 46 | o | ||
15 | G | 31 | Y | 47 | p |
Base58的编码方式与Base32一样,采用了进制转换的方式进行编码,依然可以采用求商取余的方式用余数对照索引表进行编码,同样要注意的是在转换过程依然要遵循八位对齐的原则。
这里就不再对编码过程进行赘述。
0x0A Base62
Base62与上同理,索引表在Base64的基础上只是去掉了“+”和“/”两个符号,使用0~9,A~Z,a~z构成索引表。
base62索引表
序号 | 字符 | 序号 | 字符 | 序号 | 字符 | 序号 | 字符 |
---|---|---|---|---|---|---|---|
0 | 0 | 16 | G | 32 | W | 48 | m |
1 | 1 | 17 | H | 33 | X | 49 | n |
2 | 2 | 18 | I | 34 | Y | 50 | o |
3 | 3 | 19 | J | 35 | Z | 51 | p |
4 | 4 | 20 | K | 36 | a | 52 | q |
5 | 5 | 21 | L | 37 | b | 53 | r |
6 | 6 | 22 | M | 38 | c | 54 | s |
7 | 7 | 23 | N | 39 | d | 55 | t |
8 | 8 | 24 | O | 40 | e | 56 | u |
9 | 9 | 25 | P | 41 | f | 57 | v |
10 | A | 26 | Q | 42 | g | 58 | w |
11 | B | 27 | R | 43 | h | 59 | x |
12 | C | 28 | S | 44 | i | 60 | y |
13 | D | 29 | T | 45 | j | 61 | z |
14 | E | 30 | U | 46 | k | ||
15 | F | 31 | V | 47 | l |
编码方式也是采用求商取余参照索引表进行编码。
0x0B Base85、Base91和Base92
实在写不动了,最后三个一起说了,同样只是对索引表进行了增减,相应的索引表网上都能查到,编码方式与Base36的方式一致,只要了解Base36的编码方式,后面的编码方式基本就是换汤不换药了。
0x0C 总结
虽然经常使用Base家族进行编码,但是以前真的没有认真去了解过他们编码的原理和出现的原因,这几天花了一些时间去了解和学习之后,发现还挺有趣,所以写了这篇文章与各位分享学习,如有错误欢迎指正。
原文始发于微信公众号(乌托邦安全团队):Base编码家族的前世今生
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论