# 2025 鹏程杯 逆向 wp

前两天复习课内考试一直没怎么打 ctf,赛中只解了两道,最近考完试以后又把剩下的题做完了(除了驱动),题目质量还是非常不错的,值得一做

# MoreMoreFlower

花指令 + vm,非常常规且老套的题

反编译看到 vm 不全,转汇编

经典 call + retn 花,写了一个简单 idc 脚本去除

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
#include <idc.idc>

static NopCode(Addr, Length)
{
auto i;
for (i = 0; i < Length; i++)
{
PatchByte(Addr + i, 0x90);
}
}


static main()
{
auto seg;
for (seg = get_first_seg(); seg != BADADDR; seg = get_next_seg(seg))
{
auto seg_name = get_segm_name(seg);
msg(seg_name + "\n");
if (seg_name == ".text")
{
auto current_addr = seg;
auto end_addr = current_addr + 0x10000;
while (current_addr != BADADDR && current_addr < end_addr)
{
auto push_eax = Byte(current_addr);
auto op = Byte(current_addr + 6);
auto op_offest = Dword(current_addr + 7);
if(push_eax == 0x50 && op == 0xE8 && op_offest == 0)
{
auto nop_len = 0;
auto i;
for(i = 0; i < 50; i ++)
{
auto pop_byte = Byte(current_addr + i);
nop_len += 1;
if(pop_byte == 0x58)
{
NopCode(current_addr,nop_len);
break;
}
}
}
current_addr ++;
}

}
}
}

然后下断点 trace 数据

最后得到的部分 trace 如下:

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
0 + 1 = 1
31 << 8 = 3100
3100 | 32 = 3132
1 + 1 = 2
3132 << 8 = 313200
313200 | 33 = 313233
2 + 1 = 3
313233 << 8 = 31323300
31323300 | 34 = 31323334
0 + 23251156 = 23251156
31323334 << 5 = 26466680
26466680 ^ 23251156 = 56377D6
31323334 >> 4 = 3132333
56377D6 ^ 3132333 = 67054E5
31323334 + 67054E5 = 37A28819
1E - 1 = 1D
cmp R3,0
23251156 + 23251156 = 464A22AC
37A28819 << 5 = F4510320
F4510320 ^ 464A22AC = B21B218C
37A28819 >> 4 = 37A2881
B21B218C ^ 37A2881 = B161090D
37A28819 + B161090D = E9039126
1D - 1 = 1C
cmp R3,0
464A22AC + 23251156 = 696F3402
E9039126 << 5 = 207224C0
207224C0 ^ 696F3402 = 491D10C2
E9039126 >> 4 = E903912
491D10C2 ^ E903912 = 478D29D0
E9039126 + 478D29D0 = 3090BAF6
1C - 1 = 1B
cmp R3,0
696F3402 + 23251156 = 8C944558
3090BAF6 << 5 = 12175EC0
12175EC0 ^ 8C944558 = 9E831B98
3090BAF6 >> 4 = 3090BAF
9E831B98 ^ 3090BAF = 9D8A1037
3090BAF6 + 9D8A1037 = CE1ACB2D
1B - 1 = 1A
cmp R3,0
8C944558 + 23251156 = AFB956AE
CE1ACB2D << 5 = C35965A0
C35965A0 ^ AFB956AE = 6CE0330E
CE1ACB2D >> 4 = CE1ACB2
6CE0330E ^ CE1ACB2 = 60019FBC
CE1ACB2D + 60019FBC = 2E1C6AE9
1A - 1 = 19

根据 trace 同构出加密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <windows.h>
#include <stdint.h>

int main()
{
uint32_t sum = 0;
uint32_t A = 0x31323334;
for(int i = 0; i < 0x1E; i++)
{
sum += 0x23251156;
A += (A << 5) ^ sum ^ (A >> 4);
}
printf("%X",A);
}

// 0x1C017A21, 0xF73ED333, 0x5E257803, 0x3B8BB82F, 0x5BAE8493, 0xE9D6A5DE

然后写脚本解密

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
#include <iostream>
#include <windows.h>
#include <stdint.h>

uint32_t enc(uint32_t A)
{
uint32_t sum = 0;
for(int i = 0; i < 0x1E; i++)
{
sum += 0x23251156;
A += (A << 5) ^ sum ^ (A >> 4);
}
return A;
}


int main()
{
for(int i = 33; i < 127; i++)
{
for(int j = 33; j < 127; j++)
{
for(int k = 33; k < 127; k++)
{
for(int m = 0; m < 127; m++)
{
uint32_t A = ((m << 24) | (k << 16) | (j << 8) | i) & 0xFFFFFFFF;
uint32_t B = enc(A);
if(B == 0x217A011C || B == 0xDEA5D6E9 || B == 0x9384AE5B || B == 0x2FB88B3B || B == 0x0378255E || B == 0x33D33EF7)
{
printf("%X:",B);
printf("%c%c%c%c",m,k,j,i);
printf("\n");
}
}
}
}
}

}

// 0x1C017A21, 0xF73ED333, 0x5E257803, 0x3B8BB82F, 0x5BAE8493, 0xE9D6A5DE

// DEA5D6E9:-X$
// 9384AE5B:{Fl0
// 33D33EF7:15E3
// 378255E:eAVM
// 2FB88B3B:weRT
// 9384AE5B:>'8d
// DEA5D6E9:flag
// 217A011C:s9!}

// flag{Fl0weRTeAVM15E3s9!}

需要注意的就是密文为大端序

# babyconnect

这个题真有意思吧

client.exe

程序一共接受两次输入

第一次输出,发送到服务端,如果返回’c’就继续输入 flag,并将输入与 "check" 字符串拼接,继续向服务端发送

server.exe

如果发送来的数据开头是 "check",那么对数据通过 Address 函数进行处理后用 flag.dll 里的 check 函数进行校验

否则,对数据进行处理后再调用 inside.dll 里的 check 函数进行校验,通过后返回’c’,同时自解密 Address 函数

至此逻辑就清楚了,我们第一次输入的数据必须通过 inside.dll 中的 check,然后才能解密完 Address 函数分析第二个 check

inside.dll

dword_10014AC8 为 1 时,返回值为 2 通过校验,server.exe 中的逻辑暗示 inside_check 很可能是一个迷宫,但是发现这里只有一个函数表,这个地方其实和 sekaictf 的 miku 音乐机很像

观察整个函数表,有三中不同的函数

func1

1
2
3
4
5
6
7
8
.text:10001060 way             proc near               ; DATA XREF: .data:10014004↓o
.text:10001060 push ebp
.text:10001061 mov ebp, esp
.text:10001063 xor eax, eax
.text:10001065 mov eax, 0
.text:1000106A pop ebp
.text:1000106B retn
.text:1000106B way endp

func2

1
2
3
4
5
6
7
8
.text:10001070 wall            proc near               ; DATA XREF: .data:10014008↓o
.text:10001070 push ebp
.text:10001071 mov ebp, esp
.text:10001073 xor eax, eax
.text:10001075 mov eax, [eax]
.text:10001077 pop ebp
.text:10001078 retn
.text:10001078 wall endp

func3

1
2
3
4
5
6
7
8
9
10
11
.text:100013A0 sub_100013A0    proc near               ; DATA XREF: .data:10014034↓o
.text:100013A0 push ebp
.text:100013A1 mov ebp, esp
.text:100013A3 push offset wall_30
.text:100013A8 call sub_10001000
.text:100013AD add esp, 4
.text:100013B0 xor eax, eax
.text:100013B2 mov eax, 0
.text:100013B7 pop ebp
.text:100013B8 retn
.text:100013B8 sub_100013A0 endp
1
2
3
4
5
6
7
8
9
10
11
BOOL __cdecl sub_10001000(void *a1)
{
DWORD flOldProtect; // [esp+0h] [ebp-8h] BYREF
LPVOID lpAddress; // [esp+4h] [ebp-4h]

lpAddress = a1;
VirtualProtect(a1, 6u, 0x40u, &flOldProtect);
*((_BYTE *)lpAddress + 6) = 0x90;
*((_BYTE *)lpAddress + 5) = 0x90;
return VirtualProtect(lpAddress, 6u, flOldProtect, &flOldProtect);
}

如果调试这个函数就会发现,执行 func1 是正常的,但执行 func2 就会异常,这是必然的, eax 为 0,取 [0] 的值就会触发内存访问异常,程序就会执行不下去。

所以其实 func1 就是迷宫里的路,是可执行的,func2 就是墙,不可执行的,那么 func3 呢,发现调用了 sub_10001000 函数,把一个墙函数地址的 + 5,+6 字节替换为 0x90,

也就是 nop 掉了会触发异常的那行代码,这样,原本不能走的墙,就变成了路。

根据上面的分析,我手动重命名了这些函数 (其实写脚本会更好),最后得到的地图为

1
2
3
4
5
6
7
8
9
10
0011111111
101A100001
1010101101
1010101101
1000100101
111a110101
1000010101
1011010101
10B10b01E1
1111111111

必须要经过大写字母才能解锁小写字母,手动或者 ai 跑一下能得到路径:

DSSSSDDWWWSSSSSAASSDAWWDDDSSDDWWWWAWWWDDDSSSSSSS

不过在进行迷宫运算之前还对数据进行了一个处理

异或运算,再异或一次就好了,上面的 SIMD 指令和下面这个其实是相同的,只不过编译器优化的比较难看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>
#include <stdint.h>

int main()
{
uint8_t path[] = "DSSSSDDWWWSSSSSAASSDAWWDDDSSDDWWWWAWWWDDDSSSSSSS";
for(int i = 0; i < sizeof(path);i++)
{
path[i] ^= (i + 1) % 10;
}
printf("%s",path);
}

// EQPWVBC_^WRQPWVGF[ZD@UT@ABT[MDVUTSDQP_MDEQPWVUT[

输入以后调试看关于 flag 的校验,先 ida 启动 server.exe 的调试,然后运行 client.exe

解密后的 Address 函数如下:

三次异或下一个数的操作

最后进行了调用了 flag_check 函数

flag.dll

sub_70E41020 是一个类似 rc4 的函数,里面只有这一处对 a2 进行异或,但是最终和密文比较的是 a1,就业时说这个地方其实没什么用,直接进行异或三次的解密即可

脚本如下

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

int main()
{
uint8_t data[] = {0x50, 0x73, 0x65, 0xCC, 0x00, 0x0C, 0x11, 0x2E, 0x02, 0x26, 0x02, 0x03, 0x0D, 0x7A, 0x7A, 0x1B,
0x36, 0x61, 0x4C, 0x06, 0x18, 0x4C, 0x0F, 0x46, 0x58, 0x30, 0x30, 0x53, 0x62, 0x58, 0x5A, 0x68,
0x0E, 0x34, 0x55, 0x05, 0x5B, 0x6C, 0x4A, 0x44, 0x5E, 0x36, 0x42, 0x7D};
for(int i = 0; i < 3; i++)
{
for(int j = 42; j >= 0; j--)
{
data[j] ^= data[j + 1];
}
}
printf("%s",data);
}

// Hpŭkflag{SMC_RC4_3ncryp710n_1S_e3a9_R1ght?}

# emoji_encoder

给了一个加密的图片,根据字符串提示定位主要逻辑

这边有开始加密和结束加密的提示,在中间只找到 rc4 加密,因为 rc4 的对称性,把 enc_emoji.png 重命名为 my_emoji.png 再运行程序,得到一个模糊的图片

很遗憾,看不清 flag,一定是还有像素没有被解密,所以继续找程序中还有那些加密,翻和 main 函数相近的函数找到 tea 加密 (因为都是出题人自己写的,编译的时候会编译到一起)

交叉引用发现这个函数里有花指令

把 push - pop 的一串 nop 掉就好了

通过分析发现,只有一个常量数组进行了加密,图片本身是没有经过 tea 加密的,只是加上了这个数组的值

那么反过来,只要改为减去数组的值,再结合 rc4 的对称性,就能够 patch 程序实现自解密

