base64加密算法逆向分析


简介

base64编码根据编码表将一段二进制数据映射成64个可显示字母和数字组成的字符集合,主要用于传送图形、声音等非文本数据。

标准 base64 编码表

编码原理

原理

下面我们通过将明文 “abc” 进行 base64 编码来讲解 base64 编码原理。

1.首先将明文每三个字节分为一组,每个字节8bit,共24bit。

2.将24bit划分为四组,每组6bit,4组共24bit

3.将每组用0补齐为8bit,4组共32bit。

黄色部分就是补齐的0。

将补齐后的二进制数的十进制值作为编码表的下标获取编码表中的对应值。

编码结果就是 “YWJj”

如果编码后的字符不是4的倍数,后面用 “=” 填充补齐。

代码实现

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//定义一个常量指针指向一个常量字符串
const char * const table="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

//data是用于指向需要编码的常量数据的指针
//base64指针指向一块内存用于存储编码后的basse64字符串
//length是输入数据的长度
char * encode( const unsigned char * data, char * base64, int length )
{
//用于遍历输入数据和base64字符
int i, j;

//用于存储当前处理的字符
unsigned char current;

//循环遍历输入数据,每次处理3个字节
for ( i = 0, j = 0 ; i < length ; i += 3 ){

//获取第一个字节的前六位
current = (data[i] >> 2) ;

//与0x3f进行按位与操作,取字节前6位
//0x3f=00111111
current &= (unsigned char)0x3F;

//使用编码表数组将结果映射为base64字符,并将结果存储在输出指针中
base64[j++] = table[(int)current];

//将第一个字节左移4位,与0x30按位与,取其后2位
//0x30=00110000
current = ( (unsigned char)(data[i] << 4 ) ) & ( (unsigned char)0x30 ) ;


//如果没有第二个(即输入长度不足2个字节),则直接填充“=”字母并返回
if ( i + 1 >= length )
{
base64[j++] = table[(int)current];
base64[j++] = '=';
base64[j++] = '=';
break;
}

//第二个字节右移4位,取其前4位,与之前第一个字节后两位合并
//00110000
//或
//00001111
current |= ( (unsigned char)(data[i + 1] >> 4) ) & ( (unsigned char) 0x0F );

//将结果映射到base64编码表,并存储到返回指针
base64[j++] = table[(int)current];

//将第二个字节左移2位,与0x3c按位与取其后四位
//00111100
current = ( (unsigned char)(data[i + 1] << 2) ) & ( (unsigned char)0x3C ) ;

//如果没有第三个字节,则填充=号并返回
if ( i + 2 >= length )
{
base64[j++] = table[(int)current];
base64[j++] = '=';
break;
}

//将第三个字节右移6位,取其前2位,与之前的结果合并
current |= ( (unsigned char)(data[i + 2] >> 6) ) & ( (unsigned char) 0x03 );

//将结果映射到编码表,并存储到返回指针
base64[j++] = table[(int)current];

//将第三个字节与0x3f进行按位与操作,取其后六位
current = ( (unsigned char)data[i + 2] ) & ( (unsigned char)0x3F ) ;

//将结果转换为base64字符,并存储到返回指针
base64[j++] = table[(int)current];
}

//拼接上字符串结束字符
base64[j] = '\0';

//将存储密文的指针返回
return base64;
}

解码原理

原理

解码就是编码的逆过程。

获取密文 “YWJI” 每一个字符在 base64 编码表中的下标。

然后将这些下标对应的值的二进制值连接起来,重新划分为8位为一组。

之后转换为每字节对应的ASCII码即可得到编码的内容。

在CTF比赛中一般都是以python编写解密脚本。

代码实现

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//参数1:base64密文
//参数2:解密后的明文

int decode( const char * base64, unsigned char * data )
{
int i, j;
//用于遍历base64编码表的变量
unsigned char k;

//用于存储遍历的字节
unsigned char temp[4];

//循环遍历base64字符串,一次遍历4个字节
for ( i = 0, j = 0; base64[i] != '\0' ; i += 4 )
{

memset( temp, 0xFF, sizeof(temp) );
for ( k = 0 ; k < 64 ; k ++ )
{
if ( table[k] == base64[i] )
temp[0] = k;
}
for ( k = 0 ; k < 64 ; k ++ )
{
if ( table[k] == base64[i + 1] )
temp[1] = k;
}
for ( k = 0 ; k < 64 ; k ++ )
{
if ( table[k] == base64[i + 2] )
temp[2] = k;
}
for ( k = 0 ; k < 64 ; k ++ )
{
if ( table[k] == base64[i + 3] )
temp[3] = k;
}

//将密文的前两个字符解码为原始数据的第一个字节
data[j++] = ((unsigned char)(((unsigned char)(temp[0] << 2)) & 0xFC)) |
((unsigned char)((unsigned char)(temp[1] >> 4) & 0x03));

//如果第三个字节为=号填充符,就返回
if ( base64[i + 2] == '=' )
break;
//将密文的第二个字符和第三个字符解码为原始数据的第二个字节
data[j++] = ((unsigned char)(((unsigned char)(temp[1] << 4)) & 0xF0)) |
((unsigned char)((unsigned char)(temp[2] >> 2) & 0x0F));

//如果第四个字节为=号填充符,就返回
if ( base64[i + 3] == '=' )
break;

//将密文的第三个字符和第四个字符解码为原始数据的第三个字节
data[j++] = ((unsigned char)(((unsigned char)(temp[2] << 6)) & 0xF0)) |
((unsigned char)(temp[3] & 0x3F));
}
//返回解码后的数据长度
return j;
}

