题目来源:LockedSecret from GHCTF 2025 新生赛(公开赛道)
官方 WP:GHCTF2025WP - Liv’s blog

出题人的提示

Reverse - LockedSecret Hint1:
如果加密函数 IDA 伪代码太乱看不懂,请尝试 Ghidra,或许能帮助你分析算法解出 Flag。
Ghidra 些许情况对比 IDA 有奇效。

用 DIE 查一下壳:

发现用了 UPX 壳(注意这里的 [modified])

脱壳

尝试使用 upx -d 直接脱壳,发现不行:

1
upx: .\LockedSecret.exe: CantUnpackException: file is modified/hacked/protected; take care!!!
Tip

对于 UPX 壳,大部分情况下应使用 upx -d 脱壳。

这下不知道怎么办了吧,哈哈!

后面查了一下报错信息,得到的答案是:程序的 UPX 特征被篡改过,导致 UPX 不敢直接脱壳。此时需要恢复程序的 UPX 特征。

为什么要修改 UPX 特征

UPX0UPX1 是加 UPX 壳后的两个区段名。其中 UPX1 区段包含了需要解压的数据块。.rsrc 是程序资源信息区段名,这个区段含有原资源段的完整头部以及图标、Manifest、版本等未被压缩的资源,当然还有 UPX 自身需要的导入信息等(如果程序自身不含资源段,加壳后就是 UPX2)。UPX0UPX1 可以被随意改成任何字符串,虽然这样改用处不大,但是也能起到伪装的作用。

—— 手动去 upx 特征 - whatday

用任意十六进制编辑器打开程序(我使用 ImHex):

发现 .rsrc 段,那么 LIVV 就是修改后的 UPX 特征。

参考了几个遇到相似问题的文章,发现只需要将 LIVV 的前三个字母 LIV 改成 UPX 即可。

Warning

注意查一下字符串,下面还有 LIV。要把所有的 LIV 都替换成 UPX

保存结果,再次使用 upx -d,发现成功脱壳。

解密

将脱壳后的程序拖进 IDA:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-108h]
char v5; // [esp+0h] [ebp-108h]
int i; // [esp+0h] [ebp-108h]
char input[256]; // [esp+4h] [ebp-104h] BYREF

memset(input, 0, sizeof(input));
printf("Input your flag:", v4);
scanf("%32s", (char)input);
if ( strlen(input) != 32 )
{
printf("Wrong length!\n", v5);
system("pause");
exit(0);
}
sub_401100();
sub_401190(input);
for ( i = 0; i < 32; ++i )
{
if ( flag[i] != input[i] )
{
printf("Wrong!\n", i);
system("pause");
exit(0);
}
}
printf("Right!\n", i);
system("pause");
return 0;
}

(重命名了部分函数和对象)

尝试用 Findcrypt 查一下加密算法,结果是空的!这就不好玩了。

然后看一下 sub_401100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int sub_401100()
{
int i; // [esp+0h] [ebp-4h]

srand(3246389708u);
for ( i = 0; i < 8; ++i )
{
if ( i )
dword_4043D8[i] = dword_4043D4[i] ^ rand();
else
dword_4043D8[0] = rand();
dword_4043D8[i] %= 256;
}
return 0;
}

该函数利用伪随机初始化一个全局数组。

伪随机简介

这里的伪随机 (Pseudo-random) 是统计学意义上的:在给定的随机比特流样本中,1 的数量大致等于 0 的数量,满足这类要求的数字 “一眼看上去” 是随机的。

对于随机数的使用,一般是先播种,然后使用伪随机数函数来获取随机数。不播种会使用默认的种子。这种通过伪随机数函数得到的随机数,就是伪随机数,只要种子固定那么每次生成的随机数序列就会一样。

C 语言中的 srandrand,C++ 的 mt19937default_random_engine 等都是伪随机数函数。

具体到本例,第 5 行的 srand 负责伪随机数的初始化,即播种(IDA View 也给 3246389708 标注了 seed 注释)。之后的 rand 生成伪随机数。

(参考 伪随机数问题浅析 - 天工实验室

sub_401190

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
int __cdecl sub_401190(int a1)
{
unsigned int v1; // ecx
unsigned int v2; // eax
int result; // eax
int v4; // [esp+0h] [ebp-7Ch]
unsigned int v5; // [esp+4h] [ebp-78h]
unsigned int v6; // [esp+14h] [ebp-68h]
unsigned int v7; // [esp+18h] [ebp-64h]
unsigned int v8; // [esp+1Ch] [ebp-60h]
unsigned int v9; // [esp+20h] [ebp-5Ch]
unsigned int v10; // [esp+24h] [ebp-58h]
unsigned int v11; // [esp+28h] [ebp-54h]
unsigned int v12; // [esp+30h] [ebp-4Ch]
unsigned int v13; // [esp+38h] [ebp-44h]
unsigned int v14; // [esp+3Ch] [ebp-40h]
unsigned int v15; // [esp+40h] [ebp-3Ch]
unsigned int v16; // [esp+44h] [ebp-38h]
unsigned int v17; // [esp+48h] [ebp-34h]
int i; // [esp+4Ch] [ebp-30h]
int v19; // [esp+50h] [ebp-2Ch]
int v20[4]; // [esp+54h] [ebp-28h] BYREF
int Src[5]; // [esp+64h] [ebp-18h] BYREF

strcpy((char *)Src, "IamTheKeyYouKnow");
for ( i = 0; i < 15; ++i )
*((_BYTE *)Src + i) ^= LOBYTE(dword_4043D8[i % 8]);
memcpy(v20, Src, sizeof(v20));
v19 = 4;
do
{
v17 = *(_DWORD *)(a1 + 8 * (4 - v19) + 4);
v16 = *(_DWORD *)(a1 + 8 * (4 - v19)) + ((v20[1] + (v17 >> 5)) ^ (v17 + 1579382783) ^ (v20[0] + 16 * v17));
v15 = v17 + ((v20[3] + (v16 >> 5)) ^ (v16 + 1579382783) ^ (v20[2] + 16 * v16));
v14 = v16 + ((v20[1] + (v15 >> 5)) ^ (v15 - 1136201730) ^ (v20[0] + 16 * v15));
v13 = v15 + ((v20[3] + (v14 >> 5)) ^ (v14 - 1136201730) ^ (v20[2] + 16 * v14));
v12 = v13
+ ((v20[3] + ((v14 + ((v20[1] + (v13 >> 5)) ^ (v13 + 443181053) ^ (v20[0] + 16 * v13))) >> 5)) ^ (v14 + ((v20[1] + (v13 >> 5)) ^ (v13 + 443181053) ^ (v20[0] + 16 * v13)) + 443181053) ^ (v20[2] + 16 * (v14 + ((v20[1] + (v13 >> 5)) ^ (v13 + 443181053) ^ (v20[0] + 16 * v13)))));
v1 = v14
+ ((v20[1] + (v13 >> 5)) ^ (v13 + 443181053) ^ (v20[0] + 16 * v13))
+ ((v20[1] + (v12 >> 5)) ^ (v12 + 2022563836) ^ (v20[0] + 16 * v12));
v11 = v12 + ((v20[3] + (v1 >> 5)) ^ (v1 + 2022563836) ^ (v20[2] + 16 * v1));
v10 = v1 + ((v20[1] + (v11 >> 5)) ^ (v11 - 693020677) ^ (v20[0] + 16 * v11));
v9 = v11 + ((v20[3] + (v10 >> 5)) ^ (v10 - 693020677) ^ (v20[2] + 16 * v10));
v8 = v10 + ((v20[1] + (v9 >> 5)) ^ (v9 + 886362106) ^ (v20[0] + 16 * v9));
v7 = v9 + ((v20[3] + (v8 >> 5)) ^ (v8 + 886362106) ^ (v20[2] + 16 * v8));
v6 = v8 + ((v20[1] + (v7 >> 5)) ^ (v7 - 1829222407) ^ (v20[0] + 16 * v7));
v2 = v7 + ((v20[3] + (v6 >> 5)) ^ (v6 - 1829222407) ^ (v20[2] + 16 * v6));
v5 = v2
+ ((v20[3] + ((v6 + ((v20[1] + (v2 >> 5)) ^ (v2 - 249839624) ^ (v20[0] + 16 * v2))) >> 5)) ^ (v6 + ((v20[1] + (v2 >> 5)) ^ (v2 - 249839624) ^ (v20[0] + 16 * v2)) - 249839624) ^ (v20[2] + 16 * (v6 + ((v20[1] + (v2 >> 5)) ^ (v2 - 249839624) ^ (v20[0] + 16 * v2)))));
*(_DWORD *)(a1 + 8 * (4 - v19)) = (v6 + ((v20[1] + (v2 >> 5)) ^ (v2 - 249839624) ^ (v20[0] + 16 * v2))) ^ 0xF;
*(_DWORD *)(a1 + 8 * (4 - v19) + 4) = v5 ^ 0xF;
v4 = v19;
result = --v19;
}
while ( v4 );
return result;
}

被逆向气晕(

我们还是能看出点东西的。第 25-27 行将一串明文 key 与上一个函数初始化的值进行异或计算得到用于加密的 Key。但由于 IDA 的反编译器问题,加密过程反编译代码明显缺乏规律,让人看不出特征。

切换到 Ghidra(通过搜索字符串查找主函数,再跳转):

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

void __cdecl FUN_00401190(int param_1)

{
uint uVar1;
uint uVar2;
bool bVar3;
uint local_34;
int local_30;
int local_2c;
int local_28;
int local_24;
int local_20;
byte local_1c [20];
uint local_8;

local_8 = DAT_00404000 ^ (uint)&stack0xfffffffc;
local_1c[0] = 'I';
local_1c[1] = 'a';
local_1c[2] = 'm';
local_1c[3] = 'T';
local_1c[4] = 'h';
local_1c[5] = 'e';
local_1c[6] = 'K';
local_1c[7] = 'e';
local_1c[8] = 'y';
local_1c[9] = 'Y';
local_1c[10] = 'o';
local_1c[0xb] = 'u';
local_1c[0xc] = 'K';
local_1c[0xd] = 'n';
local_1c[0xe] = 'o';
local_1c[0xf] = 'w';
local_1c[0x10] = 0;
for (local_34 = 0; (int)local_34 < 0xf; local_34 = local_34 + 1) {
uVar1 = local_34 & 0x80000007;
if ((int)uVar1 < 0) {
uVar1 = (uVar1 - 1 | 0xfffffff8) + 1;
}
local_1c[local_34] = local_1c[local_34] ^ (byte)*(undefined4 *)(&DAT_004043d8 + uVar1 * 4);
}
local_2c = 0;
local_28 = 0;
local_24 = 0;
local_20 = 0;
memcpy(&local_2c,local_1c,0x10);
local_30 = 4;
do {
uVar1 = *(uint *)(param_1 + 4 + (4 - local_30) * 8);
uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x5e2377ff ^ (uVar1 >> 5) + local_28) +
*(int *)(param_1 + (4 - local_30) * 8);
uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x5e2377ff ^ (uVar2 >> 5) + local_20) + uVar1;
uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0xbc46effe ^ (uVar1 >> 5) + local_28) + uVar2;
uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0xbc46effe ^ (uVar2 >> 5) + local_20) + uVar1;
uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x1a6a67fd ^ (uVar1 >> 5) + local_28) + uVar2;
uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x1a6a67fd ^ (uVar2 >> 5) + local_20) + uVar1;
uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x788ddffc ^ (uVar1 >> 5) + local_28) + uVar2;
uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x788ddffc ^ (uVar2 >> 5) + local_20) + uVar1;
uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0xd6b157fb ^ (uVar1 >> 5) + local_28) + uVar2;
uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0xd6b157fb ^ (uVar2 >> 5) + local_20) + uVar1;
uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x34d4cffa ^ (uVar1 >> 5) + local_28) + uVar2;
uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x34d4cffa ^ (uVar2 >> 5) + local_20) + uVar1;
uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x92f847f9 ^ (uVar1 >> 5) + local_28) + uVar2;
uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x92f847f9 ^ (uVar2 >> 5) + local_20) + uVar1;
uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0xf11bbff8 ^ (uVar1 >> 5) + local_28) + uVar2;
*(uint *)(param_1 + (4 - local_30) * 8) = uVar2 ^ 0xf;
*(uint *)(param_1 + 4 + (4 - local_30) * 8) =
(uVar2 * 0x10 + local_24 ^ uVar2 + 0xf11bbff8 ^ (uVar2 >> 5) + local_20) + uVar1 ^ 0xf;
bVar3 = local_30 != 0;
local_30 = local_30 + -1;
} while (bVar3);
FUN_0040173b(local_8 ^ (uint)&stack0xfffffffc);
return;
}

