Рейтинг:11

Уязвим ли If/else к атакам по сторонним каналам?

флаг tf
Tom

У меня есть ветвление в С++:

если (х и 1)
{
    х = функция_1 (х);
}
еще
{
    х = функция_2 (х);
}

Если функция_1 и функция_2 постоянное время, и для их вычисления требуется такое же время, такое ветвление все еще уязвимо для атак по сторонним каналам? Может ли злоумышленник каким-то образом узнать, какое условие было выполнено?

Tom avatar
флаг tf
Tom
@kelalaka хорошо, спасибо. А как насчет ((x & 1) * funtcion_1(x)) ^ ((~x & 1) * funtcion_2(x))? Лучше или все же плохо? Я подозреваю, что это тоже не очень хорошее решение, но я не уверен.
kelalaka avatar
флаг in
Разницы не вижу, кроме того, убедитесь, что ничего не оптимизировано. Вот почему блоки ASM распространены в реализациях сторонних каналов. В приведенном выше были ошибки, исправлено здесь (остальные комментарии удалены) $$x = (x \клин 1)* f_1(x) + ((x \клин 1)\oplus 0x1 ) * f_2(x)$$
Tom avatar
флаг tf
Tom
@kelalaka да, была ошибка, поэтому я подумал, что это что-то другое. Теперь результат такой же, как в моей формуле. Но я думаю, что ваша все же лучше, потому что в моей формуле есть "~ x & 1", и это, вероятно, не постоянное время (по сравнению с x & 1), особенно если x - большое число
kelalaka avatar
флаг in
Речь идет не о непостоянстве ~ или x-or.Речь идет о том, что вы всегда выполняете оба метода, независимо от значения $x$, и на последнем этапе вы добавляете их с помощью масок. Это даже не требует полагаться на ту же постоянную своевременность $f_1$ и $f_2$
kelalaka avatar
флаг in
См. канонический ответ [Брезгливого Оссифража] (https://crypto.stackexchange.com/a/96634/18298)...
Рейтинг:14
флаг ng

Да, if/else уязвим для атаки по времени. То же самое и с выбором функции для вызова по индексу массива, как в этом другой ответ.

Единственное близкое к надежному решение для выполнения с постоянным временем на языке высокого уровня без указания целевой среды: Убедитесь, что путь выполнения кода и шаблоны доступа к памяти не зависят от данных..

Это исключает вызов одного из функция_1 или же функция_2 (как это делают код вопроса и указанный другой ответ) и массивы индексации на основе данных, которые изменяются под контролем или способом, наблюдаемым противником, даже частично и косвенно.

Если дополнительные затраты на вызов обеих функций допустимы, а поведение функций четко определено и приемлемо независимо от младшего бита их входных данных, то это подойдет для большинства современных компиляторов C и оборудования:

   м = -(х&1); // переменная маски; предполагает, что m и x являются целыми
   х = (функция_1 (х) и м) | (функция_2(х) и ~м);

или эквивалентно

   х = функция_1 (х) и - (х и 1) | function_2(x) & ~ -(x&1);

Это вызывает обе функции и выбирает результат соответствующим образом, используя эту маску -(х&1) имеет все биты в 1 для нечетного Икс, в 0 в противном случае.

Я не знаю ни одного компилятора CPU + C, представленного с 1985 года, где была бы временная зависимость от Икс вводится по этой методике. Современная литература по реализации криптографии с постоянным временем предполагает, что ее нет. И это несмотря на то, что время выполнения современных высокопроизводительных процессоров не указано и может изменяться непредсказуемо с точки зрения разработчика приложений (например, из-за компиляторов/компоновщиков и их опций, выравнивания кода и данных, прерываний, состояний ожидания памяти и циклов обновления). , кэши, конвейеры, спекулятивное выполнение, предсказатели ветвлений, другие потоки на одном ядре/ЦП/связанных ЦП, арбитраж ресурсов между потоками на одном ядре, виртуализация, настройки BIOS или виртуальных машин и, казалось бы, бесконечный поток ошибочных изменений микрокода, компрометирующих производительность для смягчения последствий последней зарегистрированной атаки по побочному каналу…)

