Adreno A7xx SDS->RB privilege bypass (GPU SMMU takeover to Kernel R/W)

Reading time: 7 minutes

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

Diese Seite fasst einen in-the-wild Adreno A7xx Microcode-Logikfehler (CVE-2025-21479) in reproduzierbare Exploit-Techniken zusammen: Ausnutzung der IB-Level-Maskierung in Set Draw State (SDS), um privilegierte GPU-Pakete aus einer unprivilegierten App auszuführen, Pivot auf GPU SMMU takeover und anschließend zu schnellem, stabilem Kernel R/W via dirty-pagetable trick.

  • Affected: Qualcomm Adreno A7xx GPU firmware prior to a microcode fix that changed masking of register $12 from 0x3 to 0x7.
  • Primitive: Execute privileged CP packets (e.g., CP_SMMU_TABLE_UPDATE) from SDS, which is user-controlled.
  • Outcome: Arbitrary physical/virtual kernel memory R/W, SELinux disable, root.
  • Prereq: Ability to create a KGSL GPU context and submit command buffers that enter SDS (normal app capability).

Hintergrund: IB levels, SDS and the $12 mask

  • Der Kernel verwaltet einen Ringpuffer (RB=IB0). Userspace übergibt IB1 via CP_INDIRECT_BUFFER, wobei Verkettungen zu IB2/IB3 möglich sind.
  • SDS ist ein spezieller Kommando-Stream, der via CP_SET_DRAW_STATE betreten wird:
  • A6xx: SDS wird als IB3 behandelt
  • A7xx: SDS wurde auf IB4 verschoben
  • Der Microcode verfolgt das aktuelle IB-Level im Register $12 und lässt privilegierte Pakete nur passieren, wenn das effektive Level IB0 (kernel RB) entspricht.
  • Bug: A7xx microcode maskierte $12 weiterhin mit 0x3 (2 bits) statt mit 0x7 (3 bits). Da IB4 & 0x3 == 0, wurde SDS fälschlich als IB0 identifiziert, was privilegierte Pakete aus user-controlled SDS zuließ.

Warum das wichtig ist:

A6XX                | A7XX
RB  & 3       == 0  |  RB  & 3       == 0
IB1 & 3       == 1  |  IB1 & 3       == 1
IB2 & 3       == 2  |  IB2 & 3       == 2
IB3 (SDS) & 3 == 3  |  IB3 & 3       == 3
|  IB4 (SDS) & 3 == 0   <-- misread as IB0 if mask is 0x3

Microcode-diff-Beispiel (Patch änderte die Maske auf 0x7):

@@ CP_SMMU_TABLE_UPDATE
- and $02, $12, 0x3
+ and $02, $12, 0x7
@@ CP_FIXED_STRIDE_DRAW_TABLE
- and $02, $12, 0x3
+ and $02, $12, 0x7

Exploitation overview

Ziel: Von SDS (fälschlicherweise als IB0 gelesen) privilegierte CP-Pakete senden, um die GPU SMMU auf vom Angreifer erstellte Pagetabellen umzuleiten, und dann GPU copy/write-Pakete für beliebiges physisches R/W verwenden. Schließlich auf ein schnelles CPU-seitiges R/W via dirty pagetable pivoten.

High-level chain

  • Eine gefälschte GPU Pagetabelle im shared memory erstellen
  • SDS betreten und ausführen:
  • CP_SMMU_TABLE_UPDATE -> auf gefälschte Pagetabelle umschalten
  • CP_MEM_WRITE / CP_MEM_TO_MEM -> Write/Read-Primitiven implementieren
  • CP_SET_DRAW_STATE mit run-now Flags (sofortige Ausführung)

GPU R/W primitives via fake pagetable

  • Write: CP_MEM_WRITE an eine vom Angreifer gewählte GPU VA, deren PTEs du auf eine gewählte PA mapst -> beliebiger physischer Schreibzugriff
  • Read: CP_MEM_TO_MEM kopiert 4/8 Bytes von der Ziel-PA in einen userspace-geteilten Puffer (Batch für größere Reads)

Notes

  • Jeder Android-Prozess erhält einen KGSL-Kontext (IOCTL_KGSL_GPU_CONTEXT_CREATE). Das Wechseln der Kontexte aktualisiert normalerweise die SMMU-Tabellen im RB; der Bug erlaubt es, dies in SDS zu tun.
  • Übermäßiger GPU-Traffic kann UI-Ausfälle und Reboots verursachen; Lesevorgänge sind klein (4/8B) und Sync ist standardmäßig langsam.

Building the SDS command sequence

  • Eine gefälschte GPU-Pagetabelle in den shared memory sprühen, sodass mindestens eine Instanz an einer bekannten physischen Adresse landet (z. B. via allocator grooming und Wiederholung).
  • Einen SDS-Buffer konstruieren, der in folgender Reihenfolge enthält:
  1. CP_SMMU_TABLE_UPDATE auf die physische Adresse der gefälschten Pagetabelle
  2. Ein oder mehrere CP_MEM_WRITE und/oder CP_MEM_TO_MEM Pakete, um R/W mittels deiner neuen Adress-Übersetzungen zu implementieren
  3. CP_SET_DRAW_STATE mit Flags, um sofort auszuführen

