题好难…

Reverse

ezre

当时分析这个题不知道这是SM4,后来才知道就是一个标准的SM4.

以后遇到不认识的加密首先尝试去查找特征值!

这个题的真正逻辑不在main中,后来经分析貌似是一个子进程调试父进程的玩意…

检查逻辑在这里:

image-20240206101302873

进入true_check0_3580()中(我自己重命名的函数名),可以看到加密过程:

image-20240206101410864

实际上就是一个ECB-NoPadding模式的纯SM4…

其中v8是16byte的key;v7是加密后的32byte的密文.

提取出数据后写代码整理:

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

int main() {
unsigned long long v7[4];
v7[0] = 0x7C88631647197506LL; // enc,32bytes
v7[1] = 0x4A0D7D3FFF55668BLL;
v7[2] = 0xDEC2E93F384ED2F5LL;
v7[3] = 0x3C1FB1746F7F7CDBLL;
unsigned long long v8[2];
v8[0] = 0xEFCDAB8967452301LL; // key,16bytes
v8[1] = 0xEFCDAB8967452301LL;
unsigned char *ptr = (unsigned char *) v8;
for(int i=0;i<sizeof(v8);++i)
printf("%02x",ptr[i]);
putchar('\n');
ptr=(unsigned char *)v7;
for(int i=0;i<sizeof(v7);++i)
printf("%02x",ptr[i]);
return 0;
}

输出:

1
2
0123456789abcdef0123456789abcdef
067519471663887c8b6655ff3f7d0d4af5d24e383fe9c2dedb7c7f6f74b11f3c

直接在线解密即可:

image-20240206101824721

强网先锋-ezre

两人合作做出来的…

其中

分析过程比较费劲,而且实际上完全没有必要…

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
// main函数的分析
__int64 __fastcall main(int a1, char **a2, char **a3) {
int v3; // eax
size_t v4; // rsi
int v5; // eax
int k; // [rsp+12Ch] [rbp-114h]
int j; // [rsp+130h] [rbp-110h]
int i; // [rsp+134h] [rbp-10Ch]
int v10; // [rsp+13Ch] [rbp-104h]
char v11[64]; // [rsp+140h] [rbp-100h] BYREF
char v12[64]; // [rsp+180h] [rbp-C0h] BYREF
char v13[64]; // [rsp+1C0h] [rbp-80h] BYREF
char s[52]; // [rsp+200h] [rbp-40h] BYREF
int v15; // [rsp+234h] [rbp-Ch]
size_t v16; // [rsp+238h] [rbp-8h]

v15 = 0;
printf("Welcome to the CTF world:");
memset(s, 0, 0x32uLL);
__isoc99_scanf("%s", s);
v16 = strlen(s);
if (v16 != 34) {
printf("Wrong!");
exit(-1);
}
v3 = strlen(s);
v10 = 0;
sub_401980((__int64)s, (__int64)v11, v3); // 先对flag进行一次base64变表
while (v10 < 4) // 4次循环
{
srand(aLUsn4j5rfj0tav[2]);
v4 = strlen((const char *)(unsigned int)aLUsn4j5rfj0tav);
sub_401D10(aLUsn4j5rfj0tav, v4); // 对表干了些事情,总之是生成新的表
if ((v10 & 1) != 0) // v10 是循环变量,从0到3
// 此时当v10为奇数 1或3时
{
v5 = strlen(v11); // v11是变表base64加密一次后的flag
sub_401980((__int64)v11, (__int64)v12, v5);// 用新表进行再一次base64
} else {
sub_401250(v11, v12); // 否则执行这个加密,已分析,加密解释见内部注释
// 本质上就是将每个字符求得其在表中的下标,只需要6个二进制位来存储,然后压缩为3个字节
}
memset(v11, 0, 0x32uLL);
memcpy(v11, v12, 0x32uLL);
++v10;
}
if (check_debug == 1) // 这个if用的变量怀疑有反调试,
// 如果检测到调试就会改变base64表的处理方式,
// 导致最后一次处理发生变化!
{
sub_402EE0(aLUsn4j5rfj0tav, &aLUsn4j5rfj0tav[64]);
for (i = 0; i < 64; ++i)
aLUsn4j5rfj0tav[i] = (5 * (aLUsn4j5rfj0tav[i] + 3)) ^ 0x15;
} else { // 这个处理才是正确的流程
for (j = 0; j < 64; ++j)
aLUsn4j5rfj0tav[j] ^= 0x27u;
}
sub_401EB0(v12, v13); // 使用base64表进行某种加密
for (k = 0; ; ++k) {
if (k >= strlen(v12)) {
printf("right!");
return 0;
}
if (aKqhfyc[k] != v13[k])
break;
}
printf("wrong!");
return 0;
}

接下来就是该死的sub_401250(),后来发现这玩意就是一个标准至极的base64_decode()…只不过变表了而已…

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
82
83
84
// 分析sub_401250()
__int64 __fastcall sub_401250(__int64 str, __int64 res)
{
int v2; // edx
int v3; // edx
int v4; // eax
unsigned __int8 idx_1; // [rsp+93h] [rbp-1Dh] BYREF
unsigned __int8 idx_2; // [rsp+94h] [rbp-1Ch]
unsigned __int8 idx_3; // [rsp+95h] [rbp-1Bh]
unsigned __int8 idx_4; // [rsp+96h] [rbp-1Ah]
unsigned __int8 i; // [rsp+97h] [rbp-19h]
unsigned int res_idx; // [rsp+98h] [rbp-18h]
int str_idx; // [rsp+9Ch] [rbp-14h]
__int64 res_p; // [rsp+A0h] [rbp-10h]
__int64 str_p; // [rsp+A8h] [rbp-8h]

str_p = str;
res_p = res;
str_idx = 0; // 遍历str用的下标
res_idx = 0; // 用于访问res的下标,指向res中的已写入字符的后一个空位(即准备存储下一个加密后字符的位置)
while ( *(_BYTE *)(str_p + str_idx) ) // 遍历源str,4个字符一组
{
memset(&idx_1, 0, 4uLL);
for ( i = 0; i < 64u; ++i ) // 遍历base64表(注意这个表会发生变化)
{
if ( *(char *)(str_p + str_idx) == aLUsn4j5rfj0tav[i] )
{
idx_1 = i; // 找到str[str_idx]这个字符在表中的下标i,记录在idx_1中
break;
}
}
for ( i = 0; i < 64u; ++i )
{
if ( *(char *)(str_p + str_idx + 1) == aLUsn4j5rfj0tav[i] )
{
idx_2 = i; // 找到str[str_idx+1]这个字符在表中的下标i,记录在idx_2中
break;
}
}
v2 = res_idx++;
*(_BYTE *)(res_p + v2) = ((int)idx_2 >> 4) & 3 | (4 * idx_1);// 分析:0x40(即表长)需要6个二进制位去存,所以不考虑最高的2位(都是0),剩下丢失的位会在后面的操作中补上,不会丢失信息
// 具体操作如下:
// 假设idx_2=(二进制)00111001;idx_1=(二进制)00100011;
// 1.将idx_2右移4位丢弃低4位(1001),然后&3丢弃高2位(00),也就是说将这6个二进制位的高2位置于结果的低2位,即00000011
// 2.将idx_1乘4即左移2位,结果为(10001100),也就是
// 3.两者进行按位或,即相加,结果为(10001111)
// 也就是结果将idx_1的6位和idx_2的高2位进行组合,生成一个新的字节,idx_1无信息丢失,idx_2待补全
if ( *(_BYTE *)(str_p + str_idx + 2) == '=' )// 如果当前分组的第3个字符(?)为'='说明base编码遍历结束?,则停止遍历
break;
for ( i = 0; i < 64u; ++i )
{
if ( *(char *)(str_p + str_idx + 2) == aLUsn4j5rfj0tav[i] )
{
idx_3 = i; // 找到str[str_idx+2]这个字符在表中的下标i,记录在idx_3中
break;
}
}
v3 = res_idx++;
*(_BYTE *)(res_p + v3) = ((int)idx_3 >> 2) & 0xF | (16 * idx_2);// 仍然假设idx_2=(二进制)00111001;
// 假设idx_3=(二进制)00100011;
// 1.idx_3右移2位,与0xF,只保留原来中间的4位,放在结果的低4位
// 2.idx_2左移4位,丢弃高2位,保留低4位,放在结果的高4位
// 3.结果合并
// 此时前面idx_2舍弃的低4位存储在此处,没有信息损失
if ( *(_BYTE *)(str_p + str_idx + 3) == 61 )// 如果当前分组的第4个字符(?)为'='说明base编码遍历结束?,则停止遍历
break;
for ( i = 0; i < 64u; ++i )
{
if ( *(char *)(str_p + str_idx + 3) == aLUsn4j5rfj0tav[i] )
{
idx_4 = i; // 找到str[str_idx+3]这个字符在表中的下标i,记录在idx_4中
break;
}
}
v4 = res_idx++;
*(_BYTE *)(res_p + v4) = idx_4 & 0x3F | (idx_3 << 6);// 假设idx_3=(二进制)00100011;idx_4=00111001;
// 1.idx_4按位与0x3F(00111111),保留有效6位
// 2.idx_3左移6位,保留原来的低2位,存于结果的高2位
// 3.合并,此时idx_3补齐,idx_4无信息损失
str_idx += 4; // 循环下标+=4,准备下一组处理
} // 注意:结尾有'=',则说明填充为8个bit的0,对应前面未补全的字符则为
*(_BYTE *)(res_p + (int)res_idx) = 0; // 补齐结尾'\0'
return res_idx;
}