Важно отметить, что нереалистичная гипотеза в вопросе

функция_1 и функция_2 постоянное время, и для их вычисления требуется то же время

можно заменить правдоподобным и фальсифицируемым:

Различия во времени исполнения функция_1 не коррелируют со значением Икс. функция_2 имеет такое же свойство.

Предостережение 1: языки C/C++ дают страховку только о полученных результатах, а не о времени выполнения. Я не знаю ни одного руководства по компилятору, которое заботится об этом, и некоторых процессоров без документации, которые это делают.

Предостережение 2: -(х&1) получить маску по младшему биту Икс — это хорошо известная идиома, которая в настоящее время работает практически на всех процессорах. Это потому, что они используют дополнение до двух конвенция, с -1 представлены всеми. Но я потерял представление о том, обещает ли C скрыть любое другое поведение, которое может иметь аппаратное обеспечение (я оставляю это для полезных комментариев). И это нужно выяснить во что бы то ни стало.

Предостережение 3: Может потребоваться адаптация в зависимости от типа Икс из-за правил продвижения C. я обычно бросаю 1 к типу Икс, например если Икс является uint32_t, Я использую -(х&(uint32_t)1), и жил бы с этим счастливо, если бы не было предупреждений компилятора.

Некоторые компиляторы жалуются, когда принимают отрицательное значение беззнакового выражения. Они неверны в соответствии со стандартом C. Легкий путь - поклониться и использовать (0-(х&1)), с приведением констант, как указано выше. Другой способ — отключить эту жалобу с помощью некоторой прагмы или параметра. Еще один, безнадежный, по моему опыту, — отправить отчет о проблеме. Тщательно продуманные аргументы переходят в NUL (локальный эквивалент /dev/null), в том числе: конструкция полезна, распространена, явно поддерживается ISO C; и предупреждение об этом может привести к тому, что все предупреждения будут отключены без разбора.

Примечание. Я предположил, что возвращаемый тип функция_1 и функция_2 тот же тип, что и Икс.


Обеспечение безопасности/криптографии на стандартных процессорах, на которых злоумышленники также могут запускать код, с 1980-х годов было неизменно ненадежным. Есть полезные усовершенствования: изоляция процессов с помощью модулей управления памятью, колец безопасности или анклавов. Существуют программные эвристики для снижения уязвимости до приемлемого уровня. Но пока серебряной пули нет.

Поэтому, когда ставки высоки, зачастую лучше всего переложить функции безопасности на надежное оборудование (смарт-карты, ЦП безопасности, HSM, TPM…), разработанные в первую очередь для обеспечения безопасности, а не производительности; и где злоумышленники не могут запустить код (самый простой, значит, лучший) или который разработан в.

SAI Peregrinus avatar
флаг si
C (пока) не обещает поведение дополнения two. В грядущем стандарте C23 это будет гарантировано, поэтому некоторым платформам потребуется эмулировать дополнение до двух, чтобы иметь компиляторы C23. Также выражение не беззнаковое, даже если `x` беззнаковое. `1` — целочисленный литерал со знаком, поэтому как `x`, так и `1` повышаются в соответствии с правилами, изложенными в разделе 6.3.1.8. Затем применяется `&`, и результат инвертируется. Результат имеет (продвинутый) целочисленный тип `(x&1)`. Вы можете использовать типизированные литералы (например, `1ULL`), чтобы `1` имел тот же ранг, что и `x`.
флаг in
@SAIPeregrinus: POSIX прямо сейчас [обещает](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdint.h.html) два дополнения, поэтому, если вы разрабатываете цель, совместимую с POSIX (которая большинство из них, включая Windows), то все в порядке.
Lorenzo Donati support Ukraine avatar
флаг ru
@SAIPeregrinus Nitpick: 1ULL - это `unsigned long long`, размер которого составляет *минимум* 64 бита в соответствии со стандартом C, поэтому он имеет более высокий ранг, чем `x` (`uint32_t`). Ссылка: стандарт C (черновик N1256 - C99), раздел **5.2.4.2.1 Размеры целочисленных типов**: *максимальное значение для объекта типа unsigned long long int* `ULLONG_MAX 18446744073709551615 // 2^64 – 1`
Tom avatar
флаг tf
Tom
Как я вижу, это отличный ответ и большая тема. Кстати, я работаю над типом данных __m128i, поэтому делаю что-то вроде: _mm_xor_si128(x,k) & -_mm_cvtsi128_si64(x & 1). Чтобы получить младший значащий бит, мне нужно использовать _mm_cvtsi128_si64. _mm_cvtsi128_si64 и __m128i — это два разных типа данных, но И все равно работает правильно из-за свойств __m128i. Таким образом, не всегда 1 должен быть того же типа, что и x, и даже x&1 не обязательно должен быть того же типа, что и вывод function_1 или function_2. Но SSE2 — это особый случай.
Tom avatar
флаг tf
Tom
@fgrieu Если я все правильно понимаю, не имеет значения, когда мы заменяем представленную формулу на x = function_1(x) & -(x&1) | function_2(x) & -(x&1^1)? Поэтому мы используем xor 1 вместо ~.
fgrieu avatar
флаг ng
@Tom: Да, это тоже работает. Однако большинство компиляторов заметят, что `~ -(x&1)` может быть эффективно вычислено из `-(x&1)`, или бесплатно, если оператор `&~` имеет аппаратную поддержку, что является общим, поскольку он используется во многих распространенных и полезных программах. идиомы, напр. в `г&м | z&~m`. Но у меня есть некоторые сомнения, что многие компиляторы будут повторно использовать `-(x&1)` в оценке `& -(x&1^1)`.
fgrieu avatar
флаг ng
@Tom: Спасибо за разъяснение. Я удалил ваш комментарий, так как он прочитан, поэтому он больше не нужен. Голосование за комментарий — это уже достаточное спасибо (и по этой причине данный комментарий будет уничтожен RSN).
Рейтинг:4
флаг us

Это началось как редактирование ответа fgrieu, но оно было очень длинным, поэтому я решил опубликовать его как отдельный ответ.

Я согласен со всем, что fgrieu написал в своем ответе

Я процитирую здесь ответ fgrieu, чтобы сделать больший акцент:

Единственное близкое к надежному решение для выполнения с постоянным временем на языке высокого уровня без указания целевой среды: Убедитесь, что путь выполнения кода и шаблоны доступа к памяти не зависят от данных..

Если мы не рассматриваем шаблоны доступа к памяти, поскольку это не входит в объем вопроса. Цитата может Только теоретически достигается с постоянным временем доступа к памяти и постоянным временем выполнения каждой инструкции. Конечно, это связано с дополнительными тактовыми циклами и дополнительным доступом к памяти.

Паттерны доступа к памяти относятся к другому типу атаки по побочному каналу, который требует, чтобы Oblivious Random Access Memory (ORAM) была защищена от атак по побочному каналу.


Если функция_1 и функция_2 имеют постоянное время и занимают одинаковое время время вычислить их, такое ветвление все еще уязвимо для атаки по сторонним каналам?

Чаще всего да, если/иначе уязвим для атаки по времени, потому что при использовании оператора if/else можно легко ожидать от компилятора или среды выполнения выполнения ветки. В любом случае вы обычно стараетесь избегать этого.

Выбор функции для вызова по индексу массива, как, например, в ответе mentallurg на Python, не так уж и плох, он имеет постоянное время доступа к памяти, а код, который выполняется Python для вызова функции, является постоянным временем.


Прежде чем я начну.

  • В случае доступа к памяти обратите внимание, что, поскольку нас интересуют атаки по времени, нас интересует только доступ к памяти. время и нет узоры это может привести к утечке информации, потому что это немного усложнит нашу ситуацию.
  • Я рассматриваю кеши только в конце, а сейчас мы не рассматриваем кеши, так как всем известно, что кеши ЦП могут быть худшим кошмаром криптографа в таких ситуациях.
  • Я предполагаю, что каждая инструкция, которая не включает доступ к памяти, требует для выполнения одних и тех же тактов.

