Branch Target Reuse. Gdy JIT znika, a procesor nadal pamięta, gdzie skoczyć.

Badacze z VU Amsterdam i Scuola Superiore Sant’Anna wykazali, że po usunięciu kodu JIT procesor może nadal przechowywać w predyktorze stary cel skoku pośredniego. W praktycznym ataku na Linuksa z wykorzystaniem cBPF udało im się w ciągu kilku minut odtworzyć hash hasła roota, mimo włączonych domyślnych zabezpieczeń. Sam mechanizm zaobserwowano na procesorach Intela, AMD i Arm, ale pełny exploit udało się zbudować tylko na platformach Intela.

Paź 05, 2026 - 16:19
Zaktualizowano: 3 dni temu
0 7
Branch Target Reuse. Gdy JIT znika, a procesor nadal pamięta, gdzie skoczyć.
Obraz wygenerowany przez "sztuczną inteligencję".

Branch Target Reuse: gdy JIT znika, a procesor nadal pamięta, dokąd skoczyć

Fragment kodu wygenerowanego przez JIT zostaje zwolniony, ale wpis w predyktorze skoków może pozostać. Gdy ten sam obszar pamięci zostanie później wykorzystany przez nowy kod, procesor może przy kolejnym skoku pośrednim użyć starego celu i przejściowo rozpocząć wykonanie w miejscu, które należało do poprzedniego programu. Czasem oznacza to wejście w środek nowej instrukcji.

Na tym opiera się Branch Target Reuse (BTR) - nowy wariant Spectre-v2 opisany przez Sandera Wiebinga, Yuhui Zhu, Alessandro Biondiego i Cristiano Giuffridę.

Nie jest to nowy błąd w samym mechanizmie predykcji skoków. Spectre-v2 od 2018 roku wykorzystuje fakt, że procesor może spekulacyjnie skierować skok pośredni pod niewłaściwy adres, wykonać znajdujące się tam instrukcje, a następnie pozostawić mierzalny ślad w stanie mikroarchitektury, na przykład w pamięci podręcznej.

BTR dodaje do tego drugi problem.

Po zmianie kodu procesor przywraca jego spójność z punktu widzenia zwykłego wykonania programu. Badacze pokazali jednak, że na testowanych procesorach nie oznacza to automatycznego usunięcia wcześniejszych wpisów dotyczących celu skoku pośredniego. Nieaktualny wpis BTB może więc przeżyć kod, na podstawie którego powstał, i zostać ponownie użyty, gdy pod tym samym adresem pojawi się zupełnie inny fragment programu.

Autorzy określają ten mechanizm jako transient execute-after-free - odpowiednik use-after-free zachodzący nie na poziomie architektonicznym, lecz podczas wykonania przejściowego.

BTR jest przy tym pierwszym praktycznym atakiem Spectre-v2 typu in-place: ten sam skok pośredni może służyć zarówno do treningu predyktora, jak i późniejszego wywołania błędnej spekulacji.

Skąd bierze się okno ataku

Silniki JIT generują kod maszynowy podczas działania programu, a niepotrzebne już fragmenty później zwalniają. Odzyskany obszar pamięci może następnie zostać wykorzystany ponownie.

W przypadku BPF JIT w Linuksie wiele małych programów jest umieszczanych we wspólnych większych obszarach pamięci wykonywalnej. Po usunięciu jednego programu jego miejsce może zostać przeznaczone dla następnego. Z punktu widzenia oprogramowania stary fragment przestał być aktywnym kodem. Predyktor skoków może jednak nadal przechowywać cel związany z jego wcześniejszym adresem.

Warunki potrzebne do przeprowadzenia ataku są dość konkretne, ale w środowisku JIT występują naturalnie. Atakujący musi doprowadzić do utworzenia kontrolowanego fragmentu kodu, wielokrotnie wykonać odpowiedni skok pośredni, aby wytrenować predyktor, następnie spowodować zwolnienie kodu i ponowne wykorzystanie tego samego obszaru, a na końcu ponownie uruchomić skok, zanim stary wpis zostanie wyparty przez inną aktywność procesora.

Jeżeli wszystkie te warunki zostaną spełnione, procesor może spekulacyjnie użyć celu należącego do starego kodu, mimo że pod tym adresem znajduje się już coś innego. Na x86 szczególnie użyteczne staje się wejście do nowego kodu z niewłaściwego przesunięcia.

Instrukcje x86 mają zmienną długość. Sekwencja bajtów, która przy prawidłowym wyrównaniu oznacza jedną instrukcję wraz z jej argumentem, po rozpoczęciu dekodowania jeden lub dwa bajty dalej może stać się zupełnie innym kodem.