整个加密主要就是变表的base64,动调跑出来所有用到的base64表:

1
2
3
4
5
"l+USN4J5Rfj0TaVOcnzXiPGZIBpoAExuQtHyKD692hwmqe7/Mgk8v1sdCW3bYFLr",
"FGseVD3ibtHWR1czhLnUfJK6SEZ2OyPAIpQoqgY0w49u+7rad5CxljMXvNTBkm/8",
"Hc0xwuZmy3DpQnSgj2LhUtrlVvNYks+BX/MOoETaKqR4eb9WF8ICGzf6id1P75JA",
"pnHQwlAveo4DhGg1jE3SsIqJ2mrzxCiNb+Mf0YVd5L8c97/WkOTtuKFZyRBUPX6a",
"plxXOZtaiUneJIhk7qSYEjD1Km94o0FTu52VQgNL3vCBH8zsA/b+dycGPRMwWfr6"

最后的那个sub_401EB0(v12,v13);需要写出逆函数,分析过程没了,总之就是使用最后的base64表做一个处理,生成一个新的表,然后用它来把数据做最后一次加密.

但是其中用求模等等的运算,后来还是想办法逆出来了.

所以总的加密过程就是:

  • 将flag做一次base64编码.
  • 然后交替的进行base64解码和编码各2次,一共4次.
  • 最后做一次sub_401EB0()加密.

其中base64表就是用上面提取出来的表.

解密过程就反向即可,首先是对最后一个加密函数sub_401EB0()的逆变换:

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
#include <iostream>
#include <string>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <stdint.h>

int main() {
unsigned char aKqhfyc[48] = {
0x3A, 0x2C, 0x4B, 0x51, 0x68, 0x46, 0x59, 0x63, 0x24, 0x04,
0x5E, 0x5F, 0x00, 0x0C, 0x2B, 0x03, 0x29, 0x5C, 0x74, 0x70,
0x6A, 0x62, 0x7F, 0x3D, 0x2C, 0x4E, 0x6F, 0x13, 0x06, 0x0D,
0x06, 0x0C, 0x4D, 0x56, 0x0F, 0x28, 0x4D, 0x51, 0x76, 0x70,
0x2B, 0x05, 0x51, 0x68, 0x48, 0x55, 0x24, 0x19
};

// 构建v7_arr
int v7 = 2023;
int v7_arr[100];
for (int v6 = 0; v6 < 48; ++v6) {
if (v6 % 3 == 1) {
v7 = (v7 + 5) % 20;
v7_arr[v6] = v7;
} else if (v6 % 3 == 2) {
v7 = (v7 + 7) % 19;
v7_arr[v6] = v7;
} else {
v7 = (v7 + 3) % 17;
v7_arr[v6] = v7;
}
}

char base64_table[] = "plxXOZtaiUneJIhk7qSYEjD1Km94o0FTu52VQgNL3vCBH8zsA/b+dycGPRMwWfr6";
for (int i = 0; i < 64; ++i)
base64_table[i] ^= 0x27;

char v5[30]; // base表提取
strncpy(v5, &base64_table[6], 21);
printf("table:\n");
for (int i = 0; i < 21; ++i)
printf("%d,", v5[i]);
char v3;
for (int i = 46; i >= 0; --i) {
if (i % 3 == 1) {
v3 = v5[v7_arr[i] + 1];
} else if (i % 3 == 2) {
v3 = v5[v7_arr[i] + 2];
} else {
v3 = v5[v7_arr[i] + 3];
}
aKqhfyc[i + 1] ^= aKqhfyc[i];
aKqhfyc[i] ^= v3;
}
printf("\nenc(last):\n");
for (int i = 0; i < 48; ++i)
// putchar(aKqhfyc[i]);
printf("%c", aKqhfyc[i]);
// 得到的结果为:
// WZqSWcUtWBLlOriEfcajWBSRstLlkEfFWR7j/R7dMCDGnp==
return 0;
}

然后将该结果依次进行对应的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
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
import binascii
import urllib.parse
import base64

def base64_decode(enc='', new_table='') -> bytes:
# base64编码为可见字符
# 解码结果为bytes类型---因为可能是对字节流进行base64编码
if new_table == '':
print('basic base64 decode!')
return base64.b64decode(enc)
else:
print('base64 decode with new_table: ' + new_table)
orignal_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
dec = base64.b64decode(enc.translate(str.maketrans(new_table, orignal_table)))
return dec


def base64_encode(src=b'', new_table='') -> str:
# 待编码的数据为bytes类型,因为可能是对字节流进行base64编码
# 编码结果为str类型---因为base64编码后的数据为可见字符
if new_table == b'':
print('basic base64 encode!')
return base64.b64encode(src).decode()
else:
print('base64 encode with new_table: ' + new_table)
orignal_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
enc = base64.b64encode(src).decode().translate(str.maketrans(orignal_table, new_table))
return enc


def run_decode():
# 动调得到的表
fir_table = "l+USN4J5Rfj0TaVOcnzXiPGZIBpoAExuQtHyKD692hwmqe7/Mgk8v1sdCW3bYFLr"
tables = [
"FGseVD3ibtHWR1czhLnUfJK6SEZ2OyPAIpQoqgY0w49u+7rad5CxljMXvNTBkm/8",
"Hc0xwuZmy3DpQnSgj2LhUtrlVvNYks+BX/MOoETaKqR4eb9WF8ICGzf6id1P75JA",
"pnHQwlAveo4DhGg1jE3SsIqJ2mrzxCiNb+Mf0YVd5L8c97/WkOTtuKFZyRBUPX6a",
"plxXOZtaiUneJIhk7qSYEjD1Km94o0FTu52VQgNL3vCBH8zsA/b+dycGPRMwWfr6"
]
# 将最后一个加密的逆生成的结果复制至此
flag = 'WZqSWcUtWBLlOriEfcajWBSRstLlkEfFWR7j/R7dMCDGnp=='
print(flag)
# 逆变换
for i in reversed(range(4)):
if i % 2 != 0:
flag = base64_decode(flag, tables[i])
print(flag)
else:
flag = base64_encode(flag, tables[i])
print(flag)
flag = base64_decode(flag, fir_table)
print(flag.decode())


if __name__ == '__main__':
run_decode()

最终跑出的结果缺一个右花括号"}",加上即可:

image-20240206110056306

结果:flag{3ea590ccwxehg715264fzxnzepqz}

结束…

附注当时写的sub_401250()的逆函数:(后来发现纯闲的…这个逆函数是否正确也忘记了…)

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <stdio.h>
#include <string.h>
char table[64]; // base64表声明,为全局数组,等价于原代码中的aLUsn4j5rfj0tav[]

