Skip to content

Commit bbe09dc

Browse files
authored
Merge pull request #80 from rowingdude/dev-1
Update version number to 2.1.1
2 parents 4aa75a0 + 218fd77 commit bbe09dc

File tree

7 files changed

+147
-156
lines changed

7 files changed

+147
-156
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
This document lists the changes and version history for the AnalyzeMFT script and component scripts.
55

6-
## Version 2.1 (2024-08-02)
6+
## Version 2.1.1 (2024-08-02)
77

88
### Changes
99
- Updated to current PEP standards

analyzeMFT.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Version 2.1
1+
# Version 2.1.1
22
#
33
# Author: Benjamin Cance ([email protected])
44
#

analyzemft/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python
22

3-
# Version 2.1
3+
# Version 2.1.1
44
#
55
# Author: Benjamin Cance ([email protected])
66
# Copyright Benjamin Cance 2024

analyzemft/mft.py

Lines changed: 141 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python
22

3-
# Version 2.1
3+
# Version 2.1.1.1
44
#
55
# Author: Benjamin Cance ([email protected])
66
# Copyright Benjamin Cance 2024
@@ -14,7 +14,26 @@
1414
from argparse import ArgumentParser
1515
from . import mftutils
1616

17-
unicodeHack = True # This one is for me
17+
UNICODE_HACK = True
18+
19+
attribute_handlers = {
20+
0x10: handle_standard_information,
21+
0x20: handle_attribute_list,
22+
0x30: handle_file_name,
23+
0x40: handle_object_id,
24+
0x50: handle_security_descriptor,
25+
0x60: handle_volume_name,
26+
0x70: handle_volume_information,
27+
0x80: handle_data,
28+
0x90: handle_index_root,
29+
0xA0: handle_index_allocation,
30+
0xB0: handle_bitmap,
31+
0xC0: handle_reparse_point,
32+
0xD0: handle_ea_information,
33+
0xE0: handle_ea,
34+
0xF0: handle_property_set,
35+
0x100: handle_logged_utility_stream,
36+
}
1837

1938
def set_default_options() -> ArgumentParser:
2039
parser = ArgumentParser()
@@ -48,114 +67,24 @@ def parse_record(raw_record: bytes, options: Any) -> Dict[str, Any]:
4867

4968
read_ptr = record['attr_off']
5069

51-
while (read_ptr < 1024):
52-
70+
while read_ptr < 1024:
5371
ATRrecord = decodeATRHeader(raw_record[read_ptr:])
54-
if ATRrecord['type'] == 0xffffffff: # End of attributes
72+
if ATRrecord['type'] == 0xffffffff:
5573
break
5674

5775
if options.debug:
5876
print(f"Attribute type: {ATRrecord['type']:x} Length: {ATRrecord['len']} Res: {ATRrecord['res']:x}")
5977