可以很清楚的看出就是 Tea 加密,不过用重复计算和不同的 sum 值来代替了循环加密。

将第二次计算的 bc46effe 减去 5e2377ff 会发现结果还是 5e2377ff,所以可以知道 5e2377ff 就是 delta 值,那么这就是一个完整的从 delta 值开始的 Tea 加密。

并且两两计算为一组,可以看出是 8 轮加密的 Tea,并且最后将加密完的值再异或上了 0xf

解密脚本:

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

void tea_decrypt(uint32_t v[2], const uint32_t k[4])
{
uint32_t v0 = v[0], v1 = v[1];

uint32_t sum = 0x5E2377FF * 8;

uint32_t delta = 0x5E2377FF;

for (uint32_t i = 0; i < 8; i++)
{
v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
sum -= delta;
}

v[0] = v0;
v[1] = v1;
}

int main()
{
unsigned char Key[] =
{
0x2D,0xF7,0x3D,0x42,0x01,0x9A,0xF5,0x05,0x1D,0xCF,0x3F,0x63,0x22,0x91,0xD1,0x77
};
unsigned char EncFlag[] = {
0xDC,0x45,0x1E,0x03,0x89,
0xE9,0x76,0x27,0x47,0x48,
0x23,0x01,0x70,0xD2,0xCE,
0x64,0xDA,0x7F,0x46,0x33,
0xB1,0x03,0x49,0xA3,0x27,
0x00,0xD1,0x2C,0x37,0xB3,
0xBD,0x75 };
for (int i = 0; i < 4; i++)
{
*(uint32_t*)(EncFlag + i * 8) ^= 0xf;
*(uint32_t*)(EncFlag + i * 8 + 4) ^= 0xf;
tea_decrypt((uint32_t*)(EncFlag + i * 8), (uint32_t*)Key);
}

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

得到 Flag:NSSCTF{!!!Y0u_g3t_th3_s3cr3t!!!}


©2025-Present Watermelonabc | 萌ICP备20251229号

Powered by Hexo & Stellar latest & Vercel & 𝙌𝙞𝙪𝙙𝙪𝙣 𝘾𝘿𝙉 & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅 | 中文独立博客列表

猫猫🐱 发表了 41 篇文章 · 总计 209.8k 字