H&NCTF 2025

Misc

星辉骑士

把后缀.docx 改为.zip 后解压缩

word/media 下找到 flag.zip

伪加密修复后解压,垃圾邮件加密 spammimic - decoded 解密 999.txt 得到 flag{0231265452-you-kn*w-spanmimic}

1
H&NCTF{0231265452-you-kn*w-spanmimic}

谁动了黑线?

附件 sheidongleheixian.csv 的部分数据如下:

1
2
3
4
5
from_address,to_address,amount_sol,timestamp,tx_hash
1P8KDHGQC0NM5MD9WEBAT,1IPDXFBEFC6863BQY66KQ,0.2,1680000662,FPAYmszHFmWAYano3HfRWc
11HPSDI3AL3BBAPYOBJR1,170J437LF23601EIOTQXN,0.2,1680000616,FPAYmszHFVG7mJr2sqKitb
1JTKE95594O0YYLA6DGFU,1WRB9XEAL5I4ZP2KCRD4G,0.2,1680003430,FPAYmt3ZCaGnD1EQFquhMF
11W0LGK7HE2JSA1RHV585,1JV5TMA23YNYCFDQSJZ4Q,0.2,1680001147,FPAYmt1MVg8rvnH8Fx7XAJ

不难发现这是区块链交易数据,其中 tx_hash 还经过了 base58 编码,下面先对 tx_hash 列进行 base58 解码并将结果添加到一个新列 decoded_tx_hash 中,然后保存到文件 sheidongleheixian_decoded.csv

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
import csv
import base58
import os

input_file_path = "sheidongleheixian.csv"
file_name_without_ext, file_extension = os.path.splitext(input_file_path)
output_file_path = f"{file_name_without_ext}_decoded{file_extension}"

new_column_name = 'decoded_tx_hash'

processed_rows = []
header = []


with open(input_file_path, 'r', encoding='utf-8') as infile:
reader = csv.DictReader(infile)

header = reader.fieldnames

for row in reader:
tx_hash_val = row.get('tx_hash', '')
decoded_value = ''

decoded_bytes = base58.b58decode(tx_hash_val)
decoded_value = decoded_bytes.decode('utf-8')

row[new_column_name] = decoded_value
processed_rows.append(row)

new_fieldnames = header + [new_column_name]

with open(output_file_path, 'w', encoding='utf-8', newline='') as outfile:
writer = csv.DictWriter(outfile, fieldnames=new_fieldnames)
writer.writeheader()
writer.writerows(processed_rows)

得到的部分数据如下:

1
2
3
4
from_address,to_address,amount_sol,timestamp,tx_hash,decoded_tx_hash
1P8KDHGQC0NM5MD9WEBAT,1IPDXFBEFC6863BQY66KQ,0.2,1680000662,FPAYmszHFmWAYano3HfRWc,tx000661YGG3QDKE
11HPSDI3AL3BBAPYOBJR1,170J437LF23601EIOTQXN,0.2,1680000616,FPAYmszHFVG7mJr2sqKitb,tx000615QZN6T7KD
1JTKE95594O0YYLA6DGFU,1WRB9XEAL5I4ZP2KCRD4G,0.2,1680003430,FPAYmt3ZCaGnD1EQFquhMF,tx003429JCPKOX0V

下面根据转出地址 (from_address) 和转入地址 (to_address) 来重建资金的流动路径:

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
import pandas as pd
from collections import defaultdict

def find_transaction_chains(csv_path):
"""
从交易数据中还原出交易链。
"""
try:
# 1. 加载数据
df = pd.read_csv(csv_path)
print(f"成功加载 {len(df)} 条交易记录。")
except FileNotFoundError:
print(f"错误: 文件未找到,请检查路径 '{csv_path}' 是否正确。")
return []

# 2. 按时间戳排序,这是构建链条的基础
df = df.sort_values(by='timestamp').reset_index(drop=True)

# 3. 建立一个从 'from_address' 到其发起交易的快速查找字典
transactions_by_sender = defaultdict(list)
all_transactions = df.to_dict('records')

for tx in all_transactions:
transactions_by_sender[tx['from_address']].append(tx)

# 4. 遍历和构建链条
chains = []
visited_hashes = set()