我们看原程序是在调用完 rc4_init 函数后,跳到 loc_4021C7 这个地址进入 rc4 循环,最后调用 tea 加密,想要解密,必须先调用 tea,再调用 rc4,那么,就可以让程序先跳转到 tea 函数调用出,再跳转到 rc4,最后跳转回 tea 的下一行指令继续执行。

为了增加跳转指令,必须 nop 掉一些指令,

发现这部分汇编只是进行一个输出作用,对整体的执行流没有什么影响,所以 nop 掉这边就可以

patch 完以后如图:

最后再运行一遍程序即可得到原图

# chal

打开看到 jz 花,写脚本去除, 发现还是被混淆过的

以第一个函数 sub_5b12 为例,说明应该怎么去除这种混淆

通过观察可以发现有两种混淆方法

一是不透明谓词,二是一些直接返回值的函数,函数的返回值是存放在 rax 这个寄存器里的,那么 call 一个直接返回值的函数也就等于给 rax 赋值

且字节码刚好都是 5 个,不会改变下面的逻辑。

不透明谓词也同理

直接换成 mov 对应寄存器数即可,因为字节码比原始的少,所以同样不会有影响

完整去混淆脚本如下:

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <idc.idc>

static NopCode(Addr, Length)
{
auto i;
for (i = 0; i < Length; i++)
{
PatchByte(Addr + i, 0x90);
}
}


static main()
{
auto seg;
for (seg = get_first_seg(); seg != BADADDR; seg = get_next_seg(seg))
{
auto seg_name = get_segm_name(seg);
msg(seg_name + "\n");
if (seg_name == ".text")
{
auto current_addr = seg;
auto end_addr = current_addr + 0x100000;
while (current_addr != BADADDR && current_addr < end_addr)
{
auto insn_name = print_insn_mnem(current_addr);
auto op = print_operand(current_addr, 0);
auto op2 = print_operand(current_addr, 1);
if(Word(current_addr) == 0x0174 && Word(current_addr + 2) == 0x0F00)
{
NopCode(current_addr + 2,1);
}

if(insn_name == "call")
{
if(op == "num_2")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x2);
}

if(op == "num_0")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x0);
}

if(op == "num_7")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x7);
}

if(op == "num_6")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x6);
}

if(op == "num_3")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x3);
}

if(op == "num_9")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x9);
}
}

if(insn_name == "movzx" && op == "eax")
{
if(op2 == "cs:_9")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x9);
NopCode(current_addr + 5,2);
}

if(op2 == "cs:_3")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x3);
NopCode(current_addr + 5,2);
}

if(op2 == "cs:_7")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x7);
NopCode(current_addr + 5,2);
}

if(op2 == "cs:_1")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x1);
NopCode(current_addr + 5,2);
}

if(op2 == "cs:_0")
{
PatchByte(current_addr,0xB8);
PatchDword(current_addr + 1,0x0);
NopCode(current_addr + 5,2);
}

}

if(insn_name == "movzx" && op == "edx")
{
if(op2 == "cs:_8")
{
PatchByte(current_addr,0xBA);
PatchDword(current_addr + 1,0x8);
NopCode(current_addr + 5,2);
}
}
current_addr = next_head(current_addr, end_addr);
}

}
}
}

效果如下:

还是比较干净的,那么同时也就能看出这些函数没什么用,一定是返回 0 的,所以还是同理在 main 函数里把 call 换成对 eax 的赋值

同时像这样的代码也不必理会

1
2
3
4
5
v42 = 0;
for ( i = 0; !i; i = 1 )
HIDWORD(v42) ^= 0xFFFFFF8F;
LOBYTE(v42) = 1;
::n = ((__int64 (__fastcall *)(__int64, char **))qword_555555573500[HIDWORD(v42)])(30, a2);// sysconf

调用的函数以及参数都可以调试获得。

