2. Misc-图片隐写-LSB隐写

任务目标

给定一张PNG图片,其中通过LSB(最低有效位)隐写方式在RGB通道的每个字节最低位嵌入了信息。你需要编写脚本提取这些隐藏位,将其重组为字节流,并还原出隐藏的文本内容或图片文件。

环境依赖

本案例使用Python 3,需要安装Pillow库(用于图像读取)和numpy(可选,用于加速位运算)。安装命令:

pip install pillow numpy

实现原理

LSB隐写将秘密信息的每个bit写入载体图像每个像素RGB分量值的最低位。提取时,只需读取每个颜色分量的最低位,按顺序拼接成bit流,每8个bit组成一个字节,即可还原原始数据。本案例提供两种还原模式:文本模式(直接按UTF-8解码)和图片模式(将提取的字节保存为PNG文件)。

关键完整代码

from PIL import Image
import numpy as np
import os

def extract_lsb(image_path, output_mode='text', output_path=None):
    """
    从PNG图片中提取LSB隐写数据
    :param image_path: 载体图片路径
    :param output_mode: 'text' 或 'image'
    :param output_path: 输出文件路径(image模式必填)
    :return: 提取的原始字节串
    """
    img = Image.open(image_path).convert('RGB')
    pixels = np.array(img)

    # 提取所有像素RGB值的最低位
    lsb_bits = pixels & 1

    # 将bit矩阵展平为一维数组(按行优先,R,G,B顺序)
    flat_bits = lsb_bits.flatten()

    # 每8个bit组成一个字节
    byte_count = len(flat_bits) // 8
    extracted_bytes = bytearray()
    for i in range(byte_count):
        byte = 0
        for j in range(8):
            byte = (byte << 1) | flat_bits[i * 8 + j]
        extracted_bytes.append(byte)

    # 去除末尾的零填充(隐写时通常以连续8个0字节作为结束标记)
    # 查找最后一个非零字节位置
    last_nonzero = len(extracted_bytes) - 1
    while last_nonzero >= 0 and extracted_bytes[last_nonzero] == 0:
        last_nonzero -= 1
    data = bytes(extracted_bytes[:last_nonzero + 1])

    if output_mode == 'text':
        try:
            text = data.decode('utf-8')
            print("提取的文本内容:")
            print(text)
        except UnicodeDecodeError:
            print("数据不是有效UTF-8文本,尝试以图片模式输出")
            if output_path:
                with open(output_path, 'wb') as f:
                    f.write(data)
                print(f"已保存为文件: {output_path}")
    elif output_mode == 'image':
        if not output_path:
            raise ValueError("image模式必须指定output_path")
        with open(output_path, 'wb') as f:
            f.write(data)
        print(f"提取的图片已保存至: {output_path}")

    return data

if __name__ == "__main__":
    # 使用示例:从stego.png中提取文本
    extract_lsb("stego.png", output_mode='text')

    # 如果提取的是图片,使用以下方式:
    # extract_lsb("stego.png", output_mode='image', output_path="hidden.png")

运行与验证

准备测试图片:若你手头没有隐写图片,可用以下代码生成一张包含隐藏文本的测试图:

from PIL import Image
import numpy as np

def embed_lsb(text, output_path="stego.png", size=(100, 100)):
    """将文本嵌入随机图片的LSB中"""
    data = text.encode('utf-8') + b'\x00' * 8  # 添加结束标记
    bits = ''.join(f'{byte:08b}' for byte in data)
    bit_len = len(bits)

    img = Image.new('RGB', size, (255, 255, 255))
    pixels = np.array(img)
    flat = pixels.flatten()

    for i in range(min(bit_len, len(flat))):
        flat[i] = (flat[i] & 0xFE) | int(bits[i])

    img = Image.fromarray(flat.reshape(size[0], size[1], 3))
    img.save(output_path)
    print(f"已生成含隐写信息的图片: {output_path}")

# 生成测试图片
embed_lsb("CTF{LSB_Staganography_Success}", "stego.png")

验证步骤:

  1. 运行上述生成脚本创建测试图片stego.png;
  2. 运行提取脚本:python extract_lsb.py(假设保存为extract_lsb.py);
  3. 观察控制台输出,应显示:CTF{LSB_Staganography_Success}。

通过条件:提取出的文本与嵌入内容完全一致,或提取出的图片文件能正常打开且内容正确。

常见问题

  • 提取乱码:可能隐写时未添加结束标记,或数据本身非文本。可尝试以image模式输出为文件,用file命令检查文件类型。
  • 图片尺寸过小:若载体图片像素数少于隐藏数据位数,信息将无法完整嵌入。确保图片足够大(如100x100可容纳约3750字节)。
  • 通道顺序:本实现按RGB顺序提取,若隐写时使用BGR或其他顺序,需相应调整代码。