Как я могу создать призрачный гаджет на практике?

Я разрабатываю (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 строки загружаются из памяти

Только 64 строки загружены из памяти.

Вывод стабилен при разных прогонах. Если я установлю GAP к 1 только 32 строки извлекаются из памяти, конечно 64 * (1+0) * 64 = 32 * (1+1) * 64 = 4096, так что это может быть связано с подкачкой?

Если хранилище выполняется до профилирования (но после сброса) до одной из первых 64 строк, вывод изменится на этот

Второй блок из 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 0xf1 Мое понимание того, как это работает, основываясь на относительно небольшом количестве экспериментов, заключается в следующем. Он контролирует схему доступа к 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) Скорее всего, это задержка сбоя мягкой страницы, а не успешный просмотр страницы.

Другие вопросы по тегам