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 - MDN Web 文档术语表:Web 相关术语的定义 | MDN

例如,如果使用 Base64 编码 “Man”,则经历以下过程:

Encoding of the source string ⟨Man⟩ in Base64
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 - Wikipedia


那如何解码 Base64 呢?

假设我们得到了一串 Base64 编码的字符串,已知其原始数据是另一个字符串。

1
QUJD

我们将每个字符单独放置,以便解码:

1
2
3
4
Q
U
J
D

然后利用 Base64 编码表将各个字符映射为各自的索引 (Index)

1
2
3
4
16
20
9
3

如果无法找到可映射的字符,则直接放弃该字符。

接着将十进制索引转换为二进制:

1
2
3
4
00010000
00010100
00001001
00000011

由于 Base64 编码下的字符仅占 6bits,因此我们删去前面无意义的 00:

1
2
3
4
010000
010100
001001
000011

重新连接二进制:

1
010000010100001001000011

按一般字符的宽度 (8bits) 分解二进制:

1
2
3
01000001
01000010
01000011

按照 ASCII 表解码二进制:

1
2
3
A
B
C

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
2
*(_BYTE *)(v8 + a2) = '=';
*(_BYTE *)(++v8 + a2) = 61;

的伪代码,可认为使用了 Base64 编码。

3.3 移位拼接(含有 0x3F30xF 等)

1
2
3
*(v6 + a2) = off_529000[(a1[v12] >> 2) & 0x3F];
*(v7 + a2) = off_529000[(a1[v12 + 1] >> 4) & 0xF | v4];
*(v9 + a2) = off_529000[(a1[v12 + 2] >> 6) & 3 | v5];

4. Base64 的处理方法

4.1 标准 Base64 表

BUUCTF Reverse3

我们观察一下这个伪代码(有些函数已经重命名):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
printf("please enter the flag:", v7);
scanf("%20s", (char)Str);
v3 = j_strlen(Str);
v4 = (const char *)sub_4110BE(Str, v3, v14);
strncpy(Destination, v4, 0x28u);
v11 = j_strlen(Destination);
for ( j = 0; j < v11; ++j )
Destination[j] += j;
v5 = j_strlen(Destination);
if ( !strncmp(Destination, Str2, v5) )
printf("rigth flag!\n", v8);//揪个bug: "right"写错了!
else
printf("wrong flag!\n", v8);
return 0;

首先我们看到的是一个经典的 if..else,用来判断我们输入的字符串是否与 flag 相同。这里比较了两个字符串:DestinationStr2。哪一个代表 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
switch ( i )
{
case 1:
*((_BYTE *)v12 + v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
v5 = v4 + 1;
*((_BYTE *)v12 + v5) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
*((_BYTE *)v12 + ++v5) = aAbcdefghijklmn[64];
*((_BYTE *)v12 + ++v5) = aAbcdefghijklmn[64];
v4 = v5 + 1;
break;
case 2:
*((_BYTE *)v12 + v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
v6 = v4 + 1;
*((_BYTE *)v12 + v6) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
*((_BYTE *)v12 + ++v6) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | (4 * (byte_41A144[1] & 0xF))];
*((_BYTE *)v12 + ++v6) = aAbcdefghijklmn[64];
v4 = v6 + 1;
break;
case 3:
*((_BYTE *)v12 + v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
v7 = v4 + 1;
*((_BYTE *)v12 + v7) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
*((_BYTE *)v12 + ++v7) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | (4 * (byte_41A144[1] & 0xF))];
*((_BYTE *)v12 + ++v7) = aAbcdefghijklmn[byte_41A144[2] & 0x3F];
v4 = v7 + 1;
break;
}

由 3.3 知,这里用到了(类)Base64 加密。所以我们需要找到它的编码表。

幸运的是,我们在字符串窗口找到了程序 Base64 的编码表:

1
.rdata:00417B30	00000042	C	ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=

并且是标准的 Base64 编码表!

现在我们来解码 Str2。编码表里没有 @,说明 flag 在 Base64 加密后又经过了一次修改。参考 Destination,我们发现:

1
2
for ( j = 0; j < v11; ++j )
Destination[j] += j;

程序又对 flag 进行了一次加密:第 j+1 个字符处理后偏移为其 ASCII 码增加 j 个单位对应的字符。所以我们需要先撤销这个偏移操作,再进行 Base64 解码。

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void){
char flag[0x28] = "e3nifIH9b_C@n@dH";
for (int j = 0; j < 0x28; ++j)
flag[j] -= j;
printf("%s\n", flag);
getchar();
}

本来应该写 Python 脚本的,但由于目前学的是 C,所以就用 C 写了

编译运行,得到:

1
e2lfbDB2ZV95b3V9痫铐祀觊桤驽溷忉噙掭苒谫恇玟(

由于 Base64 的编码结果仅有 ASCII 字符,所以我们只取 e2lfbDB2ZV95b3V9 进行解码。

利用 Base64 Decode | Base64 Converter | Base64,得到:

1
{i_l0ve_you}

这才像一个 flag 嘛!


4.2 变形 Base64 表

BUUCTF 特殊的 BASE64

有了上面的经验,我们来看这个程序的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::string::string(&v8);
((void (__fastcall *)(char *))std::allocator<char>::allocator)(&v9);
std::string::string(&__rhs, "mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI==", &v9);
((void (__fastcall *)(char *))std::allocator<char>::~allocator)(&v9);
v3 = std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "Please input your flag!!!!");
((void (__fastcall *)(__int64))refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_)(v3);
std::operator>><char>(refptr__ZSt3cin, &v8);
std::string::string(v10, &v8);
base64Encode(&p_decode);
std::string::~string(v10);
if ( std::operator==<char>(&p_decode, &__rhs) )
v4 = std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "The flag is right!!!!!!!!!");
else
v4 = std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "This is a wrong flag!!!!!!!!");

这个程序是 C++ 的,因此会看到很多的 std::。但有了 C 语言的经验,我们大致能够得出 &__rhs 就是 flag 了,伪代码里也直接给了密文:

1
mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI==

拖到 Base64 解码器中,发现是乱码,说明使用的不是标准 Base64 编码表。

转到字符串窗口,找找编码表:

1
AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0987654321/+

和标准 Base64 编码表的对比:

1
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

由此我们可以写出解码器,将密文恢复到标准 Base64 的密文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>

int main(void)
{
char flag[100] = "mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI";
int length = strlen(flag);
char t1[70] = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0987654321/+"; // 修改后的Base64表
char t2[70] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // 标准Base64表
for (int i = 0; i < length; i++) // 遍历密文
{
for (int j = 0; j < 63; j++)
{
if (flag[i] == t1[j])
{
flag[i] = t2[j]; // 按标准表映射替换为标准密文
break;
}
}
}
printf("%s\n", flag);
getchar();
}

得到:

1
ZmxhZ3tTcGVjaWFsX0Jhc2U2NF9CeV9MaWNofQ

将恢复后的密文拖进标准 Base64 解码器,得到:

1
flag{Special_Base64_By_Lich}

有些网站也实现了自定义 Base64 编码表的加解密,如 锤子在线工具

4.3 多重加密

FZUCTF 2024 新生热身赛 [Week 2] REPLACEMENT

main 函数伪代码:

1
2
3
4
5
6
7
8
9
10
memset(Str, 0, sizeof(Str));
puts("input your answer:");
scanf("%s", Str);
replacement(Str);
v5 = strlen(Str2);
if ( !strncmp(Str, Str2, v5) )
puts("this is the right flag!");
else
puts("wrong flag");
return 0;

Str 是我们输入的字符串,Str2 是 flag 字符串。在和 Str2 比较前,Str 经历了 replacement() 函数的处理,所以 replacement() 函数是该函数的加密模块。

replacement 函数伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
len = strlen(Str);
encoded_len = 4 * ((len + 2) / 3);
v12 = encoded_len;
v1 = 16 * ((encoded_len + 16) / 0x10u);
v2 = alloca(v1);
v3 = alloca(v1);
p_encoded = (char (*)[])&v10;
index = 0;
func(Str);
for ( i = 0; i < len; i += 3 )
{
value = 0;
for ( j = 0; j <= 2; ++j )
{
value <<= 8;
if ( i + j < len )
value |= (unsigned __int8)Str[i + j];
}
v4 = index++;
*((_BYTE *)p_encoded + v4) = base64_table[(value >> 18) & 0x3F];
v5 = index++;
*((_BYTE *)p_encoded + v5) = base64_table[(value >> 12) & 0x3F];
v6 = index++;
if ( i + 1 >= len )
v7 = 61;
else
v7 = base64_table[(value >> 6) & 0x3F];
*((_BYTE *)p_encoded + v6) = v7;
v8 = index++;
if ( i + 2 >= len )
v9 = '=';
else
v9 = base64_table[value & 0x3F];
*((_BYTE *)p_encoded + v8) = v9;
}
*((_BYTE *)p_encoded + index) = 0;
strcpy(Str, (const char *)p_encoded);
fun(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __cdecl func(char *Str)
{
unsigned int i; // [esp+1Ch] [ebp-Ch]

for ( i = 0; i < strlen(Str); ++i )
{
if ( Str[i] == '0' )
Str[i] = 'O';
if ( Str[i] == '1' )
Str[i] = 'L';
if ( Str[i] == '3' )
Str[i] = 'E';
if ( Str[i] == '4' )
Str[i] = 'A';
}
}

发现 func() 对明文进行了如下加密:

1
0 -> O; 1 -> L; 3 -> E; 4 -> A

然后查看 fun() 的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __cdecl fun(char *str)
{
unsigned int i; // [esp+1Ch] [ebp-Ch]

for ( i = 0; i < strlen(str); ++i )
{
if ( str[i] <= '@' || str[i] > 'Z' )
{
if ( str[i] > '`' && str[i] <= 'z' )
str[i] -= ' ';
}
else
{
str[i] += ' ';
}
}
}

发现 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <ctype.h> // 包含字符处理函数的头文件

void toggleCase(char *str) {
for (int i = 0; str[i] != '\0'; i++) {
// 如果是大写字母,则转换为小写
if (str[i] >= 'A' && str[i] <= 'Z') {
str[i] = tolower(str[i]);
}
// 如果是小写字母,则转换为大写
else if (str[i] >= 'a' && str[i] <= 'z') {
str[i] = toupper(str[i]);
}
// 其他字符不变
}
}

int main() {
char str[100]; // 假设输入字符串的最大长度为99个字符

printf("请输入一个字符串: ");
fgets(str, sizeof(str), stdin); // 使用fgets读取字符串

toggleCase(str); // 调用函数进行大小写置换

printf("大小写置换后的字符串: %s", str); // 输出结果

return 0;
}

由 OpenAI GPT-4o mini 生成

1
大小写置换后的字符串: RkNURntIRUxMT19SRVBMQUNFTUVOVF9XT1JMRH0=

然后将解密后的字符串拖进 Base64 解码器,得到:

1
FCTF{HELLO_REPLACEMENT_WORLD}

接近正确的 flag 了!注意不要忘了 func() 的置换。最终得到的 flag 是:

1
FCTF{H3110_R3P14C3M3NT_W0R1D}

©2025-Present Watermelonabc | 萌ICP备20251229号

Powered by Hexo & Stellar latest & Vercel & 𝙌𝙞𝙪𝙙𝙪𝙣 𝘾𝘿𝙉 & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅 | 中文独立博客列表

猫猫🐱 发表了 41 篇文章 · 总计 209.8k 字