Анализ ответа менталурга:

Возьмем, к примеру, этот простой код Python.

защита f1(x):
    вернуть х + 1

защита f2(x):
    вернуть х + 2

деф основной():
    функции = [f1, f2]
    x = int(input("Введите значение:"))
    я = х% 2
    результат = функции [i] (x)
    печать (результат)

если __name__=='__main__':
    главный()

Это компилируется в следующий байт-код Python (вывод pycdas для .pyc, скомпилированный с помощью Python 3.10):

        [Код]
            Имя файла: test.py
            Имя объекта: основной
            Количество аргументов: 0
            Pos Only Arg Count: 0
            KW Только число аргументов: 0
            Местные жители: 4
            Размер стека: 3
            Флаги: 0x00000043 (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)
            [Имена]
                'f1'
                'f2'
                'инт'
                'вход'
                'Распечатать'
            [Имена Вар]
                «функции»
                'Икс'
                'я'
                'результат'
            [Бесплатные вары]
            [Переменные ячейки]
            [Константы]
                Никто
                'Введите значение:'
                2
            [Разборка]
                0 ЗАГРУЗКА_ГЛОБАЛЬНАЯ 0: f1
                2 ЗАГРУЗКА_ГЛОБАЛЬНАЯ 1: f2
                4 СПИСОК_СТРУКТУР 2
                6 STORE_FAST 0: функции
                8 LOAD_GLOBAL 2: целое число
                10 LOAD_GLOBAL 3: ввод
                12 LOAD_CONST 1: «Введите значение:»
                14 ВЫЗОВ_ФУНКЦИЯ 1
                16 ВЫЗОВ_ФУНКЦИЯ 1
                18 STORE_FAST 1: х
                20 LOAD_FAST 1: х
                22 ПОСТОЯННАЯ НАГРУЗКА 2: 2
                24 БИНАРНЫЙ_МОДУЛЬ           
                26 STORE_FAST 2: я
                28 LOAD_FAST 0: функции
                30 LOAD_FAST 2: я
                32 BINARY_SUBSCR           
                34 LOAD_FAST 1: х
                36 ВЫЗОВ_ФУНКЦИЯ 1
                38 STORE_FAST 3: результат
                40 LOAD_GLOBAL 4: печать
                42 LOAD_FAST 3: результат
                44 ВЫЗОВ_ФУНКЦИЯ 1
                46 ПОП_ТОП                 
                48 LOAD_CONST 0: Нет
                50 ВОЗВРАТ_ЗНАЧЕНИЕ

Поиск и выполнение строки результат = функции [i] (x) делается из строк байты 26-36. Давайте посмотрим на BINARY_SUBSCR оператор. Он принимает два аргумента, список (это может быть словарь или кортеж, но давайте не будем заострять на этом внимание) в качестве первого и индекс из стека (те, которые были ранее загружены с помощью LOAD_FAST), возвращает значение по этому индексу и уменьшает стек на 1.

Теперь давайте посмотрим, как BINARY_SUBSCR реализован на CPython. Реализацию можно найти здесь и заключается в следующем:

        ЦЕЛЬ(BINARY_SUBSCR) {
            ПРОГНОЗ (BINARY_SUBSCR);
            PyObject *sub = POP();
            PyObject *container = TOP();
            PyObject *res = PyObject_GetItem (контейнер, суб);
            Py_DECREF (контейнер);
            Py_DECREF(суб);
            SET_TOP (разрешение);
            если (рез == NULL)
                ошибка перехода;
            JUMPBY(INLINE_CACHE_ENTRIES_BINARY_SUBSCR);
            ОТПРАВЛЯТЬ();
        }

Теперь весь анализ можно сосредоточить на PyObject *res = PyObject_GetItem (контейнер, суб);. Это общий метод, и до тех пор, пока элемент не будет получен, в промежуточном звене вызываются различные другие методы. Конечно, мы можем ожидать $O(1)$ сложность. Осталось это проверить. В конце PyList_GetItem называется следующее:

