Предотвращайте вмешательство компилятора в части кода, критическим образом влияющие на безопасность
Проблема
Некоторые компиляторы оптимизируют операции, которые они считают бесполезными.
Например, компилятор MS Visual C++ посчитал лишним оператор |memset| в следующем фрагменте кода реализации анонимной сети Tor:
int
crypto_pk_private_sign_digest(...)
{
char digest[DIGEST_LEN];
(...)
memset(digest, 0, sizeof(digest));
return r;
}
Однако роль этого оператора |memset| заключается в очистке буфера |digest| от конфиденциальных данных, таким образом, чтобы при любых последующих считываниях данных из неинициализированного стека невозможно было получить конфиденциальную информацию.
Некоторые компиляторы считают, что они могут удалять проверки условий, считая код ошибочным где-либо в программе. Например, обнаружив следующий фрагмент кода
call_fn(ptr); // всегда разыменовывает ptr.
// много много строк
if (ptr == NULL) { error("ptr must not be NULL"); }
некоторые компиляторы решат, что условие |ptr == NULL| всегда должно принимать значение ЛОЖЬ, поскольку в противном случае было бы некорректным разыменовывать его в функции |call_fn()|.
Решение
Проанализируйте скомпилированный код и удостоверьтесь, что все инструкции присутствуют в нем. (Это невозможно для приложений стандартного размера, но это следует сделать для фрагмента кода, критичного с точки зрения безопасности).
Разберитесь какие оптимизации может делать ваш компилятор и аккуратно оцените эффект каждой из них с точки зрения принципов безопасного программирования. В частности, будьте аккуратны с оптимизациями, которые убирают фрагменты кода или ветвления, а также фрагменты кода, которые предотвращают ошибки, которые «не могут появиться», если оставшаяся часть программы корректна.
Когда это возможно, подумайте об отключении той оптимизации при компиляции, которая удаляет или ослабляет проверку условий, влияющих на безопасность.
Чтобы предотвратить удаление инструкций посредством оптимизации, функция может быть переопределена с использованием ключевого слова volatile. Это например используется в libottery при переопределении |memset|:
void * (*volatile memset_volatile)(void *, int, size_t) = memset;
В C11 введен вызов memset_s, для которого запрещено удаление при оптимизации.
#define __STDC_WANT_LIB_EXT1__ 1
#include <string.h>
...
memset_s(secret, sizeof(secret), 0, sizeof(secret));
Не допускайте смешения безопасных и небезопасных программных интерфейсов
Проблема
Многие среды программирования предоставляют различные реализации одних и тех же программных интерфейсов – их функционал внешне одинаков, но при этом свойства безопасности кардинально отличаются.
Эта проблема характерна для датчиков случайных чисел: в OpenSSL есть |RAND_bytes()| и |RAND_pseudo_bytes()|, в C-библиотеках BSD есть |RAND_bytes()| и |RAND_pseudo_bytes()|, в Java – |SecureRandom| и |Random|
Еще одним примером может быть тот факт, что в системах, которые предоставляют не зависящие от времени функции сравнения байтовых слов, в то же время присутствуют варианты, которые могут давать утечку по времени.
Плохие решения
Иногда функция является безопасной на одних платформах и опасной на других. В этих случаях программисты используют эту функцию, считая, что их код будет исполняться на платформах, где он безопасен. Это плохой подход, поскольку код может быть портирован на другие платформы и стать небезопасным – никто этого и не заметит.
В системах, которые разрешают переопределение платформозависимых функций, некоторые программисты переопределяют небезопасные функции безопасными и пишут программы, используя программный интерфейс, который в обычной ситуации является небезопасным. Это достаточно спорный подход, поскольку приводит к тому, что программист пишет код, который выглядит небезопасным. Более того, если переопределенный метод не сработает когда-либо, программа станет небезопасной и это нельзя будет определить. И наконец, это приведет к тому, что фрагменты кода таких программ будут небезопасны, если их скопировать в другие проекты.
Решение
По возможности не используйте небезопасные варианты безопасных функций. Например, ПДСЧ на базе стойкого поточного шифра со случайным начальным заполнением достаточно быстр для большинства приложений. Независимая от типа данных замена memcmp также достаточно быстра, чтобы быть использованной для всех операций сравнения областей памяти.
Если вы не можете удалить небезопасные функции, переопределите их таким образом, чтобы выдавалась ошибка на этапе компиляции, либо используйте утилиты для статического анализа кода, чтобы выявить и предупредить об использовании небезопасных функций. Если вы можете переопределить небезопасную функцию её безопасным вариантом, то для большей безопасности никогда не вызывайте небезопасный API и удостоверьтесь, что вы можете выявлять факт его использования.
Если необходимо оставить оба варианта (безопасный и небезопасный) удостоверьтесь, что имена функций различны настолько, что будет затруднительно случайно использовать небезопасный вариант. Например, если у вас есть безопасный и небезопасный ПДСЧ, не называйте небезопасный вариант «Random», «FastRandom», «MersenneTwister» или «LCGRand» – вместо этого назовите его, например, «InsecureRandom». Разрабатывайте свои программные интерфейсы таким образом, чтобы использование небезопасных функций всегда немного пугало.
Если ваша платформа предоставляет небезопасный вариант функции без имени, которое говорит о ее небезопасности и вы не можете удалить эту функцию, используйте обертку системного вызова с безопасным именем, затем путем статического анализа кода выявите все использования небезопасного имени.
Если функция безопасна на одних платформах и небезопасна на других, не используйте функцию непосредственно: вместо этого определите и используйте безопасную обертку.
Избегайте смешения уровней безопасности и абстракции криптографических примитивов на одном уровне API
Проблема
Когда не ясно, какого анализа требуют различные части программного интерфейса, программист может достаточно легко сделать ошибку в том, какие функциональные возможности они могут безопасно использовать.
Рассмотрим следующий пример (придуманный, но похожий на те, которые встречаются в реальной жизни) программного интерфейса RSA:
enum rsa_padding_t { no_padding, pkcs1v15_padding, oaep_sha1_padding, pss_padding };
int do_rsa(struct rsa_key *key, int encrypt, int public, enum rsa_padding_t padding_type, uint8_t *input, uint8_t *output);
Предположим, что параметр “key” содержит компоненты реквизитов, тогда функция может быть вызвана 16-ю способами, многие из которых бессмысленны, а некоторые небезопасны.
шифрование/расшифрование | симметричное/асимметричное |
тип паддинга |
замечания |
---|---|---|---|
0 | 0 | none | Расшифрование без паддинга. Возможность подделки. |
0 | 0 | pkcs1v15 | Расшифрование PKCS1 v1.5. Возможно, подвержено атаке Блейнбахера. |
0 | 0 | oaep | Расшифрование OAEP. Хороший вариант. |
0 | 0 | pss | Расшифрование PSS. Достаточно странный вариант, возможно, приводит к непреднамеренным ошибкам |
0 | 1 | none | Подпись без паддинга. Возможность подделки. |
0 | 1 | pkcs1v15 | Подпись PKCS1 v1.5. Подходит для некоторых приложений, но лучше использовать подпись PSS. |
0 | 1 | oaep | Подпись OAEP. Подходит для некоторых приложений но лучше использовать подпись PSS. |
0 | 1 | pss | Подпись PSS. Очень хороший вариант. |
... | ... | ... | оставшиеся варианты (шифрование и проверка подписи). |
Отметим, что только 4 из 16-ти возможных способов вызова этой функции безопасны, еще 6 небезопасны, а оставшиеся 6 в некоторых случаях могут вызвать проблемы при применении. Такой API подходит только для тех разработчиков, кто понимает последствия применения различных способов дополнения в системе RSA.
Теперь представьте, что мы добавим программные интерфейсы для блочного шифрования в различных режимах, генерации ключей, разнообразных кодов аутентификации сообщений и подписей. Любой программист, который попытается разработать корректную функцию, реализующую аутентификацию и шифрование данных с использованием таких программных интерфейсов, будет иметь огромное число вариантов выбора, при этом число безопасных вариантов, очевидным образом, сократится.
Решение
- Предоставляйте высокоуровневые программные интерфейсы. Например, предоставьте функции, реализующие шифрование и аутентификацию данных, которые используют только стойкие алгоритмы и при этом безопасным образом. Когда вы пишете функцию, которая предоставляет различные комбинации симметричных и асимметричных алгоритмов и их режимов работы, удостоверьтесь, что эта функция не позволяет использовать небезопасные алгоритмы и их небезопасные комбинации.
- Когда это возможно, избегайте низкоуровневых API. Большинству пользователей нет необходимости использовать RSA без дополнения, использовать блочный шифр в режиме ECB или использовать подпись DSA с выбранным пользователем случайным значением. Эти функции могут быть использованы как строительные блоки для того, чтобы реализовать что-нибудь стойкое – например, сделать OAEP-паддинг до вызова RSA без дополнения, использовать шифрование в режиме ECB для блоков 1,2,3,…, чтобы реализовать режим счетчика или использовать случайную или непредсказуемую байтовую последовательность для случайного значения DSA, но практика показывает, что они чаще будут использоваться неправильно, нежели правильно.
Некоторые другие примитивы необходимы для реализации определенных протоколов, но скорее всего не будут подходящими для реализации новых протоколов. Например, вы не можете реализовать в настоящее время совместимый с браузером TLS без CBC, PKCS1 v1.5 и RC4, но любой из данных примитивов не является хорошим вариантом.
Если вы предоставляете криптографический модуль для использования неопытными программистами, будет лучше избегать таких функций полностью и выбирать (для API) только функции, которые реализуют хорошо описанные высокоуровневые безопасные операции.
- Если же вы все-таки должны предоставлять интерфейс и опытным, и неопытным пользователям, четко разделите высокоуровневый и низкоуровневые программные интерфейсы. Функция «безопасного шифрования» не должна быть той же самой функцией, что и «некорректное шифрование» с несколько измененными аргументами. В языках, которые разделяют функции и типы в пакеты и заголовки, безопасные и небезопасные криптофункции не должны содержаться в одних и тех же пакетах и заголовках. В языках с подтипами, должны быть отдельные типы для безопасных криптореализаций.
Используйте беззнаковые типы для представления двоичных данных
Проблема
В некоторых C-подобных языках знаковые и беззнаковые целочисленные типы являются различными. В частности, в C вопрос о том является ли тип |char| знаковым зависит от реализации. Это может привести к появлению проблемного кода – такого, например, как приведенный далее:
int decrypt_data(const char *key, char *bytes, size_t len);
void fn(...) {
//...
char *name;
char buf[257];
decrypt_data(key, buf, 257);
int name_len = buf[0];
name = malloc(name_len + 1);
memcpy(name, buf+1, name_len);
name[name_len] = 0;
//...
}
Если |char| беззнаковый, то данный код ведет себя так, как мы от него ожидаем. Но если |char| знаковый, |buf[0]| может принимать отрицательные значения, приводя к очень большим значениям аргументов функций |malloc| и |memcpy| и возможности повреждения кучи, если мы пытаемся установить значение последнего знака в 0. Ситуация может быть даже хуже, если |buf[0]| равен 255, тогда name_len будет равным -1. Таким образом, мы выделим в памяти буфер размера 0 байтов, а затем произведем копирование |(size_t)-1 memcpy| в данный буфер, что приведет к засорению кучи.
Решение
В языках, которые различают знаковые и беззнаковые байтовые типы, реализации должны использовать беззнаковые типы для представления байтовых строк в своих API.
Очищайте память от секретных данных
Проблема
В большинстве операционных систем память, используемая одним процессом, может быть использована другим процессом без предварительной очистки, потому что первый процесс остановлен или возвратил память системе. Если память содержит секретные ключи, они будут доступны другому процессу, что увеличивает шанс их компрометации. В многопользовательских системах это дает возможность определять ключи других пользователей системы. Даже внутри одной системы подобная ситуация может привести к тому, что ранее сравнительно «безопасные» уязвимости могут приводить к утечке секретных данных.
Решение
Очищайте все переменные, содержащие секретные данные, до того момента как вы о них забудете и предстанете пользоваться. Используя функцию |mmap()| помните, что запуск |munmap()| моментально освобождает память и вы теряете над ней контроль.
Для очистки памяти или уничтожения объектов, которые уходят из вашего поля зрения, используйте платформозависимые функции очистки памяти, где это возможно – такие как |SecureZeroMemory()| для win32 или |OPENSSL_cleanse()| для OpenSSL.
Более-менее универсальное решение для C может быть таким:
void burn( void *v, size_t n )
{
volatile unsigned char *p = ( volatile unsigned char * )v;
while( n-- ) *p++ = 0;
}
Используйте «сильную» случайность
Проблема
Многим криптографическим системам требуются источники случайности, при этом такие системы могут становиться небезопасными даже в случае небольших отклонений от случайности в таких источниках. Например, утечка даже одного случайного числа в DSA приведет к крайне быстрому определению секретного ключа. Недостаточную случайность бывает довольно тяжело определить: ошибка генератора случайных чисел Debian в OpenSSL оставалась незамеченной на протяжении двух лет, приведя к компрометации большого числа ключей. Требования к случайным числам для криптографических приложений очень жесткие: многие генераторы псевдослучайных чисел не удовлетворяют им.
Плохие решения
Для криптографических приложений
- Не полагайтесь на предсказуемые источники случайности, такие как метки времени, идентификаторы, температурные датчики и т.д.
- не полагайтесь на функции выработки псевдослучайных чисел общего пользования, такие как |rand()|,|srand()|,|random()| библиотеки |stdlib| или |random| языка Python
- Не используйте генератор Вихрь Мерсенна (Mersenne Twister)
- Не используйте ресурсы наподобие www.random.org (случайные данные могут стать известны третьим лицам или быть также использованы ими).
- Не используйте свой собственный генератор случайных чисел, даже если он основан на стойком криптопримитиве (если только вы точно не знаете, что делаете).
- Не используйте одни и те же случайные биты в различных местах приложения, для их «экономного» расходования.
- Не делайте вывод о том, что генератор стойкий только по тому, что он проходит тесты Diehard или NIST.
- Не делайте вывод о том, что криптографически стойкий генератор обязательно защищает от чтения вперед и чтения назад.
- Никогда не используйте «случайность» в чистом виде в качестве случайных данных (аналоговые источники случайности зачастую имеют отклонения, поэтому N битов, полученных с такого источника, имеет меньше N битов случайности).
Решение
Минимизируйте использование случайности посредством выбора примитивов и их дизайна (например, Ed25519 позволяет получать кривые для электронной подписи детерминированным образом). Для выработки случайных чисел используйте источники, предоставляемые операционными системами и гарантированно удовлетворяющие криптографическим требованиям, такие как |/dev/random|. На платформах с ограниченными ресурсами рассмотрите возможность использования аналоговых источников случайного шума и хорошей процедуры замешивания.
Обязательно проверяйте значения, производимые вашим датчиком, чтобы быть уверенными, что получаемые байты такие, какие они должны быть, и что они были записаны должным образом.
Следуйте рекомендациям Нади Хенингер и др. в разделе 7 их статьи.
На процессорах Intel с архитектурой Ivy Bridge (и последующих поколений), встроенный генератор гарантирует высокою энтропию и скорость работы.
В Unix системах обычно используются |/dev/random| или |/dev/urandom|. Однако первый из них имеет свойство блокирования, т.е. он не возвращает значений в случае, если полагает, что накоплено недостаточно случайности. Это свойство ограничивает удобство
его использования, и поэтому |/dev/urandom| используется чаще. Использовать |/dev/urandom| достаточно просто:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int randint;
int bytes_read;
int fd = open("/dev/urandom", O_RDONLY);
if (fd != -1) {
bytes_read = read(fd, &randint, sizeof(randint));
if (bytes_read != sizeof(randint)) {
fprintf(stderr, "read() failed (%d bytes read)\n", bytes_read);
return -1;
}
}
else {
fprintf(stderr, "open() failed\n");
return -2;
}
printf("%08x\n", randint); /* assumes sizeof(int) <= 4 */
close(fd);
return 0;
}
Однако этой простой программы может быть недостаточно для безопасной выработки случайности: более безопасным будет выполнение дополнительных проверок на ошибки как в функции |getentropy_urandom| LibreSSL
static int
getentropy_urandom(void *buf, size_t len)
{
struct stat st;
size_t i;
int fd, cnt, flags;
int save_errno = errno;
start:
flags = O_RDONLY;
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
fd = open("/dev/urandom", flags, 0);
if (fd == -1) {
if (errno == EINTR)
goto start;
goto nodevrandom;
}
#ifndef O_CLOEXEC
fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC);
#endif
/* Lightly verify that the device node looks sane */
if (fstat(fd, &st) == -1 || !S_ISCHR(st.st_mode)) {
close(fd);
goto nodevrandom;
}
if (ioctl(fd, RNDGETENTCNT, &cnt) == -1) {
close(fd);
goto nodevrandom;
}
for (i = 0; i < len; ) {
size_t wanted = len - i;
ssize_t ret = read(fd, (char *)buf + i, wanted);
if (ret == -1) {
if (errno == EAGAIN || errno == EINTR)
continue;
close(fd);
goto nodevrandom;
}
i += ret;
}
close(fd);
if (gotdata(buf, len) == 0) {
errno = save_errno;
return 0; /* satisfied */
}
nodevrandom:
errno = EIO;
return -1;
}
В Windows-системах |CryptGenRandom| из Win32 API вырабатывает псевдослучайные биты пригодные для использования в криптографии. Microsoft предлагает следующий вариант использования:
#include <stddef.h>
#include <stdint.h>
#include <windows.h>
#pragma comment(lib, "advapi32.lib")
int randombytes(unsigned char *out, size_t outlen)
{
static HCRYPTPROV handle = 0; /* only freed when program ends */
if(!handle) {
if(!CryptAcquireContext(&handle, 0, 0, PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT | CRYPT_SILENT)) {
return -1;
}
}
while(outlen > 0) {
const DWORD len = outlen > 1048576UL ? 1048576UL : outlen;
if(!CryptGenRandom(handle, len, out)) {
return -2;
}
out += len;
outlen -= len;
}
return 0;
}
Если ориентироваться на использование в Windows XP или более поздних версиях, указанный выше код на CryptoAPI может быть заменен на |RtlGenRandom|
#include <stdint.h>
#include <stdio.h>
#include <Windows.h>
#define RtlGenRandom SystemFunction036
#if defined(__cplusplus)
extern "C"
#endif
BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength);
#pragma comment(lib, "advapi32.lib")
int main()
{
uint8_t buffer[32] = { 0 };
if (FALSE == RtlGenRandom(buffer, sizeof buffer))
return -1;
for (size_t i = 0; i < sizeof buffer; ++i)
printf("%02X ", buffer[i]);
printf("\n");
return 0;
}
Комментарии (17)
Kolyuchkin
13.11.2015 00:31+1Замечание-уточнение по поводу очистки памяти от «секретных» данных: просто забить память нулями — это недостаточно! Наши «регуляторы» в области криптографии требуют, чтобы очищаемая память многократно (обычно 3-5 раз) «забивалась» качественным «шумом».
grossws
13.11.2015 13:54Это относится к DRAM или дискам?
И нет ли ещё требования на использование membar в дополнение к этому, чтобы гарантировать инвалидацию этой части памяти в кэше?Kolyuchkin
13.11.2015 15:13Это относится к любой памяти, где находилась секретная или криптографически опасная информация.
dtestyk
13.11.2015 01:10Дилетантский вопрос: можно ли запустить несколько собственных процессов на той же машине, где и происходит, например, шифрование и по отклонению во времени выполнения операций в них извлечь полезную информацию? Есть ли название у этой разновидности атак?
xaizek
13.11.2015 01:16+1Именно такое с процессами не встречал, но по отклонению во времени есть, эти способы входят в группу под названием «Атаки по побочным каналам».
khim
13.11.2015 08:57+1Да, конечно можно. Timing attacks лет десять назад начали обсуждать теоретически, а сейчас уже вроде какие-то практические демонстрации появились.
Отсюда — некоторое количество приведённых в статье рекомендаций. Конечно на практике их не применяют: сложно и дорого, а при современном наплевательском подходе к безопасности — и не нужно (других дыр хватает).
StrangerInRed
13.11.2015 10:16Использую
#pragma GCC push_options #pragma GCC optimize ("O0") <C код> #pragma GCC pop_options
Для того чтобы компилятор не решил вдруг чего за тебя.grossws
13.11.2015 13:56Работает в предположении, что собирается только gcc и пофиг на оптимизацию от слова совсем, может образоваться hot spot, так что при таком подходе надо ещё профилировать.
Turbo
14.11.2015 12:34Возможно глупый вопрос. А может кто-то посоветовать код сочетающий свойства:
1) Pure C код в одном файле, желательно просто функцию вида get_random()
2) Максимально рандомную
3) Мультиплатформенную
Пишу небольшое статистическое приложение, а просто rand() никуда не годится.ru_crypt
14.11.2015 13:26Берете блочный шифр в режиме счетчика, например. Т.е. шифруете последовательность a,a+1,a+2,… значение а устанавливаете тем же rand, только от системного времени. В качестве шифра возьмите Кузнечик. Реализаций полно github.com/MaXaMaR/kuznezhik tc26.ru/standard/draft/PR_GOSTR-bch_v3.zip github.com/mjosaarinen/kuznechik. A rand() — это же конгруэнтный генератор, он действительно плохой
dimarick
14.11.2015 16:20Попробуйте вихрь Мерсена(Mersenne Twister). Если не подойдет — то openssl, там есть генератор псевдослучайных и случайных чисел.
Turbo
15.11.2015 13:21Похоже что-то вроде этого я сейчас и использую:
#define BITS_PER_CALL_GENERATOR 16 random_device rand_device; // non-deterministic generator mt19937 generator(rand_device()); uniform_int_distribution<> uniform_distribution(0, (1 << BITS_PER_CALL_GENERATOR) - 1);
khim
14.11.2015 20:54-1А зачем вам для «небольшого статистического приложения» «pure C код»? На современных процессорах с поддержой AESNI использовать что-то другое без «уважительных причин» попросту глупо (пример «уважительной причины»: вы хотите генерировать одни и те же последовательности на ARM и на x86 и, разумеется, вас больше волнует скорость работы на ARM).
Turbo
15.11.2015 13:19Просто потому что у меня PureC код приложения. Сейчас я использую #include , который за собой тянет уже компилятор С++. И честно говоря даже там распределение хоть и лучше чем у rand(), но все равно не очень радует. )
P.S. Моё приложение чисто для PC.
Ivan_83
«например, Ed25519 позволяет получать кривые для электронной подписи детерминированным образом» — В ECDSA тоже можно заменить рандом на HMAC от данных (или хэша данных) и ключа. DJB рекомендует :)