题目来源: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 生成伪随机数。

请注意,C 的 rand 函数默认以 srand(1) 为种子生成伪随机数。在同一种子下,rand 生成的伪随机数列相同。由于标准做法 [1] 不允许多次更换种子,因此只要知道了一个种子值,那么该程序下所有 rand 函数生成的伪随机数列均相同。Python 的 random 库也提供了这样的 randseed 函数。

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

再往下面看:

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!!!}


  1. std::rand ↩︎