for tx in all_transactions:
# 如果当前交易已经被包含在某个链中,则跳过
if tx['tx_hash'] in visited_hashes:
continue

# 发现了一个新链条的起点
current_chain = []
current_tx = tx

# 循环追踪链条,直到中断
while current_tx is not None:
# 将当前交易加入链条,并标记为已访问
current_chain.append(current_tx)
visited_hashes.add(current_tx['tx_hash'])

# 寻找下一个环节:当前交易的接收方是否是下一笔交易的发送方?
next_sender = current_tx['to_address']

next_tx_candidate = None
# 检查这个接收方是否发起过任何交易
if next_sender in transactions_by_sender:
# 遍历该发送方的所有交易,找到第一个尚未被访问的交易
for potential_next_tx in transactions_by_sender[next_sender]:
if potential_next_tx['tx_hash'] not in visited_hashes:
next_tx_candidate = potential_next_tx
break # 找到了,跳出内层循环

# 更新current_tx,如果没找到下一个环节,它会变为None,循环终止
current_tx = next_tx_candidate

# 将构建完成的链条存入结果列表
chains.append(current_chain)

return chains

def print_chains_summary(chains):
"""打印找到的交易链的摘要信息"""
print("\n" + "="*50)
print(f"分析完成!共找到 {len(chains)} 条独立的交易链。")
print("="*50 + "\n")

# 按链条长度从长到短排序
chains.sort(key=len, reverse=True)

for i, chain in enumerate(chains):
print(f"--- 链条 {i+1} (长度: {len(chain)}) ---")

# 打印链条的起点和终点
start_addr = chain[0]['from_address']
end_addr = chain[-1]['to_address']

print(f" 起点地址: {start_addr}")
print(f" 终点地址: {end_addr}")

path_str = chain[0]['from_address']
for tx in chain:
path_str += f" --({tx['decoded_tx_hash']})--> {tx['to_address']}"
print(f" 路径: {path_str}")



if __name__ == "__main__":
FILE_PATH = "sheidongleheixian_decoded.csv"
found_chains = find_transaction_chains(FILE_PATH)
if found_chains:
print_chains_summary(found_chains)

将运行结果保存到 output.txt 中,得到部分数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
成功加载 7030 条交易记录。

==================================================
分析完成!共找到 3125 条独立的交易链。
==================================================

--- 链条 1 (长度: 6) ---
起点地址: 14B43RN0FRLN2MVPS9D1C
终点地址: 14QIWXQXAET9C1H8OR09U
路径: 14B43RN0FRLN2MVPS9D1C --(tx0000017XIY8MI0)--> 1V6BOKA7ZFVIHYE0NTU2K --(tx000006R6WLJTGV)--> 1NEC9LG6OX02GUPGS9EIS --(tx0000314J815ZDZ)--> 16AXD9WB9Q5DDBTIIQLZK --(tx0001566NBB4FG1)--> 1AYRO89L2T28WOTEU9O1M --(tx000781Z43PIY3K)--> 1Q1PQ0EL8HF0PY55KYKDN --(tx003906Z6HA9JZA)--> 14QIWXQXAET9C1H8OR09U
--- 链条 2 (长度: 6) ---
起点地址: 14B43RN0FRLN2MVPS9D1C
终点地址: 14QIWXQXAET9C1H8OR09U
路径: 14B43RN0FRLN2MVPS9D1C --(tx000002litt7H6R)--> 1QLHG0KSCA7WIMUZOPPO7 --(tx000011F6JVVRGF)--> 146WVE6IQ95L936UO8Y8W --(tx00005602RR07WL)--> 1RDRUCP47T8X597CIT2A3 --(tx000281HKCM3YA1)--> 1LWW3QK5EXYZ39J2NS1C1 --(tx001406Z25GXL9L)--> 1T4T9LOYM46CQV5M8U9T5 --(tx004531CCLW6SRL)--> 14QIWXQXAET9C1H8OR09U

观察到第 2 条链有一个 decoded_tx_hash 不对劲,tx000002litt7H6R 里面包含小写字母,其他的 decoded_tx_hash 都是大写字母(当然 tx 是除外的)

