Files
png-zip/README.md
Struchkov Mark cb0a6bae22 docs: restructure bootstrap section with two extraction methods
Method A (two-step): extract unpacker first, then run it.
Method B (direct): extract file with single one-liner using
rfind/LastIndexOf — bypasses Windows Execution Policy.
2026-03-01 23:04:20 +03:00

11 KiB
Raw Blame History

png-zip

Утилита для упаковки произвольных файлов внутрь PNG-изображений. Выходной файл — валидный PNG, который открывается в любом просмотрщике как обычная картинка.

Зависимости: Python 3.6+ (только стандартная библиотека).

Режимы упаковки

Режим Флаг Где хранится payload Устойчивость
append (по умолчанию) -m append После IEND маркера Базовые проверки file, imagemagick
chunk -m chunk Приватный PNG-чанк pnZp Строгая PNG-валидация (libpng, pngcheck)
lsb -m lsb Младшие биты пикселей CDR, re-encode, строгие валидаторы

append (по умолчанию)

PNG-файлы заканчиваются маркером IEND. Все PNG-декодеры игнорируют данные после него. Утилита дописывает сжатый payload после IEND.

[PNG: signature + chunks + IEND] [magic + имя файла + zlib-payload + CRC32]

chunk

Payload упаковывается в приватный PNG-чанк pnZp перед IEND. Файл полностью валиден по спецификации PNG — приватные чанки разрешены и игнорируются декодерами. Если payload > 2 MB, разбивается на несколько чанков с sequence number.

[signature] [IHDR] ... [pnZp: payload] [IEND]

lsb

Payload записывается в младшие биты (LSB) каждого байта пикселей. Визуально изображение неотличимо от оригинала (изменение ±1 в каждом канале). Поддерживаются 8-bit RGB и RGBA PNG без interlacing.

Ёмкость: width × height × channels / 8 байт. Например, 1000×1000 RGB = ~375 KB.

Использование

Упаковка

# Append mode (по умолчанию)
python3 pack.py cover.png secret.tar.gz -o output.png

# Chunk mode — проходит строгую PNG-валидацию
python3 pack.py -m chunk cover.png secret.tar.gz -o output.png

# LSB mode — выживает CDR и re-encode
python3 pack.py -m lsb cover.png secret.tar.gz -o output.png

output.png выглядит как cover.png, но содержит secret.tar.gz внутри.

Распаковка

python3 unpack.py output.png

Извлекает файл с оригинальным именем в текущую директорию. Режим определяется автоматически.

Распаковка в заданную директорию

python3 unpack.py output.png -o ./extracted/

Просмотр содержимого

python3 unpack.py output.png -l
  secret.tar.gz (148230 bytes)

Извлечь все вложенные файлы

python3 unpack.py output.png -a

Полезно для self-extract образов, где внутри несколько файлов (unpack.py + payload).

Bootstrap: доставка на VM без ничего

Если на целевой машине нет unpack.py и передать можно только картинки:

1. На хосте — создаём bootstrap-образ

# Встроить unpack.py (для машин с Python)
python3 pack.py --self-extract cover.png secret.tar.gz -o bootstrap.png

# Встроить unpack.ps1 (для Windows без Python)
python3 pack.py --self-extract-ps cover.png secret.tar.gz -o bootstrap.png

# Встроить оба распаковщика
python3 pack.py --self-extract --self-extract-ps cover.png secret.tar.gz -o bootstrap.png

--self-extract / --self-extract-ps работают только с append режимом.

При использовании любого --self-extract флага bootstrap-однострочники автоматически сохраняются в метаданных PNG — в eXIf (EXIF UserComment) и tEXt (Comment) чанках. Их можно посмотреть:

  • Windows: правый клик → Свойства → Details → Comments (читает eXIf)
  • macOS/Linux: identify -verbose bootstrap.png | grep -A 20 Comment
  • Любая ОС: exiftool bootstrap.png

Base64/EncodedCommand варианты генерируются динамически — в метаданных будет реальное имя выходного файла, а не bootstrap.png. Примеры ниже приведены для имени bootstrap.png.

2. Передаём bootstrap.png на VM

Это валидный PNG — пройдёт любую проверку на изображение.

3. На VM — два способа извлечения

Способ A: двухшаговый (распаковщик → файл)

Шаг 1. Извлечь распаковщик — однострочник ищет первый payload. Замените IMG на имя файла:

Python:

python3 -c "d=open('IMG','rb').read();i=d.find(b'PNGZIP\x00\x01');nl=int.from_bytes(d[i+8:i+10],'big');n=d[i+10:i+10+nl].decode();pl=int.from_bytes(d[i+10+nl:i+18+nl],'big');import zlib;open(n,'wb').write(zlib.decompress(d[i+18+nl:i+18+nl+pl]));print('Extracted:',n)"

PowerShell:

$d=[IO.File]::ReadAllBytes('IMG');$t=[Text.Encoding]::GetEncoding(28591).GetString($d);$s='PNGZIP';$i=-1;do{$i=$t.IndexOf($s,$i+1,[StringComparison]::Ordinal)}while($i-ge0-and($d[$i+6]-ne0-or$d[$i+7]-ne1));$p=$i+8;$nl=$d[$p]*256+$d[$p+1];$p+=2;$n=[Text.Encoding]::UTF8.GetString($d,$p,$nl);$p+=$nl;$pl=[uint64]0;for($k=0;$k-lt8;$k++){$pl=$pl*256+$d[$p+$k]};$p+=8;$ms=New-Object IO.MemoryStream;$ms.Write($d,$p+2,$pl-6);[void]$ms.Seek(0,0);$ds=New-Object IO.Compression.DeflateStream($ms,[IO.Compression.CompressionMode]::Decompress);$os=New-Object IO.MemoryStream;$ds.CopyTo($os);[IO.File]::WriteAllBytes($n,$os.ToArray());Write-Host "Extracted: $n"

Шаг 2. Извлечь основной файл:

python3 unpack.py bootstrap.png       # Python
powershell -File unpack.ps1 bootstrap.png  # PowerShell

Дальше распаковщик уже есть на VM и можно передавать обычные packed-образы.

Способ B: прямое извлечение файла (без распаковщика)

Однострочник ищет последний payload — это сам файл. Не нужен unpack.py / unpack.ps1, обходит Windows Execution Policy. Замените IMG на имя файла:

Python:

python3 -c "d=open('IMG','rb').read();i=d.rfind(b'PNGZIP\x00\x01');nl=int.from_bytes(d[i+8:i+10],'big');n=d[i+10:i+10+nl].decode();pl=int.from_bytes(d[i+10+nl:i+18+nl],'big');import zlib;open(n,'wb').write(zlib.decompress(d[i+18+nl:i+18+nl+pl]));print('Extracted:',n)"

PowerShell:

$d=[IO.File]::ReadAllBytes('IMG');$t=[Text.Encoding]::GetEncoding(28591).GetString($d);$s='PNGZIP';$i=$d.Length;do{$i=$t.LastIndexOf($s,$i-1,[StringComparison]::Ordinal)}while($i-ge0-and($d[$i+6]-ne0-or$d[$i+7]-ne1));$p=$i+8;$nl=$d[$p]*256+$d[$p+1];$p+=2;$n=[Text.Encoding]::UTF8.GetString($d,$p,$nl);$p+=$nl;$pl=[uint64]0;for($k=0;$k-lt8;$k++){$pl=$pl*256+$d[$p+$k]};$p+=8;$ms=New-Object IO.MemoryStream;$ms.Write($d,$p+2,$pl-6);[void]$ms.Seek(0,0);$ds=New-Object IO.Compression.DeflateStream($ms,[IO.Compression.CompressionMode]::Decompress);$os=New-Object IO.MemoryStream;$ds.CopyTo($os);[IO.File]::WriteAllBytes($n,$os.ToArray());Write-Host "Extracted: $n"

Windows CMD — готовые однострочники с base64/EncodedCommand (не нужно менять имя файла) для обоих способов выводятся при паковке и сохраняются в метаданных PNG. Скопируйте из:

  • Вывода pack.py / pack.ps1 в консоли
  • Свойств файла: правый клик → Свойства → Details → Comments

Очистка PNG от встроенных данных

Удалить все payload'ы (append, chunk) и bootstrap-метаданные (eXIf, tEXt) из PNG:

python3 pack.py --clean packed.png -o clean.png

После очистки PNG возвращается к исходному виду (без trailing data, без pnZp-чанков, без bootstrap-комментариев).

Советы

Упаковка нескольких файлов — сначала создайте архив:

tar czf bundle.tar.gz file1.txt file2.bin dir/
python3 pack.py cover.png bundle.tar.gz -o output.png

Выбор обложки — используйте любой PNG. Чем крупнее картинка, тем естественнее выглядит увеличение размера файла. Для LSB режима нужна достаточно большая картинка (ёмкость ≈ W×H×channels/8 байт).

Проверка целостности — при распаковке автоматически проверяется CRC32. Если файл повреждён при передаче, будет предупреждение.

PowerShell версия

Для Windows без Python есть полная PowerShell-реализация (pack.ps1 / unpack.ps1) — см. POWERSHELL.md.

Формат payload полностью совместим — можно паковать Python'ом, распаковывать PowerShell'ом и наоборот:

Упаковка Распаковка
pack.py unpack.ps1 да
pack.ps1 unpack.py да
pack.py --self-extract Python однострочник да
pack.py --self-extract-ps PS однострочник да
pack.ps1 -SelfExtract PS однострочник да

PowerShell версия поддерживает только append-режим. Для chunk и lsb нужен Python.

Ограничения

  • append/chunk: если система передачи пережимает PNG (ресайз, re-encode) — данные будут потеряны. Для file-based трансферов (SCP, cloud storage, вложения в email, передача «как файл» в мессенджерах) проблем нет.
  • lsb: выживает re-encode без потерь (PNG→PNG), но не выживает lossy конвертацию (PNG→JPEG). Ёмкость ограничена размером изображения. Поддерживаются только 8-bit RGB/RGBA PNG без interlacing.
  • Payload загружается в память целиком. Для файлов > 500 MB может потребоваться много RAM.
  • Данные не шифруются. Для конфиденциальности — зашифруйте файл перед упаковкой:
    gpg -c secret.tar.gz          # создаст secret.tar.gz.gpg
    python3 pack.py cover.png secret.tar.gz.gpg -o output.png