DF-1469 / harness.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 | /* * DF-1469 harness โ processpptables VBIOS entry-count trusted without validation * * Reproduces the vulnerable pattern across the table parsers in * sys/dev/drm/amd/powerplay/hwmgr/processpptables.c * representative site: * :383-399 get_clock_voltage_dependency_table() * Other sites: :1089-1107 (uvd), :1122-1139 (vce), :1153-1167 (samu), * :1181-1195 (acp), :1379-1399 (cac_leakage), :1520-1537 (phase_shed). * * The pattern: the parser sizes the destination buffer by the VBIOS-supplied * UCHAR count AND iterates that many times reading `table->entries[i]` from * the VBIOS region. The count is never validated to actually fit inside the * firmware image (soft_pp_table_size is available at :844 but unused). * * Representative kernel code (processpptables.c:383-399): * * table_size = sizeof(unsigned long) + * sizeof(struct phm_clock_voltage_dependency_table) * * table->ucNumEntries; // trusted VBIOS count * dep_table = kzalloc(table_size, GFP_KERNEL); * dep_table->count = (unsigned long)table->ucNumEntries; * for (i = 0; i < dep_table->count; i++) { * dep_table->entries[i].clk = ... table->entries[i].ucClockHigh ...; // OOB read * dep_table->entries[i].v = ... table->entries[i].usVoltage ...; * } * * `table->entries[]` is a flex array in the VBIOS region. With ucNumEntries * inflated beyond what the image actually holds, the loop reads entries off * the end of the actual VBIOS data into adjacent kernel memory. The parsed * (corrupted) values are then used as clocks/voltages, driving MMIO writes * (corruption) and/or leaked via sysfs pp_dpm_sclk / hwmon. * * Build: cc -O2 -Wall -o harness harness.c * Run: ./harness * * Proof strategy: marker redzone. The "real" VBIOS table holds REAL entries; * the attacker-inflated ucNumEntries makes the loop walk past them into a * marker-filled redzone (adjacent slab/VBIOS bytes). Show the parsed values * equal the marker -> the loop read memory beyond the legitimate table. */ #include <stdio.h> #include <stdint.h> #include <stddef.h> #include <stdlib.h> #include <string.h> typedef uint8_t UCHAR; typedef uint16_t USHORT; typedef uint32_t ULONG; #define REAL_ENTRIES 2 /* the genuine table has 2 entries */ #define REDZONE_ENTRIES 6 /* marker-filled slots past the real table */ #define MARKER_CLK 0xABCDEF #define MARKER_VOLT 0x1234 /* ATOM_PPLIB_Clock_Voltage_Dependency_Record โ representative VBIOS entry. */ typedef struct __attribute__((packed)) { UCHAR ucClockHigh; /* 1 */ USHORT usClockLow; /* 2 */ USHORT usVoltage; /* 2 */ } dep_record; /* 5 bytes */ /* ATOM_PPLIB_Clock_Voltage_Dependency_Table โ flex array. The buffer below * holds REAL + REDZONE entries to expose what the kernel reads off the end. */ typedef struct { UCHAR ucNumEntries; dep_record entries[REAL_ENTRIES + REDZONE_ENTRIES]; } dep_table_vbios; /* Faithful replica of processpptables.c:391-399. The loop bound is whatever * ucNumEntries says, with NO check that it fits the image. */ static void parse_dep_table(ULONG *out_clks, ULONG *out_volts, const dep_table_vbios *t, ULONG count) { ULONG i; for (i = 0; i < count; i++) { out_clks[i] = ((ULONG)t->entries[i].ucClockHigh << 16) | t->entries[i].usClockLow; /* :394-396 */ out_volts[i] = t->entries[i].usVoltage; /* :397-398 */ } } int main(void) { dep_table_vbios *t = calloc(1, sizeof(*t)); ULONG *clks = calloc(REAL_ENTRIES + REDZONE_ENTRIES, sizeof(ULONG)); ULONG *volts = calloc(REAL_ENTRIES + REDZONE_ENTRIES, sizeof(ULONG)); if (!t || !clks || !volts) { perror("calloc"); return 1; } /* Real entries: */ t->ucNumEntries = REAL_ENTRIES; for (int i = 0; i < REAL_ENTRIES; i++) { t->entries[i].ucClockHigh = 0x01; t->entries[i].usClockLow = 0x1000 + i; t->entries[i].usVoltage = 0x0800 + i; } /* Redzone (adjacent kernel/VBIOS bytes the attacker-reached loop will read): */ for (int i = REAL_ENTRIES; i < REAL_ENTRIES + REDZONE_ENTRIES; i++) { t->entries[i].ucClockHigh = (MARKER_CLK >> 16) & 0xFF; t->entries[i].usClockLow = MARKER_CLK & 0xFFFF; t->entries[i].usVoltage = MARKER_VOLT; } printf("DF-1469 processpptables VBIOS entry-count OOB read harness\n"); printf("real ucNumEntries = %u (what the image actually holds)\n", REAL_ENTRIES); printf("sizeof(dep_record) = %zu\n", sizeof(dep_record)); printf("marker clk=0x%06x volt=0x%04x placed in redzone slots %d..%d\n", MARKER_CLK, MARKER_VOLT, REAL_ENTRIES, REAL_ENTRIES + REDZONE_ENTRIES - 1); /* In-bounds: attacker supplies the truthful count. */ parse_dep_table(clks, volts, t, REAL_ENTRIES); printf("\nIn-bounds ucNumEntries=%u -> clk[0]=0x%lx volt[0]=0x%lx (OK)\n", REAL_ENTRIES, (unsigned long)clks[0], (unsigned long)volts[0]); /* OOB: attacker inflates ucNumEntries to REAL + REDZONE. The loop walks * past the real entries into the redzone. */ ULONG inflated = REAL_ENTRIES + REDZONE_ENTRIES; parse_dep_table(clks, volts, t, inflated); printf("OOB ucNumEntries=%lu (inflated) -> clk[%u]=0x%lx volt[%u]=0x%lx\n", (unsigned long)inflated, REAL_ENTRIES, (unsigned long)clks[REAL_ENTRIES], REAL_ENTRIES, (unsigned long)volts[REAL_ENTRIES]); printf(" (expected marker clk=0x%06x volt=0x%04x)\n", MARKER_CLK, MARKER_VOLT); /* Worst case: UCHAR max = 255. */ printf("Worst case ucNumEntries=255 -> loop reads %d entries, %zu bytes past entries[0]\n", 255, (size_t)255 * sizeof(dep_record)); int leaked = (clks[REAL_ENTRIES] == MARKER_CLK && volts[REAL_ENTRIES] == MARKER_VOLT); if (leaked) { printf("\nRESULT: heap OOB read CONFIRMED (processpptables.c:393 count loop)\n"); printf("Every table parser trusts the VBIOS UCHAR count to size BOTH the\n"); printf("destination kmalloc AND the source loop; soft_pp_table_size (:844)\n"); printf("is available but never used as a bound. Inflated count -> OOB read\n"); printf("of adjacent kernel memory, parsed as clocks/voltages.\n"); free(t); free(clks); free(volts); return 0; } printf("\nUNEXPECTED: marker not observed\n"); free(t); free(clks); free(volts); return 1; } |