#!/usr/bin/env python3 """ Scan inode tables directly for directory inodes, then read their data blocks looking for target names. Much faster than full disk scan. """ import struct, os CHUNK = 128 * 512 LV_START = 5120000 * 512 BSIZE = 4096 DISKS = ['/dev/sda', '/dev/sde', '/dev/sdd', '/dev/sdc'] IPG = 8192 INODE_SZ = 256 BPG = 32768 NUM_GROUPS = 35728 MIN_GROUP = 13 # groups 0-12 are zeroed TARGETS = [ b'pterodactyl', b'var', b'mysql', b'www', b'log', b'docker', b'nginx', b'apache2', b'www-data', ] def v_to_p(virt_byte): """Virtual byte offset to physical (disk 0) byte offset.""" group = virt_byte // (5 * CHUNK) in_group = virt_byte % (5 * CHUNK) chunk_idx = in_group // CHUNK intra = in_group % CHUNK if chunk_idx == 4: return None return LV_START + group*4*CHUNK + chunk_idx*CHUNK + intra def read_virt(f, virt_byte, length): """Read from virtual address space via disk 0.""" phys = v_to_p(virt_byte) if phys is None: return b'\x00' * length f.seek(phys) return f.read(length) def is_valid_dirent(block, off, name): if off + 8 + len(name) > BSIZE: return False inode = struct.unpack_from(' 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): """Get list of physical block numbers from extent tree.""" blocks = [] magic = struct.unpack_from('=13 else 'LOST' tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?') print(f'[{status}] {target.decode()!r:15s} ' f'child_inode={child_ino:10d} ' f'parent_inode={inode_num:10d} ' f'type={tname}') 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} ' f'child={child_ino} parent={parent_ino} type={tname}')