191 lines
6.1 KiB
Python
191 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
parse_fsevents.py — Parse macOS FSEvents logs and look for 64-char hex strings (potential keys).
|
|
|
|
Usage:
|
|
python3 parse_fsevents.py /path/to/.fseventsd/
|
|
|
|
The .fseventsd directory is inside the mounted DMG. Each file in it is a
|
|
gzip-compressed binary log. This script will:
|
|
1. Parse every log file
|
|
2. Extract all file paths
|
|
3. Flag any path component that looks like a 64-char hex string (a potential key)
|
|
"""
|
|
|
|
import gzip
|
|
import os
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
# Regex to match 64-char hex string anywhere in a path
|
|
HEX64 = re.compile(r'\b([0-9a-fA-F]{64})\b')
|
|
|
|
|
|
def parse_fsevent_record(data: bytes, offset: int):
|
|
"""Parse a single FSEvent record. Returns (path, flags, event_id, next_offset)."""
|
|
# Format: null-terminated path string, then 4-byte flags, then 8-byte event_id
|
|
end = data.index(b'\x00', offset)
|
|
path = data[offset:end].decode('utf-8', errors='replace')
|
|
offset = end + 1
|
|
if offset + 12 > len(data):
|
|
return path, 0, 0, len(data)
|
|
flags = struct.unpack_from('<I', data, offset)[0]
|
|
event_id = struct.unpack_from('<Q', data, offset + 4)[0]
|
|
return path, flags, event_id, offset + 12
|
|
|
|
|
|
def parse_fsevent_file(filepath: str):
|
|
"""Parse one FSEvents log file, yield (path, flags, event_id) tuples."""
|
|
with open(filepath, 'rb') as f:
|
|
raw = f.read()
|
|
|
|
# FSEvents files start with a 'DLS' magic, then gzip chunks
|
|
# Each chunk: 4-byte magic (1SLD), 4-byte size, gzip data
|
|
# OR the whole file might be one gzip stream (older format)
|
|
|
|
results = []
|
|
|
|
# Try multi-chunk format (macOS 10.5+)
|
|
# Magic: b'\x31\x53\x4c\x44' = "1SLD" or b'1SLD'
|
|
if raw[:4] in (b'1SLD', b'2SLD'):
|
|
offset = 0
|
|
while offset < len(raw):
|
|
if offset + 8 > len(raw):
|
|
break
|
|
magic = raw[offset:offset+4]
|
|
if magic not in (b'1SLD', b'2SLD'):
|
|
break
|
|
size = struct.unpack_from('<I', raw, offset + 4)[0]
|
|
chunk_data = raw[offset + 8: offset + 8 + size]
|
|
offset += 8 + size
|
|
try:
|
|
decompressed = gzip.decompress(chunk_data)
|
|
pos = 0
|
|
while pos < len(decompressed):
|
|
try:
|
|
path, flags, event_id, pos = parse_fsevent_record(decompressed, pos)
|
|
results.append((path, flags, event_id))
|
|
except (ValueError, struct.error):
|
|
break
|
|
except Exception:
|
|
pass
|
|
else:
|
|
# Try raw gzip
|
|
try:
|
|
decompressed = gzip.decompress(raw)
|
|
pos = 0
|
|
while pos < len(decompressed):
|
|
try:
|
|
path, flags, event_id, pos = parse_fsevent_record(decompressed, pos)
|
|
results.append((path, flags, event_id))
|
|
except (ValueError, struct.error):
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
return results
|
|
|
|
|
|
def flag_description(flags: int) -> str:
|
|
"""Human-readable flags."""
|
|
names = []
|
|
flag_map = {
|
|
0x00000001: 'Created',
|
|
0x00000002: 'Removed',
|
|
0x00000004: 'InodeMetaMod',
|
|
0x00000008: 'Renamed',
|
|
0x00000010: 'Modified',
|
|
0x00000020: 'Exchange',
|
|
0x00000040: 'FinderInfoMod',
|
|
0x00000080: 'FolderCreated',
|
|
0x00000100: 'PermissionChanged',
|
|
0x00000200: 'XattrModified',
|
|
0x00000400: 'XattrRemoved',
|
|
0x00001000: 'DocumentRevision',
|
|
0x00400000: 'ItemIsFile',
|
|
0x00800000: 'ItemIsDir',
|
|
0x01000000: 'ItemIsSymlink',
|
|
0x02000000: 'OwnedByActiveUSSD',
|
|
0x04000000: 'ItemIsHardlink',
|
|
0x10000000: 'ItemIsLastHardlink',
|
|
}
|
|
for bit, name in flag_map.items():
|
|
if flags & bit:
|
|
names.append(name)
|
|
return '|'.join(names) if names else f'0x{flags:08x}'
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python3 parse_fsevents.py /path/to/.fseventsd/")
|
|
sys.exit(1)
|
|
|
|
fseventsd_dir = sys.argv[1]
|
|
|
|
all_paths = []
|
|
hex_key_hits = []
|
|
|
|
log_files = sorted([
|
|
f for f in os.listdir(fseventsd_dir)
|
|
if not f.startswith('.') and f != 'no_log' and f != 'fseventsd-uuid'
|
|
])
|
|
|
|
print(f"[*] Found {len(log_files)} FSEvents log file(s) in {fseventsd_dir}")
|
|
print()
|
|
|
|
for fname in log_files:
|
|
fpath = os.path.join(fseventsd_dir, fname)
|
|
print(f"[+] Parsing: {fname}")
|
|
records = parse_fsevent_file(fpath)
|
|
print(f" {len(records)} records found")
|
|
|
|
for path, flags, event_id in records:
|
|
all_paths.append(path)
|
|
# Look for 64-char hex strings in the path
|
|
m = HEX64.search(path)
|
|
if m:
|
|
hex_key_hits.append({
|
|
'key': m.group(1).lower(),
|
|
'path': path,
|
|
'flags': flags,
|
|
'flags_str': flag_description(flags),
|
|
'event_id': event_id,
|
|
'source_file': fname,
|
|
})
|
|
|
|
print()
|
|
print("=" * 70)
|
|
print(f"[*] Total paths parsed: {len(all_paths)}")
|
|
print(f"[*] Paths containing 64-char hex strings: {len(hex_key_hits)}")
|
|
print("=" * 70)
|
|
|
|
if hex_key_hits:
|
|
print("\n[!] POTENTIAL KEYS FOUND IN FSEVENTS:\n")
|
|
for hit in hex_key_hits:
|
|
print(f" Key: {hit['key']}")
|
|
print(f" Path: {hit['path']}")
|
|
print(f" Flags: {hit['flags_str']}")
|
|
print(f" EventID: {hit['event_id']}")
|
|
print(f" Source: {hit['source_file']}")
|
|
print()
|
|
else:
|
|
print("\n[*] No 64-char hex strings found in paths.")
|
|
print("[*] Showing all unique paths for manual analysis:\n")
|
|
for p in sorted(set(all_paths)):
|
|
print(f" {p}")
|
|
|
|
# Also dump full path list to file
|
|
out_path = "fsevents_all_paths.txt"
|
|
with open(out_path, 'w') as f:
|
|
for path, flags, event_id in []:
|
|
pass
|
|
# Write unique paths
|
|
for p in sorted(set(all_paths)):
|
|
f.write(p + '\n')
|
|
print(f"\n[*] All unique paths written to: {out_path}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|