W cBPF badacze wykorzystali czterobajtowe wartości natychmiastowe generowane w instrukcjach add. Odpowiednio dobrane bajty po wejściu z niewłaściwego przesunięcia dekodowały się jako NOP oraz skok pośredni. Skok można było następnie skierować pod adres zależny od argumentów wywołania systemowego przechowywanych w strukturze seccomp_data.

Po wykryciu błędnej predykcji procesor oczywiście wycofuje spekulacyjne wykonanie. Nie cofa jednak wszystkich jego skutków mikroarchitektonicznych. Gadget zdąży wcześniej uzależnić stan pamięci podręcznej od wartości sekretu, którą następnie można odczytać kanałem bocznym Flush+Reload.

Exploit na cBPF

Pełny exploit powstał dla klasycznego BPF działającego w jądrze Linuksa, a nie dla eBPF. To ważne rozróżnienie. eBPF oferuje znacznie większe możliwości, ale jego nieuprzywilejowane użycie jest ograniczone. Starszy cBPF nadal pozostaje dostępny dla zwykłych procesów, między innymi za pośrednictwem filtrów seccomp i Linux Socket Filtering.

cBPF ma bardzo ograniczony model wykonania: dwa 32-bitowe rejestry, wyłącznie skoki do przodu i brak dereferencji adresów znajdujących się w rejestrach. Właśnie dzięki tym ograniczeniom przez lata był traktowany jako stosunkowo bezpieczny mechanizm filtrowania dostępny dla nieuprzywilejowanego kodu.

W demonstracji BTR atakujący nie potrzebuje CAP_BPF. Badacze wykorzystali filtry seccomp. Skok pośredni znajdujący się w funkcji __bpf_prog_run wywołuje kolejne programy cBPF i został wykorzystany jako skok ofiary.

Najpierw uruchamiany jest fragment treningowy. Wielokrotne wykonanie skoku powoduje zapisanie jego celu w predyktorze. Filtr jest następnie usuwany. Autorzy osiągają to przez zainstalowanie go w osobnym wątku i zakończenie tego wątku.

W zwolnionym obszarze umieszczany jest później nowy program. Dzięki odpowiedniemu sterowaniu alokacją jego kod częściowo pokrywa się z wcześniejszym fragmentem. Stary wpis predyktora nadal wskazuje adres używany wcześniej przez kod treningowy. Po ponownym wykonaniu skoku procesor może więc przejściowo wejść w środek nowego programu.

Tam znajdują się przygotowane wcześniej bajty tworzące gadget wykorzystywany do dalszego przekierowania spekulacyjnego wykonania.

Od kilku kilobajtów na sekundę do praktycznego exploita

Wyniki wydajności trzeba rozdzielić, ponieważ dotyczą różnych etapów eksperymentu. W mikrobenchmarku badającym sam prymityw cBPF współczynnik ponownego użycia obszaru wynosił około 38 procent. Jeden wpis BTB przeżywał pełen cykl w obu testowanych mikroarchitekturach Intela.

Na tej podstawie badacze oszacowali teoretyczną przepustowość prymitywu na:

  • 5,7 KB/s dla Raptor Cove

  • 5,4 KB/s dla Lion Cove

Nie są to jednak wyniki kompletnego exploita. Ten pomiar korzystał z uproszczonego środowiska eksperymentalnego i miał określić potencjał samego mechanizmu.

Pełny exploit jest znacznie wolniejszy. Na Ubuntu 24.04 z jądrem 6.14.0-27, przy domyślnych zabezpieczeniach Spectre, badacze uzyskali około 8 B/s zarówno na Raptor Cove, jak i Lion Cove. Do demonstracji wykorzystano proces su.

Po wykonaniu su root hash hasła roota znajduje się w pamięci sterty procesu. Exploit najpierw łamie KASLR, znajduje odpowiedni task_struct, przechodzi przez listę procesów i tablice stron, a następnie lokalizuje właściwy fragment pamięci procesu.

Nie jest to wyciek hasła w postaci jawnej. Atakujący uzyskuje jego hash, który może później próbować łamać offline.

Średni czas pełnego ataku wyniósł około:

  • 3 minut na Core i9-14900K z Raptor Cove

  • 5 minut na Core Ultra 9 285K z Lion Cove

Autorzy przygotowali również drugi wariant, działający przy włączonym bpf_jit_harden. Mechanizm constant blinding utrudnia JIT spraying przez maskowanie wartości natychmiastowych. W domyślnej konfiguracji nie jest jednak włączony. Badacze obeszli tę ochronę, kodując kontrolowane bajty nie w wartościach natychmiastowych, lecz w przesunięciach skoków.

Maksymalne uzyskane przesunięcie wynosiło 0x014fc5, co pozwalało kontrolować dwa z czterech bajtów offsetu. Drugi exploit osiągał około 10 B/s na obu procesorach i odzyskiwał hash w około pięć minut. Pierwszy wariant działał więc przy domyślnych zabezpieczeniach Ubuntu, natomiast drugi pokazał dodatkowo, że sam constant blinding również nie zamyka tej klasy ataku.

Nie każdy procesor pozwala zbudować pełny exploit

Sam podstawowy problem tj. przetrwanie starego celu predykcji po zmianie kodu, potwierdzono na wszystkich testowanych procesorach Intela, AMD i Arm. Nie oznacza to jednak, że BTR jest równie praktyczny na każdej platformie.

Kluczowa jest trwałość wpisów BTB podczas rzeczywistej aktywności systemu. Na Lion Cove po dziesięciu cyklach fork + execve przeżywało około 230 wpisów, po stu około 130, a nawet po tysiącu wciąż pozostawało około 50. Na Zen 4 po dziesięciu wykonaniach execve nie pozostawał już żaden z badanych wpisów.

Na procesorach Arm - Cortex-A76 i Cortex-X3, wpisy również nie przeżywały testowanego cyklu fork + execve. Pełne exploity cBPF powstały więc tylko dla Intela.

AMD odpadało z tego konkretnego scenariusza dodatkowo dlatego, że obecna implementacja AutoIBRS wyłącza w jądrze Linuksa predykcję skoków pośrednich wykorzystywaną przez ten atak. Nie należy zatem streszczać BTR stwierdzeniem, że "każdy współczesny procesor pozwala wykraść hash hasła w kilka minut".

Wspólny dla badanych układów jest problem niespójności między stanem predyktora a aktualną zawartością kodu. Praktyczna podatność zależy jednak od mikroarchitektury i zachowania konkretnego silnika JIT.

SpiderMonkey

Autorzy zbadali również SpiderMonkey, silnik JavaScript i WebAssembly używany przez Firefoxa. Skokiem ofiary został IonGenericCallStub, czyli wspólny trampoline używany przy części dynamicznych wywołań funkcji.

Do tworzenia gadgetów wykorzystano sposób, w jaki backend WebAssembly obsługuje wielobajtowe stałe zmiennoprzecinkowe i wektorowe. Wartości z instrukcji takich jak f64.const oraz v128.const trafiają do puli literałów umieszczonej w wykonywalnym buforze kodu.

Dzięki temu kontrolowane przez atakującego dane mogą po przejściowym potraktowaniu ich jako instrukcji stać się gadgetem ujawniającym dane. Ponowne wykorzystanie obszaru pamięci wynosiło średnio:

  • 49,2% na x86-64,

  • 97,6% na ARM64.

Po pełnym cyklu zwolnienia i ponownej alokacji wpisy BTB przeżywały jednak tylko na badanych procesorach Intela: średnio siedem na Raptor Cove i sześć na Lion Cove.

Dawało to szacowaną przepustowość odpowiednio około 36 B/s i 62 B/s. Badacze nie zbudowali pełnego exploita uruchamianego z normalnej złośliwej strony internetowej. Do takiego ataku potrzebne byłyby jeszcze między innymi mechanizmy wydłużające okno spekulacji oraz kanał boczny działający mimo ograniczonej precyzji zegarów w przeglądarce. Izolacja witryn dodatkowo ogranicza potencjalny zakres danych dostępnych w tym samym procesie.

GraalVM

Trzecim badanym środowiskiem był GraalVM, a dokładniej GraalPy działający w konfiguracji chroniącej nieufny kod. GraalVM stosuje Software Fault Isolation. Dostęp do pamięci jest maskowany tak, aby kod działający w sandboxie nie mógł wyjść poza przydzielony mu obszar.

BTR pozwala jednak spekulacyjnie rozpocząć wykonanie już za instrukcją wykonującą to maskowanie. W efekcie procesor może przejściowo wykonać dostęp do danych znajdujących się poza sandboxem. Samo ponowne wykorzystanie adresów działało bardzo dobrze:

  • około 97% na Raptor Cove

  • około 95% na Lion Cove

Problem pojawiał się później, a mianowicie - kompilacja JIT i garbage collector generowały na tyle dużo dodatkowej aktywności, że wpisy BTB nie przeżywały pełnego cyklu. W eksperymencie kończyło się to zerową praktyczną przepustowością kanału.

Autorzy nie uważają tego za fundamentalną właściwość GraalVM. Lepsze sterowanie kolejnością alokacji, harmonogramem rdzeni lub aktywnością GC mogłoby zmniejszyć szum, ale w przedstawionym eksperymencie pełny wyciek nie działał.

Co już załatano?

Dla Linuksa przygotowano dwie poprawki związane z BTR:

  • CVE-2026-64507 - x86/bugs: Enable IBPB flush on BPF JIT allocation,

  • CVE-2026-64508 - bpf: Support for hardening against JIT spraying.

Pierwsza włącza na x86 czyszczenie predyktora przez IBPB podczas ponownego wykorzystania pamięci BPF JIT, gdy aktywne są odpowiednie zabezpieczenia Spectre-v2. Implementacja pomija tę operację, jeśli dispatcher BPF korzysta już z sekwencji retpoline.

Druga dodaje do alokatora BPF mechanizm wywoływania architektonicznej funkcji czyszczenia predyktora przed ponownym wykorzystaniem obszaru wcześniej zajmowanego przez inny program JIT.

Alokacje większe niż pojedynczy pakiet alokatora nie są objęte tym mechanizmem. Dla cBPF uznano to za bezpieczne założenie, ponieważ nieuprzywilejowane programy są odpowiednio małe. Jądro generuje ostrzeżenie, jeżeli założenie przestałoby być spełnione.

Producenci procesorów uznali, że odpowiednie mechanizmy sprzętowe, takie jak IBPB, już istnieją, a właściwym miejscem wdrożenia ochrony jest oprogramowanie. Linux i Oracle wprowadziły odpowiednie zmiany, natomiast Mozilla skupiła się przede wszystkim na dalszym wdrażaniu izolacji witryn.

IBT i BTI nie rozwiązują całego problemu

Indirect Branch Tracking na x86 oraz Branch Target Identification na Arm ograniczają dozwolone cele skoków pośrednich. Na x86 poprawny cel musi rozpoczynać się od endbr64, na Arm od odpowiedniej instrukcji BTI. Nie oznacza to jednak automatycznej odporności na BTR.

Na starszych procesorach Intela kontrola IBT może następować na tyle późno, że przed jej zakończeniem procesor zdąży przejściowo wykonać co najmniej jedną instrukcję. Badacze ustalili, że Lion Cove jest pierwszą testowaną przez nich generacją Intela, w której IBT nie pozostawia takiego okna. Na Raptor Cove udało się jeszcze przejściowo wykonać jedną instrukcję.

Nawet przy pozbawionym tego wyścigu IBT pozostaje możliwość osadzenia sekwencji bajtów odpowiadającej endbr64 bezpośrednio w kodzie JIT. W przypadku cBPF autorom udało się to jednak tylko przy wyłączonym constant blinding. Połączenie IBT bez okna spekulacyjnego z constant blinding stanowi więc znacznie silniejszą ochronę.

Nie jest jednak absolutną gwarancją bezpieczeństwa. Autorzy pokazują także bardziej złożone możliwości wykorzystania ścieżki powrotu i RSB.

BTR nie kończy się na jednym CVE

Aktualizacja jądra zamyka praktyczne okno wykorzystane w zaprezentowanych exploitach cBPF. Nie rozwiązuje natomiast samego zjawiska. BTR wynika z tego, że stan predyktora skoków nie musi być automatycznie synchronizowany z cyklem życia dynamicznie generowanego kodu.

Jeżeli silnik JIT:

  • pozwala wpływać na generowany kod

  • zwalnia i ponownie wykorzystuje te same adresy

  • posiada skok pośredni prowadzący do takiego kodu

  • a nieaktualne wpisy predyktora przeżywają wystarczająco długo

to powstaje wtedy potencjalny mechanizm bardzo podobny do opisanego przez autorów. IBPB może usunąć stare predykcje. Retpoline może ograniczyć spekulacyjne skoki pośrednie. Randomizacja położenia kodu może natomiast utrudnić ponowne wykorzystanie tego samego adresu.

Każde z tych rozwiązań ma jednak koszt - głównie wydajnościowy, implementacyjny albo oba naraz. Najważniejszy wniosek z BTR jest dlatego szerszy niż dwa nowe CVE w Linuksie. JIT może poprawnie usunąć stary kod i poprawnie zapisać w jego miejscu nowy, a procesor nadal może przez pewien czas pamiętać, dokąd opłacało mu się skakać wcześniej.

I właśnie ta różnica między aktualnym kodem a nieaktualnym stanem mikroarchitektury wystarczyła, aby zbudować działający exploit Spectre-v2.

Acid

Zawodowo związany z zarządzaniem infrastrukturą IT, cyberbezpieczeństwem i technologiami sieciowymi. Pasjonat prywatności cyfrowej, kryptografii, etycznego hackingu oraz wolności słowa w sieci. Zwolennik technicznej precyzji, niezależnego myślenia i zdrowego sceptycyzmu wobec wszelkich autorytetów. W HACKIN9 łączy praktyczne doświadczenie z zamiłowaniem do analizy technologii i jej wpływu na współczesny świat. Bez korporacyjnej nowomowy, niepotrzebnej sensacji i wiary w cudowne rozwiązania.

Komentarze (0)

User