Предлагает ли эта функция prefetch256 () какую-либо защиту от атак на тайминг кеша на AES? - PullRequest
7 голосов
/ 09 мая 2020

Это пограничный топи c. Поскольку я хотел узнать о программировании, кэш-памяти ЦП, чтении строк кеш-памяти ЦП и т. Д. c, я публикую это здесь.

Я реализовывал алгоритм AES на C / C ++. Поскольку выполнение умножения GF (2 8 ) требует больших вычислительных ресурсов без аппаратной поддержки, я оптимизировал использование таблиц поиска для S-блока AES. Но, к сожалению, реализации на основе таблиц поиска уязвимы для атак cache-time . Поэтому, будучи очень наивным в отношении кешей ЦП, я начал изучать, как это работает и как обойти такую ​​атаку без дополнительных вычислительных затрат.

Я понимаю, что на практике есть инструкции AES NI и реализации Bit Slice AES, но они выходят за рамки моего текущего понимания.

Я узнал из crypto.SE, что самый простой метод - это прочтите все строки кэша или всю таблицу, прежде чем я выполню поиск. ( Это также влияет на мою производительность ) Но я не знаю, как реализовать это в программном обеспечении или замысловатых концепциях, лежащих в основе этого.

В справочном руководстве по реализации C OpenSSL - aes-x86core.c авторы реализовали функцию:

static void prefetch256(const void *table)
{
    volatile unsigned long *t=(void *)table,ret;
    unsigned long sum;
    int i;

    /* 32 is common least cache-line size */
    for (sum=0,i=0;i<256/sizeof(t[0]);i+=32/sizeof(t[0]))   sum ^= t[i];

    ret = sum;
}
  • В for l oop i увеличивается на 8, если sizeof(t[0]) - 4. Поскольку sbox AES представляет собой массив unsigned char из 256 элементов, и когда мы вызываем prefetch256(sbox), указатель sbox приводится к указателю unsigned long, поэтому каждый элемент, разыменованный с помощью t[i], составляет 4 байта. Но я не понимаю причин, по которым мы пропускаем 32 байта и не читаем их полностью (для защиты от атак по времени кеширования)?
  • Какова мотивация за sum ^= t[i] и настройкой ret = sum?

  • Существуют ли другие более простые решения для защиты моей реализации от атак тайминга кеша? Поможет ли мне следующий более простой код:

       unsigned char tmp[256];
       memcpy(tmp, sbox, 256); // memcpy reads the full sbox table quickly..  
    

1 Ответ

6 голосов
/ 09 мая 2020

В for l oop i увеличивается на 8, если sizeof (t [0]) равно 4.

Но я не понимаю причин, по которым мы пропускаем 32 байта, и не прочитал его полностью (для защиты от атак тайминга кеша)?

В for l oop, i увеличивается на эквивалент 32 char s (независимо от того, что sizeof(t[0]) бывает), потому что «32 char s» (или 32 байта) - это то, что авторы определили как минимальный размер строки кэша для всех процессоров, о которых они заботятся. Обратите внимание, что вам нужно прочитать только 1 байт строки кэша, чтобы гарантировать, что вся строка кеша будет загружена в кеш.

Что является мотивацией для sum ^ = t [i] и установки ret = sum?

Хороший компилятор заметит, что данные, которые вы читаете, не используются, и избежит "ненужного" (для правильности "C абстрактной машины") чтения для повышения производительности, не зная, что «ненужное» чтение необходимо по причинам, о которых компилятор не может ожидать. Чтобы предотвратить подобную оптимизацию компилятора, вам нужно заставить его думать, что данные действительно используются, или использовать volatile. Авторы OpenSSL делают и то, и другое (пытаются обманом заставить компилятор не выполнять оптимизацию, выполняя sum ^= t[i] и ret sum; а также используя volatile), возможно потому, что (исторически) многие старые компиляторы имели ошибки, связанные с volatile.

Также обратите внимание, что есть еще очень крошечная проблема с синхронизацией - кеш может быть очищен (например, переключением задач и т.д. c) после предварительной выборки данных, но до того, как часть таблицы будет использована для AES; так что есть (очень крошечный) шанс, что злоумышленник все еще может использовать атаку по времени кэширования, чтобы определить, какая часть таблицы используется AES. См. «Уверенность в предотвращении атак по времени кэширования» (ниже).

Поможет ли мне следующий более простой код:

Очень вероятно, что компилятор превратит ваш код в буквально ничего (а если бы этого не произошло, у него были бы те же проблемы, что и у prefetch256(), и он мог бы работать медленнее, потому что вы пишете в память, а не только читаете).

Есть ли другие более простые решения для защиты моей реализации от атак тайминга кэша?

Все - это компромисс между сложностью, переносимостью, безопасностью, функциями и производительностью; а «более простой» почти всегда означает «худшую переносимость», и / или «худшее качество», и / или «худшие характеристики», и / или «худшую производительность». Вы не можете ухудшить качество или функции, по-прежнему защищая от атак по времени кэширования. Вы не можете ухудшить производительность, потому что это уже настолько просто, насколько это возможно.

Вы можете (или не можете) упростить это, пожертвовав переносимостью. Например; если вы знаете, что вся таблица умещается в одной строке кэша на одном компьютере (и выровнена по границе строки кэша), то вы ничего не можете сделать для этого одного компьютера и сказать, что код никогда не должен использоваться для любого другого компьютера.

Уверенность в предотвращении атак по времени кэширования

Одним из ключевых факторов защиты от атак по времени кэширования является степень контроля злоумышленника. Типичный сценарий состоит в том, что злоумышленник переполняет кеш (загрязняет его известными данными, чтобы его предыдущее содержимое было удалено из-за «наименее недавно использованного»), затем позволяет жертве что-то сделать, а затем измеряет, как быстро она может получить доступ к своим известным данным. чтобы определить, находятся ли эти известные данные в кэше или были удалены. Если строка из кэша с известными данными была удалена, злоумышленник знает, что жертва получила доступ к чему-то, что находится в том же месте в кэше, что и удаленная строка кэша. очень часто (например, для каждой инструкции, выполняемой жертвой), кеш не имеет ассоциативности (прямое отображение), и злоумышленник либо знает все о том, как жертва использует виртуальные адреса, и взаимосвязь между виртуальными адресами жертвы и местоположениями в кеше (вероятно, включая взаимосвязь между виртуальные адреса и физические адреса) или находятся в одном процессе (например, совместно используемая библиотека, где они могут получить доступ к таблице сами, чтобы определить, осуществлялся ли к ней доступ, вместо того, чтобы полагаться на вытеснение других данных). В этом случае единственная защита - убедиться, что все шаблоны доступа к памяти всегда одинаковы (никогда не зависят от каких-либо данных). Это чрезвычайно сложно, но не невозможно - например, если вы хотите прочитать один байт из таблицы (например, «byte = table[index]», где вы не хотите, чтобы злоумышленник знал что-либо о index), вы можете прочитать весь предыдущий кеш строк, затем прочтите нужный байт, затем прочтите все последующие строки кэша (чтобы это всегда выглядело как последовательное чтение всей таблицы) и выполняйте эти обращения с фиксированной скоростью (без «паузы перед чтением нужного байта» и нет «паузы после чтения нужного байта», включая «отсутствие паузы, вызванной неверным предсказанием ветвления»). Если вы сделаете это; тогда вы можете иметь чрезвычайно высокую уверенность в своей способности защищаться от атак на синхронизацию кэша (вплоть до гарантии того, что ваш код невосприимчив ко всем возможным атакам тайминга кэша).

Однако; злоумышленнику практически невозможно получить такой уровень контроля, очень сложно написать такой код, и такой код будет иметь большие накладные расходы на производительность.

С другой стороны; вы ничего не можете сделать и не можете быть уверены в своей способности предотвратить атаки по времени кэширования. Все остальное попадает между этими крайностями.

Тогда возникает вопрос; что такое хороший компромисс? Это зависит от слишком многих факторов - насколько сильно злоумышленник имеет контроль, находится ли злоумышленник в том же процессе, может ли злоумышленник повторить атаку (и использовать вероятностный подход c для подавления любого «шума»), насколько данные ценны для злоумышленника (здравомыслящие воры не тратят больше 2 долларов на попытку украсть что-то, что стоит 2 доллара для вора), сколько данные стоят для жертвы (никто не тратит 100 долларов в день, чтобы защитить то, что может заменить на $ 2), какие существуют другие меры (например, большинство операционных систем теперь предоставляют «рандомизацию разметки адресного пространства»), et c.

Для хорошего компромисса; для ваших целей мне лично нравится подход «сделать так, чтобы это выглядело как последовательное чтение всей таблицы», но значительно более простая версия, которая не очень заботится о фиксированной скорости доступа (или «пауза перед / после прочтения нужного фрагмента »и не заботится о размерах строк кеша (доступ к каждому байту не будет стоить много, если таблица составляет всего 256 байт, отчасти потому, что большинство обращений будут« попаданием в кеш »). Пример этого может быть:

uint16_t fetch_byte_from_table(int index) {
    size_t table_entries = sizeof(table)/sizeof(table[0]);
    uint8_t dummy = 0;
    uint8_t data = 0;

    for(i = 0; i < table_entries; i++) {
        if(i == index) {
            data ^= table[i];
        } else {
            dummy ^= table[i];
        }
    }
    return ((uint16_t)dummy << 8) | data;        // Caller can ignore the higher 8 bits
}

Конечно, есть уловки, которые вы можете использовать, чтобы попытаться скрыть или избежать ветвлений (например, data ^= table[i] * (i == index); dummy = data ^= table[i] * (i != index);), но они зависят от компилятора и целевого процессора.

...