// 该函数理论上和sub_401250()互为逆运算
int reverse_sub_401250(char *str, char *res) {
// str是加密后的字符串, res是逆向解密后的字符串,table是base64表(当前的变表,需要逆向得出)
// 代码不保证正确,麻烦自行检查,关键是当原字符串有1个=或2个=时的处理
// 当原字符串有1个=时,idx_3低2位为全0
// 当原字符串有2个=时,idx_2低4位为全0
// 因此信息不会丢失,但是逆向时(本函数)需要根据这些信息来恢复原字符串
// 这个问题麻烦检查一下

int idx_1, idx_2, idx_3, idx_4;
int res_idx = 0;

int len = strlen(str);
for (int str_idx = 0; str_idx < len; str_idx += 3) {
// 密文不是4个字节一起,而是最多3个字节一起,需要检查
int cur_case = 0;//默认无等号
if (str[str_idx + 2] == '\0') // 密文只有2个字节,有1个等号
cur_case = 1;
else if (str[str_idx + 1] == '\0') // 密文只有1个字节,有2个等号
cur_case = 2;

if (cur_case == 0) {
// 正常的3个字节一起的情况
unsigned char result; // 存储计算好的下标值
unsigned char tmp1, tmp2; // 存储某2个字节

// 第一个字符
tmp1 = str[str_idx];
// 取tmp1高6位,右移2位,高2位为0,即第一个下标值idx_1
result = (tmp1 >> 2);
res[res_idx++] = table[result];

// 第二个字符
tmp1 = str[str_idx];
tmp2 = str[str_idx + 1];
// 取tmp1低2位左移4位,和tmp2高4位右移4位,合并后的结果为第二个下标值idx_2
result = ((tmp1 & 0x3) << 4) | (tmp2 >> 4);
res[res_idx++] = table[result];

// 第三个字符
tmp1 = str[str_idx + 1];
tmp2 = str[str_idx + 2];
// 取tmp1低4位左移2位,和tmp2高2位右移6位,合并后的结果为第三个下标值idx_3
result = ((tmp1 & 0xf) << 2) | (tmp2 >> 6);
res[res_idx++] = table[result];

// 第四个字符
tmp1 = str[str_idx + 2];
// 取tmp1低6位,即第四个下标值idx_4
result = (tmp1 & 0x3f);
res[res_idx++] = table[result];

} else if (cur_case == 1) {
// 密文只有2个字节,有1个等号
unsigned char result; // 存储计算好的下标值
unsigned char tmp1, tmp2; // 存储某2个字节

// 第一个字符
tmp1 = str[str_idx];
// 取tmp1高6位,右移2位,高2位为0,即第一个下标值idx_1
result = (tmp1 >> 2);
res[res_idx++] = table[result];

// 第二个字符
tmp1 = str[str_idx];
tmp2 = str[str_idx + 1];
// 取tmp1低2位左移4位,和tmp2高4位右移4位,合并后的结果为第二个下标值idx_2
result = ((tmp1 & 0x3) << 4) | (tmp2 >> 4);
res[res_idx++] = table[result];

// 第三个字符
tmp2 = str[str_idx + 2];
// 取tmp2低4位左移2位,低2位为0,即第三个下标值idx_3
result = ((tmp2 & 0xf) << 2);
res[res_idx++] = table[result];

// 第四个字符
// 补齐一个等号
res[res_idx++] = '=';
} else if (cur_case == 2) {
// 密文只有1个字节,有2个等号
unsigned char result; // 存储计算好的下标值
unsigned char tmp1; // 存储这1个字节

// 第一个字符
tmp1 = str[str_idx];
// 取tmp1高6位,右移2位,高2位为0,即第一个下标值idx_1
result = (tmp1 >> 2);
res[res_idx++] = table[result];

// 第二个字符
tmp1 = str[str_idx];
// 取tmp1低2位左移4位,低4位为0,即第二个下标值idx_2
result = ((tmp1 & 0x3) << 4);
res[res_idx++] = table[result];

// 补齐2个等号
res[res_idx++] = '=';
res[res_idx++] = '=';
}
}
}