1. Base64 介绍及原理
Base64 是一种将二进制数据转换成文本 (binary-to-text) 的编码算法。
Base64 is a encoding algorithm that allows you to transform any characters into an alphabet which consists of Latin letters, digits, plus, and slash. Thanks to it, you can convert Chinese characters, emoji, and even images into a “readable” string, which can be saved or transferred anywhere.
——What is Base64? | Learn | Base64
通过 Base64 算法,我们可以将以一种低成本、高可靠性的方式传输非 ASCII 数据。
例如,对于一个 5x5 的图像,它的二进制数据表示为:
1 010001 110100 100101 000110 001110 000011 011101 100001 000000 010000 000000 000001 000000 001111 000000 000000 000000 001111 111100 000000 000000 000000 000000 000000 000000 000010 110000 000000 000000 000000 000000 000000 000000 010000 000000 000001 000000 000000 000000 000010 000000 100100 010000 000001 000000 000011 001011转换成 Base64 编码就变成了:
1 R0lGODdhAQABAPAAAP8AAAAAACwAAAAAAQABAAACAkQBADs
Base64 算法有几个版本。目前最常用的版本是 RFC 4648
接下来的表格是 RFC 4648 使用的 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 | / | |||
64 | Padding | = |
(来源:Base64 - Wikipedia)
一个字节占 8 bits,可以表示 256 种数据;一个 Base64 字符占 6bits ,可以表示 64 种数据。那么 Base64 要如何表示字节数据呢?
8 和 6 的最小公倍数是 24,因此 Base64 以 24bits,即 3 个字节或 4 个 Base64 字符为单位进行编码。若一个编码单位内字节数小于 3 个,则编码时填充 =
以形成 4 个 Base64 字符(称为对齐)。
这意味着字符串或文件的 Base64 版本通常比其原来的内容大三分之一左右(确切的大小增加取决于各种因素,如字符串的绝对长度、它除以 3 的长度余数,以及是否使用填充字符)。
例如,如果使用 Base64 编码 “Man”,则经历以下过程:
Source ASCII text |
Character | M | a | n | |||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Octets | 77 (0x4d) | 97 (0x61) | 110 (0x6e) | ||||||||||||||||||||||
Bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 0 | |
Base64 encoded |
Sextets | 19 | 22 | 5 | 46 | ||||||||||||||||||||
Character | T | W | F | u | |||||||||||||||||||||
Octets | 84 (0x54) | 87 (0x57) | 70 (0x46) | 117 (0x75) |
编码 “Ma” 时,则经历以下过程:
Source ASCII text |
Character | M | a | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Octets | 77 (0x4d) | 97 (0x61) | |||||||||||||||||||||||
Bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | |||||||
Base64 encoded |
Sextets | 19 | 22 | 4 | Padding | ||||||||||||||||||||
Character | T | W | E | = | |||||||||||||||||||||
Octets | 84 (0x54) | 87 (0x57) | 69 (0x45) | 61 (0x3D) |
那如何解码 Base64 呢?
假设我们得到了一串 Base64 编码的字符串,已知其原始数据是另一个字符串。
1 | QUJD |
我们将每个字符单独放置,以便解码:
1 | Q |
然后利用 Base64 编码表将各个字符映射为各自的索引 (Index):
1 | 16 |
如果无法找到可映射的字符,则直接放弃该字符。
接着将十进制索引转换为二进制:
1 | 00010000 |
由于 Base64 编码下的字符仅占 6bits,因此我们删去前面无意义的 00
:
1 | 010000 |
重新连接二进制:
1 | 010000010100001001000011 |
按一般字符的宽度 (8bits) 分解二进制:
1 | 01000001 |
按照 ASCII 表解码二进制:
1 | A |
2. Base64 的常见变形
2.1 修改编码表(换表)
将标准 Base64 的编码表换成另一张编码表。
将编码表换成标准 Base64 的编码表即可。
2.2 修改下标
例如将部分 Index 对应的字符打乱,使新表与标准 Base64 表有差别。
这时我们需要获得修改过的编码表,利用这张编码表映射索引,然后再用索引将原字符串恢复为标准 Base64 编码字符串,最后利用标准 Base64 解码字符串。
例如:Q
-> 0 ->A
->000000
-> …
2.3 多重加密
密文使用过标准 Base64 编码,但在加密时还使用了其他加密方式。一个表现是,只使用标准 Base64 解密密文,得到乱码或者不是最终 flag 的字符串。
这时需要一层一层破解加密,恢复到标准 Base64 密文。
3. Base64 的识别
3.1 常量表(索引表)
利用 IDA 的汇编窗口或字符串窗口,看到类似于:
1 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' |
的字符串,或者在加密模块的伪代码中看到 base64_table
等字样,可认为用到了 Base64 编码(或其变体)。
也可以使用 Edit >> Plugin >>Findcrypt 插件,选中加密部分的伪代码,获取编码表
3.2 Padding 计算
Base64 编码使用 =
填充不足部分,所以看到加密模块有像这样:
1 | *(_BYTE *)(v8 + a2) = '='; |
的伪代码,可认为使用了 Base64 编码。
3.3 移位拼接(含有 0x3F
、3
、0xF
等)
1 | *(v6 + a2) = off_529000[(a1[v12] >> 2) & 0x3F]; |
4. Base64 的处理方法
4.1 标准 Base64 表
BUUCTF Reverse3
我们观察一下这个伪代码(有些函数已经重命名):
1 | printf("please enter the flag:", v7); |
首先我们看到的是一个经典的 if..else
,用来判断我们输入的字符串是否与 flag 相同。这里比较了两个字符串:Destination
和 Str2
。哪一个代表 flag 呢?
往上面看。我们输入的字符串被存储在 Str
中,然后 v3
存储 Str
的长度。
接下来是重要的一步,Str
被一个不知名函数 sub_4110BE
处理,v4
指向处理后的字符串,然后 Strncpy
将字符串的前 0x28 个字节复制到 Destination
,所以 Destination
就是我们输入的字符串。换句话说,Str2
就是我们要的 flag。
当我们满怀信心,点击 Str2
时,我们得到的是:
1 | db 'e3nifIH9b_C@n@dH',0 |
e3nifIH9b_C@n@dH
是什么啊?这 “flag” 也不对啊!
一般来讲,CTF 中的 flag 应该是一串人类可辨认的字符串,但这怎么看都不像啊!这时我们就要考虑加密这件事了。
返回到 sub_4110BE
这个函数,既然人类可读的字符串经过它的处理有机会得到 Str2
,反过来,我们的 flag 是不是也是至少经过它的处理才变成这个样子的呢?
我们跳转到 sub_4110BE
最终实现的伪代码,然后可以看到:
1 | switch ( i ) |
由 3.3 知,这里用到了(类)Base64 加密。所以我们需要找到它的编码表。
幸运的是,我们在字符串窗口找到了程序 Base64 的编码表:
1 | .rdata:00417B30 00000042 C ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= |
并且是标准的 Base64 编码表!
现在我们来解码 Str2
。编码表里没有 @
,说明 flag 在 Base64 加密后又经过了一次修改。参考 Destination
,我们发现:
1 | for ( j = 0; j < v11; ++j ) |
程序又对 flag 进行了一次加密:第 j+1
个字符处理后偏移为其 ASCII 码增加 j
个单位对应的字符。所以我们需要先撤销这个偏移操作,再进行 Base64 解码。
1 |
|
本来应该写 Python 脚本的,但由于目前学的是 C,所以就用 C 写了
编译运行,得到:
1 | e2lfbDB2ZV95b3V9痫铐祀觊桤驽溷忉噙掭苒谫恇玟( |
由于 Base64 的编码结果仅有 ASCII 字符,所以我们只取 e2lfbDB2ZV95b3V9
进行解码。
利用 Base64 Decode | Base64 Converter | Base64,得到:
1 | {i_l0ve_you} |
这才像一个 flag 嘛!
4.2 变形 Base64 表
BUUCTF 特殊的 BASE64
有了上面的经验,我们来看这个程序的伪代码:
1 | std::string::string(&v8); |
这个程序是 C++ 的,因此会看到很多的 std::
。但有了 C 语言的经验,我们大致能够得出 &__rhs
就是 flag 了,伪代码里也直接给了密文:
1 | mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI== |
拖到 Base64 解码器中,发现是乱码,说明使用的不是标准 Base64 编码表。
转到字符串窗口,找找编码表:
1 | AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0987654321/+ |
和标准 Base64 编码表的对比:
1 | ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ |
由此我们可以写出解码器,将密文恢复到标准 Base64 的密文:
1 |
|
得到:
1 | ZmxhZ3tTcGVjaWFsX0Jhc2U2NF9CeV9MaWNofQ |
将恢复后的密文拖进标准 Base64 解码器,得到:
1 | flag{Special_Base64_By_Lich} |
有些网站也实现了自定义 Base64 编码表的加解密,如 锤子在线工具
4.3 多重加密
FZUCTF 2024 新生热身赛 [Week 2] REPLACEMENT
main
函数伪代码:
1 | memset(Str, 0, sizeof(Str)); |
Str
是我们输入的字符串,Str2
是 flag 字符串。在和 Str2
比较前,Str
经历了 replacement()
函数的处理,所以 replacement()
函数是该函数的加密模块。
replacement
函数伪代码:
1 | len = strlen(Str); |
很明显的 Base64 加密,这里也有 base64_table
的提示,所以点进 base64_table
看看:
1 | ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ |
是标准的 Base64 编码表!
所以我们找到 Str2
的内容:
1 | rKnurNTiruXmt19srvbmqunftuvovf9xt1jmrh0= |
拖进 Base64 解密器看看:
1 | �������_l����߶���q�X� |
寄!都是乱码!这说明 replacement()
还进行了其他的加密操作。
重新审查 replacement()
的伪代码,我们发现,除了大量的 Base64 加密操作,该函数的头尾还有两个自定义函数 func()
和 fun()
对明 / 密文进行了处理。
如果你使用 LLM 辅助阅读代码,应该会更快地注意到这一点。例如,使用 OpenAI GPT-4o mini 辅助阅读代码,GPT 为我指出了
func()
和fun()
的用途未知。
先看看 func()
的伪代码:
1 | void __cdecl func(char *Str) |
发现 func()
对明文进行了如下加密:
1 | 0 -> O; 1 -> L; 3 -> E; 4 -> A |
然后查看 fun()
的伪代码:
1 | void __cdecl fun(char *str) |
发现 fun()
对密文又进行了一次 ASCII 码的偏移操作。效果是对密文中的所有英文字母进行大小写置换。
这里我让 GPT 代劳了:
1
2
3
4 The function effectively toggles the case of alphabetic characters in the string:
- Uppercase letters ('A' to 'Z') are converted to lowercase ('a' to 'z').
- Lowercase letters ('a' to 'z') are converted to uppercase ('A' to 'Z').
- Non-alphabetic characters remain unchanged.
因此,我们需要将 Str2
所给密文的所有英文字母进行大小写置换,恢复成标准 Base64 加密的密文:
1 |
|
由 OpenAI GPT-4o mini 生成
1 | 大小写置换后的字符串: RkNURntIRUxMT19SRVBMQUNFTUVOVF9XT1JMRH0= |
然后将解密后的字符串拖进 Base64 解码器,得到:
1 | FCTF{HELLO_REPLACEMENT_WORLD} |
接近正确的 flag 了!注意不要忘了 func()
的置换。最终得到的 flag 是:
1 | FCTF{H3110_R3P14C3M3NT_W0R1D} |