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
1414from argparse import ArgumentParser
1515from . 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
1938def 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"\t CRTime: { 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
16089def 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
321247def 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-
419336def 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"\t CRTime: { 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