128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
import struct, os
|
|
|
|
DEVICE = '/dev/nbd0'
|
|
BSIZE = 4096
|
|
IPG = 8192
|
|
INODE_SZ = 256
|
|
NUM_GROUPS = 35728
|
|
MIN_GROUP = 13
|
|
|
|
TARGETS = [
|
|
b'pterodactyl',
|
|
b'mysql',
|
|
b'www',
|
|
b'log',
|
|
b'docker',
|
|
b'nginx',
|
|
b'apache2',
|
|
b'archives',
|
|
b'wings',
|
|
b'grub2-efi.cfg',
|
|
b'commons-codec',
|
|
]
|
|
|
|
def is_valid_dirent(block, off, name):
|
|
if off + 8 + len(name) > BSIZE: return False
|
|
inode = struct.unpack_from('<I', block, off)[0]
|
|
rec_len = struct.unpack_from('<H', block, off+4)[0]
|
|
name_len = block[off+6]
|
|
ftype = block[off+7]
|
|
if not (10 < inode < 500_000_000): return False
|
|
if name_len != len(name): return False
|
|
if rec_len < 8+name_len or rec_len > BSIZE or rec_len%4 != 0: return False
|
|
if ftype not in (1,2,7): return False
|
|
if block[off+8:off+8+name_len] != name: return False
|
|
pad = off+8+name_len
|
|
if pad < BSIZE and block[pad] != 0: return False
|
|
return True
|
|
|
|
def parse_extents(inode_data):
|
|
blocks = []
|
|
magic = struct.unpack_from('<H', inode_data, 40)[0]
|
|
if magic != 0xf30a:
|
|
return blocks
|
|
depth = struct.unpack_from('<H', inode_data, 46)[0]
|
|
entries = struct.unpack_from('<H', inode_data, 42)[0]
|
|
if depth == 0:
|
|
for i in range(min(entries, 4)):
|
|
off = 52 + i*12
|
|
ee_len = struct.unpack_from('<H', inode_data, off+4)[0]
|
|
ee_hi = struct.unpack_from('<H', inode_data, off+6)[0]
|
|
ee_lo = struct.unpack_from('<I', inode_data, off+8)[0]
|
|
ee_start = (ee_hi << 32) | ee_lo
|
|
if ee_len > 1024: continue
|
|
for b in range(min(ee_len, 8)):
|
|
blocks.append(ee_start + b)
|
|
return blocks
|
|
|
|
results = {}
|
|
|
|
print(f'Device: {DEVICE}')
|
|
print(f'Scanning groups {MIN_GROUP} to {NUM_GROUPS-1}...')
|
|
print()
|
|
|
|
with open(DEVICE, 'rb', buffering=0) as f:
|
|
for group in range(MIN_GROUP, NUM_GROUPS):
|
|
it_block = 1070 + group * 512
|
|
try:
|
|
f.seek(it_block * BSIZE)
|
|
inode_table = f.read(IPG * INODE_SZ)
|
|
except OSError:
|
|
continue
|
|
|
|
for idx in range(IPG):
|
|
inode_data = inode_table[idx*INODE_SZ:(idx+1)*INODE_SZ]
|
|
if not any(inode_data):
|
|
continue
|
|
|
|
mode = struct.unpack_from('<H', inode_data, 0)[0]
|
|
links = struct.unpack_from('<H', inode_data, 26)[0]
|
|
|
|
if (mode & 0xf000) != 0x4000:
|
|
continue
|
|
if links < 2:
|
|
continue
|
|
|
|
inode_num = group * IPG + idx + 1
|
|
blocks = parse_extents(inode_data)
|
|
|
|
for blk in blocks:
|
|
try:
|
|
f.seek(blk * BSIZE)
|
|
blk_data = f.read(BSIZE)
|
|
except OSError:
|
|
continue
|
|
|
|
for target in TARGETS:
|
|
if target not in blk_data:
|
|
continue
|
|
for off in range(0, BSIZE-8):
|
|
if blk_data[off+8:off+8+len(target)] != target:
|
|
continue
|
|
if is_valid_dirent(blk_data, off, target):
|
|
child_ino = struct.unpack_from('<I',blk_data,off)[0]
|
|
ftype = blk_data[off+7]
|
|
child_grp = (child_ino-1)//IPG
|
|
key = (target.decode(), child_ino)
|
|
if key not in results:
|
|
results[key] = (inode_num, ftype, child_grp)
|
|
status = 'INTACT' if child_grp>=13 else 'LOST'
|
|
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
|
|
print(f'[{status}] {target.decode()!r:15s} '
|
|
f'child={child_ino:10d} '
|
|
f'parent={inode_num:10d} '
|
|
f'type={tname}', flush=True)
|
|
|
|
if group % 1000 == 0:
|
|
print(f' Group {group}/{NUM_GROUPS}...', flush=True)
|
|
|
|
print()
|
|
print('=== SUMMARY ===')
|
|
for (name, child_ino), (parent_ino, ftype, grp) in sorted(results.items()):
|
|
status = 'INTACT' if grp >= 13 else 'LOST'
|
|
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
|
|
print(f'[{status}] {name!r:15s} child={child_ino} '
|
|
f'parent={parent_ino} type={tname}')
|
|
|