最终手动化简后得到的代码如下:

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 (__fastcall *v3)(__int64, QWORD); // rbx
double v4; // xmm0_8
double v5; // xmm0_8
double v7; // xmm0_8
double v9; // xmm0_8
double v10; // xmm0_8
double v12; // xmm0_8
int i; // [rsp+2Ch] [rbp-2E4h]
int j; // [rsp+30h] [rbp-2E0h]
int k; // [rsp+34h] [rbp-2DCh]
int m; // [rsp+38h] [rbp-2D8h]
int n; // [rsp+3Ch] [rbp-2D4h]
int i7; // [rsp+78h] [rbp-298h]
unsigned int v34; // [rsp+80h] [rbp-290h]
unsigned __int64 i8; // [rsp+D0h] [rbp-240h]
size_t v36; // [rsp+E0h] [rbp-230h]
__int64 v37; // [rsp+E8h] [rbp-228h]
QWORD v38[4]; // [rsp+270h] [rbp-A0h] BYREF
QWORD v39[4]; // [rsp+290h] [rbp-80h] BYREF
char *v42; // [rsp+2C0h] [rbp-50h] BYREF
unsigned __int64 v44; // [rsp+2D8h] [rbp-38h]

v44 = __readfsqword(0x28u);
sysconf(30, a2);// sysconf
dword_555555573984 = syscall(323, 0x80000);
if ( dword_555555573984 == -1 )
v4 = 1.0;
else
v4 = 0.0;
if (v4)
{
perror("Must use sudo");
exit(1);
}
v38[1] = 0;
v38[2] = 0;
v38[0] = 170;
if ( ioctl( (unsigned int)dword_555555573984,3222841919LL,v38) == -1 )
v5 = 1.0;
else
v5 = 0.0;
if (v5)
exit(1);
v36 = ::n + (((unsigned int)dword_555555573978 + ::n - 1) & -(__int64)::n);
v37 = mmap(0, v36, 3, 0x22, 0xFFFFFFFFLL, 0);
v39[0] = v37;
v39[1] = v36;
v39[2] = 1;
if ( ioctl( (unsigned int)dword_555555573984,3223366144LL,v39) == -1 )
v7 = 1.0;
else
v7 = 0.0;
if ( v7 )
exit(1);
if ( __madvise(v37, v36, 4) == -1 )
v9 = 1.0;
else
v9 = 0.0;
if ( v9 )
exit(1);
pthread_create(0);
while ( 1 )
{
v10 = dword_55555557397C ? 0.0 : 1.0;
if (!v10)
break;
v42 = 0;
usleep(1000);
}
v34 = syscall(0x13F, 0x100000001, 1);
if ( v34 == -1 )
v12 = 1.0;
else
v12 = 0.0;
if (v12)
exit(1);
for ( i7 = 0; i7 < 7; ++i7 )
{
for ( i8 = 0; i8 < (unsigned int)dword_555555573978; i8 += ::n )
;
}
_wirte( v34,v37,(unsigned int)dword_555555573978);
__munmap(v37, v36);
fexecve(v34, 0x7FFFFFFFE3B1, 0);
return 0;
}

可以看出这其实是一个壳,动态加载了一个 elf 文件并执行,可以断在写入函数, rsi 寄存器里就是新的 elf 文件,dump 出来进行分析即可

新的 elf 依旧是被混淆过的,可以沿用之前的脚本去混淆

手动去混淆后代码如下:

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
bool v4;
double v5;
int k;
int nn;
int m;
int mm;
int i1;
__int64 v26;
size_t v27;
unsigned __int64 n;
size_t size;
unsigned __int16 *v30;
__int64 *v33;
_BYTE *v35;
_BYTE *v36;
double v37;
double v38;
double v39;
_QWORD *v46;
_DWORD *v47;
double v48;
double v51;
__int64 v66;
_QWORD v67[4];
char s[8];
__int64 v69;
__int64 v70;
char v71;
unsigned __int64 v72;

