#!/usr/bin/env python3 import struct import uuid import hashlib import zlib import sys import pprint import datetime # --- Minimal Constants --- MFT_RECORD_MAGIC_NUMBER_OFFSET = 0 MFT_RECORD_MAGIC_NUMBER_SIZE = 4 MFT_RECORD_UPDATE_SEQUENCE_OFFSET = 4 MFT_RECORD_UPDATE_SEQUENCE_SIZE = 2 MFT_RECORD_UPDATE_SEQUENCE_SIZE_OFFSET = 6 MFT_RECORD_LOGFILE_SEQUENCE_NUMBER_OFFSET = 8 MFT_RECORD_LOGFILE_SEQUENCE_NUMBER_SIZE = 8 MFT_RECORD_SEQUENCE_NUMBER_OFFSET = 16 MFT_RECORD_SEQUENCE_NUMBER_SIZE = 2 MFT_RECORD_HARD_LINK_COUNT_OFFSET = 18 MFT_RECORD_HARD_LINK_COUNT_SIZE = 2 MFT_RECORD_FIRST_ATTRIBUTE_OFFSET = 20 MFT_RECORD_FIRST_ATTRIBUTE_SIZE = 2 MFT_RECORD_FLAGS_OFFSET = 22 MFT_RECORD_FLAGS_SIZE = 2 MFT_RECORD_USED_SIZE_OFFSET = 24 MFT_RECORD_USED_SIZE_SIZE = 4 MFT_RECORD_ALLOCATED_SIZE_OFFSET = 28 MFT_RECORD_ALLOCATED_SIZE_SIZE = 4 MFT_RECORD_FILE_REFERENCE_OFFSET = 32 MFT_RECORD_FILE_REFERENCE_SIZE = 8 MFT_RECORD_NEXT_ATTRIBUTE_ID_OFFSET = 40 MFT_RECORD_NEXT_ATTRIBUTE_ID_SIZE = 2 MFT_RECORD_RECORD_NUMBER_OFFSET = 42 MFT_RECORD_RECORD_NUMBER_SIZE = 4 # Attribute type constants (only those used in this parser) STANDARD_INFORMATION_ATTRIBUTE = 0x10 ATTRIBUTE_LIST_ATTRIBUTE = 0x20 FILE_NAME_ATTRIBUTE = 0x30 OBJECT_ID_ATTRIBUTE = 0x40 SECURITY_DESCRIPTOR_ATTRIBUTE = 0x50 VOLUME_NAME_ATTRIBUTE = 0x60 VOLUME_INFORMATION_ATTRIBUTE = 0x70 DATA_ATTRIBUTE = 0x80 INDEX_ROOT_ATTRIBUTE = 0x90 INDEX_ALLOCATION_ATTRIBUTE = 0xA0 BITMAP_ATTRIBUTE = 0xB0 REPARSE_POINT_ATTRIBUTE = 0xC0 EA_INFORMATION_ATTRIBUTE = 0xD0 EA_ATTRIBUTE = 0xE0 LOGGED_UTILITY_STREAM_ATTRIBUTE= 0x100 # --- Minimal WindowsTime Implementation --- class WindowsTime: def __init__(self, low: int, high: int): # Combine low and high into a 64-bit value (assuming little-endian) self.value = (high << 32) | low self.dt = self._filetime_to_dt(self.value) self.dtstr = self.dt.isoformat() def _filetime_to_dt(self, filetime: int) -> datetime.datetime: # FILETIME counts 100-nanosecond intervals since January 1, 1601 (UTC) epoch_start = datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc) # Convert 100-nanosecond intervals to microseconds microseconds = filetime // 10 return epoch_start + datetime.timedelta(microseconds=microseconds) # --- MftRecord Class --- class MftRecord: def __init__(self, raw_record: bytes, compute_hashes: bool = False, debug_level: int = 0, logger=None): self.raw_record = raw_record self.debug_level = debug_level self.logger = logger or self._default_logger self.magic = 0 self.upd_off = 0 self.upd_cnt = 0 self.lsn = 0 self.seq = 0 self.link = 0 self.attr_off = 0 self.flags = 0 self.size = 0 self.alloc_sizef = 0 self.base_ref = 0 self.next_attrid = 0 self.recordnum = 0 self.filename = '' self.si_times = { 'crtime': WindowsTime(0, 0), 'mtime': WindowsTime(0, 0), 'atime': WindowsTime(0, 0), 'ctime': WindowsTime(0, 0) } self.fn_times = { 'crtime': WindowsTime(0, 0), 'mtime': WindowsTime(0, 0), 'atime': WindowsTime(0, 0), 'ctime': WindowsTime(0, 0) } self.filesize = 0 self.attribute_types = set() self.attribute_list = [] self.object_id = '' self.birth_volume_id = '' self.birth_object_id = '' self.birth_domain_id = '' self.parent_ref = 0 self.md5 = None self.sha256 = None self.sha512 = None self.crc32 = None if compute_hashes: self.compute_hashes() self.parse_record() self.security_descriptor = None self.volume_name = None self.volume_info = None self.data_attribute = None self.index_root = None self.index_allocation = None self.bitmap = None self.reparse_point = None self.ea_information = None self.ea = None self.logged_utility_stream = None def _default_logger(self, message: str, level: int = 0): if level <= self.debug_level: print(message) def log(self, message: str, level: int = 0): self.logger(message, level) def parse_record(self) -> None: try: self.magic = struct.unpack(" None: si_data = self.raw_record[offset+24:offset+72] if len(si_data) >= 32: try: self.si_times = { 'crtime': WindowsTime(struct.unpack(" None: fn_data = self.raw_record[offset+24:] if len(fn_data) >= 64: try: self.fn_times = { 'crtime': WindowsTime(struct.unpack("= 66 + name_len * 2: self.filename = fn_data[66:66+name_len*2].decode('utf-16-le', errors='replace') self.parent_ref = struct.unpack(" None: obj_id_data = self.raw_record[offset+24:offset+88] if len(obj_id_data) >= 64: try: self.object_id = str(uuid.UUID(bytes_le=obj_id_data[:16])) self.birth_volume_id = str(uuid.UUID(bytes_le=obj_id_data[16:32])) self.birth_object_id = str(uuid.UUID(bytes_le=obj_id_data[32:48])) self.birth_domain_id = str(uuid.UUID(bytes_le=obj_id_data[48:64])) except (struct.error, ValueError): print(f"Error parsing Object ID attribute for record {self.recordnum}") def parse_attribute_list(self, offset: int) -> None: attr_content_offset = offset + struct.unpack(" 0: name = self.raw_record[attr_content_offset+name_offset:attr_content_offset+name_offset+name_len*2].decode('utf-16-le', errors='replace') else: name = "" vcn = struct.unpack(" None: sd_data = self.raw_record[offset+24:] if len(sd_data) >= 20: try: revision = struct.unpack("B", sd_data[0:1])[0] control = struct.unpack(" None: vn_data = self.raw_record[offset+24:] try: name_length = struct.unpack(" None: vi_data = self.raw_record[offset+24:offset+48] if len(vi_data) >= 12: try: self.volume_info = { 'major_version': struct.unpack("B", vi_data[8:9])[0], 'minor_version': struct.unpack("B", vi_data[9:10])[0], 'flags': struct.unpack(" 0: name = self.raw_record[offset+name_offset:offset+name_offset+name_length*2].decode('utf-16-le', errors='replace') else: name = "" if non_resident_flag == 0: # Resident content_size = struct.unpack(" None: ir_data = self.raw_record[offset+24:] try: attr_type = struct.unpack(" None: ia_data = self.raw_record[offset+24:] try: data_runs_offset = struct.unpack(" None: bitmap_data = self.raw_record[offset+24:] try: bitmap_size = struct.unpack(" None: rp_data = self.raw_record[offset+24:] try: reparse_tag = struct.unpack(" None: eai_data = self.raw_record[offset+24:] try: ea_size = struct.unpack(" None: ea_data = self.raw_record[offset+24:] try: next_entry_offset = struct.unpack(" None: lus_data = self.raw_record[offset+24:] try: stream_size = struct.unpack(" None: md5 = hashlib.md5() sha256 = hashlib.sha256() sha512 = hashlib.sha512() md5.update(self.raw_record) sha256.update(self.raw_record) sha512.update(self.raw_record) self.md5 = md5.hexdigest() self.sha256 = sha256.hexdigest() self.sha512 = sha512.hexdigest() self.crc32 = format(zlib.crc32(self.raw_record) & 0xFFFFFFFF, '08x') def get_file_type(self) -> str: # For illustration, this function checks a few flag bits. # You may need to refine this logic. FILE_RECORD_IS_DIRECTORY = 0x02 if self.flags & FILE_RECORD_IS_DIRECTORY: return "Directory" else: return "File" # --- Main Program --- def main(): if len(sys.argv) != 2: print("Usage: python show_mft.py ") sys.exit(1) record_path = sys.argv[1] try: with open(record_path, "rb") as f: raw_record = f.read() except Exception as e: print(f"Failed to read file: {e}") sys.exit(1) mft = MftRecord(raw_record, compute_hashes=True, debug_level=1) # Use pprint to print out all details nicely. details = { "record_number": mft.recordnum, "update_sequence_offset": mft.upd_off, "update_sequence_count": mft.upd_cnt, "logfile_sequence_number": mft.lsn, "sequence_number": mft.seq, "hard_link_count": mft.link, "attribute_offset": mft.attr_off, "flags": mft.flags, "used_size": mft.size, "allocated_size": mft.alloc_sizef, "base_file_reference": mft.base_ref, "next_attribute_id": mft.next_attrid, "file_type": mft.get_file_type(), "si_times": { k: v.dtstr for k, v in mft.si_times.items() }, "fn_times": { k: v.dtstr for k, v in mft.fn_times.items() }, "filename": mft.filename, "parent_reference": mft.parent_ref, "object_id": mft.object_id, "security_descriptor": mft.security_descriptor, "volume_name": mft.volume_name, "volume_info": mft.volume_info, "data_attribute": mft.data_attribute, "index_root": mft.index_root, "index_allocation": mft.index_allocation, "bitmap": mft.bitmap, "reparse_point": mft.reparse_point, "ea_information": mft.ea_information, "ea": mft.ea, "logged_utility_stream": mft.logged_utility_stream, "attribute_list": mft.attribute_list, "hashes": { "md5": mft.md5, "sha256": mft.sha256, "sha512": mft.sha512, "crc32": mft.crc32 } } pprint.pprint(details) if __name__ == "__main__": main()