逆向中的base64

特征识别

  • 字符串编码表识别
  • 加密填充符识别(一般为=号)
  • bit:3 * 8变4 * 6
  • 输入参数会被移位拼接,移位位数为 2、4、6位,将3字节拆成4字节
  • 理解编码原理,编码时通常都会用3个字节一组来处理比特位数据,这些特征都可以用来分析识别。

移位运算中左移1位等于乘以2,右移1位等于除以2


常规魔改:编码表(TLS、SMC等各种反调试位置)

魔改

1.修改编码表
2.修改下标
将base64的编码查表下标对应关系修改,对于这种修改,我们只需要推导出下标逆计算即可。

例题

常规

BUUCTF-reverse3

拿到程序查壳,发现无壳

之后运行一下,随意输入一串字符
判定为字符串比较

ida打开分析一下,发现主函数中只调用了一个main_0函数

进入查看

先将函数和变量改一下名,提高代码可读性。

·sub_41132f用于显示字符串,判断为printf
利用快捷键n改名为printf

sub_411375根据其后参数判定为scanf,用于向str数组输入最多20个字符。
将其函数改为scanf,将str数组改为input。
j_strlen函数判断为获取字符串长度的函数strlen,将获取结果的变量v3改为input_length便于阅读。

可以看到函数sub_4110be对我们输入的数据进行了处理
无法判断函数sub_4110be的功能,所以进入查看。

发现调用了一个函数,继续进入查看

分析主要代码逻辑

进行了很多移位拼接操作,很明显的base64加密

更简单的方法是打开字符串窗口查看一下

发现base64编码表字符串,判断为base64加密

所以sub_4110be为加密函数,变量v4为返回的密文。
根据逻辑重命名一下

接下来继续分析

strncmp函数将密文复制0x28个字节到destination中

接下来计算密文长度

然后对密文做了移位运算

然后获取运算后的密文长度

通过strncmp函数对密文和str2变量比较密文长短的内容

如果相等则输入flag正确

所以str2就是加密后的密文

我们通过密文和加密逻辑逆向构造exp。

exp
通过python的base64库来构造解密exp

1
2
3
4
5
6
7
8
9
10
11
import base64

#密文
s="e3nifIH9b_C@n@dH"
flag=""

for i in range(len(str)):
flag+=chr(ord(str[i])-i)

print(base64.b64decode(flag))
#flag{i_l0ve_you}

换表

BUUCTF 特殊的 BASE64

原理
换表就是将映射的编码表修改掉,但是加密过程是仍然不变的。
前面讲过base64编码最关键的点在于根据值取编码表中的下标对应的字符。
魔改编码表同样如此,所以我们可以获取密文在魔改编码表中的下标。然后获取下标在原编码表中的值之后。
再进行常规解密。

拿到程序后一般流程查壳,发现无壳

ida打开分析,发现为c++程序

先查看一下字符串表

第一行是很明显的base64编码字符串

选中那行可能就是魔改后的编码表

分析main函数代码逻辑

根据前面发现的魔改编码表则使用常规解密的方法一定是不行的,所以我们必须换种方法。

在构造exp之前还是先看一下加密函数

分析base64加密函数

发现在初始化a1的时候用的是unk_489084,进入查看一下

发现为字符串窗口查看到的字符串,则确实为换表加密。

根据逻辑构造exp

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import base64
import string

#密文
enc = "mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI=="

#魔改后的编码表
table1 = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0987654321/+"

#原编码表
table2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

#将密文映射为原编码表
a=enc.translate(str.maketrans(table1,table2))

print(base64.b64decode(a).decode())

#flag{Special_Base64_By_Lich}

后言

参考链接:彻底弄懂base64的编码与解码原理 - 掘金 (juejin.cn)
参考链接:reverse逆向算法之base64和RC4_base64”和 rc4-CSDN博客