ПиОбъект *
PyList_GetItem (PyObject * op, Py_ssize_t i)
{
    если (!PyList_Check(оп)) {
        PyErr_BadInternalCall();
        вернуть НУЛЬ;
    }
    если (!valid_index(i, Py_SIZE(op))) {
        _Py_DECLARE_STR(list_err, "индекс списка вне допустимого диапазона");
        PyErr_SetObject(PyExc_IndexError, &_Py_STR(list_err));
        вернуть НУЛЬ;
    }
    return ((PyListObject *)op) -> ob_item[i];
}

Как мы видим в последней строке. Оно имеет $O(1)$ сложность.Конечно, из-за сложности языков высокого уровня они никогда не используются на практике для таких приложений. Итак, давайте попробуем этот код на языке более низкого уровня, таком как C, чтобы увидеть, что он производит.

#include <stdio.h>

интервал f1 (целый х) {
    вернуть х + 1;
}

интервал f2 (целый х) {
    вернуть х + 2;
}

интервал основной () {
    интервал х;
    сканф("%d", &x);
    int (*(функции[2]))(int) = {f1, f2};
    инт я;
    я = х %2;
    результат int = functions[i](x);
}

Это x86_64 от Godbolt с последним GCC без оптимизаций:

f1:
        дополнение   $сп,$сп,-8
        SW      $fp,4($сп)
        двигаться    $fp,$сп
        SW      $4,8($фп)
        лв      $2,8($фп)
        нет
        дополнение   $2,$2,1
        двигаться    $сп,$фп
        лв      $fp,4($сп)
        дополнение   $сп,$сп,8
        младший $ 31
        нет

f2:
        дополнение   $сп,$сп,-8
        SW      $fp,4($сп)
        двигаться    $fp,$сп
        SW      $4,8($фп)
        лв      $2,8($фп)
        нет
        дополнение   $2,$2,2
        двигаться    $сп,$фп
        лв      $fp,4($сп)
        дополнение   $сп,$сп,8
        младший $ 31
        нет

$LC0:
    .ascii "%d\000"
главный:
    добавить $сп,$сп,-56
    ув $31,52($сп)
    ув $fp, 48 ($сп)
    переместить $фп,$сп
    добавить $2,$fp, 32
    переместить $5,2 доллара
    Луи $2,%привет($LC0)
    добавить $4,$2,%10($ЛК0)
        Джал __isoc99_scanf
        нет

        Луи     $2,%привет(f1)
    добавить $2,$2,%10(f1)
    ув $2,36($fp)
    Луи $2,%привет(f2)
        дополнение   $2,$2,%10(f2)
        SW      $2,40($фп)
        лв      $3,32($фп)
        ли      $2,-2147483648 # 0xffffffff80000000
    ори $2,$2,0x1
    и $2,$3,$2
        бгез    $2,$L6
        нет

        дополнение   $2,$2,-1
        ли      $3,-2 # 0xfffffffffffffffe
    или $2,$2,$3
        дополнение   $2,$2,1
$L6:
    ув $2,24($fp)
    лв $2,24($fp)
    нет
    продать $2,$2,2
    добавить $3,$fp, 24
    добавить $2,$3,$2
        лв      $2,12($2)
        лв      $3,32($фп)
        нет
        двигаться    $4,$3
        двигаться    $25,$2
        Джалр $ 25
        нет

        SW      $2,28($фп)
        двигаться    $2,$0
        двигаться    $сп,$фп
        лв      $31,52($сп)
        лв      $fp,48($сп)
        дополнение   $сп,$сп, 56
        младший $ 31
        нет

Более конкретно нас интересуют эти инструкции, в которых делается вызов соответствующей функции:

        лв      $2,24($фп)
        нет
        еще     $2,$2,2
        дополнение   $3,$фп, 24
        адду    $2,$3,2 доллара
    лв $2,12($2)
    лв $3,32($fp)
    нет
    переместить $4,$3
    переместить $25,2 доллара
    Джалр $25
        нет

        SW      $2,28($фп)
        двигаться    $2,$0

