В 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);
), но они зависят от компилятора и целевого процессора.