exorcise/exorcise/image.py

282 lines
8.4 KiB
Python

import os
import math
import enum
import destruct
from destruct import Struct, sizeof
from .common import GhostHeaderType, GhostTime, GhostEncrypted, GhostCompressionType, GhostCompressedFile, GhostCompressed
from .properties import PropertySet, make_property_enum
from .partition import select_partition_parser, select_partition_index_parser
DRIVE_PROPERTY_TAGS = (
'SRemote',
'SDrive',
'SHeadsPerCyl',
'SectorsPerTrack',
'SCylinders',
'STotalSectors',
'SSectorsUsed',
'SEstimatedUsed',
'SPartitions',
'SVersion',
'SFlags',
'SOs2Name',
'SFingerprint',
'SMbr',
'SIsSpanned',
'SDiskFormat',
'SEndOfImagePosition',
'SPatchedFileCount',
'SOrder',
'SBootable',
'SSystemId',
'SExtended',
'SFlagInp',
'SGflags',
'SFirstSect',
'SNumSects',
'SFpos',
'SEndFpos',
'SFileOperationOffset',
'SSizeAdjustment',
'SSlot',
)
PID_PROPERTY_TAGS = (
'PID_FILE_ADDITION_DATA_STREAM_ENTRY_ARRAY',
'PID_FILE_ADDITION_DATA_STREAM_TYPE',
'PID_FILE_DELETION_ENTRY_ARRAY',
'PID_FILE_ADDITION_DATA_STREAM_LENGTH',
'PID_FILE_ADDITION_IS_HIDDEN',
'PID_FILE_ADDITION_DATA_STREAM_NAME',
'PID_FILE_DELETION_OBJECT_ID',
'PID_FILE_DELETION_ENTRY_PARENT_DIR_MFTNO',
'PID_FILE_ADDITION_PATH',
'PID_FILE_ADDITION_CREATION_DATE_TIME',
'PID_FILE_ADDITION_IS_WRITABLE',
'PID_FILE_ADDITION_IS_ARCHIVE',
'PID_FILE_ADDITION_IS_READABLE',
'PID_FILE_ADDITION_IS_DIRECTORY',
'PID_FILE_ADDITION_FILE_NAME',
'PID_FILE_ADDITION_LAST_ACCESS_DATE_TIME',
'PID_FILE_ADDITION_IS_DELETED',
'PID_FILE_ADDITION_CHILD_COUNT',
'PID_FILE_ADDITION_IS_EXECUTABLE',
'PID_FILE_DELETION_ENTRY_FILENAME',
'PID_FILE_ADDITION_DATA_STREAM_OFFSET_IN_FILE',
'PID_FILE_DELETION_MAIN_MFTNO',
'PID_FILE_ADDITION_IS_SYSTEM',
'PID_FILE_DELETION_TOTAL_FILE_ENTRY_COUNT',
'PID_FILE_ADDITION_MODIFIED_DATE_TIME',
'PID_FILE_ADDITION_FILE_SIZE',
'PID_START_BYTE',
'PID_GPT_CONTAINER_PS',
'PID_BIOS_SUPERDISK_CONTAINER_PS',
'PID_PARTIES_CONTAINER_PS',
'PID_BIOS_CONTAINER_PS',
'PID_DYNAMICDISK_CONTAINER_PS',
'PID_BYTE_COUNT',
'PID_CLEAR_SIGNATURE',
'PID_LVM_CONTAINER_PS',
'PID_DISK_NOTIFY_OS',
'PID_PARTITION_NODE_FORMAT',
'PID_CONTAINER_ID',
'PID_ADDITIONAL_BOOT_CODE_SECTOR',
'PID_PARENT_BOOT_RECORD_SECTOR_OFFSET',
'PID_PARENT_BOOT_RECORD_SLOT_NUMBER',
'PID_WIN_9X_ID_PRESERVE',
'PID_WIN_9X_ID',
'PID_WIN_NT_ID',
'PID_ADDITIONAL_BOOT_CODE',
'PID_ADDITIONAL_BOOT_CODE_DATA',
'PID_BOOT_CODE',
'PID_WIN_NT_ID_PRESERVE',
'PID_GPT_SLOT_COUNT',
'PID_GPT_UUID',
'PID_VOLUME_NAME',
'PID_VOLUME_TYPE',
'PID_FORMAT_TYPE',
'PID_CLEANLY_SHUTDOWN',
'PID_NO_HARD_ERROR',
'PID_POSITION_ID',
'PID_VOLUME_DEVICE_NAME',
'PID_FIND_FIRST',
'PID_EXTENT_CONTAINER_EXTENT_INDEX',
'PID_VOLUME_SIZE_MINIMUM_PERCENT',
'PID_VOLUME_BYTES_PER_BLOCK',
'PID_FIND_BEST_FIT',
'PID_VOLUME_SIZE_PREFERRED_PERCENT',
'PID_VOLUME_DRIVE_LETTER',
'PID_VOLUME_SIZE_PREFERRED',
'PID_VOLUME_SLOT_NUMBER',
'PID_VOLUME_START',
'PID_VOLUME_ALIGNMENT',
'PID_EXTENT_START',
'PID_IS_VOLUME_CONTAINER',
'PID_FORMAT',
'PID_ACTIVE',
'PID_VOLUME_START_PREFERRED',
'PID_EXTENT_SIZE_MINIMUM',
'PID_EXTENT',
'PID_VOLUME_SIZE_ORIGINAL',
'PID_READ_ONLY',
'PID_FIND_LAST',
'PID_EXTENT_OWNER_ID',
'PID_NAME',
'PID_ROLE_FORMAT_MATCHING_TYPE',
'PID_EXTENT_SIZE_PREFERRED',
'PID_VOLUME_SIZE_MINIMUM',
'PID_FIND_WORST_FIT',
'PID_VOLUME_PARTITION_TYPE',
'PID_EXTENT_START_PREFERRED',
'PID_VOLUME_SIZE_MAXIMUM',
'PID_VOLUME_SIZE_MAXIMUM_PERCENT',
'PID_HIDDEN',
'PID_IS_VOLUME',
'PID_ALLOCATE_TYPE',
'PID_VOLUME_NOTIFY_OS',
'PID_EXTENT_SIZE_MAXIMUM',
'PID_INCOMPATIBLE_IMAGE_VERSION',
'PID_ROLE',
'PID_SYSTEM_ID',
'PID_START_CHS',
'PID_END_CHS',
'PID_SECTOR',
'PID_HEAD',
'PID_CYLINDER',
'PID_START_CHS_MAXED_OUT',
'PID_END_CHS_MAXED_OUT',
)
DrivePropertyTag = make_property_enum('DrivePropertyTag', DRIVE_PROPERTY_TAGS + PID_PROPERTY_TAGS)
class GhostPositionMetadata(Struct):
magic = Sig(b'\x23\x00\x00\x00\xD8\x18\x2F\x01')
unk1 = Data(10)
data_offset = UInt(64)
index_offset = UInt(64)
class GhostHeader(Struct):
magic = Sig(b'\xFE\xEF')
type = Enum(GhostHeaderType, UInt(8)) # 1-5
compression = Enum(GhostCompressionType, UInt(8)) # 0-10
time = GhostTime()
bool8 = Bool()
bool9 = Bool()
binaryresearch_flag1 = Bool()
binaryresearch_flag2 = Bool()
binaryresearch_checksum = Data(15)
unk27 = Data(10)
bool37 = Bool()
unk38 = Data(4)
bool42 = Bool()
bool43 = Bool()
bool44 = Bool()
bool45 = Bool()
bool46 = Bool()
data_encrypted = Bool()
bool48 = Bool()
bool49 = Bool()
bool50 = Bool()
data_at_end = Bool()
unk52 = UInt(8) # 3 or 10
bool53 = Bool()
seed_offset = UInt(8) # must be 0
wildcard_filename = Bool()
bool56 = Bool()
unk57 = Data(4)
unk61 = UInt(8) # 0 or 1 or 2
bool62 = Bool()
bool63 = Bool()
bool64 = Bool()
bool65 = Bool()
data_length = UInt(32)
unk70 = Data(5)
pad75 = Pad(437)
def on_data_length(self, spec, context):
if self.data_length == 0:
self.data_length = 2048
class GhostIndex(Struct, generics=['E']):
magic = Sig(b'\x05')
unk1 = UInt(8) # if 3 or 4, special
_pad2 = Pad(2)
total = UInt(32)
length = UInt(16)
offsets = GhostCompressed(destruct.Arr(UInt(64)), GhostCompressionType.Deflate)
entries = Arr(Lazy(E))
def on_length(self, spec, context):
spec.offsets.length = self.length
def on_offsets(self, spec, context):
spec.entries.count = len(self.offsets)
c = spec.entries.child
spec.entries.child = lambda i: destruct.Ref(c, self.offsets[i]) if self.offsets[i] else destruct.Nothing
spec.entries = destruct.WithFile(spec.entries, lambda f: GhostCompressedFile(f, GhostCompressionType.Deflate))
class GhostIndexSet(Struct, generics=['G']):
first = GhostIndex[G]
rest = Arr(GhostIndex[G])
def on_first(self, spec, context):
spec.rest.count = math.ceil(self.first.total / len(self.first.entries)) - 1
def parse(self, input, context):
value = super().parse(input, context)
for x in value.rest:
value.first.entries.extend(x.entries)
return value.first.entries
class GhostImage(Struct):
header = GhostHeader()
positions = Maybe(Ref(GhostPositionMetadata(), -destruct.sizeof(GhostPositionMetadata), os.SEEK_END))
properties = Ref(
Switch(options=dict(
encrypted=GhostEncrypted(PropertySet[DrivePropertyTag]),
plain=PropertySet[DrivePropertyTag]
)),
0, os.SEEK_CUR
)
index = Ref(GhostIndexSet[...], 0, os.SEEK_CUR)
partitions = Arr([], count=0)
def on_header(self, spec, context):
if self.header.data_encrypted:
spec.properties.child.selector = 'encrypted'
spec.properties.child.current.seed = self.header.time.seed[self.header.seed_offset]
spec.properties.child.current.length = self.header.data_length
else:
spec.properties.child.selector = 'plain'
def on_positions(self, spec, context):
spec.properties.reference = os.SEEK_END
spec.properties.point = -(self.positions.data_offset + destruct.sizeof(GhostPositionMetadata))
spec.index.reference = os.SEEK_END
spec.index.point = -(self.positions.index_offset + destruct.sizeof(GhostPositionMetadata))
def on_properties(self, spec, context):
spec.partitions.count = len(self.properties[DrivePropertyTag.SPartitions])
spec.index.child = self.get_index_parser
spec.partitions.child = self.get_partition_parser
def get_index_parser(self, i):
partition = self.properties[DrivePropertyTag.SPartitions][0] # i
p = select_partition_index_parser(partition)
return GhostIndexSet[p]()
def get_partition_parser(self, i):
partition = self.properties[DrivePropertyTag.SPartitions][0] # i
pos = partition[DrivePropertyTag.SFpos][0]
count = partition[DrivePropertyTag.SEndFpos][0] - pos
p = select_partition_parser(partition)
return destruct.Ref(destruct.Capped(p, count), pos)