Как мы видим, никаких ветвей не делается, кроме как от jalr, которые вызывают соответствующую функцию.


Анализ ответа фгрие

Конечно, легко видеть, что это постоянное время:

#include <stdio.h>

интервал f1 (целый х) {
    вернуть х + 1;
}

интервал f2 (целый х) {
    вернуть х + 2;
}

интервал основной () {
    интервал х;
    сканф("%d", &x);
    int (*(функции[2]))(int) = {f1, f2};
    интервал м = -(х&1); // переменная маски m должна иметь тот же тип, что и x
    х = (функция_1 (х) и м) | (функция_2(х) и ~м);
    печать (х);
}

Опять Голдболт с теми же опциями:

f1:
        толчок рбп
        мов рбп, рсп
        mov DWORD PTR [rbp-4], edi
        mov eax, DWORD PTR [rbp-4]
        добавить еакс, 1
        поп рбп
        рет
f2:
        толчок рбп
        мов рбп, рсп
        mov DWORD PTR [rbp-4], edi
        mov eax, DWORD PTR [rbp-4]
        добавить еакс, 2
        поп рбп
        рет
.LC0:
        .строка "%d"
главный:
        толчок рбп
        мов рбп, рсп
        толчок rbx
        саб рсп, 40
        леа ракс, [rbp-24]
        мов рси, ракс
        mov edi, OFFSET FLAT:.LC0
        движение акс, 0
        позвони __isoc99_scanf
        mov QWORD PTR [rbp-48], OFFSET FLAT:f1
        mov QWORD PTR [rbp-40], OFFSET FLAT:f2
        mov eax, DWORD PTR [rbp-24]
        и акс, 1
        отрицательный акс
        mov DWORD PTR [rbp-20], eax
        mov eax, DWORD PTR [rbp-24]
        мов эди, эакс
        движение акс, 0
        вызов функции_1
        и eax, DWORD PTR [rbp-20]
        мов ebx, eax
        mov eax, DWORD PTR [rbp-24]
        мов эди, эакс
        движение акс, 0
        вызов функции_2
        mov edx, DWORD PTR [rbp-20]
        не edx
        и еакс, эдкс
        или eax, ebx
        mov DWORD PTR [rbp-24], eax
        mov eax, DWORD PTR [rbp-24]
        cdqe
        мов рди, ракс
        движение акс, 0
        вызов printf
        движение акс, 0
        mov rbx, QWORD PTR [rbp-8]
        оставлять
        рет

Мы сосредоточимся на этой части, где выполняется присваивание m и встроенное ветвление:

        mov eax, DWORD PTR [rbp-24]
        и акс, 1
        отрицательный акс
        mov DWORD PTR [rbp-20], шт.
        mov eax, DWORD PTR [rbp-24]
        мов эди, эакс
        движение акс, 0
        вызов функции_1
        и eax, DWORD PTR [rbp-20]
        мов ebx, eax
        mov eax, DWORD PTR [rbp-24]
        мов эди, эакс
        движение акс, 0
        вызов функции_2
        mov edx, DWORD PTR [rbp-20]
        не edx
        и еакс, эдкс
        или eax, ebx
        mov DWORD PTR [rbp-24], eax

На самом деле мы можем видеть выполнение с постоянным временем.

Итак, в чем разница между этими решениями:

Оба удовлетворяют мерам антивременного анализа, но второй также скрывает шаблоны доступа. Он всегда вызывает обе функции. Поскольку мы пока не упоминали кеши, при наличии кешей второй кажется более защищенным от атак по сторонним каналам по времени.Просто потому, что для выполнения инструкции код обеих функций должен находиться в кеше. Во втором случае кэшируется только тот, который вызывается. Если предположить, что за какой-то конкретный период времени f1 был вызван и f2 был вытеснен из кеша, будет разница во времени, когда f2 будет вызван снова.