并且里面的 litt 看起来像是 little 的前半部分,在 sheidongleheixian_decoded.csv 中搜索 le 发现了 tx000014le_dWAHC

那么接下来只要找出带小写字母的然后把它们拼起来就可以了

1
2
3
4
5
6
7
import pandas as pd

file_path = "sheidongleheixian_decoded.csv"
df = pd.read_csv(file_path)
pattern = r'^tx.*[a-z]'
filtered_df = df[df['decoded_tx_hash'].str.contains(pattern, regex=True)]
print(filtered_df['decoded_tx_hash'])

得到的输出如下:

1
2
3
4
5
6
286     tx001890mr!!ZIU4
848 tx000002litt7H6R
2298 tx000075og_i43J2
2795 tx000377s_AoOCHG
3801 tx000014le_dWAHC
Name: decoded_tx_hash, dtype: object
1
H&NCTF{little_dog_is_Aomr!!}

Forensics

ez_game

对附件的磁盘镜像 challenge.vhd 进行分析,找到关键信息 readme.txtkey.jpghhhhhhhh.zip

其中 readme.txt 的内容如下:

1
2
3
4
这次是个简单的取证小游戏
1.我藏了一个电脑,它的密码是很简单的弱密码,但是你找的到吗
2.图片没有隐写,但是它凭什么可以成为key,好难猜啊
3.如果你找的了flag,注意flag的内容全部大写

首先第 1 条的 “藏了一个电脑” 指的是容器 hhhh 里的 CentOS 7 镜像(这个要靠第 2 条才能解出来),吐槽一下这里很简单的弱密码指的是系统的密码,不过都拿到镜像文件了这个提示就没啥用

第 2 条想表达的是,key.jpg 就是 VeraCrypt 容器 hhhh 的密码,解开并挂载之后就能对里面的系统镜像进行分析了

第 3 条就是字面意思

HNCTF2025-1

在系统的 /root/test 目录下发现了文件 hhhh.txt,内容如下:

1
‌‌‌‌‍‌·1234567890‌‌‌‌‍‬‬‌‌‌‌‌‍‬‬‍‌‌‌‌‍‬‍‬‌‌‌‌‍‍‌-=

这里存在零宽字符隐写

HNCTF2025-2

得到提示 shift,按住 shift 键把键盘上面那一排从左到右按一遍就行,得到:

1
~!@#$%^&*()_+

这个就是加密压缩包 hhhh.zip 的解压密码,这个压缩包被删除了,就是 $RQCSJAK.zip

解压得到 flag.drawio,用官网提供的在线工具 draw.io 打开查看

HNCTF2025-3

1
H&NCTF{YOU_R_SSSO_COOL}

OSINT

Chasing Freedom 1

查询图片 exif 得知是 5 月 3 日

图片上的渔船有名字闽平渔65599

船位查询查询发现该渔船位置

HNCTF2025-4

1
H&NCTF{0503-丁鼻垄}

Chasing Freedom 2

查询图片 exif 得知是 5 月 4 日

识图搜索图中的黑白灯塔

HNCTF2025-5

1
H&NCTF{0504-东庠岛灯塔}

Chasing Freedom 3

查询图片 exif 得知是 5 月 34 日

图片下方的水桶写着岚庠渡

搜索发现这篇文章等一刻轮渡,过一天属于岛屿的生活 - 东庠岛生活画册_码头_海面_渔船

得到两个信息:位于流水码头,船的名字是岚庠渡 x 号

由于不知道具体是几号,从 1 号开始试,最终试出来是 3 号

1
H&NCTF{0504-流水码头-岚庠渡3号}

Crypto

哈基 coke

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
import numpy as np
import cv2
import os

def arnold_decode(arnold_image, shuffle_times, a, b):
""" Arnold shuffle for rgb image
Args:
arnold_image: input arnold encoded rgb image
shuffle_times: how many times to shuffle
a: element a of the arnold transform matrix
b: element b of the arnold transform matrix
Returns:
Arnold decode image
"""
# 1: 创建新图像
decode_image = np.zeros(shape=arnold_image.shape)
# 2: 计算N
h, w = arnold_image.shape[0], arnold_image.shape[1]
N = h # 或N=w

