IO-Compress
view release on metacpan or search on metacpan
bin/zipdetails view on Meta::CPAN
my $foundCentralHeader = 0;
my $lastEndsAt = 0;
my $lastSignature = 0;
my $lastHeader = {};
$CentralDirectory->{alreadyScanned} = 1 ;
my $output_encryptedCD = 0;
reportPrefixData();
while(my $s = scanForSignature($opt_walk))
{
my $here = $FH->tell();
my $delta = $here - $lastEndsAt ;
# delta can only be negative when '--scan' is used
if ($delta < 0 )
{
# nested or overlap
# check if nested
# remember & check if matching entry in CD
# printf("### WARNING: OVERLAP/NESTED Record found 0x%X 0x%X $delta\n", $here, $lastEndsAt) ;
}
elsif ($here != $lastEndsAt)
{
# scanForSignature had to skip bytes to find the next signature
# some special cases that don't have signatures need to be checked first
seekTo($lastEndsAt);
if (! $output_encryptedCD && $CentralDirectory->isEncryptedCD())
{
displayEncryptedCD();
$output_encryptedCD = 1;
$lastEndsAt = $FH->tell();
next;
}
elsif ($lastSignature == ZIP_LOCAL_HDR_SIG && $lastHeader->{'streamed'} )
{
# Check for size of possible malformed Data Descriptor before outputting payload
if (! $lastHeader->{'gotDataDescriptorSize'})
{
my $hdrSize = checkForBadlyFormedDataDescriptor($lastHeader, $delta) ;
if ($hdrSize)
{
# remove size of Data Descriptor from payload
$delta -= $hdrSize;
$lastHeader->{'gotDataDescriptorSize'} = $hdrSize;
}
}
if(defined($lastHeader->{'payloadOutput'}) && ($lastEndsAt = BadlyFormedDataDescriptor($lastHeader, $delta)))
{
$HeaderOffsetIndex->rewindIndex();
$lastHeader->{entry}->readDataDescriptor(1) ;
next;
}
# Assume we have the payload when streaming is enabled
outSomeData($delta, "PAYLOAD", $opt_Redact) ;
$lastHeader->{'payloadOutput'} = 1;
$lastEndsAt = $FH->tell();
next;
}
elsif (Signatures::isCentralHeader($s) && $foundCentralHeader == 0)
{
# check for an APK header directly before the first Central Header
$foundCentralHeader = 1;
($START_APK, $APK, $APK_LEN) = chckForAPKSigningBlock($FH, $here, 0) ;
if ($START_APK)
{
seekTo($lastEndsAt+4);
scanApkBlock();
$lastEndsAt = $FH->tell();
next;
}
seekTo($lastEndsAt);
}
# Not a special case, so output generic padding message
if ($delta > 0)
{
reportPrefixData($delta)
if $lastEndsAt == 0 ;
outSomeDataParagraph($delta, "UNEXPECTED PADDING");
info $FH->tell() - $delta, decimalHex0x($delta) . " Unexpected Padding bytes"
if $FH->tell() - $delta ;
$POSSIBLE_PREFIX_DELTA = $delta
if $lastEndsAt == 0;
$lastEndsAt = $FH->tell();
next;
}
else
{
seekTo($here);
}
}
my ($buffer, $signature) = read_V();
$lastSignature = $signature;
my $handler = Signatures::decoder($signature);
if (!defined $handler) {
internalFatal undef, "xxx";
}
$foundZipRecords = 1;
$lastHeader = $handler->($signature, $buffer, $FH->tell() - 4) // {'streamed' => 0};
$lastEndsAt = $FH->tell();
seekTo($here + 4)
bin/zipdetails view on Meta::CPAN
}
my ($buffer, $signature) = read_V();
$expectedOffset = undef;
$expectedSignature = undef;
# Check for split archive marker at start of file
if ($here == 0 && $signature == ZIP_SINGLE_SEGMENT_MARKER)
{
# let it drop through
$expectedSignature = ZIP_SINGLE_SEGMENT_MARKER;
$expectedOffset = 0;
}
else
{
my $expectedEntry = $HeaderOffsetIndex->getNextIndex() ;
if ($expectedEntry)
{
$expectedOffset = $expectedEntry->offset();
$expectedSignature = $expectedEntry->signature();
$expectedBuffer = pack "V", $expectedSignature ;
}
}
my $delta = $expectedOffset - $here ;
# if ($here != $expectedOffset && $signature != ZIP_DATA_HDR_SIG)
# {
# rewindRelative(4);
# my $delta = $expectedOffset - $here ;
# outSomeDataParagraph($delta, "UNEXPECTED PADDING");
# $HeaderOffsetIndex->rewindIndex();
# next;
# }
# Need to check for use-case where
# * there is a ZIP_DATA_HDR_SIG directly after a ZIP_LOCAL_HDR_SIG.
# The HeaderOffsetIndex object doesn't have visibility of it.
# * APK header directly before the CD
# * zipbomb
if (defined $expectedOffset && $here != $expectedOffset && ( $CentralDirectory->exists() || $EOCD_Present) )
{
if ($here > $expectedOffset)
{
# Probable zipbomb
# Cursor $OFFSET need to rewind
$OFFSET = $expectedOffset;
$FH->seek($OFFSET + 4, SEEK_SET) ;
$signature = $expectedSignature;
$buffer = $expectedBuffer ;
}
# If get here then $here is less than $expectedOffset
# check for an APK header directly before the first Central Header
# Make sure not to miss a streaming data descriptor
if ($signature != ZIP_DATA_HDR_SIG && Signatures::isCentralHeader($expectedSignature) && $START_APK && ! $processedAPK )
{
seekTo($here+4);
# rewindRelative(4);
scanApkBlock();
$HeaderOffsetIndex->rewindIndex();
$processedAPK = 1;
next;
}
# Check Encrypted Central Directory
# if ($CentralHeaderSignatures{$expectedSignature} && $CentralDirectory->isEncryptedCD() && ! $processedECD)
# {
# # rewind the invalid signature
# seekTo($here);
# # rewindRelative(4);
# displayEncryptedCD();
# $processedECD = 1;
# next;
# }
if ($signature != ZIP_DATA_HDR_SIG && $delta >= 0)
{
rewindRelative(4);
if($lastHeader->{'streamed'} && BadlyFormedDataDescriptor($lastHeader, $delta))
{
$lastHeader->{entry}->readDataDescriptor(1) ;
$HeaderOffsetIndex->rewindIndex();
next;
}
reportPrefixData($delta)
if $here == 0;
outSomeDataParagraph($delta, "UNEXPECTED PADDING");
info $FH->tell() - $delta, decimalHex0x($delta) . " Unexpected Padding bytes"
if $FH->tell() - $delta ;
$HeaderOffsetIndex->rewindIndex();
next;
}
# ZIP_DATA_HDR_SIG drops through
}
my $handler = Signatures::decoder($signature);
if (!defined $handler)
{
# if ($CentralDirectory->exists()) {
# # Should be at offset that central directory says
# my $locOffset = $CentralDirectory->getNextLocalOffset();
# my $delta = $locOffset - $here ;
# if ($here + 4 == $locOffset ) {
# for (0 .. 3) {
# $FH->ungetc(ord(substr($buffer, $_, 1)))
# }
# outSomeData($delta, "UNEXPECTED PADDING");
# next;
# }
bin/zipdetails view on Meta::CPAN
# Central Directory knows about these, so this is a zipbomb
error undef, "Possible zipbomb -- Nested Local Entries found";
for my $loc (sort keys %bomb)
{
my $count = scalar @{ $bomb{$loc} };
my $outerEntry = $LocalDirectory->getByLocalOffset($loc);
say "# Local Header for '" . $outerEntry->outputFilename . "' at offset " . decimalHex0x($loc) . " has $count nested Local Headers";
my $table = new SimpleTable;
$table->addHeaderRow('Offset', 'Filename');
$table->addDataRow(decimalHex0x($_), $LocalDirectory->getByLocalOffset($_)->outputFilename)
for sort @{ $bomb{$loc} } ;
$table->display();
delete $cleanCentralEntries{ $_ }
for grep { defined $_ }
map { $CentralDirectory->{byLocalOffset}{$_}{centralHeaderOffset} }
@{ $bomb{$loc} } ;
}
}
}
# Check if contents of local headers match with Central Headers
#
# When Central Header encryption is used the local header values are masked (see APPNOTE 6.3.10, sec 4)
# In this usecase the Central Header will appear to be absent
#
# key fields
# filename, compressed/uncompressed lengths, crc, compression method
{
for my $centralEntry ( sort { $a->centralHeaderOffset() <=> $b->centralHeaderOffset() } values %cleanCentralEntries )
{
my $localOffset = $centralEntry->localHeaderOffset;
my $localEntry = $LocalDirectory->getByLocalOffset($localOffset);
next
unless $localEntry;
state $fields = [
# field name offset display name stringify
['filename', ZIP_CD_FILENAME_OFFSET,
'Filename', undef, ],
['extractVersion', 7, 'Extract Zip Spec', sub { decimalHex0xUndef($_[0]) . " " . decodeZipVer($_[0]) }, ],
['generalPurposeFlags', 8, 'General Purpose Flag', \&decimalHex0xUndef, ],
['compressedMethod', 10, 'Compression Method', sub { decimalHex0xUndef($_[0]) . " " . getcompressionMethodName($_[0]) }, ],
['lastModDateTime', 12, 'Modification Time', sub { decimalHex0xUndef($_[0]) . " " . LastModTime($_[0]) }, ],
['crc32', 16, 'CRC32', \&decimalHex0xUndef, ],
['compressedSize', 20, 'Compressed Size', \&decimalHex0xUndef, ],
['uncompressedSize', 24, 'Uncompressed Size', \&decimalHex0xUndef, ],
] ;
my $table = new SimpleTable;
$table->addHeaderRow('Field Name', 'Central Offset', 'Central Value', 'Local Offset', 'Local Value');
for my $data (@$fields)
{
my ($field, $offset, $name, $stringify) = @$data;
# if the local header uses streaming and we are running a scan/walk, the compressed/uncompressed sizes will not be known
my $localValue = $localEntry->{$field} ;
my $centralValue = $centralEntry->{$field};
if (($localValue // '-1') ne ($centralValue // '-2'))
{
if ($stringify)
{
$localValue = $stringify->($localValue);
$centralValue = $stringify->($centralValue);
}
$table->addDataRow($name,
decimalHex0xUndef($centralEntry->centralHeaderOffset() + $offset),
$centralValue,
decimalHex0xUndef($localOffset+$offset),
$localValue);
}
}
my $badFields = $table->hasData;
if ($badFields)
{
error undef, "Found $badFields Field Mismatch for Filename '". $centralEntry->outputFilename . "'";
$table->display();
}
}
}
}
elsif ($CentralDirectory->exists())
{
my @messages = "Central Directory exists, but Local Directory not found" ;
push @messages , "Try running with --walk' or '--scan' options"
unless $opt_scan || $opt_walk ;
error undef, @messages;
}
elsif ($LocalDirectory->exists())
{
if ($CentralDirectory->isEncryptedCD())
{
warning undef, "Local Directory exists, but Central Directory is encrypted"
}
else
{
error undef, "Local Directory exists, but Central Directory not found"
}
}
if ($ErrorCount ||$WarningCount || $InfoCount )
{
say "#"
unless $lastWasMessage ;
say "# Error Count: $ErrorCount"
if $ErrorCount;
say "# Warning Count: $WarningCount"
if $WarningCount;
say "# Info Count: $InfoCount"
if $InfoCount;
bin/zipdetails view on Meta::CPAN
sub compressionMethod
{
my $id = shift ;
Value_v($id) . getcompressionMethodName($id);
}
sub LocalHeader
{
my $signature = shift ;
my $data = shift ;
my $startRecordOffset = shift ;
my $locHeaderOffset = $FH->tell() -4 ;
++ $LocalHeaderCount;
print "\n";
out $data, "LOCAL HEADER #$LocalHeaderCount" , Value_V($signature);
need 26, Signatures::name($signature);
my $buffer;
my $orphan = 0;
my ($loc, $CDcompressedSize, $cdZip64, $zip64Sizes, $cdIndex, $cdEntryOffset) ;
my $CentralEntryExists = $CentralDirectory->localOffset($startRecordOffset);
my $localEntry = LocalDirectoryEntry->new();
my $cdEntry;
if (! $opt_scan && ! $opt_walk && $CentralEntryExists)
{
$cdEntry = $CentralDirectory->getByLocalOffset($startRecordOffset);
if (! $cdEntry)
{
out1 "Orphan Entry: No matching central directory" ;
$orphan = 1 ;
}
$cdZip64 = $cdEntry->zip64ExtraPresent;
$zip64Sizes = $cdEntry->zip64SizesPresent;
$cdEntryOffset = $cdEntry->centralHeaderOffset ;
$localEntry->addCdEntry($cdEntry) ;
if ($cdIndex && $cdIndex != $LocalHeaderCount)
{
# fatal undef, "$cdIndex != $LocalHeaderCount"
}
}
my $extractVer = out_C "Extract Zip Spec", \&decodeZipVer;
out_C "Extract OS", \&decodeOS;
my ($bgp, $gpFlag) = read_v();
my ($bcm, $compressedMethod) = read_v();
out $bgp, "General Purpose Flag", Value_v($gpFlag) ;
GeneralPurposeBits($compressedMethod, $gpFlag);
my $LanguageEncodingFlag = $gpFlag & ZIP_GP_FLAG_LANGUAGE_ENCODING ;
my $streaming = $gpFlag & ZIP_GP_FLAG_STREAMING_MASK ;
$localEntry->languageEncodingFlag($LanguageEncodingFlag) ;
out $bcm, "Compression Method", compressionMethod($compressedMethod) ;
info $FH->tell() - 2, "Unknown 'Compression Method' ID " . decimalHex0x($compressedMethod, 2)
if ! defined $ZIP_CompressionMethods{$compressedMethod} ;
my $lastMod = out_V "Modification Time", sub { LastModTime($_[0]) };
my $crc = out_V "CRC";
# Weak encryption if
# * encrypt flag (bit 0) set in General Purpose Flags
# * strong encrypt (bit ) not set in General Purpose Flags
# * not using AES encryption (compression method 99)
my $weakEncryption = ($gpFlag & ZIP_GP_FLAG_ALL_ENCRYPT) == ZIP_GP_FLAG_ENCRYPTED_MASK &&
$compressedMethod != ZIP_CM_AES;
# Weak encryption uses the CRC value even when streaming is in play.
# This conflicts with appnote 6.3.10 section 4.4.4
warning $FH->tell() - 4, "CRC field should be zero when streaming is enabled"
if $streaming && $crc != 0 && ! $weakEncryption;
my $compressedSize = out_V "Compressed Size";
# warning $FH->tell(), "Compressed Size should be zero when streaming is enabled";
my $uncompressedSize = out_V "Uncompressed Size";
# warning $FH->tell(), "Uncompressed Size should be zero when streaming is enabled";
my $filenameLength = out_v "Filename Length";
if ($filenameLength == 0)
{
info $FH->tell()- 2, "Zero Length filename";
}
my $extraLength = out_v "Extra Length";
my $filename = '';
if ($filenameLength)
{
need $filenameLength, Signatures::name($signature), 'Filename';
myRead(my $raw_filename, $filenameLength);
$localEntry->filename($raw_filename) ;
$filename = outputFilename($raw_filename, $LanguageEncodingFlag);
$localEntry->outputFilename($filename);
}
$localEntry->localHeaderOffset($locHeaderOffset) ;
$localEntry->offsetStart($locHeaderOffset) ;
$localEntry->compressedSize($compressedSize) ;
$localEntry->uncompressedSize($uncompressedSize) ;
$localEntry->extractVersion($extractVer);
$localEntry->generalPurposeFlags($gpFlag);
$localEntry->lastModDateTime($lastMod);
$localEntry->crc32($crc) ;
$localEntry->zip64ExtraPresent($cdZip64) ;
$localEntry->zip64SizesPresent($zip64Sizes) ;
$localEntry->compressedMethod($compressedMethod) ;
$localEntry->streamed($gpFlag & ZIP_GP_FLAG_STREAMING_MASK) ;
$localEntry->std_localHeaderOffset($locHeaderOffset + $PREFIX_DELTA) ;
$localEntry->std_compressedSize($compressedSize) ;
$localEntry->std_uncompressedSize($uncompressedSize) ;
$localEntry->std_diskNumber(0) ;
if ($extraLength)
{
need $extraLength, Signatures::name($signature), 'Extra';
walkExtra($extraLength, $localEntry);
}
# Defer test for directory payload until Central Header processing.
# Need to have external file attributes to deal with sme edge conditions.
# # APPNOTE 6.3.10, sec 4.3.8
# warning $FH->tell - $filenameLength, "Directory '$filename' must not have a payload"
# if ! $streaming && $filename =~ m#/$# && $localEntry->uncompressedSize ;
my @msg ;
# if ($cdZip64 && ! $ZIP64)
# {
# # Central directory said this was Zip64
# # some zip files don't have the Zip64 field in the local header
# # seems to be a streaming issue.
# push @msg, "Missing Zip64 extra field in Local Header #$hexHdrCount\n";
# if (! $zip64Sizes)
# {
# # Central has a ZIP64 entry that doesn't have sizes
# # Local doesn't have a Zip 64 at all
# push @msg, "Unzip may complain about 'overlapped components' #$hexHdrCount\n";
# }
# else
# {
# $ZIP64 = 1
# }
# }
my $minizip_encrypted = $localEntry->minizip_secure;
my $pk_encrypted = ($gpFlag & ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK) && $compressedMethod != ZIP_CM_AES && ! $minizip_encrypted;
# Detecting PK strong encryption from a local header is a bit convoluted.
# Cannot just use ZIP_GP_FLAG_ENCRYPTED_CD because minizip also uses this bit.
# so jump through some hoops
# extract ver is >= 5.0'
# all the encryption flags are set in gpflags
# TODO - add zero lengths for crc, compressed & uncompressed
if (($gpFlag & ZIP_GP_FLAG_ALL_ENCRYPT) == ZIP_GP_FLAG_ALL_ENCRYPT && $extractVer >= 0x32 )
{
$CentralDirectory->setPkEncryptedCD()
}
my $size = 0;
# If no CD scanned, get compressed Size from local header.
# Zip64 extra field takes priority
my $cdl = defined $cdEntry
? $cdEntry->compressedSize()
: undef;
$CDcompressedSize = $localEntry->compressedSize ;
$CDcompressedSize = $cdl
if defined $cdl && $gpFlag & ZIP_GP_FLAG_STREAMING_MASK;
my $cdu = defined $CentralDirectory->{byLocalOffset}{$locHeaderOffset}
? $CentralDirectory->{byLocalOffset}{$locHeaderOffset}{uncompressedSize}
: undef;
my $CDuncompressedSize = $localEntry->uncompressedSize ;
$CDuncompressedSize = $cdu
if defined $cdu && $gpFlag & ZIP_GP_FLAG_STREAMING_MASK;
my $fullCompressedSize = $CDcompressedSize;
my $payloadOffset = $FH->tell();
$localEntry->payloadOffset($payloadOffset) ;
$localEntry->offsetEnd($payloadOffset + $fullCompressedSize -1) ;
if ($CDcompressedSize)
{
# check if enough left in file for the payload
my $available = $FILELEN - $FH->tell;
bin/zipdetails view on Meta::CPAN
sub DataDescriptor
{
# Data header record or Spanned archive marker.
#
# ZIP_DATA_HDR_SIG at start of file flags a spanned zip file.
# If it is a true marker, the next four bytes MUST be a ZIP_LOCAL_HDR_SIG
# See APPNOTE 6.3.10, sec 8.5.3, 8.5.4 & 8.5.5
# If not at start of file, assume a Data Header Record
# See APPNOTE 6.3.10, sec 4.3.9 & 4.3.9.3
my $signature = shift ;
my $data = shift ;
my $startRecordOffset = shift ;
my $here = $FH->tell();
if ($here == 4)
{
# Spanned Archive Marker
out $data, "SPLIT ARCHIVE MULTI-SEGMENT MARKER", Value_V($signature);
return;
# my (undef, $next_sig) = read_V();
# seekTo(0);
# if ($next_sig == ZIP_LOCAL_HDR_SIG)
# {
# print "\n";
# out $data, "SPLIT ARCHIVE MULTI-SEGMENT MARKER", Value_V($signature);
# seekTo($here);
# return;
# }
}
my $sigName = Signatures::titleName(ZIP_DATA_HDR_SIG);
print "\n";
out $data, $sigName, Value_V($signature);
need 24, Signatures::name($signature);
# Ignore header payload if nested (assume 64-bit descriptor)
if (Nesting::isNested( $here - 4, $here - 4 + 24 - 1))
{
out "", "Skipping Nested Payload";
return {};
}
my $compressedSize;
my $uncompressedSize;
my $localEntry = $LocalDirectory->lastStreamedEntryAdded();
my $centralEntry = $localEntry && $localEntry->getCdEntry ;
if (!$localEntry)
{
# found a Data Descriptor without a local header
out "", "Skipping Data Descriptor", "No matching Local header with streaming bit set";
error $here - 4, "Orphan '$sigName' found", "No matching Local header with streaming bit set";
return {};
}
my $crc = out_V "CRC";
my $payloadLength = $here - 4 - $localEntry->payloadOffset;
my $deltaToNext = deltaToNextSignature();
my $cl32 = unpack "V", peekAtOffset($here + 4, 4);
my $cl64 = unpack "Q<", peekAtOffset($here + 4, 8);
# use delta to next header & payload length
# deals with use case where the payload length < 32 bit
# will use a 32-bit value rather than the 64-bit value
# see if delta & payload size match
if ($deltaToNext == 16 && $cl64 == $payloadLength)
{
if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64))
{
error $here, "'$sigName': expected 32-bit values, got 64-bit";
}
$compressedSize = out_Q "Compressed Size" ;
$uncompressedSize = out_Q "Uncompressed Size" ;
}
elsif ($deltaToNext == 8 && $cl32 == $payloadLength)
{
if ($localEntry->zip64)
{
error $here, "'$sigName': expected 64-bit values, got 32-bit";
}
$compressedSize = out_V "Compressed Size" ;
$uncompressedSize = out_V "Uncompressed Size" ;
}
# Try matching just payload lengths
elsif ($cl32 == $payloadLength)
{
if ($localEntry->zip64)
{
error $here, "'$sigName': expected 64-bit values, got 32-bit";
}
$compressedSize = out_V "Compressed Size" ;
$uncompressedSize = out_V "Uncompressed Size" ;
warning $here, "'$sigName': Zip Header not directly after Data Descriptor";
}
elsif ($cl64 == $payloadLength)
{
if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64))
{
error $here, "'$sigName': expected 32-bit values, got 64-bit";
}
$compressedSize = out_Q "Compressed Size" ;
$uncompressedSize = out_Q "Uncompressed Size" ;
warning $here, "'$sigName': Zip Header not directly after Data Descriptor";
( run in 0.974 second using v1.01-cache-2.11-cpan-39bf76dae61 )