[ 刷题记录 ] NSSCTF
2026-08-22 20:22:46

NSSCTF 刷题记录

📎 相关:
| 1.图片隐写
| 3.编码分析
| 4.压缩包分析

[NCTF 2022]zystego

题目只提供了一张图片fd.png

Step1:binwalk分类+压缩包爆破

binwalk -eM fd.png(-e是自动提取识别到的嵌入内容 -M是递归提取

从图片中分离出一个加密压缩包

使用ARCHPR爆破,获得密码114514

flag.txt是一个假flag 不用理会

1
2
3
Base混合多重解码:
[解码4次] Base64 -> Base64 -> Base64 -> Base64
混合解码结果:NCTF{1t_1s_N0t_th@t_s1mpL3_S0_th1s_1s_A_F@k3_f1a9_L0L}

something实际上是一个PGP私钥

Step2:图像边缘LSB隐写提取

用Photoshop把fd.png放大到1000%,观察到图片最右侧三列像素颜色异常,这是LSB隐写典型特征

NSSCTF刷题-01

NSSCTF刷题-02

LSB(RGB最低位)隐写

NSSCTF刷题-03

每个像素的RGB通道各有8bit(0~255)

最低位(LSB)的改变只会让像素值变化±1,肉眼完全无法察觉

那么每个像素可以藏3bit(R、G、B各1bit)

为什么选最低位

位权 bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 (LSB)
权重 128 64 32 16 8 4 2 1

修改bit0最多让像素值变化1,而修改bit7会变化128。所以LSB修改对图像质量的影响最小

exp1

编写脚本提取右侧三列像素的RGB最低位(LSB数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from PIL import Image

# 打开图像并转换为 RGB 模式以确保一致性
img = Image.open('fd.png').convert('RGB')
w, h = img.width, img.height

bin_data = ''
for y in range(h):
for x in range(w - 3, w): # 遍历最右边的3列
color = img.getpixel((x, y))
for i in range(3): # R, G, B 三个通道
bin_data += str(color[i] % 2)

# 将二进制数据写入文件
with open('output_bits.txt', 'w', encoding='utf-8') as f:
f.write(bin_data)

print("二进制数据已保存到 output_bits.txt")

# # color[i] % 2 就是取最低位。例如:
# 142 % 2 = 0 → bit 为 0
# 143 % 2 = 1 → bit 为 1

得到一长列01字符串

exp2

output_bits.txt每八位一组转成ASCII:

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
# 读取 output_bits.txt 中的二进制字符串,并还原为中文文本
with open('output_bits.txt', 'r') as f:
bin_data = f.read().strip()

# 将二进制字符串转换为字节
# 先补全到8的倍数(防止最后几位不完整)
padding = (8 - len(bin_data) % 8) % 8
bin_data_padded = bin_data + '0' * padding

# 每8位一组,转为整数,再转为字节
byte_data = bytes(int(bin_data_padded[i:i+8], 2) for i in range(0, len(bin_data_padded), 8))

# 使用 UTF-8 解码,得到原始文本
try:
text = byte_data.decode('utf-8')
print("解码成功!")
print(text)

# 可选:保存为文本文件
with open('decoded_text.txt', 'w', encoding='utf-8') as f:
f.write(text)
print("已保存到 decoded_text.txt")

except UnicodeDecodeError as e:
print("UTF-8 解码失败,可能是编码错误或数据损坏:", e)

Step3: 分析 DCT 隐写编码脚本

分析提取出的python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import secret  # secret 是要隐藏的二进制字符串

丁真 = np.float32(cv2.imread("fadian.png", 1)) # 读取原图,转 float32

for i in range(64):
for j in range(64):
芝士 = randint(0, 2) # 随机选一个通道 (0=B, 1=G, 2=R)
小马珍珠 = 丁真[:, :, 芝士] # 取出该通道
雪豹 = cv2.dct(小马珍珠[8*i:8*i+8, 8*j:8*j+8]) # 对 8×8 块做 DCT
if secret[i*64+j] == '1':
雪豹[7,7] = 20 # bit=1 → 系数设为 +20
elif secret[i*64+j] == '0':
雪豹[7,7] = -20 # bit=0 → 系数设为 -20
小马珍珠[8*i:8*i+8, 8*j:8*j+8] = cv2.idct(雪豹) # IDCT 写回
丁真[:, :, 芝士] = 小马珍珠

cv2.imwrite("fd.png", 丁真)
# a gift for you : %$#%$#jhgasdfg76342t ← PGP passphrase

DCT(离散余弦变换)隐写

DCT 是什么?

DCT将图像从空间域变换到频率域。是 JPEG 压缩的核心步骤。

NSSCTF刷题-04

8*8DCT系数矩阵的频率分布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
低频 ←————————————→ 高频
┌────┬────┬────┬────┬────┬────┬────┬────┐
│DC │ │ │ │ │ │ │ │ ← 低频(整体亮度/结构)
├────┼────┼────┼────┼────┼────┼────┼────┤
│ │ │ │ │ │ │ │ │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ │ │ │ │ │ │ │ │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ │ │ │ │ │ │ │ │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ │ │ │ │ │ │ │ │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ │ │ │ │ │ │ │ │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ │ │ │ │ │ │ │ │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ │ │ │ │ │ │ │ ★ │ ← [7,7] 最高频,人眼最不敏感
└────┴────┴────┴────┴────┴────┴────┴────┘
↑ 高频
  • [0,0]是 DC 系数(直流分量),代表整个块的平均亮度
  • [7,7]是最高频分量,代表最精细的细节变化
  • 人眼对高频分量的变化极不敏感,所以修改[7,7]不会引起视觉差异

为什么用 ±20?

这是出题人设定的嵌入强度。

  • 值太小(如 ±1)容易被图像保存时的精度损失淹没;
  • 值太大(如 ±100)可能产生可见伪影。
  • ±20 是一个平衡点——足够被检测到,又不破坏图像质量。

DCT 隐写 vs LSB 隐写对比:

特性 LSB 隐写 DCT 隐写
操作域 空间域(直接改像素值) 频率域(改 DCT 系数)
抗压缩性 差(JPEG 压缩会破坏) 强(JPEG 本身就基于 DCT)
隐蔽性 一般(统计分析可检测) 较好(利用人眼视觉特性)
容量 大(每像素每通道 1 bit) 小(每 8×8 块 1 bit)
检测难度 较易(chi-square 等) 较难

坑:编码脚本用 randint(0, 2) 随机选 R/G/B 通道之一嵌入,解码时不知道每个块用了哪个通道,三个通道都要遍历。

Step4:DCT域提取secret

根据编码脚本逆向写出提取脚本

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

img = np.float32(cv2.imread('fd.png', 1))
bits = []

for i in range(64):
for j in range(64):
bit_found = None
for k in range(3): # 检查 R, G, B
block = img[8*i:8*i+8, 8*j:8*j+8, k]
if block.shape != (8, 8):
continue
dct_block = cv2.dct(block)
coeff = dct_block[7, 7]
if coeff >= 12:
bit_found = '1'
break # 找到就停,避免重复
elif coeff <= -12:
bit_found = '0'
break
# 如果三个通道都没触发,保守设为 '0'
if bit_found is None:
bit_found = '0'
bits.append(bit_found)

secret = ''.join(bits)
print(secret)

# 可选:保存
with open('secret.txt', 'w') as f:
f.write(secret)

为什么阈值用 12 而不是 20?

图像经过 cv2.imwrite 保存为 PNG 时,虽然 PNG 是无损格式,但 float32 → uint8 的类型转换会引入舍入误差。原本设为 ±20 的系数,读回后做 DCT 可能变成 ±15~±20 左右。所以用一个宽松的阈值(如 12)来判断。

将提取到的 4096 bit(64×64)二进制串转为字节,得到 hidden.zip

NSSCTF刷题-05

Step5:PGP解密获取flag

hidden.zip解压获得flag.pgp这是一个PGP加密文件

PGP 加密

PGP(Pretty Good Privacy)是一种非对称加密体系:

  • 公钥加密数据
  • 私钥解密数据
  • 私钥本身还有一个 passphrase 保护

本题中:

私钥文件是Step1中从压缩包解出的something

Passphrase是Step3中脚本注释里的%$#%$#jhgasdfg76342t

1
2
3
4
5
6
# 导入私钥
gpg --import something
# 输入 passphrase: %$#%$#jhgasdfg76342t

# 解密
gpg --decrypt flag.pgp
1
NCTF{zys_1s_s0_V3g3T@13lE_qwq}

NSSCTF刷题-06

[SWPU 2019]伟大的侦探

用010打开txt

视图-字符集选择 EBCDIC ,得到密码wllm_is_the_best_team!

解密跳舞的小人即可

[SWPUCTF 2021 新生赛]zipbomb

一个极大数级的压缩包,后缀改成rar,解压

[SWPUCTF 2021 新生赛]我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
from collections import Counter
import re

# 读取整个文件
with open(r'附件.txt', 'r', encoding='utf-8') as f:
content = f.read()

# 提取所有 {...} 中的内容
pattern = re.compile(r'{(.*?)}', re.S)
items = re.findall(pattern, content)

# 合并所有匹配内容
combined = ''.join(items)

# 【可选】只保留字母(根据题目需求决定是否启用)
# combined = ''.join(ch for ch in combined.lower() if ch.isalpha())

# 统计小写字母频率(若上面没转,这里转)
freq = Counter(combined.lower())

# 按频率降序排列
most_common = freq.most_common()

# 输出字符序列(按频率从高到低)
result = ''.join(char for char, _ in most_common)
print("提取内容:", combined)
print("词频排序:", most_common)
print("频率序列:", result)

频率序列: yourflagis81e57d2bc90364

[SWPUCTF 2021 新生赛]我的银行卡密码

ARCHPR爆破获得密码768521

1.T9键盘密码(手机九宫格键盘)

原理:

老式手机的拨号键盘,每个数组键对应若干字母

1
2
2 → abc    3 → def    4 → ghi    5 → jkl
6 → mno 7 → pqrs 8 → tuv 9 → wxyz

编码规则:两位数字表示一个字母——十位的键位编号,个位的该键上字母的序号

1
2
3
4
例如:
93 → 键 9(wxyz)的第 3 个字母 → y
53 → 键 5(jkl)的第 3 个字母 → l
71 → 键 7(pqrs)的第 1 个字母 → p

所以整串 93 53 63 71 51 63 41 51 83 63 23 23 93 62 61 94 93 71 41 92 41 71 63 41 51 31 83 43 41 21 81 22 21 74 42 解出来就是:

1
ylopjogjvoccynmzypgxgpogjdvigatbash

题目提示”下一阶段的加密方案由最后的字母决定”,结果末尾是 atbash,所以用Atbash 解密

2.Atbash密码

原理: 一种简单的字母替换密码,源自希伯来语。规则是字母表首尾对调:

1
2
明文: a b c d e f g h i j k l m n o p q r s t u v w x y z
密文: z y x w v u t s r q p o n m l k j i h g f e d c b a

即 a↔z, b↔y, c↔x, d↔w…它是自逆的——加密和解密用同一个操作。

http://www.hiencode.com/atbash.html

Atbash 没有密钥,是固定映射,属于最简单的替换密码之一。

1
ylopjogjvoccynmzypgxgpogjdvig → bolkqltqelxxbmnabktctkltqwert

3. Qwerty 密码

原理: 将标准字母表 abcdefghijklmnopqrstuvwxyz 映射到 QWERTY 键盘布局的顺序 qwertyuiopasdfghjklzxcvbnm

对照表:

1
2
标准:   a b c d e f g h i j k l m n o p q r s t u v w x y z
QWERTY: q w e r t y u i o p a s d f g h j k l z x c v b n m

解密时做反向映射:在 QWERTY 行找到密文字母,对应回标准行的位置。

1
2
3
例如:
● 密文 b → 在 QWERTY 行位置是第 24 位 → 标准行第 24 位是 x
● 密文 o → 在 QWERTY 行位置是第 9 位 → 标准行第 9 位是 i
1
bolkqltqelxxbmnabktctklt → xisraseacsuuxzykxreverse

4. Reverse(逆序)

原理: 最简单的变换——把字符串倒过来写。

1
'xisraseacsuuxzykx'[::-1]  # → xkyzxuuscaesarsix

5. Caesar 密码(凯撒密码),偏移量 6

原理: 将字母表中的每个字母向前或向后移动固定位数。偏移量为 6 时

1
2
密文: a b c d e f g h i j k l m n o p q r s t u v w x y z
明文: u v w x y z a b c d e f g h i j k l m n o p q r s t
1
xkyzxuus → restroom

最后根据题目模板NSSCTF{REVERSE(@@@@@@@@)},对restroom再做一次reverse得到最终 flag:NSSCTF{moortser}

NSSCTF刷题-07

[LitCTF 2023]两仪生四象 (中级)

每个明文字符被转换成10位二进制,所有二进制拼接后。按每3位一组,映射到对应的八卦字。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_hash = {"乾": "111", "兑": "011", "离": "101", "震": "001", "巽": "110", "坎": "010", "艮": "100", "坤": "000"}

encoded_text = "坤乾兑艮兑坎坤坤巽震坤巽震艮兑坎坤震兑乾坤巽坤艮兑震巽坤巽艮坤巽艮艮兑兑艮震兑乾坤乾坤坤兑艮艮坤巽坤坤巽坎坤兑离坎震艮兑坤巽坎艮兑震坤震兑乾坤乾坎坤兑坎坤震艮离坤离乾艮震艮巽震离震坤巽兑艮兑坎坤震巽艮坤离乾艮坎离坤震巽坎坤兑坤艮兑震巽震巽坎坤巽坤艮兑兑坎震巽兑"

binary_str = ''.join(_hash[c] for c in encoded_text)

assert len(binary_str) % 10 == 0, "Bit length not divisible by 10!"

text = ''.join(
chr(int(binary_str[i:i+10], 2))
for i in range(0, len(binary_str), 10)
)

print("Decoded:", text)
print("Flag: LitCTF{" + text + "}")

Flag: LitCTF{wh1ch_ag4in_pr0duced_the_3ight_Tr1grams}

[CISCN 2018]Picture

上一页
2026-08-22 20:22:46
下一页