# 3: 遍历像素坐标变换
for time in range(shuffle_times):
for ori_x in range(h):
for ori_y in range(w):
# 按照公式坐标变换
new_x = ((a * b + 1) * ori_x + (-b) * ori_y) % N
new_y = ((-a) * ori_x + ori_y) % N
decode_image[new_x, new_y, :] = arnold_image[ori_x, ori_y, :]

# 更新坐标
arnold_image = np.copy(decode_image)

return decode_image

# 检查文件路径是否存在
image_path = 'en_flag.png'
if not os.path.exists(image_path):
print(f"文件 {image_path} 不存在")
exit()

encoded_image = cv2.imread(image_path)
if encoded_image is None:
print(f"无法读取文件 {image_path}")
exit()

decoded_image = arnold_decode(encoded_image,6,9,1)
output_path = "coke.png"
cv2.imwrite(output_path, decoded_image)

print("处理完成")

猫变换

HNCTF2025-6

1
H&NCTF{haji_coke_you_win}

Reverse

签到 re

Gemini 一把梭

代码分析:

  1. main 函数:

    • 程序使用硬编码的密钥 "MySecretKey123!"
    • 调用 sub_11B9 函数,基于该密钥生成一个 32 位的加密密钥 v5
    • 读取用户输入 s
    • 调用 sub_1452 函数,使用 v5 对用户输入 s 进行加密。
    • 将加密后的结果与 byte_4080 的内容逐字节比较。如果完全相同,则输出 “right”。
  2. sub_11B9 函数:

    • 计算 "MySecretKey123!" 的 SHA256 哈希值。
    • 取哈希值的前 4 个字节,记为 d0, d1, d2, d3
    • 通过位运算生成最终的 4 字节加密密钥 k = [k0, k1, k2, k3]
      • k0 = d0 | 1
      • k1 = d1 & 0xFE
      • k2 = d2 & 0xFE
      • k3 = d3 | 1
    • 这个过程确保了 k0k3 是奇数,而 k1k2 是偶数。
  3. sub_13ACsub_1452 函数:

    • sub_1452 是主加密函数。它将输入数据填充到 4 字节的倍数,然后以 4 字节为一块进行处理。
    • 每一块 4 字节的明文被分成两个 2 字节的块。
    • sub_13AC 是核心的加密算法。它对每个 2 字节的块进行操作。其运算本质上是一个模 256 的 2x2 矩阵乘法:

      (c0c1)=(k0k1k2k3)(p0p1)(mod256)\begin{pmatrix} c_0 \\ c_1 \end{pmatrix} = \begin{pmatrix} k_0 & k_1 \\ k_2 & k_3 \end{pmatrix} \begin{pmatrix} p_0 \\ p_1 \end{pmatrix} \pmod{256}

      其中 (p0, p1) 是 2 字节的明文,(c0, c1) 是加密后的 2 字节密文,k 是从 sub_11B9 得到的 4 字节密钥。

解题思路:

要得到 flag,我们需要执行以下步骤:

  1. 重新生成加密密钥: 完全按照 sub_11B9 的逻辑,从 "MySecretKey123!" 生成 4 字节的密钥矩阵 K
  2. 求逆矩阵: 为了解密,我们需要找到加密矩阵 K 在模 256 下的逆矩阵 K_inv。解密操作就是 P = K_inv * C mod 256
    • 求矩阵的行列式: det(K) = (k0*k3 - k1*k2) mod 256
    • 求行列式的模逆元: det(K)^-1 mod 256
    • 计算逆矩阵: K_inv = det(K)^-1 * [[k3, -k1], [-k2, k0]] mod 256
  3. 解密数据:
    • byte_4080 中提取密文数据(从第 5 个字节开始)。
    • 将密文数据以 4 字节为单位进行分割。
    • 将每个 4 字节的块再分成两个 2 字节的块。
    • 使用求得的逆矩阵 K_inv 对每个 2 字节的密文块进行解密。
    • 将解密后的数据拼接起来。
  4. 提取 Flag: byte_4080 的前 4 个字节 (00 00 00 25) 指明了原始明文的长度是 37 字节。从解密后的数据中截取前 37 个字节,即为最终的 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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import hashlib

