[GHCTF 2025]FishingKit题解

​ bait数据

​ 可以使用z3求解器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from z3 import *
s = Solver()
a0,a1,a2,a3,a4,a5,a6,a7,a8,a9 = BitVecs("a0 a1 a2 a3 a4 a5 a6 a7 a8 a9",12)
s.add(202 * a8 + 216 * a5 - 4 * a4 - 330 * a9 - 13 * a4 - 268 * a6 == -14982)
s.add(325 * a8 + 195 * a0 + 229 * a1 - 121 * a6 - 409 * a6 - (a1 << 7) == 22606)
s.add(489 * a1 + 480 * a6 + 105 * a2 + 367 * a3 - 135 * a4 - 482 * a9 == 63236)
s.add(493 * a1 - 80 * a4 - 253 * a8 - 121 * a2 - 177 * a0 - 243 * a9 == -39664)
s.add(275 * a4 + 271 * a6 + 473 * a7 - 72 * a5 - 260 * a4 - 367 * a4 == 14255)
s.add(286 * a0 + 196 * a7 + 483 * a2 + 442 * a1 - 495 * a8 - 351 * a4 == 41171)
s.add(212 * a2 + 283 * a7 - 329 * a8 - 429 * a9 - 362 * a2 - 261 * a6 == -90284)
s.add(456 * a5 + 244 * a7 + 92 * a4 + 348 * a7 - 225 * a1 - 31 * a2 == 88447)
s.add(238 * a9 + 278 * a7 + 216 * a6 + 237 * a0 + 8 * a2 - 17 * a9 == 83838)
s.add(323 * a9 + 121 * a1 + 370 * a7 - (a4 << 6) - 196 * a9 - 422 * a0 == 26467)
s.add(166 * a9 + 90 * a1 + 499 * a2 + 301 * a8 - 31 * a2 - 206 * a2 == 88247)
s.add(355 * a0 + 282 * a4 + 44 * a9 + 359 * a8 - 167 * a5 - 62 * a3 == 76658)
s.add(488 * a6 + 379 * a9 + 318 * a2 - 85 * a1 - 357 * a2 - 277 * a5 == 35398)
s.add(40 * a0 + 281 * a4 + 217 * a5 - 241 * a1 - 407 * a7 - 309 * a7 == -35436)
s.add(429 * a3 + 441 * a3 + 115 * a1 + 96 * a8 + 464 * a1 - 133 * a7 == 157448)
if s.check() == sat:
ans = s.model()
for i in range(10):
t = int(f"{ans[eval(f'a{i}')]}")
print(chr(t),end='')

​ 求出bait(v4):DeluxeBait。

​ 分析sub_7FF69CE42460函数发现,这是一个变种的RC4加密算法,解开后得到NSSCTF{Fake!Fake!Fake!},此时我才发现这是一个假flag,弄的我百思不得其解。 实在没办法,一个一个翻

