png-zip: pack/unpack files inside PNG images

Supports three packing modes:
- append (default) — data after IEND marker
- chunk — private pnZp PNG chunk
- lsb — least significant bits of pixels

Includes Python (pack.py/unpack.py) and PowerShell (pack.ps1/unpack.ps1)
implementations with full cross-compatibility.

Features: self-extract bootstrap, metadata one-liners (eXIf/tEXt),
CRC32 integrity check, PNG clean mode.
This commit is contained in:
Struchkov Mark
2026-02-21 14:48:13 +03:00
commit 6e54e0f411
8 changed files with 1382 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
__pycache__/
.DS_Store
*.pyc

80
POWERSHELL.md Normal file
View File

@@ -0,0 +1,80 @@
# png-zip: PowerShell версия
Упаковка и распаковка файлов внутри PNG без Python. Только PowerShell 5.1+ (встроен в Windows 10/11).
## Упаковка
```powershell
powershell -File pack.ps1 cover.png secret.txt -Output packed.png
```
### Self-extract
Встраивает `unpack.ps1` внутрь PNG — на целевой машине не нужны никакие скрипты:
```powershell
powershell -File pack.ps1 cover.png secret.txt -Output bootstrap.png -SelfExtract
```
Внутри `bootstrap.png`: `unpack.ps1` + `secret.txt`. Извлечение на целевой машине — см. раздел «Bootstrap».
При `-SelfExtract` все bootstrap-однострочники (Python, Python CMD, PowerShell, PowerShell CMD) автоматически сохраняются в метаданных PNG — eXIf (EXIF UserComment) и tEXt (Comment). Их видно в свойствах файла:
- **Windows**: правый клик → Свойства → Details → Comments
- **PowerShell**: `exiftool bootstrap.png`
> Base64/EncodedCommand варианты генерируются динамически с реальным именем выходного файла.
## Распаковка
```powershell
powershell -File unpack.ps1 packed.png
powershell -File unpack.ps1 packed.png -OutDir .\out
powershell -File unpack.ps1 packed.png -List
powershell -File unpack.ps1 packed.png -All
```
## Bootstrap: доставка без ничего
На целевой машине есть только `bootstrap.png` (созданный с `-SelfExtract`) и PowerShell.
### 1. Извлечь unpack.ps1 однострочником
Замените `IMG` на имя вашего файла:
```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"
```
Ищет первый `PNGZIP` с байтами `\x00\x01` после — это встроенный `unpack.ps1`.
**Windows CMD** — готовый EncodedCommand-однострочник (с уже подставленным именем файла) выводится при паковке и сохраняется в метаданных PNG. Скопируйте из консоли или из свойств файла (Details → Comments).
### 2. Извлечь основной файл
```powershell
powershell -File unpack.ps1 bootstrap.png
```
### Однострочник (без bootstrap)
Извлечь последний payload напрямую (если self-extract не использовался). Замените `IMG` на имя файла:
```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"
```
## Очистка PNG
Удалить все встроенные данные (payload, bootstrap-метаданные eXIf/tEXt):
```powershell
powershell -File pack.ps1 packed.png -Clean -Output clean.png
```
## Ограничения
- Только append-режим. Для chunk и lsb используйте Python-версию (`pack.py` / `unpack.py`).
- CRC32 и Adler32 реализованы через inline C# (`Add-Type`), первый запуск чуть медленнее из-за компиляции.
- Для файлов > 100 MB может быть медленнее Python-версии.
- Формат payload полностью совместим с Python-версией — можно паковать на одной ОС, распаковывать на другой.

193
README.md Normal file
View File

