Skip to content

Instantly share code, notes, and snippets.

@angea
Last active December 23, 2024 14:41
Show Gist options
  • Save angea/0173f21075bede2c1df8feafbd1a8801 to your computer and use it in GitHub Desktop.
Save angea/0173f21075bede2c1df8feafbd1a8801 to your computer and use it in GitHub Desktop.
a file crafting tool
#!/usr/bin/env python3
# FileCraft - Ange Albertini 2024
import hashlib
import struct
import sys
import zlib
def crc32(data):
return zlib.crc32(data)
assert crc32(b"P") == 0xb969be79
HELLOWORLD = b"Hello world!"
assert crc32(HELLOWORLD) == 0x1b851995
EICARCONTENTS = b"X5O!P%@AP[4\\PZX54(P^)7CC)7}$" +\
b"EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
assert crc32(EICARCONTENTS) == 0x6851cf3c
def ASCII(c):
if c >= 0x20 and c <= 0x79:
return chr(c)
else:
return " "
def b2h(data):
return " ".join("%02X" % char for char in data)
def xxd(data):
i = 0
while i < len(data):
print("%08x:" % i, end=" ")
print(b2h(data[i:i+16]).ljust(48), end=" ")
print("".join(ASCII(c) for c in data[i:i+16]))
i += 16
class EoCDR:
headerSignature = b"PK\5\6"
diskNum = 0
diskStart = 0
CDRCount = 1
CentralDirectoryRecordCount = 1
CDSize = 0
CDOffset = 0
commentLength = 0
comment = b""
def set(self):
self.commentLength = len(self.comment)
def make(self):
self._contents = struct.pack(
"<4sHHHHLLH",
self.headerSignature,
self.diskNum,
self.diskStart,
self.CDRCount,
self.CentralDirectoryRecordCount,
self.CDSize,
self.CDOffset,
self.commentLength,
) + self.comment
self._size = len(self._contents)
return self._contents
class Cdfh:
headerSignature = b"PK\1\2"
versionMade = 0
versionExtract = 0
flags = 0 # general purpose bit flag
compressionMethod = 0
fileLastModifyTime = 0
fileLastModifyDate = 0
fileData = b""
crc32 = crc32(fileData)
compressedSize = len(fileData)
uncompressedSize = compressedSize
fileName = b""
fileNameLength = len(fileName)
extraField = b""
extraFieldLength = len(extraField)
fileComment = b""
fileCommentLength = len(fileComment)
diskNumber = 0
internalFileAttributes = 0
externalFileAttributes = 0
relativeOffset = 0
def set(self):
self.fileNameLength = len(self.fileName)
self.extraFieldLength = len(self.extraField)
self.compressedSize = len(self.fileData)
self.uncompressedSize = self.compressedSize
self.crc32 = crc32(self.fileData)
def make(self):
self._contents = struct.pack(
"<4sHHHHHHLLLHHHHHLL",
self.headerSignature,
self.versionMade,
self.versionExtract,
self.flags,
self.compressionMethod,
self.fileLastModifyTime,
self.fileLastModifyDate,
self.crc32,
self.compressedSize,
self.uncompressedSize,
self.fileNameLength,
self.extraFieldLength,
self.fileCommentLength,
self.diskNumber,
self.internalFileAttributes,
self.externalFileAttributes,
self.relativeOffset,
) + self.fileName + self.extraField + self.fileComment
self._size = len(self._contents)
return self._contents
class Mac2_ExtraField: # 4.6.4
tag = b"\x05\x26"
tsize = None # 2
signature = b"ZPIT" # extra-field signature (wrong endianness?)
fnlen = 0 # 1
filename = b""
filetype = b"ZIP " # unknown filetype code
creator = b"SITx" # Stuffit Expander
def set(self):
self.fnlen = len(self.filename)
self.tsize = 4 + 1 + self.fnlen + 4 + 4
def make(self):
self._contents = struct.pack(
"<2sH4sB",
self.tag,
self.tsize,
self.signature,
self.fnlen,
) + self.filename + struct.pack(
"<4s4s",
self.filetype,
self.creator,
)
self._size = len(self._contents)
return self._contents
m2_ef = Mac2_ExtraField()
m2_ef.filename = b"Mac Name"
m2_ef.set()
assert (b2h(m2_ef.make()) ==
"05 26 15 00 5A 50 49 54 08 4D 61 63 20 4E 61 6D " "65 5A 49 50 20 53 49 54 78")
class ExtendedTimestamp_ExtraField:
tag = b"UT" # 0x5455
tsize = None # 2
flags = 0 # 1
mod_time = None # 4
access_time = None # 4
creation_time = None # 4
def set(self):
self.tsize = 1
self.flags = 0
if self.mod_time is not None:
self.flags |= 1
self.tsize += 4
if self.access_time is not None:
self.flags |= 2
self.tsize += 4
if self.creation_time is not None:
self.flags |= 4
self.tsize += 4
def make(self):
self._contents = struct.pack(
"<2sHB",
self.tag,
self.tsize,
self.flags,
)
for time in [
self.mod_time,
self.access_time,
self.creation_time]:
if time is not None:
self._contents += struct.pack(
"<L",
time)
self._size = len(self._contents)
return self._contents
xt_ef = ExtendedTimestamp_ExtraField()
xt_ef.set()
assert (b2h(xt_ef.make()) == "55 54 01 00 00")
xt_ef.mod_time = 0x12345678
xt_ef.set()
assert (b2h(xt_ef.make()) == "55 54 05 00 01 78 56 34 12")
class UnicodePath_ExtraField: # 4.6.9
tag = b"up" # 0x7075
tsize = 0 # 2
version = 1 # 1
nameCRC32 = 0 # 4
unicodeName = b""
def set(self):
self.nameCRC32 = crc32(self.unicodeName)
self.tsize = 1 + 4 + len(self.unicodeName)
def make(self):
self._contents = struct.pack(
"<2sHBL",
self.tag,
self.tsize,
self.version,
self.nameCRC32,
) + self.unicodeName
self._size = len(self._contents)
return self._contents
up_ef = UnicodePath_ExtraField()
up_ef.unicodeName = b"Hello"
up_ef.set()
assert (b2h(up_ef.make()) == "75 70 0A 00 01 82 89 D1 F7 48 65 6C 6C 6F")
class Lfh:
headerSignature = b"PK\3\4"
version = 0 # version needed to extract
flags = 0 # general purpose bit flag
compressionMethod = 0
lastModifyTime = 0
lastModifyDate = 0
crc32 = 0
compressedSize = 0
uncompressedSize = 0
fileNameLength = 0
fileName = b""
extraFieldLength = 0
extraField = b""
fileCommentLength = 0
fileComment = b""
fileData = b""
def set(self):
self.fileNameLength = len(self.fileName)
self.extraFieldLength = len(self.extraField)
self.compressedSize = len(self.fileData)
self.uncompressedSize = self.compressedSize
self.crc32 = crc32(self.fileData)
def make(self):
self._contents = struct.pack(
"<4sHHHHHLLLHH",
self.headerSignature,
self.version,
self.flags,
self.compressionMethod,
self.lastModifyTime,
self.lastModifyDate,
self.crc32,
self.compressedSize,
self.uncompressedSize,
self.fileNameLength,
self.extraFieldLength,
) + self.fileName + self.extraField + self.fileData
self._size = len(self._contents)
return self._contents
class Zip64eocdr:
headerSignature = b"PK\6\6"
versionMade = 0
versionExtract = 0
diskNumber = 0
diskStart = 0
totaldisknumber = 1
totalnumber = 1
CDsize = 0
offset = 0
dataSector = b""
Eocd64size = len(dataSector) + 0x38 - 12 # cf AppNote 4.3.14.1
def prepare(self):
self.Eocd64size = len(self.dataSector) + 0x38 - \
12 # cf AppNote 4.3.14.1
def make(self):
self._contents = struct.pack(
"<4sQHHLLQQQQ",
self.headerSignature,
self.Eocd64size,
self.versionMade,
self.versionExtract,
self.diskNumber,
self.diskStart,
self.totaldisknumber,
self.totalnumber,
self.CDsize,
self.offset,
) + self.dataSector
self._size = len(self._contents)
return self._contents
class Zip64eocdl:
headerSignature = b"PK\6\7"
startNumber = 0
offset = 0
diskNumber = 1
def prepare(self):
pass
def make(self):
self._contents = struct.pack(
"<4sLQL",
self.headerSignature,
self.startNumber,
self.offset,
self.diskNumber,
)
self._size = len(self._contents)
return self._contents
class DataDescriptor:
headerSignature = b"PK\7\x08"
crc32 = 0
compressedSize = 0
uncompressedSize = 0
def prepare(self):
pass
def make(self):
return struct.pack(
"<4sLLL",
self.headerSignature,
self.crc32,
self.compressedSize,
self.uncompressedSize,
)
bDataDescriptor = 0x8
isEicar = True
archived_filename = b"eicar"
contents = HELLOWORLD
if len(sys.argv) > 1:
isEicar = False
filename = sys.argv[1].encode()
with open(filename, "rb") as f:
contents = f.read()
def makeSimpleZip(archived_filename, contents):
output = bytes()
lfh = Lfh()
lfh.fileName = archived_filename
lfh.fileData = contents
lfh.set()
output += lfh.make()
cdh_offset = len(output)
cdfh = Cdfh()
cdfh.fileName = lfh.fileName
cdfh.set()
cdfh.crc32 = lfh.crc32
cdfh.compressedSize = lfh.compressedSize
cdfh.uncompressedSize = lfh.uncompressedSize
output += cdfh.make()
eocd = EoCDR()
eocd.CDSize = cdfh._size
eocd.CDOffset = cdh_offset
output += eocd.make()
return output
def makeMultiName(archived_filename, contents):
output = bytes()
up_ef = UnicodePath_ExtraField()
up_ef.unicodeName = b"Unicode Name"
up_ef.set()
lfh = Lfh()
lfh.fileName = b"LFH Name"
lfh.flags = 0x800
lfh.fileData = contents
lfh.extraField = up_ef.make() + m2_ef.make()
lfh.set()
output += lfh.make()
cdh_offset = len(output)
cdfh = Cdfh()
cdfh.fileName = b"CDFH Name"
cdfh.set()
cdfh.crc32 = lfh.crc32
cdfh.compressedSize = lfh.compressedSize
cdfh.uncompressedSize = lfh.uncompressedSize
output += cdfh.make()
eocd = EoCDR()
eocd.CDSize = cdfh._size
eocd.CDOffset = cdh_offset
output += eocd.make()
return output
fileCases = {
"hello": [b"helloworld.txt", HELLOWORLD],
"eicar": [b"eicar", EICARCONTENTS],
}
for count, test in enumerate([
["hello", makeSimpleZip,
"366860aa4fb3140b779916e2c5e645a3884003df45f159b860ae550e12f1e217"],
["eicar", makeSimpleZip,
"d12db806fd47d73c44ae4eb23199097681a3987f2f8bb416544a705d3125ceb1"],
["hello", makeMultiName,
"f9e8941f8d076349c84dca32303f672e955734d4ab31ba2cb3cbf2d9b808bbf8"],
["eicar", makeMultiName,
"44eeeb29fba6646eaedb1b44fb3a2aecb44cc25bb60cec586fac737254c90c53"],
]):
filecase, test_function, expected_hash = test
filename, contents = fileCases[filecase]
output = test_function(filename, contents)
hash = hashlib.sha256(output).hexdigest()
outputname = "test-%s-%i-%s.zip" % (
test_function.__name__,
count, hash[:16]
)
print()
print("Test: %s" % outputname)
if hash != expected_hash:
print("Hash error: %s" % expected_hash)
print("%s" % hash)
xxd(output)
with open(outputname, "wb") as f:
f.write(output)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment