CTF Misc 入门:文件头、binwalk 与 LSB 隐写

CTF Misc 入门

杂项三板斧:file/xxd 识别文件头、binwalk 提取隐藏文件、Python 实现 LSB 隐写的隐藏与提取。

14th Aug 2026

5 min read

一、现实背景:扩展名不可信

操作系统按扩展名选择打开程序(.png 用图片查看器),但文件的真实类型由文件头(magic bytes)决定。日常中"把 zip 改成 png"会让图片查看器报错;取证中则要反过来:先看文件头,再信扩展名。

必须记住的三个文件头:

文件文件头(hex)明文
PNG89 50 4E 47 0D 0A 1A 0A‰PNG
JPEGFF D8 FF—
ZIP50 4B 03 04PK

二、环境准备

apt install file binutils binwalk foremost exiftool
pip3 install pillow

三、核心技能 1:file 与 xxd

3.1 file:看真实类型

目的:判断题目文件到底是图片、压缩包还是其他东西,避免被扩展名误导。

验证方式:看输出的第一段描述(PNG image data / Zip archive data / PDF document ...)。

file hidden.png

实际输出:

hidden.png: PNG image data, 8 x 8, 8-bit/color RGB, non-interlaced

如果输出是 Zip archive data,说明文件其实是 zip:

mv flag.png flag.zip && unzip flag.zip

3.2 xxd:看文件头

目的:用肉眼确认文件开头的魔数,验证 file 的判断,也为手工分析文件结构做准备。

xxd -l 16 hidden.png
# -l 16:只显示前 16 个字节,看文件头足够。

实际输出:

00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR

开头的 89 50 4e 47 0d 0a 1a 0a 正是 PNG 魔数,右侧明文是 .PNG。

3.3 strings:搜字符串

目的:快速判断 flag 是不是直接以文本形式藏在文件里。

思路:strings 把可打印字符串抽出来,grep -i flag 过滤含 flag 的行。有输出 = 直接提取;无输出 = 可能是隐写,进入下一步。

strings flag.png | grep -i flag

有输出说明 flag 直接以文本形式藏在文件里;无输出则可能是隐写。

四、核心技能 2:binwalk 提取隐藏文件

文件可以追加在其他文件之后(如图片尾部拼接 zip)。扫描:

binwalk flag.png

预期输出:

DECIMAL  HEXADECIMAL  DESCRIPTION
0        0x0           PNG image, 500 x 500
12345    0x3039        Zip archive data

提取:

binwalk -e flag.png

会在当前目录生成 _flag.png.extracted 文件夹,里面是提取出的 zip。

备选工具 foremost(按文件头暴力切割):

foremost flag.png

五、核心技能 3:LSB 隐写(完整实现)

5.1 原理

RGB 像素每个通道是一个 0-255 的字节。把每个字节的最低位(LSB)替换成秘密比特,像素值最多变化 1,肉眼不可察觉:

原来: 11001000  10010110  01100100
写入: 11001001  10010110  01100101    ← 最低位依次写入 1,0,1

一张 500×500 的图能藏 500×500×3 比特 ≈ 93 KB。

5.2 隐藏(出题人视角)

目的:理解"把秘密写进像素最低位"的具体实现,便于反向写出提取脚本。

from PIL import Image
# PIL 是 Python 图像处理库,Image 模块负责打开、修改、保存图片。

img = Image.new('RGB', (8, 8), (200, 150, 100))
# Image.new(mode, size, color):新建一张图片。
# mode='RGB' 表示三通道;size=(8,8);color=(200,150,100) 是初始颜色。
message = "flag{lsb_steganography}"

data = message.encode() + b'\x00'
# message.encode():把字符串按 UTF-8 转成字节串。
# 末尾追加 b'\x00'(字节 0)作为结束符,提取时遇到 0 就停止。
bits = ''.join(f'{b:08b}' for b in data)
# 把每个字节转成 8 位二进制字符串并拼接。
# 例:b'f'=0x66=01100110,8 位拼进 bits。
pixels = list(img.getdata())
# img.getdata():返回所有像素的迭代器,转成列表后每项是 (r,g,b) 三元组。

new_pixels = []
i = 0
for r, g, b in pixels:
    if i < len(bits):
        r = (r & 0xfe) | int(bits[i]); i += 1
        # r & 0xfe:把红色通道的最低位清零(0xfe=11111110)。
        # | int(bits[i]):把秘密比特写进最低位;i 是当前比特下标。
    if i < len(bits):
        g = (g & 0xfe) | int(bits[i]); i += 1
        # 绿色通道同样处理。
    if i < len(bits):
        b = (b & 0xfe) | int(bits[i]); i += 1
        # 蓝色通道同样处理。三个通道依次消耗秘密比特。
    new_pixels.append((r, g, b))
    # 修改后的像素放入新列表。

img.putdata(new_pixels)
# putdata():把新像素列表写回图片对象。
img.save('hidden.png')
# save():按扩展名格式保存为 PNG 文件。
print("已生成 hidden.png")

r & 0xfe 把最低位清零(0xfe = 11111110),| bit 写入目标比特。

5.3 提取(解题视角)

目的:把像素最低位收集起来,按 8 位一组还原成字节,得到隐藏消息。

思路:读取所有像素 → 依次取 R、G、B 的最低位 → 每 8 个比特拼成一个字节 → 遇到 0 结束。

from PIL import Image

img = Image.open('hidden.png').convert('RGB')
# Image.open(path):打开图片文件。
# .convert('RGB'):统一转成 RGB 模式,避免调色板/透明度干扰。
bits = []
for r, g, b in img.getdata():
    bits += [r & 1, g & 1, b & 1]
    # r & 1:取出红色通道最低位(0 或 1),绿、蓝同理。
    # 三个最低位依次追加到 bits,顺序与隐藏时一致。

data = bytearray()
for i in range(0, len(bits) - 7, 8):
    # range 步长 8:每 8 个比特处理一次,组成 1 字节。
    byte = 0
    for bit in bits[i:i+8]:
        byte = (byte << 1) | bit
        # 左移一位再或上新比特,等价于二进制拼接。
        # 例:1,0,1 → ((1<<1)|0)=2, (2<<1)|1=5,即二进制 101。
    if byte == 0:
        # 字节 0 是隐藏时追加的结束符,到此停止。
        break
    data.append(byte)

print(data.decode())
# decode():字节串按 UTF-8 解码成字符串显示。

实际输出:

flag{lsb_steganography}

5.4 现成工具

gem install zsteg
zsteg -a hidden.png

-a 全自动检测各种 LSB 变体。

六、必学工具

工具核心功能命令
file看类型、看编码、看无后缀文件file flag
xxd / hexdump查看文件头、指定长度xxd -l 16 flag.png
strings提取字符串、指定长度、管道过滤strings flag.png、strings -n 3 a、strings a | grep flag
binwalk扫描、提取binwalk flag.png、binwalk -e flag.png

七、小 CTF 实战:藏了 flag 的 PNG

题目:flag.png。

第一步:

file flag.png

输出:PNG image data。

第二步:

strings flag.png | grep -i flag

无输出。

第三步:

binwalk flag.png

无隐藏文件。

第四步:怀疑 LSB,运行提取脚本,输出:

flag{lsb_steganography}

八、小结

Misc 入门三板斧:file 看类型、strings/binwalk 排除简单情况、LSB 脚本提数据。音频隐写(Audacity 频谱图)、ZIP 伪加密、USB 流量分析在《隐写与取证深入实战》中完整展开。