60-
if ATRrecord['type'] == 0x10: # Standard Information
61-
if options.debug:
62-
print(f"Standard Information:\n++Type: {hex(ATRrecord['type'])} Length: {ATRrecord['len']} Resident: {ATRrecord['res']} Name Len:{ATRrecord['nlen']} Name Offset: {ATRrecord['name_off']}")
63-
SIrecord = decodeSIAttribute(raw_record[read_ptr+ATRrecord['soff']:], options.localtz)
64-
record['si'] = SIrecord
65-
if options.debug:
66-
print(f"++CRTime: {SIrecord['crtime'].dtstr}\n++MTime: {SIrecord['mtime'].dtstr}\n++ATime: {SIrecord['atime'].dtstr}\n++EntryTime: {SIrecord['ctime'].dtstr}")
67-
68-
elif ATRrecord['type'] == 0x20: # Attribute list
69-
if options.debug:
70-
print("Attribute list")
71-
if ATRrecord['res'] == 0:
72-
ALrecord = decodeAttributeList(raw_record[read_ptr+ATRrecord['soff']:], record)
73-
record['al'] = ALrecord
74-
if options.debug:
75-
print(f"Name: {ALrecord['name']}")
76-
else:
77-
if options.debug:
78-
print("Non-resident Attribute List?")
79-
record['al'] = None
80-
81-
82-
elif ATRrecord['type'] == 0x30: # File name
83-
if options.debug:
84-
print("File name record")
85-
FNrecord = decodeFNAttribute(raw_record[read_ptr+ATRrecord['soff']:], options.localtz, record)
86-
record[('fn', record['fncnt'])] = FNrecord
87-
if options.debug:
88-
print(f"Name: {FNrecord['name']} ({record['fncnt']})")
89-
record['fncnt'] += 1
90-
if FNrecord['crtime'] != 0:
91-
if options.debug:
92-
print(f"\tCRTime: {FNrecord['crtime'].dtstr} MTime: {FNrecord['mtime'].dtstr} ATime: {FNrecord['atime'].dtstr} EntryTime: {FNrecord['ctime'].dtstr}")
93-
94-
elif ATRrecord['type'] == 0x40: # Object ID
95-
ObjectIDRecord = decodeObjectID(raw_record[read_ptr+ATRrecord['soff']:])
96-
record['objid'] = ObjectIDRecord
97-
if options.debug: print (f"Object ID")
98-
99-
elif ATRrecord['type'] == 0x50: # Security descriptor
100-
record['sd'] = True
101-
if options.debug: print (f"Security descriptor")
102-
103-
elif ATRrecord['type'] == 0x60: # Volume name
104-
record['volname'] = True
105-
if options.debug: print (f"Volume name")
106-
107-
elif ATRrecord['type'] == 0x70: # Volume information
108-
if options.debug: print (f"Volume info attribute")
109-
VolumeInfoRecord = decodeVolumeInfo(raw_record[read_ptr+ATRrecord['soff']:],options)
110-
record['volinfo'] = VolumeInfoRecord
111-
112-
elif ATRrecord['type'] == 0x80: # Data
113-
record['data'] = True
114-
if options.debug: print (f"Data attribute")
115-
116-
elif ATRrecord['type'] == 0x90: # Index root
117-
record['indexroot'] = True
118-
if options.debug: print (f"Index root")
119-
120-
elif ATRrecord['type'] == 0xA0: # Index allocation
121-
record['indexallocation'] = True
122-
if options.debug: print (f"Index allocation")
123-
124-
elif ATRrecord['type'] == 0xB0: # Bitmap
125-
record['bitmap'] = True
126-
if options.debug: print (f"Bitmap")
127-
128-
elif ATRrecord['type'] == 0xC0: # Reparse point
129-
record['reparsepoint'] = True
130-
if options.debug: print (f"Reparse point")
131-
132-
elif ATRrecord['type'] == 0xD0: # EA Information
133-
record['eainfo'] = True
134-
if options.debug: print (f"EA Information")
135-
136-
elif ATRrecord['type'] == 0xE0: # EA
137-
record['ea'] = True
138-
if options.debug: print (f"EA")
139-
140-
elif ATRrecord['type'] == 0xF0: # Property set
141-
record['propertyset'] = True
142-
if options.debug: print (f"Property set")
143-
144-
elif ATRrecord['type'] == 0x100: # Logged utility stream
145-
record['loggedutility'] = True
146-
if options.debug: print (f"Logged utility stream")
147-
148-
else:
149-
if options.debug: print (f"Found an unknown attribute")
78+
handler = attribute_handlers.get(ATRrecord['type'], handle_unknown_attribute)
79+
handler(ATRrecord, raw_record[read_ptr:], record, options)
15080

15181
if ATRrecord['len'] > 0:
152-
read_ptr = read_ptr + ATRrecord['len']
82+
read_ptr += ATRrecord['len']
15383
else:
154-
if options.debug: print (f"ATRrecord->len < 0, exiting loop")
84+
if options.debug:
85+
print("ATRrecord->len <= 0, exiting loop")
15586
break
15687

157-
return record
158-
15988

16089
def mft_to_csv(record: Dict[str, Any], ret_header: bool) -> List[str]:
16190
if ret_header:
@@ -314,9 +243,6 @@ def mft_to_body(record, full, std):
314243

315244
return (rec_bodyfile)
316245

317-
# l2t CSV output support
318-
# date,time,timezone,MACB,source,sourcetype,type,user,host,short,desc,version,filename,inode,notes,format,extra
319-
# http://code.google.com/p/log2timeline/wiki/l2t_csv
320246

321247
def mft_to_l2t(record):
322248
' Return a MFT record in l2t CSV output format'
@@ -407,15 +333,6 @@ def decodeMFTmagic(record: Dict[str, Any]) -> str:
407333
}
408334
return magic_values.get(record['magic'], 'Unknown')
409335

410-
# decodeMFTisactive and decodeMFTrecordtype both look at the flags field in the MFT header.
411-
# The first bit indicates if the record is active or inactive. The second bit indicates if it
412-
# is a file or a folder.
413-
#
414-
# I had this coded incorrectly initially. Spencer Lynch identified and fixed the code. Many thanks!
415-
#
416-
# 02-August-2024 - These are now updated to current Python syntax
417-
#
418-
419336
def decodeMFTisactive(record: Dict[str, Any]) -> str:
420337
return 'Active' if record['flags'] & 0x0001 else 'Inactive'
421338