Для других решений и дальнейшего чтения по этому вопросу вы можете рассмотреть [1] и [2].

Рейтинг:-1
флаг kr

Вы можете помещать функции в массив и ссылаться на них по индексу.

В Python это будет выглядеть следующим образом:

защита f1(x):
    ...

защита f2(x):
    ...

функции = [ f1, f2 ]

я = х% 2

результат = функции [ я ] ( х )
Tom avatar
флаг tf
Tom
Спасибо. Это тоже кажется умным. Конечно, я просто пытаюсь сделать свой код лучше, пока это не для профессионального использования.
kelalaka avatar
флаг in
Вы уверены, что это (будет) не преобразовано как если бы еще в фоновом режиме? Полагаться на компилятор слишком рискованно, если вы не работаете с [Thomas Pornin T1](https://www.youtube.com/watch?v=IHbtK5Kwt6A)
флаг kr
@kelalaka: я уверен, что это работает так, как ожидалось. Это не тот случай, который может решить компилятор или среда выполнения на распространенных платформах, таких как C, C++, C#, Java, Python. Часто оптимизируются **логические** выражения. Например. если есть выражение `a & b` и среда выполнения знает `a == false` (или `a == 0`), то на многих платформах это даже не делегируется среде выполнения, а требуется спецификацией языка, которая среда выполнения *должна* пропустить оценку второго операнда.
fgrieu avatar
флаг ng
Это может быть улучшением. Но **это не решает проблему**. Всевозможные вещи, включая кеши, выравнивание… делают невозможным создание переносимого кода для современных компьютеров с постоянным временем, имея пути кода или шаблоны доступа к данным, которые зависят от данных, как в вопросе. Добиться этого можно только при жестком контроле среды выполнения (например, на языке ассемблера на ЦП с четкой моделью циклов выполнения), а языки высокого уровня этого не позволяют.
флаг kr
@fgrieu: Начиная с некоторых, вы **не можете** контролировать выполнение.Даже если вы реализуете это на ассемблере, вы не можете контролировать, как ОС организует доступ к памяти, как разделяется память, как ОС использует различные оптимизации. На более дальнем уровне даже ОС не может контролировать, как компьютер использует кэши разных типов. Кроме того, большинство современных ЦП в ПК используют **предсказание ветвлений**, так что некоторый код может быть выполнен **заранее** (до того, как он действительно может понадобиться), а его результат может быть использован позже или может быть не использован. использовали и просто выбросили. И нет возможности это контролировать.
флаг kr
@fgrieu: ... Вот почему имеет смысл говорить об уменьшении утечки информации по сторонним каналам только на каком-то макроуровне. Некоторые шаги по уменьшению утечки на низком уровне могут включать использование специализированных ОС, использование специализированного оборудования, использование инструментов, которые создают больше «шума» во время выполнения. Я рассматриваю ОП именно как вопрос об уменьшении утечки побочного канала на *макро* уровне.
fgrieu avatar
флаг ng
@mentallurg: я полностью согласен с тем, что «вы не можете контролировать исполнение» и с остальным комментарием.Я принимаю в качестве аргумента решение, изложенное в ответе, и любое решение, которое приводит к выполнению одной функции в соответствии с четностью `x`, следует ожидать только для _уменьшения_ временных вариаций, коррелирующих с четностью `x`, когда та, что в мой ответ, когда и если это возможно, _удаляет_ их на всех известных мне архитектурах, независимо от ОС, ЦП, прогнозирования ветвлений и состояния микрокода, столкнувшегося с последней атакой.

Ответить или комментировать

Большинство людей не понимают, что склонность к познанию нового открывает путь к обучению и улучшает межличностные связи. В исследованиях Элисон, например, хотя люди могли точно вспомнить, сколько вопросов было задано в их разговорах, они не чувствовали интуитивно связи между вопросами и симпатиями. В четырех исследованиях, в которых участники сами участвовали в разговорах или читали стенограммы чужих разговоров, люди, как правило, не осознавали, что задаваемый вопрос повлияет — или повлиял — на уровень дружбы между собеседниками.