Base编码家族的前世今生

admin 2022年11月17日09:47:32评论60 views字数 5925阅读19分45秒阅读模式

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。

Base编码家族的前世今生

使用求商取余的方式计算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。

为验证最终结果,我使用工具也进行了编码测试,最终结果与我们得到的结果一致。

Base编码家族的前世今生

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编码家族的前世今生

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年11月17日09:47:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Base编码家族的前世今生https://cn-sec.com/archives/1411275.html

发表评论

匿名网友 填写信息