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.
11 KiB
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