惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
Engineering at Meta
Engineering at Meta
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
博客园 - Franky
The GitHub Blog
The GitHub Blog
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
B
Blog RSS Feed
云风的 BLOG
云风的 BLOG
小众软件
小众软件
罗磊的独立博客
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
美团技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
C
Check Point Blog
WordPress大学
WordPress大学
博客园 - 【当耐特】
博客园 - 司徒正美
D
Docker

Watermelonabc的Blog

又一个黑客模拟游戏 又一个黑客模拟游戏 家庭辅导一晚,有点感想 家庭辅导一晚,有点感想 福州大学单片机原理与应用(跨学科)复习 福州大学单片机原理与应用(跨学科)复习 福州大学毛概期末复习 福州大学毛概期末复习 x86 汇编语言详解 - 2 x86 汇编语言详解 -2 CCB 2026 Half-Final Guangzhou CCB 2026 Half-Final Guangzhou 视频也可以含恶意代码吗 视频也可以含恶意代码吗 Hello,2026 Hello,2026 2025 福州大学算法与数据结构复习 2025 福州大学算法与数据结构复习 2025 福州大学习概期末复习 2025 福州大学习概期末复习 Frida 入门 Frida 入门 记一场贴吧交流 记一场贴吧交流 虚拟机保护 虚拟机保护 2025 强网杯 S9 Reverse 复现 2025 强网杯 S9 Reverse 复现 你如何在 FZU 取到快递 你如何在 FZU 取到快递
出题人的 “恶意”
Watermelonabc · 2025-07-27 · via Watermelonabc的Blog

在做 BUUCTF 上的 [2019 红帽杯] easyRE 时,出题人将我引导进了一篇看雪的文章:[原创] 看雪 CTF 从入门到存活(六)主动防御。由于时过境迁,评论区已经给出了正确的 flag,但在 2019 年的评论中,我们依然能体会到当时参赛者们满头问号的感受。这样一篇高视角的理论文章,到底对解题有多大作用呢?

实际上,出题人就是以该文章的理念设计了这么一道题目。

什么是 “主动防御”

在初期我们做的 CTF 题目中,出题人都是将 flag 获取逻辑放在一个函数中,然后设置障碍,我们称为 “被动防御”。虽然这些障碍可能难以跨越,但参赛者心中都有一条明确的思路:只要越过这些障碍,就能得到 flag。

与之相对的 “主动防御”,是指将参赛者引导到一条无法获取正确 flag 的路径。这条路径同样有一些障碍,参赛者往往会错判预期,认为只需要跨过这些障碍,就可以得到 flag,结果却折服于出题人的小巧思。

主动防御基于过程激励理论,该理论又有三大理论:期望理论、公平理论和强化理论。总结起来就是让参赛者认为 “此路可通” 并不断强化这种信念。

这本身需要出题人有一定水平,可以合理安排真假线索。

示例

我们先从头分析开头的题目。下面的伪代码已经经过我的修改:

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
__int64 sub_4009C6()
{
__int64 result;
int i;
__int64 v2;
__int64 v3;
__int64 v4;
__int64 v5;
__int64 v6;
__int64 v7;
__int64 v8;
__int64 v9;
__int64 v10;
__int64 v11;
_BYTE key[13];
_BYTE key1[4];
_BYTE key2[19];
_QWORD buf[5];
_QWORD buf_1[8];
char v17;
unsigned __int64 v18;

v18 = __readfsqword(0x28u);
qmemcpy(key, "Iodl>Qnb(ocy", 12);
key[12] = 127;
qmemcpy(key1, "y.i", 3);
key1[3] = 127;
qmemcpy(key2, "d`3w}wek9{iy=~yL@EC", sizeof(key2));
memset(buf, 0, 37);
bufRead(0, buf, 37);
BYTE4(buf[4]) = 0;
if ( bufLength(buf) == 36 )
{
for ( i = 0; i < bufLength(buf); ++i )
{
if ( (*(buf + i) ^ i) != key[i] )
{
result = 4294967294LL;
goto LABEL_13;
}
}
puts("continue!");
memset(buf_1, 0, sizeof(buf_1));
v17 = 0;
bufRead(0, buf_1, 64);
HIBYTE(buf_1[4]) = 0;
if ( bufLength(buf_1) == 39 )
{
v2 = base64Encode(buf_1);
v3 = base64Encode(v2);
v4 = base64Encode(v3);
v5 = base64Encode(v4);
v6 = base64Encode(v5);
v7 = base64Encode(v6);
v8 = base64Encode(v7);
v9 = base64Encode(v8);
v10 = base64Encode(v9);
v11 = base64Encode(v10);
if ( !flagCmp(v11, fakeFlag) )



{
puts("You found me!!!");
puts("bye bye~");
}
result = 0;
}
else
{
result = 4294967293LL;
}
}
else
{
result = 0xFFFFFFFFLL;
}
LABEL_13:
if ( __readfsqword(0x28u) != v18 )
StackErrorexit();
return result;
}

对 fakeFlag 进行十次 Base64 解码会得到我们所说的网址。但不是我们想要的 flag。

