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.
272 lines
12 KiB
PowerShell
272 lines
12 KiB
PowerShell
# 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"
|
|
}
|