Base64 编码
- 1 视频也可以含恶意代码吗
- 2 Frida 入门
- 3 虚拟机保护
- 4 C#逆向入门
- 5 安卓刷机一点通
- 6 Z3-Solver教程
- 7 出题人的“恶意”
- 8 编译器为我做了什么
- 9 CSAPP Lab2 Note
- 10 Hash系列算法
- 11 程序脱壳
- 12 安卓逆向介绍
- 13 IDA类型修复
- 14 IDA疑难杂症
- 15 花指令
- 16 反动态调试技术与应对手段
- 17 AES加密算法
- 18 如何解决魔改后的加密算法
- 19 常见加密算法识别与变种分析
- 20 TEA 加密算法
- 21 RC4 加密算法
- 22 Base64 编码 本文
- 23 IDA Dynamic Debugging
- 24 IDA Usage With Reverse Basic
1. Base64介绍及原理
Base64是一种将二进制数据转换成文本 (binary-to-text) 的编码算法。
早期的信息传输系统只支持文本,如果我们想传输音频、图片、视频等非 ASCII 文本文件,最粗糙的方法就是直接传二进制序列。可以想见,这样单次传输的文本量会非常大,因为一个 bit 写在文本里就占了一个 byte,相当于大小翻了 8 倍!并且中间损坏的概率也很高。
通过 Base64 算法,我们可以将以一种低成本、高可靠性的方式传输非 ASCII 数据。
例如,对于一个 5x5 的图像,它的二进制数据表示为:
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 编码就变成了:
R0lGODdhAQABAPAAAP8AAAAAACwAAAAAAQABAAACAkQBADsBase64 算法有几个版本。目前最常用的版本是 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 字符占 6 bits ,可以表示 64 种数据。那么 Base64 要如何表示字节数据呢?
8 和 6 的最小公倍数是 24,因此 Base64 以 24 bits,即 3 个字节或 4 个 Base64 字符为单位进行编码。若一个编码单位内字节数小于 3 个,则编码时填充 = 以形成 4 个 Base64 字符(称为对齐)。
这意味着字符串或文件的 Base64 版本通常比其原来的内容大三分之一左右(确切的大小增加取决于各种因素,如字符串的绝对长度、它除以 3 的长度余数,以及是否使用填充字符)。
——Base64 - MDN Web 文档术语表:Web 相关术语的定义 | MDN
虽然比原始二进制流大了三分之一,但和直接写二进制流翻 8 倍相比,成本还是大大降低了。
例如,如果使用 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编码的字符串,已知其原始数据是另一个字符串。
QUJD我们将每个字符单独放置,以便解码:
QUJD然后利用Base64编码表将各个字符映射为各自的索引(Index):
162093如果无法找到可映射的字符,则直接放弃该字符。
接着将十进制索引转换为二进制:
00010000000101000000100100000011由于Base64编码下的字符仅占6bits,因此我们删去前面无意义的00:
010000010100001001000011重新连接二进制:
010000010100001001000011按一般字符的宽度(8bits)分解二进制:
010000010100001001000011按照ASCII表解码二进制:
ABC2. 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的汇编窗口或字符串窗口,看到类似于:
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'的字符串,或者在加密模块的伪代码中看到base64_table等字样,可认为用到了Base64编码(或其变体)。
也可以使用Edit >> Plugin >>Findcrypt插件,选中加密部分的伪代码,获取编码表
3.2 Padding 计算
Base64编码使用=填充不足部分,所以看到加密模块有像这样:
*(_BYTE *)(v8 + a2) = '=';*(_BYTE *)(++v8 + a2) = 61;的伪代码,可认为使用了Base64编码。
3.3 移位拼接(含有0x3F、3、0xF等)
*(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
我们观察一下这个伪代码(有些函数已经重命名):
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相同。这里比较了两个字符串:Destination和Str2。哪一个代表flag呢?
往上面看。我们输入的字符串被存储在Str中,然后v3存储Str的长度。
接下来是重要的一步,Str被一个不知名函数sub_4110BE处理,v4指向处理后的字符串,然后Strncpy将字符串的前0x28个字节复制到Destination,所以Destination就是我们输入的字符串。换句话说,Str2就是我们要的flag。
当我们满怀信心,点击Str2时,我们得到的是:
db 'e3nifIH9b_C@n@dH',0e3nifIH9b_C@n@dH是什么啊?这“flag”也不对啊!
一般来讲,CTF中的flag应该是一串人类可辨认的字符串,但这怎么看都不像啊!这时我们就要考虑加密这件事了。
返回到sub_4110BE这个函数,既然人类可读的字符串经过它的处理有机会得到Str2,反过来,我们的flag是不是也是至少经过它的处理才变成这个样子的呢?
我们跳转到sub_4110BE最终实现的伪代码,然后可以看到:
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的编码表:
.rdata:00417B30 00000042 C ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=并且是标准的Base64编码表!
现在我们来解码Str2。编码表里没有@,说明flag在Base64加密后又经过了一次修改。参考Destination,我们发现:
for ( j = 0; j < v11; ++j ) Destination[j] += j;程序又对flag进行了一次加密:第j+1个字符处理后偏移为其ASCII码增加j个单位对应的字符。所以我们需要先撤销这个偏移操作,再进行Base64解码。
#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写了
编译运行,得到:
e2lfbDB2ZV95b3V9痫铐祀觊桤驽溷忉噙掭苒谫恇玟(由于Base64的编码结果仅有ASCII字符,所以我们只取e2lfbDB2ZV95b3V9进行解码。
利用Base64 Decode | Base64 Converter | Base64,得到:
{i_l0ve_you}这才像一个flag嘛!
4.2 变形Base64表
BUUCTF 特殊的BASE64
有了上面的经验,我们来看这个程序的伪代码:
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了,伪代码里也直接给了密文:
mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI==拖到Base64解码器中,发现是乱码,说明使用的不是标准Base64编码表。
转到字符串窗口,找找编码表:
AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0987654321/+和标准Base64编码表的对比:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/由此我们可以写出解码器,将密文恢复到标准Base64的密文:
#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();}得到:
ZmxhZ3tTcGVjaWFsX0Jhc2U2NF9CeV9MaWNofQ将恢复后的密文拖进标准Base64解码器,得到:
flag{Special_Base64_By_Lich}有些网站也实现了自定义Base64编码表的加解密,如 锤子在线工具
4.3 多重加密
FZUCTF 2024新生热身赛 [Week 2]REPLACEMENT
main函数伪代码:
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函数伪代码:
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看看:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/是标准的Base64编码表!
所以我们找到Str2的内容:
rKnurNTiruXmt19srvbmqunftuvovf9xt1jmrh0=拖进Base64解密器看看:
�������_l����߶���q�X�寄!都是乱码!这说明replacement()还进行了其他的加密操作。
重新审查replacement()的伪代码,我们发现,除了大量的Base64加密操作,该函数的头尾还有两个自定义函数func()和fun()对明/密文进行了处理。
如果你使用LLM辅助阅读代码,应该会更快地注意到这一点。例如,使用OpenAI GPT-4o mini辅助阅读代码,GPT为我指出了
func()和fun()的用途未知。
先看看func()的伪代码:
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()对明文进行了如下加密:
0 -> O; 1 -> L; 3 -> E; 4 -> A然后查看fun()的伪代码:
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代劳了:
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加密的密文:
#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生成
大小写置换后的字符串: RkNURntIRUxMT19SRVBMQUNFTUVOVF9XT1JMRH0=然后将解密后的字符串拖进Base64解码器,得到:
FCTF{HELLO_REPLACEMENT_WORLD}接近正确的flag了!注意不要忘了func()的置换。最终得到的flag是:
FCTF{H3110_R3P14C3M3NT_W0R1D}文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!