然而,在 fakeFlag 的下方,还有一串没有被使用的常量:

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
.data:00000000006CC0A0     
.data:00000000006CC0A0 byte_6CC0A0 db 40h
.data:00000000006CC0A0
.data:00000000006CC0A1 db 35h
.data:00000000006CC0A2 db 20h
.data:00000000006CC0A3 byte_6CC0A3 db 56h
.data:00000000006CC0A4 db 5Dh
.data:00000000006CC0A5 db 18h
.data:00000000006CC0A6 db 22h
.data:00000000006CC0A7 db 45h
.data:00000000006CC0A8 db 17h
.data:00000000006CC0A9 db 2Fh
.data:00000000006CC0AA db 24h
.data:00000000006CC0AB db 6Eh
.data:00000000006CC0AC db 62h
.data:00000000006CC0AD db 3Ch
.data:00000000006CC0AE db 27h
.data:00000000006CC0AF db 54h
.data:00000000006CC0B0 db 48h
.data:00000000006CC0B1 db 6Ch
.data:00000000006CC0B2 db 24h
.data:00000000006CC0B3 db 6Eh
.data:00000000006CC0B4 db 72h
.data:00000000006CC0B5 db 3Ch
.data:00000000006CC0B6 db 32h
.data:00000000006CC0B7 db 45h
.data:00000000006CC0B8 db 5Bh
.data:00000000006CC0B9 db 0

通过查询 byte_6CC0A0 的交叉引用,我们发现它被函数 sub_400D35() 使用了。分析该函数会发现 byte_6CC0A3 其实也属于 byte_6CC0A0,于是进行修复,得到最终的函数:

sub_400D35()
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
unsigned __int64 __fastcall sub_400D35(__int64 a1, __int64 a2)
{
unsigned __int64 result;
unsigned int v3;
int i;
int j;
unsigned int v6;
unsigned __int64 v7;

v7 = __readfsqword(0x28u);
v3 = sub_43FD20(0) - qword_6CEE38;
for ( i = 0; i <= 1233; ++i )
{
sub_40F790(v3);
sub_40FE60();
sub_40FE60();
v3 = sub_40FE60() ^ 0x98765432;
}
v6 = v3;
if ( (v3 ^ realFlag[0]) == 'f' && (HIBYTE(v6) ^ realFlag[3]) == 'g' )
{
for ( j = 0; j <= 24; ++j )
sub_410E90(realFlag[j] ^ *(&v6 + j % 4));
}
result = __readfsqword(0x28u) ^ v7;
if ( result )
StackErrorexit();
return result;
}

循环 1 生成随机值 v3,然后赋值给 v6。接下来是一个判断,v3(取低位的 1 字节)和 realFlag 的第一个字符进行异或后等于 fv6 取高位的 1 字节和 realFlag 的第四个字符进行异或后得到 g,则进入第 2 个循环,将 realFlagv6 异或得到 flag

因此我们需要得到符合条件的 v6。根据进入循环 2 的条件,我们可以将 flagrealFlag 的前四个元素进行异或,得到正确的 v6,然后用 v6realFlag 异或得到 flag。

1
2
3
4
5
6
7
cipher = [0x40, 0x35, 0x20, 0x56, 0x5D, 0x18, 0x22, 0x45, 0x17, 0x2F, 0x24, 0x6E, 0x62, 0x3C, 0x27, 0x54, 0x48, 0x6C, 0x24, 0x6E, 0x72, 0x3C, 0x32, 0x45, 0x5B]
key = [0x40^ord('f'),0x35^ord('l'),0x20^ord('a'),0x56^ord('g')]
flag = ""
for i in range(len(cipher)):
flag += chr(cipher[i]^(key[i%4]))
print(flag)

题做完了,不过我们还想看一下出题人是怎么把这个函数埋入程序执行流程中的。

查询 sub_400D35() 的交叉引用,会发现这个函数是存放于 .fini_array 中的。在程序启动流程中,main 函数返回之后便会执行 __libc_csu_fini() 函数,静态编译下该函数会按降序取出 .fini_array 中的函数指针并执行。

_fini_array 是一个特殊的 ELF 符号数组,用于存储在程序或共享对象的终止(清理)阶段将要执行的终止函数的地址。运行时的链接器 / 加载器会在程序或共享对象终止时依次调用这些函数,以完成清理工作。_fini_array 是 ELF 文件中的一部分,属于特殊的节 (section) 之一。这些特殊节在程序执行过程中具有特定的目的和执行顺序。

在 C 程序中,可以通过 __attribute__((destructor)) 指定一个函数在程序退出时执行,这些函数的地址会被放入 .fini_array 中。例如:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

void cleanup() __attribute__((destructor));

void cleanup() {
printf("Program is exiting. Cleaning up resources...\n");
}

int main() {
printf("Hello, World!\n");
return 0;
}

这个题目属于 “可执行代码段中没有被显式 CALL 的函数”,是准确反汇编的主要障碍之一。[1] 很显然,大量选手(包括我哈)都缺乏应对这类题的经验。在我耻辱下播查阅 WP 时,有相当数量的作者说自己没有想到这一点,只能参考别人的 WP。


  1. Disassembly Challenges ↩︎