@@ -0,0 +1,193 @@
# 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.
## Использование
### Упаковка
```bash
# 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` внутри.
### Распаковка
```bash
python3 unpack.py output.png
```
Извлекает файл с оригинальным именем в текущую директорию. Режим определяется автоматически.
### Распаковка в заданную директорию
```bash
python3 unpack.py output.png -o ./extracted/
```
### Просмотр содержимого
```bash
python3 unpack.py output.png -l
```
```
secret.tar.gz (148230 bytes)
```
### Извлечь все вложенные файлы
```bash
python3 unpack.py output.png -a
```
Полезно для self-extract образов, где внутри несколько файлов (unpack.py + payload).
## Bootstrap: доставка на VM без ничего
Если на целевой машине нет `unpack.py` и передать можно только картинки:
### 1. На хосте — создаём bootstrap-образ
```bash
# Встроить 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 — извлекаем распаковщик однострочником
Однострочник извлекает **первый** встроенный файл (распаковщик). Замените `IMG` на имя вашего файла:
**Python (Linux / macOS / PowerShell):**
```bash
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 (без Python):**
```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"
```
**Windows CMD** — готовые однострочники с base64/EncodedCommand (не нужно менять имя файла) выводятся при паковке и сохраняются в метаданных PNG. Скопируйте из:
- Вывода `pack.py` / `pack.ps1` в консоли
- Свойств файла: правый клик → Свойства → Details → Comments
### 4. Извлекаем основной файл
```bash
# Python
python3 unpack.py bootstrap.png
# PowerShell
powershell -File unpack.ps1 bootstrap.png
```
Дальше распаковщик уже есть на VM и можно передавать обычные packed-образы.
### Очистка PNG от встроенных данных
Удалить все payload'ы (append, chunk) и bootstrap-метаданные (eXIf, tEXt) из PNG:
```bash
python3 pack.py --clean packed.png -o clean.png
```
После очистки PNG возвращается к исходному виду (без trailing data, без pnZp-чанков, без bootstrap-комментариев).
## Советы
**Упаковка нескольких файлов** — сначала создайте архив:
```bash
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](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.
- Данные не шифруются. Для конфиденциальности — зашифруйте файл перед упаковкой:
```bash
gpg -c secret.tar.gz # создаст secret.tar.gz.gpg
python3 pack.py cover.png secret.tar.gz.gpg -o output.png
```

271
pack.ps1 Normal file
View File

@@ -0,0 +1,271 @@
# pack.ps1 — Pack a file inside a PNG image (append after IEND)
# Usage: powershell -File pack.ps1 cover.png secret.txt [-Output packed.png] [-SelfExtract]
# powershell -File pack.ps1 packed.png -Clean [-Output clean.png]
param(
[Parameter(Mandatory, Position=0)][string]$Cover,
[Parameter(Position=1)][string]$Payload,
[string]$Output = "packed.png",
[switch]$SelfExtract,
[switch]$Clean
)
Add-Type -TypeDefinition @"
using System;
public static class PngZipHelper {
static readonly uint[] crcTable;
static PngZipHelper() {
crcTable = new uint[256];
for (uint i = 0; i < 256; i++) {
uint c = i;
for (int j = 0; j < 8; j++)
c = (c & 1) != 0 ? 0xEDB88320u ^ (c >> 1) : c >> 1;
crcTable[i] = c;
}
}
public static uint Crc32(byte[] data) {
uint crc = 0xFFFFFFFF;
foreach (byte b in data) crc = crcTable[(crc ^ b) & 0xFF] ^ (crc >> 8);
return crc ^ 0xFFFFFFFF;
}
public static uint Adler32(byte[] data) {
uint a = 1, b = 0;
foreach (byte d in data) { a = (a + d) % 65521; b = (b + a) % 65521; }
return (b << 16) | a;
}
}
"@
function Write-BE16([System.IO.Stream]$s, [uint16]$v) {
$s.WriteByte(($v -shr 8) -band 0xFF)
$s.WriteByte($v -band 0xFF)
}
function Write-BE32([System.IO.Stream]$s, [uint32]$v) {
$s.WriteByte(($v -shr 24) -band 0xFF)
$s.WriteByte(($v -shr 16) -band 0xFF)
$s.WriteByte(($v -shr 8) -band 0xFF)
$s.WriteByte($v -band 0xFF)
}
function Write-BE64([System.IO.Stream]$s, [uint64]$v) {
for ($i = 56; $i -ge 0; $i -= 8) { $s.WriteByte(($v -shr $i) -band 0xFF) }
}
# ── Read cover PNG ──
$coverData = [IO.File]::ReadAllBytes((Resolve-Path $Cover))
$sig = [byte[]](0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A)
for ($i = 0; $i -lt 8; $i++) {
if ($coverData[$i] -ne $sig[$i]) { Write-Error "Not a valid PNG"; exit 1 }
}
# ── Clean PNG: strip pnZp chunks + bootstrap comments + trailing data after IEND ──
function Get-CleanPng([byte[]]$Data) {
$ms = New-Object IO.MemoryStream
$ms.Write($sig, 0, 8)
$pos = 8
while ($pos + 8 -le $Data.Length) {
$cLen = $Data[$pos]*16777216 + $Data[$pos+1]*65536 + $Data[$pos+2]*256 + $Data[$pos+3]
$cType = [Text.Encoding]::ASCII.GetString($Data, $pos+4, 4)
$cSize = 4 + 4 + $cLen + 4 # len + type + data + crc
$skip = $false
if ($cType -eq "pnZp") { $skip = $true }
if ($cType -eq "tEXt" -and $cLen -gt 0) {
$chunkText = [Text.Encoding]::GetEncoding(28591).GetString($Data, $pos+8, $cLen)
if ($chunkText.Contains("png-zip bootstrap")) { $skip = $true }
}
if ($cType -eq "eXIf" -and $cLen -gt 0) {
$chunkText = [Text.Encoding]::GetEncoding(28591).GetString($Data, $pos+8, $cLen)
if ($chunkText.Contains("png-zip bootstrap")) { $skip = $true }
}
if (-not $skip) {
$ms.Write($Data, $pos, $cSize)
}
$pos += $cSize
if ($cType -eq "IEND") { break }
}
return $ms.ToArray()
}
# ── Build a raw PNG chunk: len(4) + type(4) + data + crc(4) ──
function Build-PngChunk([string]$Type, [byte[]]$ChunkData) {
$typeBytes = [Text.Encoding]::ASCII.GetBytes($Type)
$crcMs = New-Object IO.MemoryStream
$crcMs.Write($typeBytes, 0, 4)
$crcMs.Write($ChunkData, 0, $ChunkData.Length)
$crc = [PngZipHelper]::Crc32($crcMs.ToArray())
$ms = New-Object IO.MemoryStream
Write-BE32 $ms ([uint32]$ChunkData.Length)
$ms.Write($typeBytes, 0, 4)
$ms.Write($ChunkData, 0, $ChunkData.Length)
Write-BE32 $ms $crc
return $ms.ToArray()
}
# ── Insert chunk(s) into PNG after IHDR ──
function Add-ChunksAfterIHDR([byte[]]$Data, [byte[][]]$Chunks) {
$ms = New-Object IO.MemoryStream
$ms.Write($Data, 0, 8) # PNG signature
$pos = 8
while ($pos + 8 -le $Data.Length) {
$cLen = $Data[$pos]*16777216 + $Data[$pos+1]*65536 + $Data[$pos+2]*256 + $Data[$pos+3]
$cType = [Text.Encoding]::ASCII.GetString($Data, $pos+4, 4)
$cSize = 4 + 4 + $cLen + 4
$ms.Write($Data, $pos, $cSize)
if ($cType -eq "IHDR") {
foreach ($chunk in $Chunks) { $ms.Write($chunk, 0, $chunk.Length) }
}
$pos += $cSize
if ($cType -eq "IEND") { break }
}
return $ms.ToArray()
}
# ── Build minimal eXIf chunk data with UserComment ──
function Build-ExifUserComment([string]$Text) {
$ucPrefix = [Text.Encoding]::ASCII.GetBytes("ASCII")
$ucText = [Text.Encoding]::ASCII.GetBytes($Text)
# UserComment = "ASCII\x00\x00\x00" + text
$ucMs = New-Object IO.MemoryStream
$ucMs.Write($ucPrefix, 0, 5)
$ucMs.WriteByte(0); $ucMs.WriteByte(0); $ucMs.WriteByte(0)
$ucMs.Write($ucText, 0, $ucText.Length)
$uc = $ucMs.ToArray()
$ucLen = $uc.Length
# TIFF big-endian header + IFD0 (1 entry: ExifIFD ptr) + ExifIFD (1 entry: UserComment)
$exifIfdOffset = 26
$ucDataOffset = 44
$ms = New-Object IO.MemoryStream
# TIFF header
$ms.WriteByte(0x4D); $ms.WriteByte(0x4D) # "MM" big-endian
Write-BE16 $ms ([uint16]42)
Write-BE32 $ms ([uint32]8) # IFD0 offset
# IFD0: 1 entry
Write-BE16 $ms ([uint16]1)
Write-BE16 $ms ([uint16]0x8769) # ExifIFD tag
Write-BE16 $ms ([uint16]4) # type LONG
Write-BE32 $ms ([uint32]1) # count
Write-BE32 $ms ([uint32]$exifIfdOffset) # value
Write-BE32 $ms ([uint32]0) # next IFD
# ExifIFD: 1 entry
Write-BE16 $ms ([uint16]1)
Write-BE16 $ms ([uint16]0x9286) # UserComment tag
Write-BE16 $ms ([uint16]7) # type UNDEFINED
Write-BE32 $ms ([uint32]$ucLen) # count
Write-BE32 $ms ([uint32]$ucDataOffset) # value offset
Write-BE32 $ms ([uint32]0) # next IFD
# UserComment data
$ms.Write($uc, 0, $uc.Length)
return $ms.ToArray()
}
# ── Helper: write one payload block to stream ──
$magic = [byte[]](0x50,0x4E,0x47,0x5A,0x49,0x50,0x00,0x01) # PNGZIP\x00\x01
function Write-PayloadBlock([System.IO.Stream]$s, [string]$Name, [byte[]]$Raw) {
# Compress
$ms = New-Object IO.MemoryStream
$ds = New-Object IO.Compression.DeflateStream($ms, [IO.Compression.CompressionMode]::Compress)
$ds.Write($Raw, 0, $Raw.Length)
$ds.Close()
$deflated = $ms.ToArray()
# Build zlib stream: header(2) + deflate + adler32(4)
$adler = [PngZipHelper]::Adler32($Raw)
$zlibMs = New-Object IO.MemoryStream
$zlibMs.WriteByte(0x78); $zlibMs.WriteByte(0x9C)
$zlibMs.Write($deflated, 0, $deflated.Length)
Write-BE32 $zlibMs $adler
$compressed = $zlibMs.ToArray()
$crc = [PngZipHelper]::Crc32($Raw)
$nameBytes = [Text.Encoding]::UTF8.GetBytes($Name)
$s.Write($magic, 0, 8)
Write-BE16 $s ([uint16]$nameBytes.Length)
$s.Write($nameBytes, 0, $nameBytes.Length)
Write-BE64 $s ([uint64]$compressed.Length)
$s.Write($compressed, 0, $compressed.Length)
Write-BE32 $s $crc
}
# ── Clean mode ──
if ($Clean) {
$cleaned = Get-CleanPng $coverData
[IO.File]::WriteAllBytes($Output, $cleaned)
$beforeKB = [math]::Round($coverData.Length / 1024, 1)
$afterKB = [math]::Round($cleaned.Length / 1024, 1)
Write-Host "Cleaned: $Output ($afterKB KB, was $beforeKB KB)"
return
}
# ── Pack mode — payload required ──
if (-not $Payload) { Write-Error "Payload argument is required (unless -Clean)"; exit 1 }
# Auto-clean: strip existing payloads (append + pnZp) before packing
$cleanCover = Get-CleanPng $coverData
# Embed all bootstrap one-liners in PNG metadata (tEXt Comment) for self-extract
if ($SelfExtract) {
$outName = [IO.Path]::GetFileName($Output)
# Common tails
$pyTail = '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)"'
$psTail = '$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"'
$psHead = '$d=[IO.File]::ReadAllBytes(''IMG'');$t=[Text.Encoding]::GetEncoding(28591).GetString($d);'
$psHead = $psHead.Replace('IMG', $outName)
$psFindFirst = '$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));'
$psFindLast = '$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));'
# --- First payload (extract unpacker) ---
$pyCmd = ('python3 -c "d=open(''IMG'',''rb'').read();i=d.find(b''PNGZIP\x00\x01'');' + $pyTail).Replace('IMG', $outName)
$pyScript = "import zlib`nd=open('$outName','rb').read()`nm=b'PNGZIP'+bytes([0,1])`ni=d.find(m)`nnl=int.from_bytes(d[i+8:i+10],'big')`nn=d[i+10:i+10+nl].decode()`npl=int.from_bytes(d[i+10+nl:i+18+nl],'big')`nopen(n,'wb').write(zlib.decompress(d[i+18+nl:i+18+nl+pl]))`nprint('Extracted:',n)"
$pyB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pyScript))
$pyCmdB64 = 'python -c "import base64;exec(base64.b64decode(''' + $pyB64 + '''))"'
$psCmd = $psHead + $psFindFirst + $psTail
$psB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($psCmd))
$psCmdB64 = 'powershell -EncodedCommand ' + $psB64
# --- Last payload (extract file directly) ---
$pyCmdLast = ('python3 -c "d=open(''IMG'',''rb'').read();i=d.rfind(b''PNGZIP\x00\x01'');' + $pyTail).Replace('IMG', $outName)
$pyScriptLast = "import zlib`nd=open('$outName','rb').read()`nm=b'PNGZIP'+bytes([0,1])`ni=d.rfind(m)`nnl=int.from_bytes(d[i+8:i+10],'big')`nn=d[i+10:i+10+nl].decode()`npl=int.from_bytes(d[i+10+nl:i+18+nl],'big')`nopen(n,'wb').write(zlib.decompress(d[i+18+nl:i+18+nl+pl]))`nprint('Extracted:',n)"
$pyB64Last = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pyScriptLast))
$pyCmdB64Last = 'python -c "import base64;exec(base64.b64decode(''' + $pyB64Last + '''))"'
$psCmdLast = $psHead + $psFindLast + $psTail
$psB64Last = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($psCmdLast))
$psCmdB64Last = 'powershell -EncodedCommand ' + $psB64Last
# --- Build bootstrap text ---
$bsText = "png-zip bootstrap`n`n=== Extract unpacker (first payload) ===`n`nPython:`n" + $pyCmd + "`n`nPython (Windows CMD):`n" + $pyCmdB64 + "`n`nPowerShell:`n" + $psCmd + "`n`nPowerShell (Windows CMD):`n" + $psCmdB64
$bsText += "`n`n=== Extract file (last payload) ===`n`nPython:`n" + $pyCmdLast + "`n`nPython (Windows CMD):`n" + $pyCmdB64Last + "`n`nPowerShell:`n" + $psCmdLast + "`n`nPowerShell (Windows CMD):`n" + $psCmdB64Last
# Build tEXt chunk
$kwBytes = [Text.Encoding]::GetEncoding(28591).GetBytes("Comment")
$txtBytes = [Text.Encoding]::GetEncoding(28591).GetBytes($bsText)
$textCdMs = New-Object IO.MemoryStream
$textCdMs.Write($kwBytes, 0, $kwBytes.Length)
$textCdMs.WriteByte(0)
$textCdMs.Write($txtBytes, 0, $txtBytes.Length)
$textChunk = Build-PngChunk "tEXt" $textCdMs.ToArray()
# Build eXIf chunk (Windows reads UserComment from EXIF)
$exifData = Build-ExifUserComment $bsText
$exifChunk = Build-PngChunk "eXIf" $exifData
$cleanCover = Add-ChunksAfterIHDR $cleanCover @($exifChunk, $textChunk)
}
$fs = [IO.File]::Create($Output)
$fs.Write($cleanCover, 0, $cleanCover.Length) # clean cover up to IEND
# Self-extract: embed unpack.ps1 first
if ($SelfExtract) {
$unpackPath = Join-Path $PSScriptRoot "unpack.ps1"
if (-not (Test-Path $unpackPath)) { Write-Error "unpack.ps1 not found next to pack.ps1"; exit 1 }
$unpackRaw = [IO.File]::ReadAllBytes($unpackPath)
Write-PayloadBlock $fs "unpack.ps1" $unpackRaw
}
# Main payload
$raw = [IO.File]::ReadAllBytes((Resolve-Path $Payload))
Write-PayloadBlock $fs ([IO.Path]::GetFileName($Payload)) $raw
$fs.Close()
$coverKB = [math]::Round($cleanCover.Length / 1024, 1)
$totalKB = [math]::Round((Get-Item $Output).Length / 1024, 1)
Write-Host "Packed: $Output ($totalKB KB, cover $coverKB KB)"
if ($SelfExtract) {
Write-Host "Self-extract: unpack.ps1 embedded"
$displayText = $bsText.Replace("png-zip bootstrap`n", "Bootstrap one-liners:`n")
Write-Host "`n$displayText"
}

