Как я могу создать призрачный гаджет на практике?
Я разрабатываю (NASM + GCC для ELF64) PoC, который использует гаджет-призрак, который измеряет время для доступа к набору строк кэша ( FLUSH+RELOAD).
Как я могу сделать надежный гаджет призрак?
Я полагаю, что понимаю теорию, лежащую в основе техники FLUSH+RELOAD, однако на практике, несмотря на некоторый шум, я не могу произвести работающий PoC.
Поскольку я использую счетчик меток времени и нагрузки очень регулярные, я использую этот скрипт, чтобы отключить предварительные выборки, турбо-буст и фиксировать / стабилизировать частоту процессора:
#!/bin/bash
sudo modprobe msr
#Disable turbo
sudo wrmsr -a 0x1a0 0x4000850089
#Disable prefetchers
sudo wrmsr -a 0x1a4 0xf
#Set performance governor
sudo cpupower frequency-set -g performance
#Minimum freq
sudo cpupower frequency-set -d 2.2GHz
#Maximum freq
sudo cpupower frequency-set -u 2.2GHz
У меня есть непрерывный буфер, выровненный на 4 КБ, достаточно большой, чтобы охватить 256 строк кэша, разделенных целым числом GAP строк.
SECTION .bss ALIGN=4096
buffer: resb 256 * (1 + GAP) * 64
Я использую эту функцию, чтобы очистить 256 строк.
flush_all:
lea rdi, [buffer] ;Start pointer
mov esi, 256 ;How many lines to flush
.flush_loop:
lfence ;Prevent the previous clflush to be reordered after the load
mov eax, [rdi] ;Touch the page
lfence ;Prevent the current clflush to be reordered before the load
clflush [rdi] ;Flush a line
add rdi, (1 + GAP)*64 ;Move to the next line
dec esi
jnz .flush_loop ;Repeat
lfence ;clflush are ordered with respect of fences ..
;.. and lfence is ordered (locally) with respect of all instructions
ret
Функция проходит по всем строкам, касаясь каждой страницы между ними (каждая страница более одного раза) и сбрасывая каждую строку.
Затем я использую эту функцию для профилирования доступа.
profile:
lea rdi, [buffer] ;Pointer to the buffer
mov esi, 256 ;How many lines to test
lea r8, [timings_data] ;Pointer to timings results
mfence ;I'm pretty sure this is useless, but I included it to rule out ..
;.. silly, hard to debug, scenarios
.profile:
mfence
rdtscp
lfence ;Read the TSC in-order (ignoring stores global visibility)
mov ebp, eax ;Read the low DWORD only (this is a short delay)
;PERFORM THE LOADING
mov eax, DWORD [rdi]
rdtscp
lfence ;Again, read the TSC in-order
sub eax, ebp ;Compute the delta
mov DWORD [r8], eax ;Save it
;Advance the loop
add r8, 4 ;Move the results pointer
add rdi, (1 + GAP)*64 ;Move to the next line
dec esi ;Advance the loop
jnz .profile
ret
MCVE приведен в приложении, а репозиторий доступен для клонирования.
Когда собран с GAP
установить в 0, связать и выполнить с taskset -c 0
циклы, необходимые для извлечения каждой строки, показаны ниже.
Только 64 строки загружены из памяти.
Вывод стабилен при разных прогонах. Если я установлю GAP
к 1 только 32 строки извлекаются из памяти, конечно 64 * (1+0) * 64 = 32 * (1+1) * 64 = 4096, так что это может быть связано с подкачкой?
Если хранилище выполняется до профилирования (но после сброса) до одной из первых 64 строк, вывод изменится на этот
Любое хранилище других строк дает первый тип вывода.
Я подозреваю, что математика сломана, но мне нужно еще пару глаз, чтобы выяснить, где.
РЕДАКТИРОВАТЬ
Хади Брейс указал на неправильное использование энергозависимого регистра, после того как исправил, что выходные данные теперь противоречивы.
Я вижу в основном прогоны, в которых время ограничено (~50 циклов), а иногда - в тех случаях, когда время выше (~130 циклов).
Я не знаю, откуда взялась цифра в 130 циклов (слишком мало для памяти, слишком много для кеша?).
Код исправлен в MCVE (и хранилище).
Если сохранение до какой-либо из первых строк выполняется перед профилированием, в выводе не отражается никаких изменений.
ПРИЛОЖЕНИЕ - MCVE
BITS 64
DEFAULT REL
GLOBAL main
EXTERN printf
EXTERN exit
;Space between lines in the buffer
%define GAP 0
SECTION .bss ALIGN=4096
buffer: resb 256 * (1 + GAP) * 64
SECTION .data
timings_data: TIMES 256 dd 0
strNewLine db `\n0x%02x: `, 0
strHalfLine db " ", 0
strTiming db `\e[48;5;16`,
.importance db "0",
db `m\e[38;5;15m%03u\e[0m `, 0
strEnd db `\n\n`, 0
SECTION .text
;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .'
; ' ' ' ' ' ' ' ' ' ' '
; _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \
;/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \
;
;
;FLUSH ALL THE LINES OF A BUFFER FROM THE CACHES
;
;
flush_all:
lea rdi, [buffer] ;Start pointer
mov esi, 256 ;How many lines to flush
.flush_loop:
lfence ;Prevent the previous clflush to be reordered after the load
mov eax, [rdi] ;Touch the page
lfence ;Prevent the current clflush to be reordered before the load
clflush [rdi] ;Flush a line
add rdi, (1 + GAP)*64 ;Move to the next line
dec esi
jnz .flush_loop ;Repeat
lfence ;clflush are ordered with respect of fences ..
;.. and lfence is ordered (locally) with respect of all instructions
ret
;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .'
; ' ' ' ' ' ' ' ' ' ' '
; _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \
;/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \
;
;
;PROFILE THE ACCESS TO EVERY LINE OF THE BUFFER
;
;
profile:
lea rdi, [buffer] ;Pointer to the buffer
mov esi, 256 ;How many lines to test
lea r8, [timings_data] ;Pointer to timings results
mfence ;I'm pretty sure this is useless, but I included it to rule out ..
;.. silly, hard to debug, scenarios
.profile:
mfence
rdtscp
lfence ;Read the TSC in-order (ignoring stores global visibility)
mov ebp, eax ;Read the low DWORD only (this is a short delay)
;PERFORM THE LOADING
mov eax, DWORD [rdi]
rdtscp
lfence ;Again, read the TSC in-order
sub eax, ebp ;Compute the delta
mov DWORD [r8], eax ;Save it
;Advance the loop
add r8, 4 ;Move the results pointer
add rdi, (1 + GAP)*64 ;Move to the next line
dec esi ;Advance the loop
jnz .profile
ret
;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .'
; ' ' ' ' ' ' ' ' ' ' '
; _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \
;/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \
;
;
;SHOW THE RESULTS
;
;
show_results:
lea rbx, [timings_data] ;Pointer to the timings
xor r12, r12 ;Counter (up to 256)
.print_line:
;Format the output
xor eax, eax
mov esi, r12d
lea rdi, [strNewLine] ;Setup for a call to printf
test r12d, 0fh
jz .print ;Test if counter is a multiple of 16
lea rdi, [strHalfLine] ;Setup for a call to printf
test r12d, 07h ;Test if counter is a multiple of 8
jz .print
.print_timing:
;Print
mov esi, DWORD [rbx] ;Timing value
;Compute the color
mov r10d, 60 ;Used to compute the color
mov eax, esi
xor edx, edx
div r10d ;eax = Timing value / 78
;Update the color
add al, '0'
mov edx, '5'
cmp eax, edx
cmova eax, edx
mov BYTE [strTiming.importance], al
xor eax, eax
lea rdi, [strTiming]
call printf WRT ..plt ;Print a 3-digits number
;Advance the loop
inc r12d ;Increment the counter
add rbx, 4 ;Move to the next timing
cmp r12d, 256
jb .print_line ;Advance the loop
xor eax, eax
lea rdi, [strEnd]
call printf WRT ..plt ;Print a new line
ret
.print:
call printf WRT ..plt ;Print a string
jmp .print_timing
;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .'
; ' ' ' ' ' ' ' ' ' ' '
; _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \
;/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \
;
;
;E N T R Y P O I N T
;
;
;'._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .''._ .'
; ' ' ' ' ' ' ' ' ' ' '
; _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \ _' \
;/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \
main:
;Flush all the lines of the buffer
call flush_all
;Test the access times
call profile
;Show the results
call show_results
;Exit
xor edi, edi
call exit WRT ..plt
1 ответ
Мне удалось устранить ложноположительные попадания в кэш L3, выполнив следующие действия:
- Используйте относительно большой
GAP
, Если вы хотите, чтобы все доступы пропускали L3, GAP должен быть 63. - Удалить звонок
flush_all
,
Существует средство предварительной выборки данных, кроме тех, которые можно отключить с помощью sudo wrmsr -a 0x1a4 0xf
1 Мое понимание того, как это работает, основываясь на относительно небольшом количестве экспериментов, заключается в следующем. Он контролирует схему доступа к L3. Если он обнаруживает, что доступ к L3 осуществляется способом, аналогичным (или, возможно, идентичным) ранее наблюдаемому шаблону доступа в недавней истории, он агрессивно предварительно выбирает строки кэша, руководствуясь этим предыдущим шаблоном доступа (это объясняет, почему вызов flush_all
должны быть удалены независимо от GAP
). В противном случае, если есть несколько обращений к L3, которые находятся на одной и той же странице, он будет предварительно выбирать строки кэша со следующей страницы в зависимости от того, как осуществляется доступ к текущей странице (так GAP
должно быть большим). Я думаю, что этот prefetcher существует в Haswell и позже (оригинальная статья, которая предлагала атаку FLUSH+RELOAD, использовала Ivy Bridge). Таким образом, без этих двух изменений большинство обращений попадет в L3, что приведет к множеству ложных срабатываний.
Кэши MMU могут оказать существенное влияние на измеренную задержку. Например, удалив вызов flush_all
и настройка GAP
до 0 измеренная задержка при первом доступе к каждой странице очень велика (более 2000 циклов TSC), что указывает на то, что ЦП пропустил во многих / всех кэшах MMU2. Также установив GAP
до 63, все обращения будут демонстрировать такие чрезвычайно большие задержки. Если вы хотите, чтобы все обращения ударили по TLB, но все еще пропускаете L3, flush_all
может использоваться для заполнения TLB, но затем необходимо что-то сделать, чтобы предварительный сборщик L3 забыл о шаблоне, который он наблюдал, не загрязняя TLB. Я не приложил никаких усилий, чтобы сделать это.
Кроме того, может не потребоваться фиксировать частоту или отключать какие-либо из средств предварительной выборки, которые можно отключить, поскольку задержку пропуска L3 можно легко отличить от других задержек.
(1) Устройство предварительной потоковой передачи L2 на Haswell иногда выполняет предварительную выборку строк в L3 вместо L2. В частности, линии, которые находятся дальше в целевых линиях предварительной выборки, предварительно выбираются в L3 (а затем позже в L2), в то время как линии, которые находятся ближе к линии, к которой в настоящее время осуществляется доступ (для отслеживаемого потока), предварительно выбираются в L2. Там может быть не выделенный предварительный сборщик L3. Эффект sudo wrmsr -a 0x1a4 0xf
в Haswell можно было просто полностью отключить средства предварительной выборки L1 и отключить средства предварительной выборки L2 от линий предварительной выборки в L2, но, возможно, они все еще могут предварительно выбирать строки в L3. По крайней мере, на Haswell, я думаю, более вероятно, что это то, что действительно происходит. На Ivy Bridge (микроархитектура, используемая авторами FLUSH+RELOAD), эффект sudo wrmsr -a 0x1a4 0xf
может быть полностью отключить всех предварительных сборщиков. Также может быть так, что потоковый предварительный сборщик L2 на Ivy Bridge менее агрессивен, чем на Haswell, и поэтому его эффекты были не очень заметны. Было бы интересно узнать, есть ли патент Intel на выделенную аппаратную предварительную выборку L3.
(2) Скорее всего, это задержка сбоя мягкой страницы, а не успешный просмотр страницы.