#!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # # Copyright Fortra, LLC and its affiliated companies # # All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # # Description: # Mini shell for browsing an NTFS volume # # Author: # Alberto Solino (@agsolino) # # Reference for: # Structure. Quick and dirty implementation.. just for fun.. ;) # # NOTE: Lots of info (mainly the structs) taken from the NTFS-3G project.. # # ToDo: # [] Support compressed, encrypted files # from __future__ import division from __future__ import print_function import os import sys import logging import struct import argparse import cmd import ntpath from six import PY2, text_type from datetime import datetime from impacket.examples import logger from impacket import version from impacket.structure import Structure, hexdump # Reserved/fixed MFTs FIXED_MFTS = 16 # Attribute types UNUSED = 0 STANDARD_INFORMATION = 0x10 ATTRIBUTE_LIST = 0x20 FILE_NAME = 0x30 OBJECT_ID = 0x40 SECURITY_DESCRIPTOR = 0x50 VOLUME_NAME = 0x60 VOLUME_INFORMATION = 0x70 DATA = 0x80 INDEX_ROOT = 0x90 INDEX_ALLOCATION = 0xa0 BITMAP = 0xb0 REPARSE_POINT = 0xc0 EA_INFORMATION = 0xd0 EA = 0xe0 PROPERTY_SET = 0xf0 LOGGED_UTILITY_STREAM = 0x100 FIRST_USER_DEFINED_ATTRIBUTE = 0x1000 END = 0xffffffff # Attribute flags ATTR_IS_COMPRESSED = 0x0001 ATTR_COMPRESSION_MASK = 0x00ff ATTR_IS_ENCRYPTED = 0x4000 ATTR_IS_SPARSE = 0x8000 # FileName type flags FILE_NAME_POSIX = 0x00 FILE_NAME_WIN32 = 0x01 FILE_NAME_DOS = 0x02 FILE_NAME_WIN32_AND_DOS = 0x03 # MFT Record flags MFT_RECORD_IN_USE = 0x0001 MFT_RECORD_IS_DIRECTORY = 0x0002 MFT_RECORD_IS_4 = 0x0004 MFT_RECORD_IS_VIEW_INDEX = 0x0008 MFT_REC_SPACE_FILLER = 0xfffff # File Attribute Flags FILE_ATTR_READONLY = 0x0001 FILE_ATTR_HIDDEN = 0x0002 FILE_ATTR_SYSTEM = 0x0004 FILE_ATTR_DIRECTORY = 0x0010 FILE_ATTR_ARCHIVE = 0x0020 FILE_ATTR_DEVICE = 0x0040 FILE_ATTR_NORMAL = 0x0080 FILE_ATTR_TEMPORARY = 0x0100 FILE_ATTR_SPARSE_FILE = 0x0200 FILE_ATTR_REPARSE_POINT = 0x0400 FILE_ATTR_COMPRESSED = 0x0800 FILE_ATTR_OFFLINE = 0x1000 FILE_ATTR_NOT_CONTENT_INDEXED = 0x2000 FILE_ATTR_ENCRYPTED = 0x4000 FILE_ATTR_VALID_FLAGS = 0x7fb7 FILE_ATTR_VALID_SET_FLAGS = 0x31a7 FILE_ATTR_I30_INDEX_PRESENT = 0x10000000 FILE_ATTR_VIEW_INDEX_PRESENT = 0x20000000 # NTFS System files FILE_MFT = 0 FILE_MFTMirr = 1 FILE_LogFile = 2 FILE_Volume = 3 FILE_AttrDef = 4 FILE_Root = 5 FILE_Bitmap = 6 FILE_Boot = 7 FILE_BadClus = 8 FILE_Secure = 9 FILE_UpCase = 10 FILE_Extend = 11 # Index Header Flags SMALL_INDEX = 0 LARGE_INDEX = 1 LEAF_NODE = 0 INDEX_NODE = 1 NODE_MASK = 0 # Index Entry Flags INDEX_ENTRY_NODE = 1 INDEX_ENTRY_END = 2 INDEX_ENTRY_SPACE_FILLER = 0xffff class NTFS_BPB(Structure): structure = ( ('BytesPerSector',' 0 and self.AttributeHeader['Type'] != END: self.AttributeName = data[self.AttributeHeader['NameOffset']:][:self.AttributeHeader['NameLength']*2].decode('utf-16le') def getFlags(self): return self.AttributeHeader['Flags'] def getName(self): return self.AttributeName def isNonResident(self): return self.AttributeHeader['NonResident'] def dump(self): return self.AttributeHeader.dump() def getTotalSize(self): return self.AttributeHeader['Length'] def getType(self): return self.AttributeHeader['Type'] class AttributeResident(Attribute): def __init__(self, iNode, data): logging.debug("Inside AttributeResident: iNode: %s" % iNode.INodeNumber) Attribute.__init__(self,iNode,data) self.ResidentHeader = NTFS_ATTRIBUTE_RECORD_RESIDENT(data[len(self.AttributeHeader):]) self.AttrValue = data[self.ResidentHeader['ValueOffset']:][:self.ResidentHeader['ValueLen']] def dump(self): return self.ResidentHeader.dump() def getFlags(self): return self.ResidentHeader['Flags'] def getValue(self): return self.AttrValue def read(self,offset,length): logging.debug("Inside Read: offset: %d, length: %d" %(offset,length)) return self.AttrValue[offset:][:length] def getDataSize(self): return len(self.AttrValue) class AttributeNonResident(Attribute): def __init__(self, iNode, data): logging.debug("Inside AttributeNonResident: iNode: %s" % iNode.INodeNumber) Attribute.__init__(self,iNode,data) self._raw_attr_data = data self.NonResidentHeader = NTFS_ATTRIBUTE_RECORD_NON_RESIDENT(data[len(self.AttributeHeader):]) self.AttrValue = data[self.NonResidentHeader['DataRunsOffset']:][:self.NonResidentHeader['AllocatedSize']] self.DataRuns = [] self.ClusterSize = 0 # Effective sizes for OS-like reads; default to on-disk header values. self.data_size = self.NonResidentHeader['DataSize'] self.initialized_size = self.NonResidentHeader['InitializedSize'] self.parseDataRuns() def dump(self): return self.NonResidentHeader.dump() def getDataSize(self): return self.data_size def getValue(self): return None def parseDataRuns(self): """Parse data run descriptors from the attribute value.""" data = self.AttrValue if data is None: return vcn = 0 prevLcn = 0 # LCN is delta-encoded, track previous while data and data[0:1] != b'\x00': if len(data) < 1: break header = data[0] data = data[1:] lengthBytes = header & 0x0F offsetBytes = header >> 4 # Parse cluster count (length) if len(data) < lengthBytes: break clusterCount = struct.unpack(' dr['LastVCN']: continue # Stop if we've passed all relevant data runs if vcn < dr['StartVCN']: break # Calculate how many clusters to read from this data run clustersInRun = dr['LastVCN'] - vcn + 1 clustersToRead = min(clustersLeft, clustersInRun) # For sparse runs, LCN is -1; readClusters handles this if dr['LCN'] == -1: lcn = -1 else: lcn = dr['LCN'] + (vcn - dr['StartVCN']) tmpBuf = self.readClusters(clustersToRead, lcn) if tmpBuf is None: break buf += tmpBuf clustersLeft -= clustersToRead vcn += clustersToRead return buf def read(self, offset, length): """Read bytes from non-resident attribute, respecting data_size and initialized_size.""" logging.debug("Inside Read: offset: %d, length: %d" % (offset, length)) # Clamp read to data_size (EOF) if offset >= self.data_size: return b'' length = min(length, self.data_size - offset) self.ClusterSize = self.NTFSVolume.BPB['BytesPerSector'] * self.NTFSVolume.BPB['SectorsPerCluster'] buf = b'' curOffset = offset bytesLeft = length while bytesLeft > 0: vcn = curOffset // self.ClusterSize vcnOffset = curOffset % self.ClusterSize # Calculate clusters needed for remaining bytes bytesInFirstCluster = self.ClusterSize - vcnOffset if bytesLeft <= bytesInFirstCluster: clustersToRead = 1 else: clustersToRead = 1 + ((bytesLeft - bytesInFirstCluster + self.ClusterSize - 1) // self.ClusterSize) clusterData = self.readVCN(vcn, clustersToRead) if not clusterData: break # Extract the portion we need chunk = clusterData[vcnOffset:vcnOffset + bytesLeft] buf += chunk curOffset += len(chunk) bytesLeft -= len(chunk) if len(chunk) == 0: break if not buf: return None # Zero-fill beyond InitializedSize (OS behavior for uninitialized data) if self.initialized_size < offset + len(buf): validBytes = max(0, self.initialized_size - offset) buf = buf[:validBytes] + (b'\x00' * (len(buf) - validBytes)) return buf class NonResidentDataAttribute(AttributeNonResident): @classmethod def _shift_runs(cls, attr, start_vcn): if start_vcn <= 0: return for dr in attr.DataRuns: dr['StartVCN'] += start_vcn dr['LastVCN'] += start_vcn @classmethod def _base_sizes(cls, attr): return attr.NonResidentHeader['DataSize'], attr.NonResidentHeader['InitializedSize'] @classmethod def _ensure_base(cls, collected, base_attr, base_data_size, base_initialized_size): if base_attr is not None: return base_attr, base_data_size, base_initialized_size _, base_attr = collected[0] base_data_size, base_initialized_size = cls._base_sizes(base_attr) return base_attr, base_data_size, base_initialized_size @classmethod def _collect_extents(cls, iNode, matches, attribute_name): """Collect $DATA attributes from extent records, identifying the base extent.""" collected = [] base_attr = None base_data_size = None base_initialized_size = None for entry in matches: extension_inode = iNode.NTFSVolume.getINode(entry.MftRecordNumber) attr = extension_inode.searchAttribute(DATA, attribute_name) if attr is None: continue collected.append((entry, attr)) # Base extent has StartingVCN == 0 and contains authoritative sizes if entry.StartingVCN == 0: base_attr = attr if isinstance(attr, AttributeNonResident): base_data_size, base_initialized_size = cls._base_sizes(attr) return collected, base_attr, base_data_size, base_initialized_size def __init__(self, iNode, entries, attribute_name=None): """ Build a unified $DATA stream from multiple attribute list entries. Args: iNode: Owning inode for volume access entries: AttributeListEntry items for the target $DATA stream attribute_name: Stream name (None for default $DATA) """ matches = list(entries) if not matches: raise ValueError('No $DATA extents found') # Sort extents by StartingVCN to define logical order matches.sort(key=lambda e: e.StartingVCN) # Collect attributes and find base extent (StartingVCN == 0) collected, base_attr, base_data_size, base_initialized_size = self._collect_extents( iNode, matches, attribute_name, ) if not collected: raise ValueError('No usable $DATA extents found') # Ensure we have valid base sizes base_attr, base_data_size, base_initialized_size = self._ensure_base( collected, base_attr, base_data_size, base_initialized_size, ) # Initialize from base extent super(NonResidentDataAttribute, self).__init__(iNode, base_attr._raw_attr_data) if len(collected) == 1: # Single extent - apply VCN offset if needed entry, _ = collected[0] self._shift_runs(self, entry.StartingVCN) else: # Multi-extent - merge all runs with proper VCN offsets self._merge_extents_from_collected(collected, base_data_size, base_initialized_size) def _merge_extents_from_collected(self, collected, data_size, init_size): """Merge data runs from multiple extents into unified stream.""" merged_runs = [] for entry, attr in collected: if not isinstance(attr, AttributeNonResident): continue for dr in attr.DataRuns: new_dr = NTFS_DATA_RUN() new_dr['LCN'] = dr['LCN'] new_dr['Clusters'] = dr['Clusters'] # Apply VCN offset from attribute list entry new_dr['StartVCN'] = dr['StartVCN'] + entry.StartingVCN new_dr['LastVCN'] = dr['LastVCN'] + entry.StartingVCN merged_runs.append(new_dr) merged_runs.sort(key=lambda dr: dr['StartVCN']) self.DataRuns = merged_runs self.data_size = data_size self.initialized_size = init_size class AttributeStandardInfo: def __init__(self, attribute): logging.debug("Inside AttributeStandardInfo") self.Attribute = attribute self.StandardInfo = NTFS_STANDARD_INFORMATION(self.Attribute.AttrValue) def getFileAttributes(self): return self.StandardInfo['FileAttributes'] def getFileTime(self): if self.StandardInfo['LastDataChangeTime'] > 0: return datetime.fromtimestamp(getUnixTime(self.StandardInfo['LastDataChangeTime'])) else: return 0 def dump(self): return self.StandardInfo.dump() class AttributeFileName: def __init__(self, attribute): logging.debug("Inside AttributeFileName") self.Attribute = attribute self.FileNameRecord = NTFS_FILE_NAME_ATTR(self.Attribute.AttrValue) def getFileNameType(self): return self.FileNameRecord['FileNameType'] def getFileAttributes(self): return self.FileNameRecord['FileAttributes'] def getFileName(self): return self.FileNameRecord['FileName'].decode('utf-16le') def getFileSize(self): return self.FileNameRecord['DataSize'] def getFlags(self): return self.FileNameRecord['FileAttributes'] def dump(self): return self.FileNameRecord.dump() class AttributeIndexAllocation: def __init__(self, attribute): logging.debug("Inside AttributeIndexAllocation") self.Attribute = attribute def dump(self): print(self.Attribute.dump()) for i in self.Attribute.DataRuns: print(i.dump()) def read(self, offset, length): return self.Attribute.read(offset, length) class AttributeIndexRoot: def __init__(self, attribute): logging.debug("Inside AttributeIndexRoot") self.Attribute = attribute self.IndexRootRecord = NTFS_INDEX_ROOT(attribute.AttrValue) self.IndexEntries = [] self.parseIndexEntries() def parseIndexEntries(self): data = self.Attribute.AttrValue[len(self.IndexRootRecord):] while True: ie = IndexEntry(data) self.IndexEntries.append(ie) if ie.isLastNode(): break data = data[ie.getSize():] def dump(self): self.IndexRootRecord.dump() for i in self.IndexEntries: i.dump() def getType(self): return self.IndexRootRecord['Type'] class IndexEntry: def __init__(self, entry): self.entry = NTFS_INDEX_ENTRY(entry) def isSubNode(self): return self.entry['EntryHeader']['Flags'] & INDEX_ENTRY_NODE def isLastNode(self): return self.entry['EntryHeader']['Flags'] & INDEX_ENTRY_END def getVCN(self): return struct.unpack('> 48) & 0xFFFF self.AttributeName = None name_len = self.EntryHeader['AttributeNameLength'] if name_len > 0: name_offset = self.EntryHeader['AttributeNameOffset'] name_bytes = entry_data[name_offset : name_offset + (name_len * 2)] self.AttributeName = name_bytes.decode('utf-16le') class AttributeList: """Parses ATTRIBUTE_LIST attribute (can be resident or non-resident).""" def __init__(self, attribute): self.attribute = attribute self.Entries = [] self._parseEntries() def _parseEntries(self): """Parse attribute list entries from raw data.""" # getValue() works for resident, read() for non-resident if hasattr(self.attribute, 'getValue') and self.attribute.getValue() is not None: data = self.attribute.getValue() else: data = self.attribute.read(0, self.attribute.getDataSize()) if not data: return offset = 0 while offset < len(data): entry_data = data[offset:] if len(entry_data) < 26: # Minimum entry size break list_entry = AttributeListEntry(entry_data) if list_entry.EntryLength == 0: break self.Entries.append(list_entry) offset += list_entry.EntryLength def getEntries(self): return self.Entries class INODE: def __init__(self, NTFSVolume): self.NTFSVolume = NTFSVolume # This is the entire file record self.INodeNumber = None self.Attributes = {} self.AttributesRaw = None self.AttributesLastPos = None # Some interesting Attributes to parse self.FileAttributes = 0 self.LastDataChangeTime = None self.FileName = None self.FileSize = 0 # Debug counters for directory listings self._walk_root_count = 0 self._walk_subnode_count = 0 def isDirectory(self): return self.FileAttributes & FILE_ATTR_I30_INDEX_PRESENT def isCompressed(self): return self.FileAttributes & FILE_ATTR_COMPRESSED def isEncrypted(self): return self.FileAttributes & FILE_ATTR_ENCRYPTED def isSparse(self): return self.FileAttributes & FILE_ATTR_SPARSE_FILE def displayName(self): if self.LastDataChangeTime is not None and self.FileName is not None: try: # print "%d - %s %s %s " %( self.INodeNumber, self.getPrintableAttributes(), self.LastDataChangeTime.isoformat(' '), self.FileName) print("%s %s %15d %s " %( self.getPrintableAttributes(), self.LastDataChangeTime.isoformat(' '), self.FileSize, self.FileName)) except Exception as e: logging.error('Exception when trying to display inode %d: %s' % (self.INodeNumber,str(e))) def getPrintableAttributes(self): mask = '' if self.FileAttributes & FILE_ATTR_I30_INDEX_PRESENT: mask += 'd' else: mask += '-' if self.FileAttributes & FILE_ATTR_HIDDEN: mask += 'h' else: mask += '-' if self.FileAttributes & FILE_ATTR_SYSTEM: mask += 'S' else: mask += '-' if self.isCompressed(): mask += 'C' else: mask += '-' if self.isEncrypted(): mask += 'E' else: mask += '-' if self.isSparse(): mask += 's' else: mask += '-' return mask def parseAttributes(self): # Parse Standard Info attr = self.searchAttribute(STANDARD_INFORMATION, None) if attr is not None: si = AttributeStandardInfo(attr) self.Attributes[STANDARD_INFORMATION] = si self.FileAttributes |= si.getFileAttributes() self.LastDataChangeTime = si.getFileTime() self.Attributes[STANDARD_INFORMATION] = si # Parse Filename attr = self.searchAttribute(FILE_NAME, None) while attr is not None: fn = AttributeFileName(attr) if fn.getFileNameType() != FILE_NAME_DOS: self.FileName = fn.getFileName() self.FileSize = fn.getFileSize() self.FileAttributes |= fn.getFileAttributes() self.Attributes[FILE_NAME] = fn break attr = self.searchAttribute(FILE_NAME, None, True) # Parse Attribute list before Index Allocation, because it might be there attr = self.searchAttribute(ATTRIBUTE_LIST, None) if attr is not None: al = AttributeList(attr) self.Attributes[ATTRIBUTE_LIST] = al # Parse Index Allocation attr = self.searchAttribute(INDEX_ALLOCATION, u'$I30') if attr is not None: ia = AttributeIndexAllocation(attr) self.Attributes[INDEX_ALLOCATION] = ia attr = self.searchAttribute(INDEX_ROOT, u'$I30') if attr is not None: ir = AttributeIndexRoot(attr) self.Attributes[INDEX_ROOT] = ir def searchAttribute(self, attributeType, attributeName, findNext = False): logging.debug("Inside searchAttribute: type: 0x%x, name: %s" % (attributeType, attributeName)) record = None if findNext is True: data = self.AttributesLastPos else: data = self.AttributesRaw while True: if len(data) <= 8: record = None break record = Attribute(self,data) if record.getType() == END: record = None break if record.getTotalSize() == 0: record = None break if record.getType() == attributeType and record.getName() == attributeName: if record.isNonResident() == 1: record = AttributeNonResident(self, data) else: record = AttributeResident(self, data) self.AttributesLastPos = data[record.getTotalSize():] break data = data[record.getTotalSize():] # Look for attribute on Attribute List if record is None and ATTRIBUTE_LIST in self.Attributes: attr_list = self.Attributes[ATTRIBUTE_LIST] if attributeType == DATA: entries = [ entry for entry in attr_list.getEntries() if entry.AttributeType == DATA and entry.AttributeName == attributeName ] try: return NonResidentDataAttribute(self, entries, attributeName) except ValueError: return None for entry in attr_list.getEntries(): if entry.AttributeType == attributeType and entry.AttributeName == attributeName: extension_inode = self.NTFSVolume.getINode(entry.MftRecordNumber) return extension_inode.searchAttribute(attributeType, attributeName) return record def PerformFixUp(self, record, buf, numSectors): # It fixes the sequence WORDS on every sector of a cluster # FixUps are used by: # FILE Records in the $MFT # INDX Records in directories and other indexes # RCRD Records in the $LogFile # RSTR Records in the $LogFile logging.debug("Inside PerformFixUp..." ) magicNum = struct.unpack(' 0 and entry.getINodeNumber() > 16: fn = NTFS_FILE_NAME_ATTR(entry.getKey()) if fn['FileNameType'] != FILE_NAME_DOS: #inode = INODE(self.NTFSVolume) #inode.FileAttributes = fn['FileAttributes'] #inode.FileSize = fn['DataSize'] #inode.LastDataChangeTime = datetime.fromtimestamp(getUnixTime(fn['LastDataChangeTime'])) #inode.INodeNumber = entry.getINodeNumber() #inode.FileName = fn['FileName'].decode('utf-16le') #inode.displayName() files.append(fn) # if inode.FileAttributes & FILE_ATTR_I30_INDEX_PRESENT and entry.getINodeNumber() > 16: # inode2 = self.NTFSVolume.getINode(entry.getINodeNumber()) # inode2.walk() return files def walk(self): logging.debug("Inside Walk... ") files = [] self._walk_root_count = 0 self._walk_subnode_count = 0 if INDEX_ROOT in self.Attributes: ir = self.Attributes[INDEX_ROOT] if ir.getType() & FILE_NAME: for ie in ir.IndexEntries: if ie.isSubNode(): logging.debug("walk: INDEX_ROOT entry points to subnode VCN %d", ie.getVCN()) sub_files = self.walkSubNodes(ie.getVCN()) self._walk_subnode_count += len(sub_files) files += sub_files else: if len(ie.getKey()) > 0 and ie.getINodeNumber() > 16: fn = NTFS_FILE_NAME_ATTR(ie.getKey()) if fn['FileNameType'] != FILE_NAME_DOS: logging.debug("walk: INDEX_ROOT inline entry %s", fn['FileName'].decode('utf-16le')) self._walk_root_count += 1 files.append(fn) return files else: return None def findFirstSubNode(self, vcn, toSearch): def getFileName(entry): if len(entry.getKey()) > 0 and entry.getINodeNumber() > 16: fn = NTFS_FILE_NAME_ATTR(entry.getKey()) if fn['FileNameType'] != FILE_NAME_DOS: return fn['FileName'].decode('utf-16le').upper() return None entries = self.parseIndexBlocks(vcn) for ie in entries: name = getFileName(ie) if name is not None: if name == toSearch: # Found! return ie if toSearch < name: if ie.isSubNode(): res = self.findFirstSubNode(ie.getVCN(), toSearch) if res is not None: return res else: # Bye bye.. not found return None else: if ie.isSubNode(): res = self.findFirstSubNode(ie.getVCN(), toSearch) if res is not None: return res def findFirst(self, fileName): # Searches for a file and returns an Index Entry. None if not found def getFileName(entry): if len(entry.getKey()) > 0 and entry.getINodeNumber() > 16: fn = NTFS_FILE_NAME_ATTR(entry.getKey()) if fn['FileNameType'] != FILE_NAME_DOS: return fn['FileName'].decode('utf-16le').upper() return None toSearch = text_type(fileName.upper()) if INDEX_ROOT in self.Attributes: ir = self.Attributes[INDEX_ROOT] if ir.getType() & FILE_NAME or 1==1: for ie in ir.IndexEntries: name = getFileName(ie) if name is not None: if name == toSearch: # Found! return ie if toSearch < name: if ie.isSubNode(): res = self.findFirstSubNode(ie.getVCN(), toSearch) if res is not None: return res else: # Bye bye.. not found return None else: if ie.isSubNode(): res = self.findFirstSubNode(ie.getVCN(), toSearch) if res is not None: return res def getStream(self, name): return self.searchAttribute( DATA, name, findNext = False) class NTFS: def __init__(self, volumeName): self.__volumeName = volumeName self.__bootSector = None self.__MFTStart = None self.volumeFD = None self.BPB = None self.ExtendedBPB = None self.RecordSize = None self.IndexBlockSize = None self.SectorSize = None self.MFTINode = None self.mountVolume() def mountVolume(self): logging.debug("Mounting volume...") self.volumeFD = open(self.__volumeName,"rb") self.readBootSector() self.MFTINode = self.getINode(FILE_MFT) # Check whether MFT is fragmented attr = self.MFTINode.searchAttribute(DATA, None) if attr is None: # It's not del self.MFTINode self.MFTINode = None def readBootSector(self): logging.debug("Reading Boot Sector for %s" % self.__volumeName) self.volumeFD.seek(0,0) data = self.volumeFD.read(512) while len(data) < 512: data += self.volumeFD.read(512) self.__bootSector = NTFS_BOOT_SECTOR(data) self.BPB = NTFS_BPB(self.__bootSector['BPB']) self.ExtendedBPB = NTFS_EXTENDED_BPB(self.__bootSector['ExtendedBPB']) self.SectorSize = self.BPB['BytesPerSector'] self.__MFTStart = self.BPB['BytesPerSector'] * self.BPB['SectorsPerCluster'] * self.ExtendedBPB['MFTClusterNumber'] if self.ExtendedBPB['ClusterPerFileRecord'] > 0: self.RecordSize = self.BPB['BytesPerSector'] * self.BPB['SectorsPerCluster'] * self.ExtendedBPB['ClusterPerFileRecord'] else: self.RecordSize = 1 << (-self.ExtendedBPB['ClusterPerFileRecord']) if self.ExtendedBPB['ClusterPerIndexBuffer'] > 0: self.IndexBlockSize = self.BPB['BytesPerSector'] * self.BPB['SectorsPerCluster'] * self.ExtendedBPB['ClusterPerIndexBuffer'] else: self.IndexBlockSize = 1 << (-self.ExtendedBPB['ClusterPerIndexBuffer']) logging.debug("MFT should start at position %d" % self.__MFTStart) def getINode(self, iNodeNum): logging.debug("Trying to fetch inode %d" % iNodeNum) newINode = INODE(self) recordLen = self.RecordSize # Read MFT record from disk or through fragmented $MFT if self.MFTINode and iNodeNum > FIXED_MFTS: # Fragmented $MFT - read through MFT's $DATA attribute attr = self.MFTINode.searchAttribute(DATA, None) if attr is None: logging.error("Cannot find MFT $DATA attribute for inode %d" % iNodeNum) return newINode record = attr.read(iNodeNum * self.RecordSize, self.RecordSize) else: diskPosition = self.__MFTStart + iNodeNum * self.RecordSize self.volumeFD.seek(diskPosition, 0) record = self.volumeFD.read(recordLen) while len(record) < recordLen: record += self.volumeFD.read(recordLen - len(record)) if not record or len(record) < recordLen: logging.error("Failed to read MFT record for inode %d" % iNodeNum) return newINode mftRecord = NTFS_MFT_RECORD(record) record = newINode.PerformFixUp(mftRecord, record, self.RecordSize // self.SectorSize) if record is None: logging.error("FixUp failed for inode %d" % iNodeNum) return newINode newINode.INodeNumber = iNodeNum newINode.AttributesRaw = record[mftRecord['AttributesOffset'] - recordLen:] newINode.parseAttributes() return newINode class MiniShell(cmd.Cmd): def __init__(self, volume): cmd.Cmd.__init__(self) self.volumePath = volume self.volume = NTFS(volume) self.rootINode = self.volume.getINode(FILE_Root) self.prompt = '\\>' self.intro = 'Type help for list of commands' self.currentINode = self.rootINode self.completion = [] self.pwd = '\\' self.do_ls('',False) self.last_output = '' def emptyline(self): pass def onecmd(self,s): retVal = False try: retVal = cmd.Cmd.onecmd(self,s) except Exception as e: logging.debug('Exception:', exc_info=True) logging.error(str(e)) return retVal def do_exit(self,line): return True def do_shell(self, line): output = os.popen(line).read() print(output) self.last_output = output def do_help(self,line): print(""" cd {path} - changes the current directory to {path} pwd - shows current remote directory ls - lists all the files in the current directory lcd - change local directory get {filename} - downloads the filename from the current path cat {filename} - prints the contents of filename hexdump {filename} - hexdumps the contents of filename exit - terminates the server process (and this session) """) def do_lcd(self,line): if line == '': print(os.getcwd()) else: os.chdir(line) print(os.getcwd()) def do_cd(self, line): p = line.replace('/','\\') oldpwd = self.pwd newPath = ntpath.normpath(ntpath.join(self.pwd,p)) if newPath == self.pwd: # Nothing changed return common = ntpath.commonprefix([newPath,oldpwd]) if common == oldpwd: res = self.findPathName(ntpath.normpath(p)) else: res = self.findPathName(newPath) if res is None: logging.error("Directory not found") self.pwd = oldpwd return if res.isDirectory() == 0: logging.error("Not a directory!") self.pwd = oldpwd return else: self.currentINode = res self.do_ls('', False) self.pwd = ntpath.join(self.pwd,p) self.pwd = ntpath.normpath(self.pwd) self.prompt = self.pwd + '>' def findPathName(self, pathName): if pathName == '\\': return self.rootINode tmpINode = self.currentINode parts = pathName.split('\\') for part in parts: if part == '': tmpINode = self.rootINode else: res = tmpINode.findFirst(part) if res is None: return res else: tmpINode = self.volume.getINode(res.getINodeNumber()) return tmpINode def do_pwd(self,line): print(self.pwd) def do_ls(self, line, display=True): entries = self.currentINode.walk() if entries is None: entries = [] logging.debug( "ls summary for %s: total=%d index_root=%d index_allocation=%d", self.pwd, len(entries), self.currentINode._walk_root_count, self.currentINode._walk_subnode_count, ) self.completion = [] for entry in entries: inode = INODE(self.volume) inode.FileAttributes = entry['FileAttributes'] inode.FileSize = entry['DataSize'] inode.LastDataChangeTime = datetime.fromtimestamp(getUnixTime(entry['LastDataChangeTime'])) inode.FileName = entry['FileName'].decode('utf-16le') if display is True: inode.displayName() self.completion.append((inode.FileName,inode.isDirectory())) def complete_cd(self, text, line, begidx, endidx): return self.complete_get(text, line, begidx, endidx, include = 2) def complete_cat(self,text,line,begidx,endidx): return self.complete_get(text, line, begidx, endidx) def complete_hexdump(self,text,line,begidx,endidx): return self.complete_get(text, line, begidx, endidx) def complete_get(self, text, line, begidx, endidx, include = 1): # include means # 1 just files # 2 just directories items = [] if include == 1: mask = 0 else: mask = FILE_ATTR_I30_INDEX_PRESENT for i in self.completion: if i[1] == mask: items.append(i[0]) if text: return [ item for item in items if item.upper().startswith(text.upper()) ] else: return items def do_hexdump(self,line): return self.do_cat(line,command = hexdump) def do_cat(self, line, command=None): if command is None: command = getattr(sys.stdout, 'buffer', sys.stdout).write pathName = line.replace('/', '\\') pathName = ntpath.normpath(ntpath.join(self.pwd, pathName)) res = self.findPathName(pathName) if res is None: logging.error("Not found!") return if res.isDirectory() > 0: logging.error("It's a directory!") return if res.isCompressed() or res.isEncrypted(): logging.error('Cannot handle compressed/encrypted files! :(') return stream = res.getStream(None) if stream is None: logging.error("Cannot read file stream!") return dataSize = stream.getDataSize() if dataSize == 0: logging.info("0 bytes read (empty file)") return chunkSize = 4096 * 10 offset = 0 while offset < dataSize: toRead = min(chunkSize, dataSize - offset) buf = stream.read(offset, toRead) if not buf: break try: command(buf) except (BrokenPipeError, OSError): return offset += len(buf) logging.info("%d bytes read" % offset) def do_get(self, line): pathName = line.replace('/','\\') pathName = ntpath.normpath(ntpath.join(self.pwd,pathName)) fh = open(ntpath.basename(pathName),"wb") self.do_cat(line, command = fh.write) fh.close() def main(): print(version.BANNER) parser = argparse.ArgumentParser(add_help = True, description = "NTFS explorer (read-only)") parser.add_argument('volume', action='store', help='NTFS volume to open (e.g. \\\\.\\C: or /dev/disk1s1)') parser.add_argument('-extract', action='store', help='extracts pathname (e.g. \\windows\\system32\\config\\sam)') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') if len(sys.argv)==1: parser.print_help() sys.exit(1) options = parser.parse_args() # Init the example's logger theme logger.init(options.ts, options.debug) shell = MiniShell(options.volume) if options.extract is not None: shell.onecmd("get %s"% options.extract) else: shell.cmdloop() if __name__ == '__main__': main() sys.exit(1)