@@ -493,23 +410,7 @@ def decodeFNAttribute(s, localtz, record):
493410
d['nlen'] = struct.unpack("B",s[64])[0]
494411
d['nspace'] = struct.unpack("B",s[65])[0]
495412

496-
# The $MFT string is stored as \x24\x00\x4D\x00\x46\x00\x54. Ie, the first character is a single
497-
# byte and the remaining characters are two bytes with the first byte a null.
498-
# Note: Actually, it can be stored in several ways and the nspace field tells me which way.
499-
#
500-
# I found the following:
501-
#
502-
# NTFS allows any sequence of 16-bit values for name encoding (file names, stream names, index names,
503-
# etc.). This means UTF-16 codepoints are supported, but the file system does not check whether a
504-
# sequence is valid UTF-16 (it allows any sequence of short values, not restricted to those in the
505-
# Unicode standard).
506-
#
507-
# If true, lovely. But that would explain what I am seeing.
508-
#
509-
# I just ran across an example of "any sequence of ..." - filenames with backspaces and newlines
510-
# in them. Thus, the "isalpha" check. I really need to figure out how to handle Unicode better.
511-
512-
if (unicodeHack):
413+
if UNICODE_HACK:
513414
d['name'] = ''
514415
for i in range(66, 66 + d['nlen']*2):
515416
if s[i] != '\x00': # Just skip over nulls
@@ -519,23 +420,9 @@ def decodeFNAttribute(s, localtz, record):
519420
d['name'] = "%s0x%02s" % (d['name'], s[i].encode("hex"))
520421
hexFlag = True
521422

522-
# This statement produces a valid unicode string, I just cannot get it to print correctly
523-
# so I'm temporarily hacking it with the if (unicodeHack) above.
524423
else:
525424
d['name'] = s[66:66+d['nlen']*2]
526425

527-
# This didn't work
528-
# d['name'] = struct.pack("\u
529-
# for i in range(0, d['nlen']*2, 2):
530-
# d['name']=d['name'] + struct.unpack("<H",s[66+i:66+i+1])
531-
532-
# What follows is ugly. I'm trying to deal with the filename in Unicode and not doing well.
533-
# This solution works, though it is printing nulls between the characters. It'll do for now.
534-
# d['name'] = struct.unpack("<%dH" % (int(d['nlen'])*2),s[66:66+(d['nlen']*2)])
535-
# d['name'] = s[66:66+(d['nlen']*2)]
536-
# d['decname'] = unicodedata.normalize('NFKD', d['name']).encode('ASCII','ignore')
537-
# d['decname'] = unicode(d['name'],'iso-8859-1','ignore')
538-
539426
if hexFlag:
540427
add_note(record, 'Filename - chars converted to hex')
541428

@@ -554,7 +441,7 @@ def decodeAttributeList(s, record):
554441
d['file_ref'] = struct.unpack("<Lxx",s[16:22])[0] # 6
555442
d['seq'] = struct.unpack("<H",s[22:24])[0] # 2
556443
d['id'] = struct.unpack("<H",s[24:26])[0] # 4
557-
if (unicodeHack):
444+
if (UNICODE_HACK):
558445
d['name'] = ''
559446
for i in range(26, 26 + d['nlen']*2):
560447
if s[i] != '\x00': # Just skip over nulls
@@ -580,13 +467,13 @@ def decodeVolumeInfo(s,options):
580467
d['flags'] = struct.unpack("<H",s[10:12])[0] # 2
581468
d['f2'] = struct.unpack("<I",s[12:16])[0] # 4
582469

583-
if (options.debug):
584-
print (f"+Volume Info")
585-
print (f"++F1%d" % d['f1'])
586-
print (f"++Major Version: %d" % d['maj_ver'])
587-
print (f"++Minor Version: %d" % d['min_ver'])
588-
print (f"++Flags: %d" % d['flags'])
589-
print (f"++F2: %d" % d['f2'])
470+
if options.debug:
471+
print(f"+Volume Info")
472+
print(f"++F1%d" % d['f1'])
473+
print(f"++Major Version: %d" % d['maj_ver'])
474+
print(f"++Minor Version: %d" % d['min_ver'])
475+
print(f"++Flags: %d" % d['flags'])
476+
print(f"++F2: %d" % d['f2'])
590477

591478
return d
592479

@@ -604,3 +491,107 @@ def ObjectID(s: bytes) -> str:
604491
if s == b'\x00' * 16:
605492
return 'Undefined'
606493
return f"{s[:4].hex()}-{s[4:6].hex()}-{s[6:8].hex()}-{s[8:10].hex()}-{s[10:16].hex()}"
494+
495+
def handle_standard_information(ATRrecord, raw_record, record, options):
496+
if options.debug:
497+
print(f"Standard Information:\n++Type: {hex(ATRrecord['type'])} Length: {ATRrecord['len']} Resident: {ATRrecord['res']} Name Len: {ATRrecord['nlen']} Name Offset: {ATRrecord['name_off']}")
498+
SIrecord = decodeSIAttribute(raw_record[ATRrecord['soff']:], options.localtz)
499+
record['si'] = SIrecord
500+
if options.debug:
501+
print(f"++CRTime: {SIrecord['crtime'].dtstr}\n++MTime: {SIrecord['mtime'].dtstr}\n++ATime: {SIrecord['atime'].dtstr}\n++EntryTime: {SIrecord['ctime'].dtstr}")
502+
503+
def handle_attribute_list(ATRrecord, raw_record, record, options):
504+
if options.debug:
505+
print("Attribute list")
506+
if ATRrecord['res'] == 0:
507+
ALrecord = decodeAttributeList(raw_record[ATRrecord['soff']:], record)
508+
record['al'] = ALrecord
509+
if options.debug:
510+
print(f"Name: {ALrecord['name']}")
511+
else:
512+
if options.debug:
513+
print("Non-resident Attribute List?")
514+
record['al'] = None
515+
516+
def handle_file_name(ATRrecord, raw_record, record, options):
517+
if options.debug:
518+
print("File name record")
519+
FNrecord = decodeFNAttribute(raw_record[ATRrecord['soff']:], options.localtz, record)
520+
record[('fn', record['fncnt'])] = FNrecord
521+
if options.debug:
522+
print(f"Name: {FNrecord['name']} ({record['fncnt']})")
523+
record['fncnt'] += 1
524+
if FNrecord['crtime'] != 0:
525+
if options.debug:
526+
print(f"\tCRTime: {FNrecord['crtime'].dtstr} MTime: {FNrecord['mtime'].dtstr} ATime: {FNrecord['atime'].dtstr} EntryTime: {FNrecord['ctime'].dtstr}")
527+
528+
def handle_object_id(ATRrecord, raw_record, record, options):
529+
ObjectIDRecord = decodeObjectID(raw_record[ATRrecord['soff']:])
530+
record['objid'] = ObjectIDRecord
531+
if options.debug:
532+
print("Object ID")
533+
534+
def handle_security_descriptor(ATRrecord, raw_record, record, options):
535+
record['sd'] = True
536+
if options.debug:
537+
print("Security descriptor")
538+
539+
def handle_volume_name(ATRrecord, raw_record, record, options):
540+
record['volname'] = True
541+
if options.debug:
542+
print("Volume name")
543+
544+
def handle_volume_information(ATRrecord, raw_record, record, options):
545+
if options.debug:
546+
print("Volume info attribute")
547+
VolumeInfoRecord = decodeVolumeInfo(raw_record[ATRrecord['soff']:], options)
548+
record['volinfo'] = VolumeInfoRecord
549+
550+
def handle_data(ATRrecord, raw_record, record, options):
551+
record['data'] = True
552+
if options.debug:
553+
print("Data attribute")
554+
555+
def handle_index_root(ATRrecord, raw_record, record, options):
556+
record['indexroot'] = True
557+
if options.debug:
558+
print("Index root")
559+
560+
def handle_index_allocation(ATRrecord, raw_record, record, options):
561+
record['indexallocation'] = True
562+
if options.debug:
563+
print("Index allocation")
564+
565+
def handle_bitmap(ATRrecord, raw_record, record, options):
566+
record['bitmap'] = True
567+
if options.debug:
568+
print("Bitmap")
569+
570+
def handle_reparse_point(ATRrecord, raw_record, record, options):
571+
record['reparsepoint'] = True
572+
if options.debug:
573+
print("Reparse point")
574+
575+
def handle_ea_information(ATRrecord, raw_record, record, options):
576+
record['eainfo'] = True
577+
if options.debug:
578+
print("EA Information")
579+
580+
def handle_ea(ATRrecord, raw_record, record, options):
581+
record['ea'] = True
582+
if options.debug:
583+
print("EA")
584+
585+
def handle_property_set(ATRrecord, raw_record, record, options):
586+
record['propertyset'] = True
587+
if options.debug:
588+
print("Property set")
589+
590+
def handle_logged_utility_stream(ATRrecord, raw_record, record, options):
591+
record['loggedutility'] = True
592+
if options.debug:
593+
print("Logged utility stream")
594+
595+
def handle_unknown_attribute(ATRrecord, raw_record, record, options):
596+
if options.debug:
597+
print(f"Found an unknown attribute type: {ATRrecord['type']:x}")

0 commit comments

Comments
 (0)