462
pack.py Normal file
View File

@@ -0,0 +1,462 @@
#!/usr/bin/env python3
"""Pack a file inside a PNG image.
Modes:
append (default) — data after IEND
chunk — data in private pnZp PNG chunk
lsb — data in least-significant bits of pixels
Usage:
python3 pack.py cover.png payload [-o output.png] [-m mode] [--self-extract]
"""
import argparse
import os
import struct
import sys
import zlib
MAGIC = b"PNGZIP\x00\x01"
PNG_SIG = b"\x89PNG\r\n\x1a\n"
CHUNK_MAX = 2 * 1024 * 1024 # 2 MB per pnZp chunk
BOOTSTRAP_MARKER = "png-zip bootstrap"
def find_iend(data: bytes) -> int:
"""Return offset of the first byte AFTER the IEND chunk, or -1."""
idx = data.rfind(b"IEND")
if idx == -1:
return -1
chunk_start = idx - 4 # length field before "IEND"
length = struct.unpack(">I", data[chunk_start:chunk_start + 4])[0]
return chunk_start + 4 + 4 + length + 4 # len + type + data + crc
def make_payload_block(filename: str, raw: bytes) -> bytes:
compressed = zlib.compress(raw, 6)
name_bytes = os.path.basename(filename).encode("utf-8")
header = MAGIC
header += struct.pack(">H", len(name_bytes))
header += name_bytes
header += struct.pack(">Q", len(compressed))
crc = struct.pack(">I", zlib.crc32(raw) & 0xFFFFFFFF)
return header + compressed + crc
# ── PNG chunk helpers ─────────────────────────────────────────────
def parse_chunks(data: bytes):
"""Parse PNG data into list of (chunk_type, chunk_data)."""
if data[:8] != PNG_SIG:
raise ValueError("Not a valid PNG")
pos = 8
chunks = []
while pos + 8 <= len(data):
length = struct.unpack(">I", data[pos:pos + 4])[0]
chunk_type = data[pos + 4:pos + 8]
chunk_data = data[pos + 8:pos + 8 + length]
pos += 4 + 4 + length + 4 # len + type + data + crc
chunks.append((chunk_type, chunk_data))
return chunks
def make_png_chunk(chunk_type: bytes, chunk_data: bytes) -> bytes:
"""Build a valid PNG chunk: length + type + data + CRC32."""
length = struct.pack(">I", len(chunk_data))
crc = struct.pack(">I", zlib.crc32(chunk_type + chunk_data) & 0xFFFFFFFF)
return length + chunk_type + chunk_data + crc
def assemble_png(chunks) -> bytes:
"""Reassemble PNG from list of (type, data) tuples."""
result = bytearray(PNG_SIG)
for chunk_type, chunk_data in chunks:
result += make_png_chunk(chunk_type, chunk_data)
return bytes(result)
# ── Chunk mode ────────────────────────────────────────────────────
def pack_chunk(cover_bytes: bytes, payload_block: bytes) -> bytes:
chunks = parse_chunks(cover_bytes)
# Split payload into ≤ 2 MB fragments
pnzp_chunks = []
for seq, offset in enumerate(range(0, len(payload_block), CHUNK_MAX)):
fragment = payload_block[offset:offset + CHUNK_MAX]
pnzp_chunks.append((b"pnZp", struct.pack("B", seq) + fragment))
# Insert pnZp chunk(s) before IEND
result = []
for ct, cd in chunks:
if ct == b"IEND":
result.extend(pnzp_chunks)
result.append((ct, cd))
return assemble_png(result)
# ── LSB mode ──────────────────────────────────────────────────────
def _paeth(a, b, c):
p = a + b - c
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
if pa <= pb and pa <= pc:
return a
return b if pb <= pc else c
def _unfilter_row(ftype, row, prev, bpp):
out = bytearray(len(row))
for i in range(len(row)):
x = row[i]
a = out[i - bpp] if i >= bpp else 0
b = prev[i]
c = prev[i - bpp] if i >= bpp else 0
if ftype == 0:
out[i] = x
elif ftype == 1:
out[i] = (x + a) & 0xFF
elif ftype == 2:
out[i] = (x + b) & 0xFF
elif ftype == 3:
out[i] = (x + (a + b) // 2) & 0xFF
elif ftype == 4:
out[i] = (x + _paeth(a, b, c)) & 0xFF
else:
raise ValueError(f"Unknown PNG filter type {ftype}")
return out
def pack_lsb(cover_bytes: bytes, payload_block: bytes) -> bytes:
chunks = parse_chunks(cover_bytes)
# ── IHDR ──
ihdr = next(cd for ct, cd in chunks if ct == b"IHDR")
width, height = struct.unpack(">II", ihdr[:8])
bit_depth, color_type = ihdr[8], ihdr[9]
interlace = ihdr[12]
if bit_depth != 8 or color_type not in (2, 6):
print("Error: LSB mode requires 8-bit RGB or RGBA PNG", file=sys.stderr)
sys.exit(1)
if interlace != 0:
print("Error: LSB mode does not support interlaced PNG", file=sys.stderr)
sys.exit(1)
channels = 3 if color_type == 2 else 4
bpp = channels
# ── Collect & decompress IDAT ──
idat_data = b"".join(cd for ct, cd in chunks if ct == b"IDAT")
raw = zlib.decompress(idat_data)
# ── Unfilter ──
stride = width * bpp
pixels = bytearray()
prev = bytearray(stride)
pos = 0
for _ in range(height):
ftype = raw[pos]; pos += 1
row = raw[pos:pos + stride]; pos += stride
prev = _unfilter_row(ftype, row, prev, bpp)
pixels.extend(prev)
# ── Capacity check ──
capacity = len(pixels) # 1 bit per byte
needed = len(payload_block) * 8
if needed > capacity:
cap_kb = capacity // 8 // 1024
need_kb = len(payload_block) // 1024
print(f"Error: payload ({need_kb} KB) exceeds LSB capacity ({cap_kb} KB)",
file=sys.stderr)
sys.exit(1)
# ── Embed bits into LSB ──
bit_idx = 0
for byte in payload_block:
for shift in range(7, -1, -1):
pixels[bit_idx] = (pixels[bit_idx] & 0xFE) | ((byte >> shift) & 1)
bit_idx += 1
# ── Re-filter (type 0 = None) & compress ──
filtered = bytearray()
for y in range(height):
filtered.append(0)
filtered.extend(pixels[y * stride:(y + 1) * stride])
compressed = zlib.compress(bytes(filtered), 6)
# ── Reassemble PNG ──
result = []
idat_done = False
for ct, cd in chunks:
if ct == b"IDAT":
if not idat_done:
result.append((b"IDAT", compressed))
idat_done = True
else:
result.append((ct, cd))
return assemble_png(result)
# ── Clean mode ────────────────────────────────────────────────────
def clean_png(data: bytes) -> bytes:
"""Strip all embedded payloads (append + chunk) and bootstrap metadata from a PNG."""
marker = BOOTSTRAP_MARKER.encode("latin-1")
chunks = parse_chunks(data)
cleaned = []
for ct, cd in chunks:
if ct == b"pnZp":
continue # strip chunk-mode payloads
if ct == b"tEXt" and cd.startswith(b"Comment\x00") and marker in cd:
continue # strip old bootstrap tEXt comment
if ct == b"eXIf" and b"ASCII\x00\x00\x00" + marker in cd:
continue # strip old bootstrap eXIf
cleaned.append((ct, cd))
if ct == b"IEND":
break # stop here — strips append-mode trailing data
return assemble_png(cleaned)
def build_exif_user_comment(text: str) -> bytes:
"""Build minimal eXIf chunk data with UserComment field."""
user_comment = b"ASCII\x00\x00\x00" + text.encode("ascii")
uc_len = len(user_comment)
# TIFF header (8) + IFD0: count(2) + entry(12) + next(4) = 18
# ExifIFD: count(2) + entry(12) + next(4) = 18
# Total fixed = 8 + 18 + 18 = 44, then UserComment data
exif_ifd_offset = 26
uc_data_offset = 44
buf = bytearray()
buf += b"MM"
buf += struct.pack(">H", 42)
buf += struct.pack(">I", 8) # IFD0 offset
buf += struct.pack(">H", 1) # IFD0: 1 entry
buf += struct.pack(">HHI", 0x8769, 4, 1) # ExifIFD pointer tag
buf += struct.pack(">I", exif_ifd_offset)
buf += struct.pack(">I", 0) # next IFD = none
buf += struct.pack(">H", 1) # ExifIFD: 1 entry
buf += struct.pack(">HHI", 0x9286, 7, uc_len) # UserComment tag
buf += struct.pack(">I", uc_data_offset)
buf += struct.pack(">I", 0) # next IFD = none
buf += user_comment
return bytes(buf)
def _make_py_b64(fname: str, last: bool = False) -> str:
"""Generate base64-encoded Python bootstrap script for CMD."""
import base64
find = "d.rfind(m)" if last else "d.find(m)"
script = (
"import zlib\n"
"d=open('" + fname + "','rb').read()\n"
"m=b'PNGZIP'+bytes([0,1])\n"
"i=" + find + "\n"
"nl=int.from_bytes(d[i+8:i+10],'big')\n"
"n=d[i+10:i+10+nl].decode()\n"
"pl=int.from_bytes(d[i+10+nl:i+18+nl],'big')\n"
"open(n,'wb').write(zlib.decompress(d[i+18+nl:i+18+nl+pl]))\n"
"print('Extracted:',n)"
)
return base64.b64encode(script.encode()).decode()
# Common PowerShell payload-extraction tail (after $i is set to MAGIC position)
_PS_EXTRACT_TAIL = (
"$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"'
)
# PowerShell: find FIRST MAGIC (for bootstrap — extracts unpacker)
_PS_FIND_FIRST = (
"$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));"
)
# PowerShell: find LAST MAGIC (for direct extraction — extracts payload)
_PS_FIND_LAST = (
"$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));"
)
_PS_READ_HEAD = (
"$d=[IO.File]::ReadAllBytes('IMG');"
"$t=[Text.Encoding]::GetEncoding(28591).GetString($d);"
)
def _make_ps_encoded(fname: str, last: bool = False) -> str:
"""Generate base64-encoded PowerShell bootstrap script for CMD."""
import base64
find = _PS_FIND_LAST if last else _PS_FIND_FIRST
script = _PS_READ_HEAD.replace("IMG", fname) + find + _PS_EXTRACT_TAIL
return base64.b64encode(script.encode("utf-16-le")).decode()
def _py_oneliner(fname: str, last: bool = False) -> str:
"""Python one-liner for shell (Linux/macOS/PowerShell)."""
find = "d.rfind" if last else "d.find"
return (
"python3 -c \"d=open('IMG','rb').read();"
"i=" + 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)\""
).replace("IMG", fname)
def _ps_oneliner(fname: str, last: bool = False) -> str:
"""PowerShell one-liner."""
find = _PS_FIND_LAST if last else _PS_FIND_FIRST
return (_PS_READ_HEAD + find + _PS_EXTRACT_TAIL).replace("IMG", fname)
def _add_oneliner_block(parts: list, fname: str, last: bool, label: str):
"""Append a full set of 4 one-liners to parts list."""
parts.append("")
parts.append(f"=== {label} ===")
parts.append("")
parts.append("Python:")
parts.append(_py_oneliner(fname, last))
parts.append("")
parts.append("Python (Windows CMD):")
parts.append(
'python -c "import base64;exec(base64.b64decode(\''
+ _make_py_b64(fname, last) + '\'))"')
parts.append("")
parts.append("PowerShell:")
parts.append(_ps_oneliner(fname, last))
parts.append("")
parts.append("PowerShell (Windows CMD):")
parts.append("powershell -EncodedCommand " + _make_ps_encoded(fname, last))
def build_bootstrap_comment(output_name: str, has_self_extract: bool = True) -> bytes:
"""Build tEXt chunk data with all bootstrap one-liners for PNG metadata."""
fname = os.path.basename(output_name)
parts = [BOOTSTRAP_MARKER]
if has_self_extract:
_add_oneliner_block(parts, fname, last=False,
label="Extract unpacker (first payload)")
_add_oneliner_block(parts, fname, last=True,
label="Extract file (last payload)")
text = "\n".join(parts)
return b"Comment\x00" + text.encode("latin-1")
# ── Main ──────────────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description="Pack file into PNG")
ap.add_argument("cover", help="Cover PNG image")
ap.add_argument("payload", nargs="?", help="File to hide")
ap.add_argument("-o", "--output", default="packed.png", help="Output PNG")
ap.add_argument("-m", "--mode", choices=("append", "chunk", "lsb"),
default="append", help="Embedding mode (default: append)")
ap.add_argument("--clean", action="store_true",
help="Strip all embedded payloads from PNG")
ap.add_argument("--self-extract", action="store_true",
help="Embed unpack.py for bootstrap (append mode only)")
ap.add_argument("--self-extract-ps", action="store_true",
help="Embed unpack.ps1 for PowerShell bootstrap (append mode only)")
args = ap.parse_args()
with open(args.cover, "rb") as f:
cover = f.read()
if cover[:8] != PNG_SIG:
print("Error: not a valid PNG", file=sys.stderr)
sys.exit(1)
# ── Clean mode ──
if args.clean:
result = clean_png(cover)
out = args.output if args.payload is None else args.output
with open(out, "wb") as f:
f.write(result)
before_kb = len(cover) / 1024
after_kb = len(result) / 1024
print(f"Cleaned: {out} ({after_kb:.1f} KB, was {before_kb:.1f} KB)")
return
# ── Pack mode — payload required ──
if args.payload is None:
print("Error: payload argument is required (unless --clean)", file=sys.stderr)
sys.exit(1)
if (args.self_extract or args.self_extract_ps) and args.mode != "append":
print("Error: --self-extract/--self-extract-ps works only with append mode",
file=sys.stderr)
sys.exit(1)
# Strip any existing payloads (append + chunk) before packing
cover = clean_png(cover)
with open(args.payload, "rb") as f:
raw = f.read()
block = make_payload_block(args.payload, raw)
if args.mode == "chunk":
result = pack_chunk(cover, block)
elif args.mode == "lsb":
result = pack_lsb(cover, block)
else: # append
se_blocks = b""
for flag, name in [
(args.self_extract, "unpack.py"),
(args.self_extract_ps, "unpack.ps1"),
]:
if flag:
path = os.path.join(os.path.dirname(__file__), name)
if not os.path.exists(path):
print(f"Error: {name} not found next to pack.py", file=sys.stderr)
sys.exit(1)
with open(path, "rb") as f:
se_blocks += make_payload_block(name, f.read())
# Embed bootstrap one-liners in PNG metadata (eXIf + tEXt)
if args.self_extract or args.self_extract_ps:
comment_text = build_bootstrap_comment(
args.output,
has_self_extract=args.self_extract or args.self_extract_ps)
exif_data = build_exif_user_comment(
comment_text.split(b"\x00", 1)[1].decode("latin-1"))
chunks = parse_chunks(cover)
new_chunks = []
for ct, cd in chunks:
new_chunks.append((ct, cd))
if ct == b"IHDR":
new_chunks.append((b"eXIf", exif_data))
new_chunks.append((b"tEXt", comment_text))
cover = assemble_png(new_chunks)
result = cover + se_blocks + block
with open(args.output, "wb") as f:
f.write(result)
cover_kb = len(cover) / 1024
total_kb = len(result) / 1024
print(f"Packed [{args.mode}]: {args.output} ({total_kb:.1f} KB, cover {cover_kb:.1f} KB)")
if args.self_extract:
print("Self-extract: unpack.py embedded")
if args.self_extract_ps:
print("Self-extract: unpack.ps1 embedded")
if args.self_extract or args.self_extract_ps:
text = comment_text.split(b"\x00", 1)[1].decode("latin-1")
print("\n" + text.replace(BOOTSTRAP_MARKER + "\n", "Bootstrap one-liners:\n", 1))
if __name__ == "__main__":
main()