Die genauen Packet-Encodings variieren je nach Firmware; benutze freedreno’s afuc/packet docs, um die Worte zusammenzusetzen, und stelle sicher, dass der SDS-Submission-Pfad vom Treiber genommen wird.

Finding Samsung kernel physbase under physical KASLR

Samsung randomisiert die physische Kernel-Basis innerhalb eines bekannten Bereichs auf Snapdragon-Geräten. Brute-force den erwarteten Bereich und suche nach den ersten 16 Bytes von _stext.

Representative loop

c
while (!ctx->kernel.pbase) {
offset += 0x8000;
uint64_t d1 = kernel_physread_u64(ctx, base + offset);
if (d1 != 0xd10203ffd503233f) continue;   // first 8 bytes of _stext
uint64_t d2 = kernel_physread_u64(ctx, base + offset + 8);
if (d2 == 0x910083fda9027bfd) {           // second 8 bytes of _stext
ctx->kernel.pbase = base + offset - 0x10000;
break;
}
}

Sobald physbase bekannt ist, berechne das kernel virtual mit der linearen Abbildung:

_stext = 0xffffffc008000000 + (Kernel Code & ~0xa8000000)

Stabilisierung auf schnellen, zuverlässigen CPU-seitigen Kernel R/W (dirty pagetable)

GPU R/W ist langsam und hat eine geringe Granularität. Wechsle zu einer schnellen/stabilen Primitive, indem du deine eigenen Prozess-PTEs korrumpierst („dirty pagetable“):

Steps

  • Finde current task_struct -> mm_struct -> mm_struct->pgd mithilfe der langsamen GPU R/W-Primitives
  • mmap zwei benachbarte userspace pages A und B (z. B. bei 0x1000)
  • Durchlaufe PGD->PMD->PTE, um die physischen Adressen der PTEs von A/B aufzulösen (Hilfsfunktionen: get_pgd_offset, get_pmd_offset, get_pte_offset)
  • Überschreibe B’s PTE, damit sie auf die last-level pagetable zeigt, die A/B verwaltet, mit RW-Attributen (phys_to_readwrite_pte)
  • Schreibe über B’s VA, um A’s PTE zu verändern, sodass es Ziel-PFNs abbildet; lese/schreibe Kernel-Speicher über A’s VA und flush TLB, bis ein Sentinel umschlägt
Beispiel dirty-pagetable pivot snippet
c
uint64_t *map = mmap((void*)0x1000, PAGE_SIZE*2, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, 0, 0);
uint64_t *page_map = (void*)((uint64_t)map + PAGE_SIZE);
page_map[0] = 0x4242424242424242;

uint64_t tsk = get_curr_task_struct(ctx);
uint64_t mm = kernel_vread_u64(ctx, tsk + OFFSETOF_TASK_STRUCT_MM);
uint64_t mm_pgd = kernel_vread_u64(ctx, mm + OFFSETOF_MM_PGD);

uint64_t pgd_off = get_pgd_offset((uint64_t)map);
uint64_t phys_pmd = kernel_vread_u64(ctx, mm_pgd + pgd_off) & ~((1<<12)-1);
uint64_t pmd_off = get_pmd_offset((uint64_t)map);
uint64_t phys_pte = kernel_pread_u64(ctx, phys_pmd + pmd_off) & ~((1<<12)-1);
uint64_t pte_off = get_pte_offset((uint64_t)map);
uint64_t pte_addr = phys_pte + pte_off;
uint64_t new_pte = phys_to_readwrite_pte(pte_addr);
kernel_write_u64(ctx, pte_addr + 8, new_pte, false);
while (page_map[0] == 0x4242424242424242) flush_tlb();

Detection and hardening

  • Firmware/microcode: alle Stellen, die $12 maskieren, so ändern, dass 0x7 verwendet wird (A7xx) und privileged packet gates auditieren
  • Driver: den effektiven IB-Level für privileged packets validieren und per-Kontext allowlists durchsetzen
  • Telemetry: Alarm auslösen, wenn CP_SMMU_TABLE_UPDATE (oder ähnliche privileged opcodes) außerhalb von RB/IB0 erscheint, besonders in SDS; anomale Bursts von 4/8-Byte CP_MEM_TO_MEM und übermäßige TLB-flush-Muster überwachen
  • Kernel: pagetable metadata härten und Muster von user PTE-Korruption erkennen

Impact

Eine lokale App mit GPU-Zugriff kann privileged GPU packets ausführen, die GPU SMMU übernehmen, beliebige kernel physical/virtual R/W erreichen, SELinux deaktivieren und auf betroffenen Snapdragon A7xx-Geräten (z. B. Samsung S23) root erlangen. Severity: High (kernel compromise).

References

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks