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:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
*.pyc
|
||||
80
POWERSHELL.md
Normal file
80
POWERSHELL.md
Normal 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
193
README.md
Normal 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
271
pack.ps1
Normal 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
462
pack.py
Normal 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()
|
||||
90
unpack.ps1
Normal file
90
unpack.ps1
Normal 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
282
unpack.py
Normal 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()
|
||||
Reference in New Issue
Block a user