1
test.txt Normal file
View File

@@ -0,0 +1 @@
test

90
unpack.ps1 Normal file
View File

@@ -0,0 +1,90 @@
# unpack.ps1 — Extract file(s) hidden inside a PNG image
# Usage: powershell -File unpack.ps1 packed.png [-OutDir .] [-List]
param(
[Parameter(Mandatory, Position=0)][string]$Image,
[string]$OutDir = ".",
[switch]$List,
[switch]$All
)
Add-Type -TypeDefinition @"
using System;
public static class PngZipHelper2 {
static readonly uint[] crcTable;
static PngZipHelper2() {
crcTable = new uint[256];
for (uint i = 0; i < 256; i++) {
uint c = i;
for (int j = 0; j < 8; j++)
c = (c & 1) != 0 ? 0xEDB88320u ^ (c >> 1) : c >> 1;
crcTable[i] = c;
}
}
public static uint Crc32(byte[] data) {
uint crc = 0xFFFFFFFF;
foreach (byte b in data) crc = crcTable[(crc ^ b) & 0xFF] ^ (crc >> 8);
return crc ^ 0xFFFFFFFF;
}
}
"@
$d = [IO.File]::ReadAllBytes((Resolve-Path $Image))
$text = [Text.Encoding]::GetEncoding(28591).GetString($d)
$sig = 'PNGZIP'
$payloads = @()
$offset = 0
while ($true) {
# Find next 'PNGZIP' and validate \x00\x01 suffix via byte array
$i = $offset
while (($i = $text.IndexOf($sig, $i, [StringComparison]::Ordinal)) -ge 0) {
if ($d[$i+6] -eq 0 -and $d[$i+7] -eq 1) { break }
$i++
}
if ($i -lt 0) { break }
$p = $i + 8
# name
$nl = $d[$p] * 256 + $d[$p+1]; $p += 2
$name = [Text.Encoding]::UTF8.GetString($d, $p, $nl); $p += $nl
# compressed length (big-endian uint64)
[uint64]$pl = 0
for ($k = 0; $k -lt 8; $k++) { $pl = $pl * 256 + $d[$p+$k] }; $p += 8
# decompress zlib: skip 2-byte header, exclude 4-byte adler32
$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)
$raw = $os.ToArray()
$p += $pl
# CRC check
[uint32]$storedCrc = $d[$p]*16777216 + $d[$p+1]*65536 + $d[$p+2]*256 + $d[$p+3]
$p += 4
$actualCrc = [PngZipHelper2]::Crc32($raw)
if ($actualCrc -ne $storedCrc) { Write-Warning "CRC mismatch for $name" }
$payloads += @{ Name = $name; Data = $raw }
$offset = $p
}
if ($payloads.Count -eq 0) { Write-Error "No payload found"; exit 3 }
if ($List) {
foreach ($p in $payloads) { Write-Host " $($p.Name) ($($p.Data.Length) bytes)" }
return
}
$toExtract = if ($All) { $payloads } else { @($payloads[-1]) }
$null = New-Item -ItemType Directory -Force -Path $OutDir
foreach ($entry in $toExtract) {
$safeName = [IO.Path]::GetFileName($entry.Name)
$outPath = Join-Path $OutDir $safeName
[IO.File]::WriteAllBytes($outPath, $entry.Data)
Write-Host "Extracted: $outPath ($($entry.Data.Length) bytes)"
}