def generate_key(secret: str) -> list[int]:
"""
根据 sub_11B9 的逻辑从密钥字符串生成加密密钥。
"""
sha256 = hashlib.sha256(secret.encode()).digest()
d = sha256[:4]

# k = [k0, k1, k2, k3]
key = [0] * 4
key[0] = d[0] | 1
key[1] = d[1] & 0xFE
key[2] = d[2] & 0xFE
key[3] = d[3] | 1

return key

def get_inverse_matrix(key: list[int]) -> list[int]:
"""
计算加密矩阵在模256下的逆矩阵。
"""
k0, k1, k2, k3 = key

# 计算行列式 det(K) = (k0*k3 - k1*k2) mod 256
det = (k0 * k3 - k1 * k2) % 256

# 计算行列式的模256乘法逆元
# 在 Python 3.8+ 中,可以直接使用 pow(det, -1, 256)
try:
det_inv = pow(det, -1, 256)
except ValueError:
raise ValueError(f"Determinant {det} has no modular inverse for modulus 256")

# 计算逆矩阵 K_inv = det_inv * [[k3, -k1], [-k2, k0]] mod 256
inv_matrix = [0] * 4
inv_matrix[0] = (det_inv * k3) % 256
inv_matrix[1] = (det_inv * -k1) % 256
inv_matrix[2] = (det_inv * -k2) % 256
inv_matrix[3] = (det_inv * k0) % 256

return inv_matrix

def decrypt_block(cipher_block: bytes, inv_matrix: list[int]) -> bytes:
"""
使用逆矩阵解密一个2字节的数据块。
P = K_inv * C mod 256
"""
c0, c1 = cipher_block
m0, m1, m2, m3 = inv_matrix

p0 = (m0 * c0 + m1 * c1) % 256
p1 = (m2 * c0 + m3 * c1) % 256

return bytes([p0, p1])

def solve():
"""
主函数,执行完整的解密流程并输出Flag。
"""
# .data:0000000000004080
byte_4080 = bytes([
0x00, 0x00, 0x00, 0x25, 0x0C, 0xE2, 0x70, 0x89, 0x98, 0xB2, 0xBB, 0xE4,
0x94, 0xA0, 0x95, 0xAC, 0x38, 0x92, 0x22, 0xF8, 0x0E, 0x7B, 0x76, 0x1A,
0x66, 0xC8, 0x03, 0x05, 0x2E, 0x7D, 0xA1, 0x04, 0x3D, 0xC0, 0x62, 0xFE,
0x66, 0x67, 0x02, 0x87, 0x81, 0xF4, 0x00, 0x00
])

# 1. 生成密钥
secret = "MySecretKey123!"
key = generate_key(secret)
print(f"🔑 生成的加密密钥 (k0,k1,k2,k3): {key}")

# 2. 计算逆矩阵
inv_matrix = get_inverse_matrix(key)
print(f"🔢 逆矩阵 (mod 256): {inv_matrix}")

# 3. 提取密文和明文长度
original_len = int.from_bytes(byte_4080[:4], 'big')
ciphertext = byte_4080[4:]

decrypted_padded = bytearray()

# 4. 循环解密
# 将密文按4字节分块
for i in range(0, len(ciphertext), 4):
chunk = ciphertext[i : i+4]
if len(chunk) < 4:
# 处理可能的填充不完整情况(虽然在此题中不会发生)
continue

# 将4字节块分成两个2字节块并解密
decrypted_padded.extend(decrypt_block(chunk[0:2], inv_matrix))
decrypted_padded.extend(decrypt_block(chunk[2:4], inv_matrix))

# 5. 截取原始长度,得到Flag
flag = decrypted_padded[:original_len].decode('utf-8')

print("\n" + "="*40)
print(f"🚩 解密得到的 Flag: {flag}")
print("="*40)

if __name__ == "__main__":
solve()

运行结果

1
2
3
4
5
6
🔑 生成的加密密钥 (k0,k1,k2,k3): [81, 22, 52, 251]
🔢 逆矩阵 (mod 256): [217, 238, 4, 171]

========================================
🚩 解密得到的 Flag: H&NCTF{840584fb08a26f01c471054628e451
========================================
1
H&NCTF{840584fb08a26f01c471054628e451}