-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmft_parser.py
More file actions
147 lines (114 loc) · 4.87 KB
/
Copy pathmft_parser.py
File metadata and controls
147 lines (114 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""
mft_parser.py — NTFS $MFT Parser
Project Helix CTF · TCM Security
Author: Dibyanshu Sekhar
Walks all FILE records in $MFT, applies USA fixup, and extracts:
- Filename attributes (0x30) to identify files by name
- Data attributes (0x80) to recover Zone.Identifier ADS content
Usage:
python mft_parser.py --mft /path/to/$MFT --target freq.txt
"""
import struct
import argparse
def apply_usa_fixup(record: bytearray) -> bytearray:
"""Apply the Update Sequence Array fixup to an MFT record."""
usa_off = struct.unpack_from('<H', record, 4)[0]
usa_cnt = struct.unpack_from('<H', record, 6)[0]
usa_val = bytes(record[usa_off:usa_off + 2])
for i in range(1, usa_cnt):
fix = bytes(record[usa_off + i * 2: usa_off + i * 2 + 2])
pos = i * 512 - 2
if pos + 2 <= len(record) and record[pos:pos + 2] == bytearray(usa_val):
record[pos:pos + 2] = fix
return record
def extract_filename(record: bytearray, attr_off: int, attr_len: int) -> str | None:
"""Extract filename from a $FILE_NAME (0x30) attribute."""
try:
content_off = struct.unpack_from('<H', record, attr_off + 20)[0]
name_len = record[attr_off + content_off + 64]
name_start = attr_off + content_off + 66
name_bytes = record[name_start: name_start + name_len * 2]
return name_bytes.decode('utf-16-le', errors='replace')
except Exception:
return None
def extract_data_ads(record: bytearray, attr_off: int, attr_len: int) -> str | None:
"""
Extract content from a resident $DATA (0x80) attribute.
Returns the content as a string if it looks like Zone.Identifier text.
"""
try:
non_resident = record[attr_off + 8]
if non_resident:
return None # Skip non-resident data (clusters wiped on deletion)
content_off = struct.unpack_from('<H', record, attr_off + 20)[0]
content_len = struct.unpack_from('<I', record, attr_off + 16)[0]
content = record[attr_off + content_off: attr_off + content_off + content_len]
text = content.decode('utf-8', errors='replace')
if 'ZoneTransfer' in text or 'HostUrl' in text:
return text.strip()
except Exception:
pass
return None
def parse_mft(mft_path: str, target_name: str):
"""Walk all FILE records in $MFT, find target file, dump Zone.Identifier."""
record_size = 1024
results = []
print(f"[*] Parsing $MFT: {mft_path}")
print(f"[*] Looking for: {target_name}\n")
with open(mft_path, 'rb') as f:
data = f.read()
total_records = len(data) // record_size
print(f"[*] Total records to scan: {total_records:,}")
for record_num in range(total_records):
offset = record_num * record_size
raw = bytearray(data[offset: offset + record_size])
if raw[:4] != b'FILE':
continue
record = apply_usa_fixup(raw)
# Check flags: bit 0 = in-use, bit 1 = directory
flags = struct.unpack_from('<H', record, 22)[0]
is_deleted = not (flags & 0x01)
attr_pos = struct.unpack_from('<H', record, 20)[0]
filename = None
zone_content = None
while attr_pos < len(record) - 8:
attr_type = struct.unpack_from('<I', record, attr_pos)[0]
if attr_type == 0xFFFFFFFF:
break
attr_len = struct.unpack_from('<I', record, attr_pos + 4)[0]
if attr_len == 0:
break
if attr_type == 0x30: # $FILE_NAME
name = extract_filename(record, attr_pos, attr_len)
if name:
filename = name
if attr_type == 0x80: # $DATA
zone = extract_data_ads(record, attr_pos, attr_len)
if zone:
zone_content = zone
attr_pos += attr_len
if filename and target_name.lower() in filename.lower():
status = "[DELETED]" if is_deleted else "[ACTIVE]"
print(f"[+] Record {record_num}: {filename} {status}")
if zone_content:
print(f"[+] Zone.Identifier ADS found:")
print("-" * 40)
print(zone_content)
print("-" * 40)
results.append({
'record': record_num,
'name': filename,
'deleted': is_deleted,
'zone': zone_content,
})
if not results:
print(f"[-] No records found for '{target_name}'")
else:
print(f"\n[*] Done. Found {len(results)} matching record(s).")
return results
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='NTFS $MFT Parser — Project Helix')
parser.add_argument('--mft', required=True, help='Path to $MFT file')
parser.add_argument('--target', default='freq.txt', help='Filename to search for')
args = parser.parse_args()
parse_mft(args.mft, args.target)