​ 发现sub_7FF651DB1CE0函数

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
__int64 __fastcall sub_7FF69CE41CE0(_BYTE *a1, unsigned __int8 *a2)
{
unsigned int v3; // [rsp+20h] [rbp-108h]
unsigned int v4; // [rsp+20h] [rbp-108h]
unsigned int v5; // [rsp+20h] [rbp-108h]
unsigned int v6; // [rsp+24h] [rbp-104h]
unsigned int v7; // [rsp+24h] [rbp-104h]
unsigned int v8; // [rsp+24h] [rbp-104h]
unsigned int v9; // [rsp+28h] [rbp-100h]
unsigned int v10; // [rsp+28h] [rbp-100h]
unsigned int v11; // [rsp+28h] [rbp-100h]
char v12; // [rsp+2Ch] [rbp-FCh]
int i; // [rsp+30h] [rbp-F8h]
int j; // [rsp+34h] [rbp-F4h]
int ii; // [rsp+38h] [rbp-F0h]
int jj; // [rsp+3Ch] [rbp-ECh]
int kk; // [rsp+40h] [rbp-E8h]
int mm; // [rsp+44h] [rbp-E4h]
int nn; // [rsp+48h] [rbp-E0h]
int v20; // [rsp+4Ch] [rbp-DCh]
int k; // [rsp+50h] [rbp-D8h]
int m; // [rsp+54h] [rbp-D4h]
int n; // [rsp+58h] [rbp-D0h]
HMODULE hModule; // [rsp+60h] [rbp-C8h]
FARPROC ProcAddress; // [rsp+68h] [rbp-C0h]
CHAR LibFileName[16]; // [rsp+70h] [rbp-B8h] BYREF
CHAR ProcName[16]; // [rsp+80h] [rbp-A8h] BYREF
char v28[3]; // [rsp+90h] [rbp-98h] BYREF
char v29[11]; // [rsp+93h] [rbp-95h] BYREF
char v30[8]; // [rsp+9Eh] [rbp-8Ah] BYREF
char v31[32]; // [rsp+A8h] [rbp-80h] BYREF
int v32[6]; // [rsp+C8h] [rbp-60h] BYREF
int v33[14]; // [rsp+E0h] [rbp-48h] BYREF
_BYTE *v34; // [rsp+130h] [rbp+8h]

v34 = a1;
memset(v32, 0, 0x14ui64); // 初始化v32,v33数组为0
memset(v33, 0, 0x32ui64);
for ( i = 0; i < 20; ++i )
*((_BYTE *)v32 + i) = a1[i - 80];
for ( j = 0; j < 50; ++j )
*((_BYTE *)v33 + j) = a1[j - 56];
v3 = 0; // 第一部分处理了v33[0],v33[1]
v6 = v33[0];
v9 = v33[1];
for ( k = 0; k < 24; ++k ) // XTEA
//
{
v6 += (v32[v3 & 3] + v3) ^ (v9 + ((v9 >> 5) ^ (16 * v9)));
v3 += 1719109785; // v3每次循环都增加一个常数
v9 += (v32[(v3 >> 11) & 3] + v3) ^ (v6 + ((v6 >> 5) ^ (16 * v6)));// v32数组中的元素作为密钥
}
v33[0] = v6;
v33[1] = v9;
v4 = 0;
v7 = v33[2]; // 第二部分处理了v33[2],v33[3]
v10 = v33[3];
for ( m = 0; m < 24; ++m )
{
v7 += (v32[v4 & 3] + v4) ^ (v10 + ((v10 >> 5) ^ (16 * v10)));// 在计算机中,左移运算(<<)本质上是将二进制数向左移动指定位数,低位补零。对于整数运算,左移 4 位等价于乘以 16
v4 += 1719109785;
v10 += (v32[(v4 >> 11) & 3] + v4) ^ (v7 + ((v7 >> 5) ^ (16 * v7)));
}
v33[2] = v7;
v33[3] = v10;
v5 = 0; // 第三部分处理了v33[4],v33[5]
v8 = v33[4];
v11 = v33[5];
for ( n = 0; n < 24; ++n )
{
v8 += (v32[v5 & 3] + v5) ^ (v11 + ((v11 >> 5) ^ (16 * v11)));
v5 += 1719109785;
v11 += (v32[(v5 >> 11) & 3] + v5) ^ (v8 + ((v8 >> 5) ^ (16 * v8)));
}
v33[4] = v8;
v33[5] = v11;
v12 = 1;
for ( ii = 0; ii < 24; ++ii ) // 处理后的v33数组的前24个字节byte_7FF6BF4463C8处的数据进行比较
{
if ( byte_7FF69CE463C8[ii] != *((unsigned __int8 *)v33 + ii) )// 要知道24节密文,逆推出v33加密前的明文
{
v12 = 0;
break;
}
}
if ( v12 ) // 如果匹配成功
{
strcpy(LibFileName, "dbtc\"#?u}}"); // 一堆异或
for ( jj = 0; jj < 10; ++jj )
LibFileName[jj] ^= 0x11u;
hModule = LoadLibraryA(LibFileName); // user32.dll
strcpy(ProcName, "\\tbbpvtS~iP");
for ( kk = 0; kk < 11; ++kk )
ProcName[kk] ^= 0x11u;
ProcAddress = GetProcAddress(hModule, ProcName);// MessageBoxA
strcpy(v31, "H~d6gt1rpdvye1p1sxv1wxby0");
for ( mm = 0; mm < 25; ++mm )
v31[mm] ^= 0x11u; // You've caught a big fish!
qmemcpy(v28, "R~", 2);
v28[2] = 127;
qmemcpy(v29, "vcped}pex~", 10);
v29[10] = 127;
strcpy(v30, "000");
for ( nn = 0; nn < 17; ++nn )
v28[nn] ^= 0x11u; // Congratulations
((void (__fastcall *)(_QWORD, char *, char *, _QWORD))ProcAddress)(0i64, v31, v28, 0i64);
}
while ( 1 )
{
v20 = (unsigned __int8)*v34 - *a2;
if ( v20 || !*v34 )
break;
++v34;
++a2;
}
if ( v20 > 0 )
return 1i64;
if ( v20 >= 0 )
return 0i64;
return 0xFFFFFFFFi64;
}

​ 该函数使用类似XTEA的加密算法,对三个64位数据块进行加密,每块24轮,使用一个20字节的密钥数组v32。加密后的结果与预设的24字节密文比较,正确则触发成功流程。

​ 还有密文

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
void *__fastcall sub_7FF651DB1060(void *a1)
{
int i; // [rsp+0h] [rbp-28h]
char v3[32]; // [rsp+8h] [rbp-20h] BYREF

memset(a1, 0, 0x18ui64);
qmemcpy(v3, "!V", 2);
v3[2] = -105;
v3[3] = -90;
v3[4] = 26;
v3[5] = -43;
v3[6] = -60;
v3[7] = -34;
v3[8] = -92;
v3[9] = -100;
v3[10] = -126;
v3[11] = 77;
v3[12] = -47;
v3[13] = 69;
v3[14] = -56;
v3[15] = 86;
v3[16] = -89;
v3[17] = -76;
v3[18] = -106;
v3[19] = 92;
v3[20] = 77;
v3[21] = 73;
v3[22] = -121;
v3[23] = 32;
for ( i = 0; i < 24; ++i )
*((_BYTE *)a1 + i) = v3[i];
return a1;
}

