Skip to content

Instantly share code, notes, and snippets.

@aaaddress1
Created May 18, 2026 08:46
Show Gist options
  • Select an option

  • Save aaaddress1/ff2338c7167d2ea3bfbdedb72ede1ad3 to your computer and use it in GitHub Desktop.

Select an option

Save aaaddress1/ff2338c7167d2ea3bfbdedb72ede1ad3 to your computer and use it in GitHub Desktop.
OpenFile("\\.\C:") + ReadFile - Forensic based Read All Your FIles without Other Win32 APIs
"""
NTFS resident $DATA demo (v2 — fragmentation-aware + force flush)
"""
import ctypes, struct, os, uuid, time
from ctypes import wintypes
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
k32.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD,
wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE]
k32.CreateFileW.restype = wintypes.HANDLE
k32.SetFilePointerEx.argtypes = [wintypes.HANDLE, ctypes.c_longlong,
ctypes.POINTER(ctypes.c_longlong), wintypes.DWORD]
k32.SetFilePointerEx.restype = wintypes.BOOL
k32.ReadFile.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD,
ctypes.POINTER(wintypes.DWORD), wintypes.LPVOID]
k32.ReadFile.restype = wintypes.BOOL
k32.FlushFileBuffers.argtypes = [wintypes.HANDLE]
k32.FlushFileBuffers.restype = wintypes.BOOL
k32.CloseHandle.argtypes = [wintypes.HANDLE]
GENERIC_READ, GENERIC_WRITE = 0x80000000, 0x40000000
FILE_SHARE_RW, OPEN_EXISTING, FILE_BEGIN = 0x3, 3, 0
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
def open_volume(path, write=False):
access = GENERIC_READ | (GENERIC_WRITE if write else 0)
h = k32.CreateFileW(path, access, FILE_SHARE_RW, None, OPEN_EXISTING, 0, None)
if h == INVALID_HANDLE_VALUE:
raise OSError(f"CreateFile failed: err={ctypes.get_last_error()}")
return h
def read_at(h, offset, size):
pos = ctypes.c_longlong(0)
k32.SetFilePointerEx(h, offset, ctypes.byref(pos), FILE_BEGIN)
buf = (ctypes.c_ubyte * size)()
n = wintypes.DWORD(0)
k32.ReadFile(h, buf, size, ctypes.byref(n), None)
return bytes(buf[:n.value])
# ─── NTFS structures ───
def parse_vbr(vbr):
assert vbr[3:11] == b'NTFS '
bps = struct.unpack_from('<H', vbr, 0x0B)[0]
spc = struct.unpack_from('<B', vbr, 0x0D)[0]
mft_lcn = struct.unpack_from('<Q', vbr, 0x30)[0]
cpr = struct.unpack_from('<b', vbr, 0x40)[0]
cluster = bps * spc
rec_size = cpr * cluster if cpr >= 0 else (1 << -cpr)
return {'bps': bps, 'cluster': cluster,
'mft_off': mft_lcn * cluster, 'rec_size': rec_size}
def apply_fixups(rec, sector_size):
"""USA fixup. 失敗回 None(壞 record 跳過而不是 abort)"""
if len(rec) < 0x30 or rec[:4] != b'FILE':
return None
try:
rec = bytearray(rec)
usa_off = struct.unpack_from('<H', rec, 0x04)[0]
usa_cnt = struct.unpack_from('<H', rec, 0x06)[0]
if usa_off == 0 or usa_cnt < 2 or usa_off + usa_cnt * 2 > len(rec):
return None
usa = bytes(rec[usa_off:usa_off + usa_cnt * 2])
sig = usa[:2]
for i in range(1, usa_cnt):
end = i * sector_size
if end > len(rec) or bytes(rec[end - 2:end]) != sig:
return None
rec[end - 2:end] = usa[i * 2:i * 2 + 2]
return bytes(rec)
except Exception:
return None
def walk_attrs(rec):
try:
pos = struct.unpack_from('<H', rec, 0x14)[0]
while pos < len(rec) - 4:
t = struct.unpack_from('<I', rec, pos)[0]
if t == 0xFFFFFFFF:
return
length = struct.unpack_from('<I', rec, pos + 4)[0]
if length == 0 or pos + length > len(rec):
return
yield {'type': t, 'length': length,
'resident': rec[pos + 8] == 0,
'data': bytes(rec[pos:pos + length])}
pos += length
except Exception:
return
def parse_data_runs(runs):
out, pos, prev_lcn = [], 0, 0
while pos < len(runs) and runs[pos] != 0:
hdr = runs[pos]; pos += 1
l_sz, o_sz = hdr & 0xF, (hdr >> 4) & 0xF
if pos + l_sz + o_sz > len(runs): break
length = int.from_bytes(runs[pos:pos + l_sz], 'little'); pos += l_sz
if o_sz == 0:
out.append({'lcn': None, 'length': length, 'sparse': True})
else:
delta = int.from_bytes(runs[pos:pos + o_sz], 'little', signed=True)
pos += o_sz; prev_lcn += delta
out.append({'lcn': prev_lcn, 'length': length, 'sparse': False})
return out
def get_filename(d):
v_off = struct.unpack_from('<H', d, 0x14)[0]
fn_len = d[v_off + 0x40]
return d[v_off + 0x42 : v_off + 0x42 + fn_len * 2].decode('utf-16-le', 'replace')
def get_resident_data(d):
v_len = struct.unpack_from('<I', d, 0x10)[0]
v_off = struct.unpack_from('<H', d, 0x14)[0]
return bytes(d[v_off:v_off + v_len])
def iter_mft_records(h, vbr, mft_runs):
"""沿著 $MFT data runs 走訪整個 MFT(正確處理 fragmentation)"""
rec_num = 0
for run in mft_runs:
recs_per_run = (run['length'] * vbr['cluster']) // vbr['rec_size']
if run['sparse']:
rec_num += recs_per_run
continue
disk_off = run['lcn'] * vbr['cluster']
byte_len = run['length'] * vbr['cluster']
CHUNK = 4 * 1024 * 1024
for off in range(0, byte_len, CHUNK):
sz = min(CHUNK, byte_len - off)
data = read_at(h, disk_off + off, sz)
for i in range(0, len(data) - vbr['rec_size'] + 1, vbr['rec_size']):
yield rec_num, data[i:i + vbr['rec_size']]
rec_num += 1
# ─── Demo ───
def main():
# 1. 寫檔
os.makedirs(r'C:\tmp', exist_ok=True)
path = r'C:\tmp\hello.txt'
marker = f"hello from raw disk! id={uuid.uuid4()}"
with open(path, 'w', encoding='utf-8') as f:
f.write(marker)
f.flush()
os.fsync(f.fileno())
print(f"[Setup] wrote {len(marker)} bytes to {path}")
print(f" marker = {marker!r}")
# 2. 強制 volume flush(讓 MFT dirty page 寫回 disk)
hv = open_volume(r'\\.\C:', write=True)
ok = bool(k32.FlushFileBuffers(hv))
k32.CloseHandle(hv)
print(f"\n[Flush] FlushFileBuffers on \\\\.\\C: = {ok}")
time.sleep(1)
# 3. 讀 volume
h = open_volume(r'\\.\C:')
try:
vbr = parse_vbr(read_at(h, 0, 512))
print(f"\n[VBR] cluster={vbr['cluster']} "
f"$MFT @ 0x{vbr['mft_off']:X} rec_size={vbr['rec_size']}")
# 4. 讀 $MFT record 0,解出 $MFT 自己的 data runs
rec0 = apply_fixups(read_at(h, vbr['mft_off'], vbr['rec_size']), vbr['bps'])
assert rec0, "$MFT record 0 corrupt"
mft_runs = None
for a in walk_attrs(rec0):
if a['type'] == 0x80 and not a['resident']:
run_off = struct.unpack_from('<H', a['data'], 0x20)[0]
mft_runs = parse_data_runs(a['data'][run_off:a['length']])
assert mft_runs, "no $DATA in $MFT record 0"
total_recs = sum(r['length'] * vbr['cluster'] // vbr['rec_size'] for r in mft_runs)
print(f"\n[MFT] {len(mft_runs)} extent(s), {total_recs:,} records total")
for i, r in enumerate(mft_runs):
if r['sparse']:
print(f" [{i}] SPARSE × {r['length']} clusters")
else:
off = r['lcn'] * vbr['cluster']
sz_mb = r['length'] * vbr['cluster'] / (1 << 20)
print(f" [{i}] LCN {r['lcn']:>12} × {r['length']:>8} "
f"clusters @ 0x{off:X} ({sz_mb:.1f} MB)")
# 5. 沿 data runs 掃整個 MFT
print(f"\n[Scan] scanning all {total_recs:,} MFT records...")
scanned, valid, found = 0, 0, None
for rec_num, raw in iter_mft_records(h, vbr, mft_runs):
scanned += 1
rec = apply_fixups(raw, vbr['bps'])
if rec is None:
continue
if not (struct.unpack_from('<H', rec, 0x16)[0] & 1):
continue
valid += 1
fname, data_attr = None, None
for a in walk_attrs(rec):
if a['type'] == 0x30 and a['resident']:
fname = get_filename(a['data'])
elif a['type'] == 0x80:
data_attr = a
if fname == 'hello.txt' and data_attr:
found = (rec_num, data_attr); break
print(f" scanned {scanned:,} records ({valid:,} in-use)")
if not found:
print(" ❌ hello.txt not found"); return
rec_num, data_attr = found
print(f"\n[Hit] hello.txt @ MFT record #{rec_num} "
f"({'resident' if data_attr['resident'] else 'NON-resident'} $DATA)")
if data_attr['resident']:
content = get_resident_data(data_attr['data'])
print(f"\n[Extracted from raw disk]\n {content!r}")
print(f"\n[Match] {'✅ identical' if content.decode() == marker else '❌ MISMATCH'}")
else:
print(" (non-resident — 解析 data runs 略)")
finally:
k32.CloseHandle(h)
if __name__ == '__main__':
main()
@aaaddress1
Copy link
Copy Markdown
Author

Execution Results

[Setup] wrote 60 bytes to C:\tmp\hello.txt
        marker = 'hello from raw disk! id=fdff7f33-32a5-4c92-9a08-9690e397a430'

[VBR]  cluster=4096  $MFT @ disk offset 0xC0000000  record_size=1024
找不到 hello.txt — 可能 MFT cache 還沒 flush,或檔案 record 號超過搜尋範圍
PS C:\Users\adr-workspace>  c:; cd 'c:\Users\adr-workspace'; & 'C:\Program Files\Python313\python.exe' 'c:\Users\adr-workspace\.vscode\extensions\ms-python.debugpy-2026.6.0-win32-x64\bundled\libs\debugpy\launcher' '59643' '--' 'C:\Users\adr-workspace\test.py' 
[Setup] wrote 60 bytes to C:\tmp\hello.txt
        marker = 'hello from raw disk! id=e2f33c6b-8858-4f13-bc39-0df19f0a71df'

[Flush] FlushFileBuffers on \\.\C: = True

[VBR]   cluster=4096  $MFT @ 0xC0000000  rec_size=1024

[MFT]   81 extent(s), 14,101,504 records total
        [0] LCN       786432 ×    51264 clusters @ 0xC0000000 (200.2 MB)
        [1] LCN      7472574 ×    51202 clusters @ 0x7205BE000 (200.0 MB)
        [2] LCN     14854608 ×    49662 clusters @ 0xE2A9D0000 (194.0 MB)
        [3] LCN    115893536 ×    51568 clusters @ 0x6E86520000 (201.4 MB)
        [4] LCN       225260 ×    24208 clusters @ 0x36FEC000 (94.6 MB)
        [5] LCN    149272292 ×    54770 clusters @ 0x8E5B6E4000 (213.9 MB)
        [6] LCN    153725168 ×    52174 clusters @ 0x929A8F0000 (203.8 MB)
        [7] LCN    174473720 ×    51208 clusters @ 0xA6641F8000 (200.0 MB)
        [8] LCN    143472221 ×     3320 clusters @ 0x88D365D000 (13.0 MB)
        [9] LCN    166470212 ×    51228 clusters @ 0x9EC2244000 (200.1 MB)
        [10] LCN    142241108 ×    51228 clusters @ 0x87A6D54000 (200.1 MB)
        [11] LCN    145322849 ×    51231 clusters @ 0x8A97361000 (200.1 MB)
        [12] LCN    147881364 ×    51212 clusters @ 0x8D07D94000 (200.0 MB)
        [13] LCN    185748580 ×    51228 clusters @ 0xB124C64000 (200.1 MB)
        [14] LCN    146266440 ×    51224 clusters @ 0x8B7D948000 (200.1 MB)
        [15] LCN    164910976 ×    51200 clusters @ 0x9D45780000 (200.0 MB)
        [16] LCN    144730156 ×    51220 clusters @ 0x8A0682C000 (200.1 MB)
        [17] LCN    207348432 ×    51216 clusters @ 0xC5BE2D0000 (200.1 MB)
        [18] LCN    205220346 ×    52584 clusters @ 0xC3B69FA000 (205.4 MB)
        [19] LCN    215110275 ×    66013 clusters @ 0xCD25283000 (257.9 MB)
        [20] LCN    199475120 ×    66622 clusters @ 0xBE3BFB0000 (260.2 MB)
        [21] LCN    205949884 ×    79042 clusters @ 0xC468BBC000 (308.8 MB)
        [22] LCN    216697404 ×    51332 clusters @ 0xCEA8A3C000 (200.5 MB)
        [23] LCN    106538768 ×    51428 clusters @ 0x659A710000 (200.9 MB)
        [24] LCN    219898028 ×    51220 clusters @ 0xD1B60AC000 (200.1 MB)
        [25] LCN    227864676 ×    74565 clusters @ 0xD94F064000 (291.3 MB)
        [26] LCN    217457884 ×    28287 clusters @ 0xCF624DC000 (110.5 MB)
        [27] LCN    206077360 ×    51216 clusters @ 0xC487DB0000 (200.1 MB)
        [28] LCN    301102096 ×    51216 clusters @ 0x11F27410000 (200.1 MB)
        [29] LCN    206456876 ×    51220 clusters @ 0xC4E482C000 (200.1 MB)
        [30] LCN    238251028 ×    51398 clusters @ 0xE336C14000 (200.8 MB)
        [31] LCN    205081784 ×    27782 clusters @ 0xC394CB8000 (108.5 MB)
        [32] LCN    259560740 ×    51228 clusters @ 0xF789524000 (200.1 MB)
        [33] LCN    253000576 ×    51200 clusters @ 0xF147B80000 (200.0 MB)
        [34] LCN    272648360 ×    51389 clusters @ 0x104048A8000 (200.7 MB)
        [35] LCN    277989561 ×    51515 clusters @ 0x1091C8B9000 (201.2 MB)
        [36] LCN    265594444 ×    51220 clusters @ 0xFD4A64C000 (200.1 MB)
        [37] LCN    277811904 ×    51200 clusters @ 0x108F12C0000 (200.0 MB)
        [38] LCN    264837740 ×    51220 clusters @ 0xFC91A6C000 (200.1 MB)
        [39] LCN    278165187 ×    55243 clusters @ 0x109476C3000 (215.8 MB)
        [40] LCN    265927732 ×    51212 clusters @ 0xFD9BC34000 (200.0 MB)
        [41] LCN    335705089 ×       48 clusters @ 0x14027401000 (0.2 MB)
        [42] LCN    335710513 ×       63 clusters @ 0x14028931000 (0.2 MB)
        [43] LCN    335721264 ×      129 clusters @ 0x1402B330000 (0.5 MB)
        [44] LCN    335722929 ×      576 clusters @ 0x1402B9B1000 (2.2 MB)
        [45] LCN    335728625 ×      128 clusters @ 0x1402CFF1000 (0.5 MB)
        [46] LCN    335731537 ×       63 clusters @ 0x1402DB51000 (0.2 MB)
        [47] LCN    335737040 ×      127 clusters @ 0x1402F0D0000 (0.5 MB)
        [48] LCN    335788256 ×      896 clusters @ 0x1403B8E0000 (3.5 MB)
        [49] LCN    335790528 ×     3329 clusters @ 0x1403C1C0000 (13.0 MB)
        [50] LCN    336113464 ×     2816 clusters @ 0x1408AF38000 (11.0 MB)
        [51] LCN    336117880 ×      385 clusters @ 0x1408C078000 (1.5 MB)
        [52] LCN    336122617 ×      256 clusters @ 0x1408D2F9000 (1.0 MB)
        [53] LCN    336151117 ×     6720 clusters @ 0x1409424D000 (26.2 MB)
        [54] LCN    336259680 ×    15487 clusters @ 0x140AEA60000 (60.5 MB)
        [55] LCN    327842455 ×    55550 clusters @ 0x138A7A97000 (217.0 MB)
        [56] LCN    323860052 ×    51212 clusters @ 0x134DB654000 (200.0 MB)
        [57] LCN    336411632 ×    51216 clusters @ 0x140D3BF0000 (200.1 MB)
        [58] LCN    345629600 ×    51483 clusters @ 0x1499E3A0000 (201.1 MB)
        [59] LCN    286500332 ×    51220 clusters @ 0x1113A5EC000 (200.1 MB)
        [60] LCN    278735348 ×    40312 clusters @ 0x109D29F4000 (157.5 MB)
        [61] LCN    181811548 ×    51204 clusters @ 0xAD6395C000 (200.0 MB)
        [62] LCN    161953391 ×    51220 clusters @ 0x9A7366F000 (200.1 MB)
        [63] LCN    331270648 ×    39208 clusters @ 0x13BEC9F8000 (153.2 MB)
        [64] LCN    331630420 ×    51212 clusters @ 0x13C44754000 (200.0 MB)
        [65] LCN    281145240 ×    51672 clusters @ 0x10C1EF98000 (201.8 MB)
        [66] LCN    325942324 ×    51212 clusters @ 0x136D7C34000 (200.0 MB)
        [67] LCN    312424988 ×    21965 clusters @ 0x129F3A1C000 (85.8 MB)
        [68] LCN    321566652 ×    88607 clusters @ 0x132AB7BC000 (346.1 MB)
        [69] LCN    312369556 ×    13153 clusters @ 0x129E6194000 (51.4 MB)
        [70] LCN    274300928 ×    51200 clusters @ 0x10598000000 (200.0 MB)
        [71] LCN    364615084 ×    51220 clusters @ 0x15BB95AC000 (200.1 MB)
        [72] LCN    308294132 ×    58540 clusters @ 0x126031F4000 (228.7 MB)
        [73] LCN    304313560 ×    51208 clusters @ 0x122374D8000 (200.0 MB)
        [74] LCN    413348296 ×    51224 clusters @ 0x18A331C8000 (200.1 MB)
        [75] LCN    413897552 ×    52620 clusters @ 0x18AB9350000 (205.5 MB)
        [76] LCN    339134924 ×    91028 clusters @ 0x1436C9CC000 (355.6 MB)
        [77] LCN    304611328 ×    51200 clusters @ 0x12280000000 (200.0 MB)
        [78] LCN    277289028 ×    51228 clusters @ 0x10871844000 (200.1 MB)
        [79] LCN    278944484 ×    51228 clusters @ 0x10A05AE4000 (200.1 MB)
        [80] LCN    414253036 ×   143496 clusters @ 0x18B0FFEC000 (560.5 MB)

[Scan]  scanning all 14,101,504 MFT records...
        scanned 646,110 records (646,107 in-use)

[Hit]   hello.txt @ MFT record #646109  (resident $DATA)

[Extracted from raw disk]
   b'hello from raw disk! id=e2f33c6b-8858-4f13-bc39-0df19f0a71df'

[Match] ✅ identical

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment