Artykuł wspomniany przez sgbj w komentarzach napisanych przez Paula Turnera z Google wyjaśnia znacznie bardziej szczegółowo, ale dam mu szansę:
O ile potrafię poskładać to razem z ograniczonymi informacjami w tej chwili, retpolina jest trampoliną powrotną, która wykorzystuje nieskończoną pętlę, która nigdy nie jest wykonywana, aby zapobiec spekulowaniu procesora na celu pośredniego skoku.
Podstawowe podejście można znaleźć w gałęzi jądra Andi Kleen zajmującej się tym problemem:
Wprowadza nowe __x86.indirect_thunk
wywołanie, które ładuje cel wywołania, którego adres pamięci (który zadzwonię ADDR
) jest przechowywany na górze stosu i wykonuje skok za pomocą RET
instrukcji. Sam thunk jest następnie wywoływany za pomocą makra NOSPEC_JMP / CALL , który został użyty do zastąpienia wielu (jeśli nie wszystkich) pośrednich wywołań i skoków. Makro po prostu umieszcza cel wywołania na stosie i ustawia adres zwrotny, jeśli to konieczne (zwróć uwagę na nieliniowy przepływ sterowania):
.macro NOSPEC_CALL target
jmp 1221f /* jumps to the end of the macro */
1222:
push \target /* pushes ADDR to the stack */
jmp __x86.indirect_thunk /* executes the indirect jump */
1221:
call 1222b /* pushes the return address to the stack */
.endm
Umieszczenie call
na końcu jest konieczne, aby po zakończeniu wywołania pośredniego przepływ sterujący był kontynuowany za użyciem NOSPEC_CALL
makra, dzięki czemu można go używać zamiast zwykłegocall
Sam thunk wygląda następująco:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
Przepływ kontroli może być tutaj nieco mylący, więc wyjaśnię:
call
wypycha bieżący wskaźnik instrukcji (etykieta 2) na stos.
lea
dodaje 8 do wskaźnika stosu , skutecznie odrzucając ostatnio wypisane cztero słowo, które jest ostatnim adresem zwrotnym (do etykiety 2). Następnie górna część stosu ponownie wskazuje na prawdziwy adres zwrotny ADDR.
ret
przeskakuje *ADDR
i resetuje wskaźnik stosu na początek stosu wywołań.
W końcu całe to zachowanie jest praktycznie równoważne skokowi bezpośrednio *ADDR
. Jedną z korzyści, jakie otrzymujemy, jest to, że predyktor gałęzi używany dla instrukcji return (Return Stack Buffer, RSB), podczas wykonywania call
instrukcji, zakłada, że odpowiednia ret
instrukcja przejdzie do etykiety 2.
Część po etykiecie 2 tak naprawdę nigdy nie zostanie wykonana, jest to po prostu nieskończona pętla, która teoretycznie wypełniłaby potok JMP
instrukcji instrukcjami. Za pomocą LFENCE
, PAUSE
lub bardziej ogólnie instrukcja powodując rurociąg instrukcja będzie stoisko zatrzymuje CPU od marnować energię i czas na wykonanie tego spekulacyjnego. Wynika to z tego, że w przypadku normalnego powrotu wywołania retpoline_call_target LFENCE
następna instrukcja do wykonania. Tego też przewidzi predyktor gałęzi na podstawie oryginalnego adresu zwrotnego (etykieta 2)
Cytat z podręcznika architektury Intela:
Instrukcje następujące po LFENCE mogą zostać pobrane z pamięci przed LFENCE, ale nie zostaną wykonane, dopóki LFENCE się nie zakończy.
Zauważ jednak, że specyfikacja nigdy nie wspomina, że LFENCE i PAUZA powodują zatrzymanie potoku, więc czytam trochę między wierszami tutaj.
Wróćmy do pierwotnego pytania: Ujawnienie informacji o pamięci jądra jest możliwe dzięki połączeniu dwóch pomysłów:
Mimo że wykonywanie spekulacyjne powinno być wolne od skutków ubocznych, gdy spekulacja była błędna, wykonywanie spekulacyjne nadal wpływa na hierarchię pamięci podręcznej . Oznacza to, że gdy ładowanie pamięci jest wykonywane spekulacyjnie, mogło nadal powodować eksmisję linii pamięci podręcznej. Tę zmianę w hierarchii pamięci podręcznej można zidentyfikować, dokładnie mierząc czas dostępu do pamięci, który jest mapowany na ten sam zestaw pamięci podręcznej.
Możesz nawet wyciec niektóre fragmenty dowolnej pamięci, gdy adres źródłowy odczytanej pamięci został odczytany z pamięci jądra.
Pośredni predyktor rozgałęzienia procesorów Intel wykorzystuje tylko 12 najniższych bitów instrukcji źródłowej, dlatego łatwo jest otruć wszystkie możliwe historie prognoz 2 ^ 12 adresami pamięci kontrolowanymi przez użytkownika. Mogą one wówczas, gdy przewidywany skok pośredni w jądrze, zostać spekulacyjnie wykonany z uprawnieniami jądra. Korzystając z bocznego kanału synchronizacji pamięci podręcznej, możesz w ten sposób przeciekać dowolną pamięć jądra.
AKTUALIZACJA: Na liście mailingowej jądra trwa dyskusja, która prowadzi mnie do przekonania, że retpoliny nie łagodzą w pełni problemów prognozowania gałęzi, tak jak gdy bufor zwrotny stosu (RSB) jest pusty, nowsze architektury Intel (Skylake +) wycofują się do podatnego na oddział bufora docelowego (BTB):
Retpolina jako strategia łagodzenia zamienia pośrednie gałęzie na zwroty, aby uniknąć korzystania z prognoz pochodzących z BTB, ponieważ mogą zostać zatrute przez atakującego. Problem ze Skylake + polega na tym, że niedomiar RSB wraca do używania prognozy BTB, która pozwala atakującemu przejąć kontrolę nad spekulacjami.