v72 = __readfsqword(0x28u);
sub_1D23();
size = 6048;
v30 = (unsigned __int16 *)malloc(0x17A0u);
memcpy(v30,byte_C060,size);
*(_QWORD *)s = 0;
v69 = 0;
v70 = 0;
v71 = 0;
v66 = 0;
v33 = &v66;
init_str(v67);
v35 = v67;
if ( LOBYTE(v67[0]) != 1 )
{
v36 = v35 + 1;
for ( k = 0; (unsigned __int64)k <= 0x1A; ++k ) // dec_str
v36[k] ^= (_BYTE)k + 124;
}
printf(v35 + 1, &v26); // Enter 24 characters Flag:
v37 = (double)(int)fgets(s, 25, stdin);
if (v37)
{
v27 = strlen(s);
v4 = v27 && s[v27 - 1] == 10;
v38 = (double)v4;
if (v38)
s[--v27] = 0;
for ( m = 0; ; ++m )
{
v39 = (double)1;
if ( m >= 7 )
break;
for ( n = v27; n <= 0x17; ++n )
s[n] = 32;
}
}
memset(byte_DB40, 0, &unk_10000);
memcpy(byte_DB40, s, 24);
memcpy(&unk_DB58, off_D808, 24);
for ( mm = 0; ; ++mm )
{
v48 = (double)1;
if ( mm >= 7 )
break;
v67[0] = 0;
v46 = v67;
v47 = (_DWORD *)v67 + 1;
for ( nn = 0; !nn; nn = 1 )
*v47 ^= 0xFFFFFF86;
*(_BYTE *)v46 = 1;
for ( i1 = *((_DWORD *)v46 + 1); i1 < 1008; ++i1 )
byte_DB40[v30[3 * i1 + 2]] = ~(byte_DB40[v30[3 * i1 + 1]] & byte_DB40[v30[3 * i1]]);
}
if ( memcmp(&unk_9080,byte_DB40,24) )
{
v5 = 0.0;
}
else
{
v5 = 1.0;
}
v51 = v5;
if ( (int)v5 )
{
printf("no");
}
else
{
printf("yes");
}
free(v30);
return 0;
}

现在的逻辑很清晰了,让 ai 写了一个 z3 脚本得到 flag: flag{B62o3$cC..*cE70O7?}

# medddgo

题目名称就可以看出来是 go 语言,根据字符串定位到关键函数

通过调试发现 sub_14007DF20 函数对输入进行读取,sub_1400B3740 是主要 check 函数

进入 check,里面代码不太好看

可以通过’\n’和’\r’的检查判断出是在计算输入的长度,下面的的 check_format 函数非常混乱,调试看逻辑

![](markdown-img/2025 鹏程杯 逆向 wp.assets/6947cc24441ea36249955cc1.png)

从内存中取值放入 rsi 和 rdi,再相减,返回右移后的值,其中 rsi 是用户输入,rdi 是传进来的字符串 flag{ ,这其实是在检查输入格式,同理下边的另一次调用也是在检查是否以 } 结尾

下面进行加密以及判断密文是否与 off_7FF684D80310 相同,通过 s 盒识别为 sm4 算法

转为汇编看 sm4 参数,key 是这个 xmm 值

检查 key 的初始化,交叉引用发现有一个函数一直在不断修改 key 的值

这边是一个反调试,只要检测到调试,这边的 while 就会一直循环改变 key 的值,直到用户输入完毕跳转到加密那里,所以可以在函数开头断下,获取到原始未修改的 key 值

直接解密未能解出,通过与标准 sm4 对比,发现密匙生成的地方多异或了一个数组

修改 fk 的值即可解出

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include <stdio.h>
#include <string.h>
#include <stdint.h>

#define SM4_BLOCK_SIZE 16
#define SM4_KEY_SIZE 16
#define SM4_ROUNDS 32

static const uint8_t SM4_SBOX[256] = {
0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, 0x16, 0xb6, 0x14, 0xc2, 0x28, 0xfb, 0x2c, 0x05,
0x2b, 0x67, 0x9a, 0x76, 0x2a, 0xbe, 0x04, 0xc3, 0xaa, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
0x9c, 0x42, 0x50, 0xf4, 0x91, 0xef, 0x98, 0x7a, 0x33, 0x54, 0x0b, 0x43, 0xed, 0xcf, 0xac, 0x62,
0xe4, 0xb3, 0x1c, 0xa9, 0xc9, 0x08, 0xe8, 0x95, 0x80, 0xdf, 0x94, 0xfa, 0x75, 0x8f, 0x3f, 0xa6,
0x47, 0x07, 0xa7, 0xfc, 0xf3, 0x73, 0x17, 0xba, 0x83, 0x59, 0x3c, 0x19, 0xe6, 0x85, 0x4f, 0xa8,
0x68, 0x6b, 0x81, 0xb2, 0x71, 0x64, 0xda, 0x8b, 0xf8, 0xeb, 0x0f, 0x4b, 0x70, 0x56, 0x9d, 0x35,
0x1e, 0x24, 0x0e, 0x5e, 0x63, 0x58, 0xd1, 0xa2, 0x25, 0x22, 0x7c, 0x3b, 0x01, 0x21, 0x78, 0x87,
0xd4, 0x00, 0x46, 0x57, 0x9f, 0xd3, 0x27, 0x52, 0x4c, 0x36, 0x02, 0xe7, 0xa0, 0xc4, 0xc8, 0x9e,
0xea, 0xbf, 0x8a, 0xd2, 0x40, 0xc7, 0x38, 0xb5, 0xa3, 0xf7, 0xf2, 0xce, 0xf9, 0x61, 0x15, 0xa1,
0xe0, 0xae, 0x5d, 0xa4, 0x9b, 0x34, 0x1a, 0x55, 0xad, 0x93, 0x32, 0x30, 0xf5, 0x8c, 0xb1, 0xe3,
0x1d, 0xf6, 0xe2, 0x2e, 0x82, 0x66, 0xca, 0x60, 0xc0, 0x29, 0x23, 0xab, 0x0d, 0x53, 0x4e, 0x6f,
0xd5, 0xdb, 0x37, 0x45, 0xde, 0xfd, 0x8e, 0x2f, 0x03, 0xff, 0x6a, 0x72, 0x6d, 0x6c, 0x5b, 0x51,
0x8d, 0x1b, 0xaf, 0x92, 0xbb, 0xdd, 0xbc, 0x7f, 0x11, 0xd9, 0x5c, 0x41, 0x1f, 0x10, 0x5a, 0xd8,
0x0a, 0xc1, 0x31, 0x88, 0xa5, 0xcd, 0x7b, 0xbd, 0x2d, 0x74, 0xd0, 0x12, 0xb8, 0xe5, 0xb4, 0xb0,
0x89, 0x69, 0x97, 0x4a, 0x0c, 0x96, 0x77, 0x7e, 0x65, 0xb9, 0xf1, 0x09, 0xc5, 0x6e, 0xc6, 0x84,
0x18, 0xf0, 0x7d, 0xec, 0x3a, 0xdc, 0x4d, 0x20, 0x79, 0xee, 0x5f, 0x3e, 0xd7, 0xcb, 0x39, 0x48
};

static const uint32_t FK[4] = {
0x6141F63,0x6A960F6C,0x3D27CBCD,0x71B3E11F
};

static const uint32_t CK[32] = {
0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269,
0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9,
0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249,
0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9,
0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229,
0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299,
0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209,
0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279
};

static uint32_t rotl(uint32_t x, int n) {
return (x << n) | (x >> (32 - n));
}

static uint32_t tau(uint32_t x) {
uint32_t a0 = SM4_SBOX[(x >> 16) & 0xFF];
uint32_t a1 = SM4_SBOX[(x >> 8) & 0xFF];
uint32_t a2 = SM4_SBOX[(x >> 0) & 0xFF];
uint32_t a3 = SM4_SBOX[(x >> 24)& 0xFF];
return (a0 << 16) | (a1 << 8) | (a2 << 0) | (a3 << 24);
}

static uint32_t t(uint32_t x) {
uint32_t b = tau(x);
return b ^ rotl(b, 2) ^ rotl(b, 10) ^ rotl(b, 18) ^ rotl(b, 24);
}

static uint32_t t_key(uint32_t x) {
uint32_t b = tau(x);
return b ^ rotl(b, 13) ^ rotl(b, 23);
}

static uint32_t F(uint32_t x0, uint32_t x1, uint32_t x2, uint32_t x3, uint32_t rk) {
return x0 ^ t(x1 ^ x2 ^ x3 ^ rk);
}