​ 给出解密

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
#include <iostream>

void decipher(uint32_t v[2], const uint32_t key[4])
{
unsigned int i;
uint32_t v0 = v[0], v1 = v[1], delta = 0x66778899, sum = delta * 24;
for (i = 0; i < 24; i++)
{
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
sum -= delta;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0] = v0;
v[1] = v1;
}

int main()
{
uint8_t EncFlag[24]{};
memcpy(EncFlag, "!V", 2);
EncFlag[2] = -105;
EncFlag[3] = -90;
EncFlag[4] = 26;
EncFlag[5] = -43;
EncFlag[6] = -60;
EncFlag[7] = -34;
EncFlag[8] = -92;
EncFlag[9] = -100;
EncFlag[10] = -126;
EncFlag[11] = 77;
EncFlag[12] = -47;
EncFlag[13] = 69;
EncFlag[14] = -56;
EncFlag[15] = 86;
EncFlag[16] = -89;
EncFlag[17] = -76;
EncFlag[18] = -106;
EncFlag[19] = 92;
EncFlag[20] = 77;
EncFlag[21] = 73;
EncFlag[22] = -121;
EncFlag[23] = 32;

uint8_t Key[] = "DeluxeBait\0\0\0\0\0\0";

decipher((uint32_t*)(EncFlag), (uint32_t*)Key);
decipher((uint32_t*)(EncFlag + 8), (uint32_t*)Key);
decipher((uint32_t*)(EncFlag + 16), (uint32_t*)Key);

printf("%.23s\n", EncFlag);
return 0;
}

​ 得到正确答案NSSCTF{Wh@t_@_b1g_F1sh}

附:

什么是hook?

hook后调用原函数时,直接跳到指定的函数。

inlinehook是hook的一种方式。通过修改原函数开头的汇编指令,直接跳转到指定函数。

VirtualProtect 是 Windows 操作系统中的一个 API 函数,它允许应用程序改变一个内存页的保护属性。这个函数的原型如下:

1
2
3
4
5
6
BOOL VirtualProtect(
LPCVOID lpAddress, // 要改变保护属性的内存页的起始地址
SIZE_T dwSize, // 内存页的大小
DWORD flNewProtect, // 新的保护属性
PDWORD lpflOldProtect // 存储旧的保护属性
);

逆向分析中,VirtualProtect 函数通常用于代码自加密的场景。代码自加密是一种保护代码不被轻易逆向分析的技术,通过在程序运行时动态地修改代码的内存保护属性,使得代码在执行时可以被修改和执行,但在不执行时则不能被读取或修改。

这道题可以搜索字符串VirtualProtect,发行端倪

1
2
3
4
.idata:00007FF651DB4008                 extrn VirtualProtect:qword
.idata:00007FF651DB4008 ; CODE XREF: sub_7FF651DB1C00+8B↑p
.idata:00007FF651DB4008 ; sub_7FF651DB1C00+BA↑p
.idata:00007FF651DB4008 ; DATA XREF: ...

进入sub_7FF651DB1C00

1
2
3
4
5
6
7
8
9
10
11
12
13
char __fastcall sub_7FF651DB1C00(void *a1, __int64 a2)
{
DWORD flOldProtect; // [rsp+20h] [rbp-28h] BYREF
__int64 Src[2]; // [rsp+28h] [rbp-20h] BYREF

LODWORD(Src[0]) = 9727;
WORD2(Src[0]) = 0;
*(__int64 *)((char *)Src + 6) = a2;
VirtualProtect(a1, 0xEui64, 0x40u, &flOldProtect);
memcpy(a1, Src, 0xEui64);
VirtualProtect(a1, 0xEui64, flOldProtect, &flOldProtect);
return 1;
}

查看调用上层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
char sub_7FF651DB1AD0()
{
int i; // [rsp+20h] [rbp-48h]
int j; // [rsp+24h] [rbp-44h]
HMODULE hModule; // [rsp+28h] [rbp-40h]
FARPROC ProcAddress; // [rsp+30h] [rbp-38h]
CHAR ProcName[8]; // [rsp+38h] [rbp-30h] BYREF
CHAR ModuleName[16]; // [rsp+40h] [rbp-28h] BYREF

qmemcpy(ModuleName, "fpagqr`v=w", 10);
ModuleName[10] = 127;
ModuleName[11] = 127;
ModuleName[12] = 0;
for ( i = 0; i < 12; ++i )
ModuleName[i] ^= 0x13u;
hModule = GetModuleHandleA(ModuleName);
strcpy(ProcName, "`gap~c");
for ( j = 0; j < 6; ++j )
ProcName[j] ^= 0x13u;
ProcAddress = GetProcAddress(hModule, ProcName);
sub_7FF651DB1C00(ProcAddress, (__int64)sub_7FF651DB1CE0);
return 1;
}

“`gap~c”解密出来是strcmp,也就是对strcmp做了Hook,如此才解我心中疑惑。