HGAME 2026 复现

HGAME 2026 复现

本文整理 HGAME 2026 中 Crypto、MISC 和 Web 题。附件和截图没有随文章保存,因此只保留可以从文字材料复现的关键步骤。

题目索引

shiori 不想找女友

题目分析

压缩包中包含一个加密 ZIP 和一张 PNG。PNG 中的像素点按照固定间隔排列,图片的 EXIF 信息给出了取样规则:

1
2
3
4
5
6
7
8
{
"block": 1,
"start_x": 10,
"start_y": 10,
"step_x": 7,
"step_y": 7,
"column_num": 450
}

像素重组

按照起点和步长扫描像素,再按行优先顺序写入新图片:

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
import math

from PIL import Image


def extract_pixels(input_file, output_file, rules):
source = Image.open(input_file).convert("RGB")
width, height = source.size
block = rules.get("block", 1)
start_x = rules.get("start_x", 0)
start_y = rules.get("start_y", 0)
step_x = rules.get("step_x", 1)
step_y = rules.get("step_y", 1)
columns = rules.get("column_num", 100)

pixels = []
for y in range(start_y, height, step_y):
for x in range(start_x, width, step_x):
if x + block <= width and y + block <= height:
pixels.append(source.getpixel((x, y)))

if not pixels:
raise ValueError("没有提取到像素,请检查 EXIF 规则")

rows = math.ceil(len(pixels) / columns)
output = Image.new("RGB", (columns, rows), (0, 0, 0))
for index, pixel in enumerate(pixels):
output.putpixel((index % columns, index // columns), pixel)
output.save(output_file)


将题目附件路径传入 extract_pixels 得到重组结果,再将结果作为 ZIP 密码解压后,使用 StegSolve 扫描隐写层即可得到 flag。文章不保存图片附件或图片文件名,也没有在材料中找到最终 flag。

[REDACTED]

题目附件为 manual.pdf,主要是 PDF 隐写。

文本中出现了这句提示:

In case of an undampened local chrono-logical shift, initiate the SCRAMBLE protocol with passphrase 1:PAR4D0X before notifying the on-site Coordinator.

按提示得到 JWT 后进行 Base64 解码:

1
2
Header:  {"alg": "HS256", "typ": "JWT"}
Payload: {"command": "2:AllCl3arToPr0ceud"}

题目中使用了特殊字体,直接复制文本会得到乱码。继续对后续内容进行 LSB 分析即可恢复隐藏数据。由于截图和 PDF 附件没有保存,文章不补写最终 flag。

Endless Emails

题目分析

服务端使用相同的明文和较小指数 e = 3,向多个不同模数的 RSA 公钥发送密文。这是典型的 RSA 广播攻击:当收集到至少 3 组互素模数后,可以使用中国剩余定理恢复 m^3,再计算整数立方根得到明文。

解题脚本

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
from itertools import combinations

import gmpy2
from Cryptodome.Util import number


def load_output(filename):
result = {"n": [], "c": []}
with open(filename, "rb") as file:
for raw_line in file:
line = raw_line.strip().decode()
if not line:
continue
key, value = (part.strip() for part in line.split("=", 1))
if key != "e":
result[key].append(int(value))
return result


def decrypt(groups, exponent):
for group in combinations(zip(groups["n"], groups["c"]), exponent):
modulus_product = 1
for modulus, _ in group:
modulus_product *= modulus

combined = 0
for modulus, ciphertext in group:
partial = modulus_product // modulus
inverse = number.inverse(partial, modulus)
combined += ciphertext * inverse * partial
combined %= modulus_product

message, exact = gmpy2.iroot(gmpy2.mpz(combined), exponent)
if exact:
print(number.long_to_bytes(message))


groups = load_output(
"output_0ef6d6343784e59e2f44f61d2d29896f.txt"
)
decrypt(groups, 3)

成功计算出整数立方根后,输出的字节串就是 flag。

Flux

题目分析

题目给出一个自定义哈希函数和二次同余随机数生成器:

已知 n 和四个连续输出值。初始状态是:

1
shash("Welcome to HGAME 2026!", key)

而 key 小于 70 bit,实际搜索范围很小。先用前三个输出拟合二次函数,再用第四个输出验证,得到初始状态的两个二次根候选,最后穷举 key。

解题步骤

  1. 使用 x1、x2、x3 建立关于 a、b、c 的线性方程组。
  2. 用模 n 的矩阵逆求出二次函数参数。
  3. 由 a*x0^2 + b*x0 + c = x1 得到关于 x0 的二次方程。
  4. 用 Tonelli-Shanks 求判别式平方根。
  5. 穷举 key,验证 shash 的结果。
  6. 对 I get the key now! 计算哈希,并按 VIDAR{...} 输出。

自定义哈希可以直接按题目定义实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
def shash(value, key):
mask = (1 << 256) - 1
state = (ord(value[0]) << 7) & mask
for char in value:
state = ((key * state) & mask) ^ ord(char)
return state ^ len(value)


for key in range(1_000_000):
if shash("Welcome to HGAME 2026!", key) in (x0_1, x0_2):
flag_hash = shash("I get the key now!", key)
print(f"VIDAR{{{flag_hash:x}}}")
break

Classical

题目附件为 task.zip。最初可以从题面猜到 Vigenere 和密钥 hgame,但 RSA 部分仍然可以正常分解。

RSA 部分

已知 p、q、c 和 e = 65537 后,直接计算私钥并解密:

1
2
3
4
5
6
7
from Crypto.Util.number import long_to_bytes


phi = (p - 1) * (q - 1)
d = pow(65537, -1, phi)
message = pow(c, d, p * q)
print(long_to_bytes(message).decode())

得到提示:

1
Vigenere,key=hgame

再使用 Vigenere、密钥 hgame 解密剩余内容。

打好基础

题目文件是 stuck_out_tongue_.txt,内容经过多层 Base 编码。先识别 Base100,再依次处理 Base91、Base85、Base64 等编码层,直到得到明文。

这道题的重点是观察每一层字符集,而不是把所有内容当作同一种 Base 编码直接尝试。

魔理沙的魔法目录

题目提示需要阅读文章一小时。抓取请求后,发现请求头有时间戳,请求体中也有 time 属性。

将时间相关字段修改为满足题目条件的较大值后重新发送请求,即可通过校验并得到 flag。