282
unpack.py Normal file
View File

@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""Extract file(s) hidden inside a PNG image.
Supports three embedding modes (auto-detected):
append — data after IEND
chunk — data in private pnZp PNG chunk
lsb — data in least-significant bits of pixels
Usage:
python3 unpack.py packed.png [-o output_dir] [-a]
"""
import argparse
import os
import struct
import sys
import zlib
MAGIC = b"PNGZIP\x00\x01"
PNG_SIG = b"\x89PNG\r\n\x1a\n"
# ── Shared helpers ────────────────────────────────────────────────
def find_iend(data: bytes) -> int:
"""Return offset of the first byte AFTER the IEND chunk, or -1."""
idx = data.rfind(b"IEND")
if idx == -1:
return -1
chunk_start = idx - 4
length = struct.unpack(">I", data[chunk_start:chunk_start + 4])[0]
return chunk_start + 4 + 4 + length + 4
def parse_chunks(data: bytes):
"""Parse PNG data into list of (chunk_type, chunk_data)."""
pos = 8 # skip PNG signature
chunks = []
while pos + 8 <= len(data):
length = struct.unpack(">I", data[pos:pos + 4])[0]
chunk_type = data[pos + 4:pos + 8]
chunk_data = data[pos + 8:pos + 8 + length]
pos += 4 + 4 + length + 4
chunks.append((chunk_type, chunk_data))
return chunks
def parse_payload_blocks(data: bytes):
"""Yield (filename, raw_bytes) for each payload block in data."""
offset = 0
while True:
idx = data.find(MAGIC, offset)
if idx == -1:
break
pos = idx + len(MAGIC)
if pos + 2 > len(data):
break
name_len = struct.unpack(">H", data[pos:pos + 2])[0]
pos += 2
if pos + name_len > len(data):
break
filename = data[pos:pos + name_len].decode("utf-8")
pos += name_len
if pos + 8 > len(data):
break
payload_len = struct.unpack(">Q", data[pos:pos + 8])[0]
pos += 8
if pos + payload_len > len(data):
break
compressed = data[pos:pos + payload_len]
pos += payload_len
if pos + 4 > len(data):
break
stored_crc = struct.unpack(">I", data[pos:pos + 4])[0]
pos += 4
raw = zlib.decompress(compressed)
actual_crc = zlib.crc32(raw) & 0xFFFFFFFF
if actual_crc != stored_crc:
print(f"Warning: CRC mismatch for {filename}", file=sys.stderr)
yield filename, raw
offset = pos
# ── Append mode ───────────────────────────────────────────────────
def find_append_payloads(data: bytes):
"""Find payloads appended after IEND."""
iend = find_iend(data)
if iend == -1 or iend >= len(data):
return []
return list(parse_payload_blocks(data[iend:]))
# ── Chunk mode ────────────────────────────────────────────────────
def find_chunk_payloads(data: bytes):
"""Find payloads stored in pnZp chunks."""
if data[:8] != PNG_SIG:
return []
chunks = parse_chunks(data)
pnzp = [(cd[0], cd[1:]) for ct, cd in chunks if ct == b"pnZp" and len(cd) > 0]
if not pnzp:
return []
pnzp.sort(key=lambda x: x[0])
combined = b"".join(frag for _, frag in pnzp)
return list(parse_payload_blocks(combined))
# ── LSB mode ──────────────────────────────────────────────────────
def _paeth(a, b, c):
p = a + b - c
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
if pa <= pb and pa <= pc:
return a
return b if pb <= pc else c
def _unfilter_row(ftype, row, prev, bpp):
out = bytearray(len(row))
for i in range(len(row)):
x = row[i]
a = out[i - bpp] if i >= bpp else 0
b = prev[i]
c = prev[i - bpp] if i >= bpp else 0
if ftype == 0:
out[i] = x
elif ftype == 1:
out[i] = (x + a) & 0xFF
elif ftype == 2:
out[i] = (x + b) & 0xFF
elif ftype == 3:
out[i] = (x + (a + b) // 2) & 0xFF
elif ftype == 4:
out[i] = (x + _paeth(a, b, c)) & 0xFF
else:
raise ValueError(f"Unknown PNG filter type {ftype}")
return out
def find_lsb_payloads(data: bytes):
"""Find payloads encoded in LSB of pixel data."""
if data[:8] != PNG_SIG:
return []
try:
chunks = parse_chunks(data)
except Exception:
return []
# Parse IHDR
ihdr = None
for ct, cd in chunks:
if ct == b"IHDR":
ihdr = cd
break
if ihdr is None or len(ihdr) < 13:
return []
width, height = struct.unpack(">II", ihdr[:8])
bit_depth, color_type = ihdr[8], ihdr[9]
interlace = ihdr[12]
if bit_depth != 8 or color_type not in (2, 6) or interlace != 0:
return []
channels = 3 if color_type == 2 else 4
bpp = channels
# Decompress IDAT
idat_data = b"".join(cd for ct, cd in chunks if ct == b"IDAT")
if not idat_data:
return []
try:
raw = zlib.decompress(idat_data)
except zlib.error:
return []
# Unfilter
stride = width * bpp
pixels = bytearray()
prev = bytearray(stride)
pos = 0
for _ in range(height):
if pos >= len(raw):
return []
ftype = raw[pos]; pos += 1
if pos + stride > len(raw):
return []
row = raw[pos:pos + stride]; pos += stride
prev = _unfilter_row(ftype, row, prev, bpp)
pixels.extend(prev)
# Quick check: extract MAGIC-length bytes first
total_pixels = len(pixels)
magic_len = len(MAGIC)
if total_pixels < magic_len * 8:
return []
quick = bytearray(magic_len)
for i in range(magic_len):
b = 0
base = i * 8
for bit in range(8):
b = (b << 1) | (pixels[base + bit] & 1)
quick[i] = b
if quick != bytearray(MAGIC):
return []
# MAGIC found — extract all available bytes
total_bytes = total_pixels // 8
extracted = bytearray(total_bytes)
for i in range(total_bytes):
b = 0
base = i * 8
for bit in range(8):
b = (b << 1) | (pixels[base + bit] & 1)
extracted[i] = b
return list(parse_payload_blocks(bytes(extracted)))
# ── Auto-detect all modes ────────────────────────────────────────
def find_all_payloads(data: bytes):
"""Try all detection methods: append → chunk → lsb."""
results = find_append_payloads(data)
if results:
return results
results = find_chunk_payloads(data)
if results:
return results
results = find_lsb_payloads(data)
if results:
return results
return []
# ── Main ──────────────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description="Extract file from PNG")
ap.add_argument("image", help="PNG with hidden payload")
ap.add_argument("-o", "--outdir", default=".", help="Output directory")
ap.add_argument("-a", "--all", action="store_true",
help="Extract all payloads (including embedded unpack.py)")
ap.add_argument("-l", "--list", action="store_true",
help="List payloads without extracting")
args = ap.parse_args()
with open(args.image, "rb") as f:
data = f.read()
payloads = list(find_all_payloads(data))
if not payloads:
print("No payload found in this PNG.", file=sys.stderr)
sys.exit(3)
if args.list:
for name, raw in payloads:
print(f" {name} ({len(raw)} bytes)")
return
# By default extract only the last payload (the main one).
# With --all, extract everything.
to_extract = payloads if args.all else [payloads[-1]]
os.makedirs(args.outdir, exist_ok=True)
for filename, raw in to_extract:
safe_name = os.path.basename(filename) # path traversal protection
out_path = os.path.join(args.outdir, safe_name)
with open(out_path, "wb") as f:
f.write(raw)
print(f"Extracted: {out_path} ({len(raw)} bytes)")
if __name__ == "__main__":
main()