static void sm4_key_schedule(const uint8_t key[SM4_KEY_SIZE], uint32_t rk[SM4_ROUNDS]) {
uint32_t mk[4];
uint32_t k[36];

for (int i = 0; i < 4; i++) {
mk[i] = ((uint32_t)key[4*i] << 24) | ((uint32_t)key[4*i+1] << 16) |
((uint32_t)key[4*i+2] << 8) | ((uint32_t)key[4*i+3] << 0);
k[i] = mk[i] ^ FK[i];
}

for (int i = 0; i < 32; i++) {
k[i+4] = k[i] ^ t_key(k[i+1] ^ k[i+2] ^ k[i+3] ^ CK[i]);
rk[i] = k[i+4];
}
}

static void sm4_round(const uint32_t rk[SM4_ROUNDS], const uint8_t in[SM4_BLOCK_SIZE], uint8_t out[SM4_BLOCK_SIZE], int encrypt) {
uint32_t x[36];

for (int i = 0; i < 4; i++) {
x[i] = ((uint32_t)in[4*i] << 24) | ((uint32_t)in[4*i+1] << 16) |
((uint32_t)in[4*i+2] << 8) | ((uint32_t)in[4*i+3] << 0);
}

for (int i = 0; i < 32; i++) {
int rk_idx = encrypt ? i : (31 - i);
x[i+4] = F(x[i], x[i+1], x[i+2], x[i+3], rk[rk_idx]);
}

for (int i = 0; i < 4; i++) {
out[4*i] = (x[35-i] >> 24) & 0xFF;
out[4*i+1] = (x[35-i] >> 16) & 0xFF;
out[4*i+2] = (x[35-i] >> 8) & 0xFF;
out[4*i+3] = (x[35-i] >> 0) & 0xFF;
}
}

void sm4_ecb_decrypt(const uint8_t *ciphertext, int ciphertext_len,
const uint8_t key[SM4_KEY_SIZE],
uint8_t *plaintext) {
uint32_t rk[SM4_ROUNDS];
uint8_t block[SM4_BLOCK_SIZE];

sm4_key_schedule(key, rk);

for (int i = 0; i < ciphertext_len; i += SM4_BLOCK_SIZE) {
sm4_round(rk, &ciphertext[i], &plaintext[i], 0);
}
}

void print_hex(const uint8_t *data, int len) {
for (int i = 0; i < len; i++) {
printf("%02x", data[i]);
}
printf("\n");
}

int main() {

uint8_t ciphertext[] = {
0xFC, 0x66, 0xB2, 0x70, 0xE8, 0x87, 0x4C, 0x9D, 0x73, 0x4E, 0xCD, 0x5B, 0x76, 0x6A, 0xA5, 0x89,
0x6D, 0xDA, 0x6C, 0xC5, 0x34, 0x9D, 0x5F, 0x3B, 0x44, 0xB5, 0x4A, 0xAF, 0x5E, 0xF9, 0xCE, 0x49
};
int ciphertext_len = sizeof(ciphertext);

uint8_t key[SM4_KEY_SIZE] = {0xAD, 0xA4, 0x22, 0x75, 0x8D, 0xB6, 0xE3, 0xF1,
0x45, 0xB6, 0x92, 0x32, 0x3B, 0x8B, 0x99, 0x26};

uint8_t decryptedtext[64];

printf("Ciphertext (hex): ");
print_hex(ciphertext, ciphertext_len);
printf("Key (hex): ");
print_hex(key, SM4_KEY_SIZE);

sm4_ecb_decrypt(ciphertext, ciphertext_len, key, decryptedtext);

printf("Decrypted data (hex): ");
print_hex(decryptedtext, ciphertext_len);

printf("Decrypted text: ");
for (int i = 0; i < ciphertext_len; i++) {
if (decryptedtext[i] >= 32 && decryptedtext[i] <= 126) {
printf("%c", decryptedtext[i]);
} else {
printf("\\x%02x", decryptedtext[i]);
}
}
printf("\n");

return 0;
}
// 12d8b17b8ae52636a1211299451f9f6c