Безопасная очистка памяти и перераспределение
После обсуждения здесь, если вы хотите иметь безопасный класс для хранения конфиденциальной информации (например, паролей) в памяти, вам необходимо:
- memset/очистить память перед ее освобождением.
- reallocations также должны следовать тому же правилу - вместо использования realloc, используйте malloc для создания новой области памяти, скопируйте старый в новый, а затем memset/очистите старую память, прежде чем освободить ее.
Итак, это звучит неплохо, и я создал тестовый класс, чтобы увидеть, работает ли это. Поэтому я сделал простой тестовый сценарий, в котором я продолжаю добавлять слова "LOL" и "WUT", а затем номер в этот класс защищенного буфера около тысячи раз, уничтожая этот объект, прежде чем, наконец, сделать что-то, что вызывает основной дамп.
Поскольку класс должен надежно очистить память до разрушения, я не должен быть в состоянии найти "LOLWUT" на coredump. Тем не менее, мне удалось найти их еще и подумал, не работает ли моя реализация. Тем не менее, я попробовал то же самое, используя библиотеку CryptoPP SecByteBlock:
#include <cryptopp/osrng.h>
#include <cryptopp/dh.h>
#include <cryptopp/sha.h>
#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/filters.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
using namespace std;
int main(){
{
CryptoPP::SecByteBlock moo;
int i;
for(i = 0; i < 234; i++){
moo += (CryptoPP::SecByteBlock((byte*)"LOL", 3));
moo += (CryptoPP::SecByteBlock((byte*)"WUT", 3));
char buffer[33];
sprintf(buffer, "%d", i);
string thenumber (buffer);
moo += (CryptoPP::SecByteBlock((byte*)thenumber.c_str(), thenumber.size()));
}
moo.CleanNew(0);
}
sleep(1);
*((int*)NULL) = 1;
return 0;
}
И затем скомпилируйте, используя:
g++ clearer.cpp -lcryptopp -O0
И затем включите дамп ядра
ulimit -c 99999999
Но затем включив дамп ядра и запустив его
./a.out ; grep LOLWUT core ; echo hello
дает следующий вывод
Segmentation fault (core dumped)
Binary file core matches
hello
Что вызывает это? Изменилась ли вся область памяти для приложения realloc сама из-за перераспределения, вызванного добавлением SecByteBlock?
Кроме того, Это документация SecByteBlock
изменить. После проверки дампа ядра с помощью vim я получил следующее:
http://imgur.com/owkaw
edit2: обновленный код, чтобы он был более компилируемым и команды компиляции
final edit3. Похоже, что memcpy является виновником. См. Реализацию Rasmus mymemcpy
в его ответе ниже.
Ответы
Ответ 1
Вот еще одна программа, которая напрямую воспроизводит проблему:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
inline void SecureWipeBuffer(char* buf, size_t n){
volatile char* p = buf;
asm volatile("rep stosb" : "+c"(n), "+D"(p) : "a"(0) : "memory");
}
void mymemcpy(char* b, const char* a, size_t n){
char* s1 = b;
const char* s2= a;
for(; 0<n; --n) *s1++ = *s2++;
}
int main(){
const size_t size1 = 200;
const size_t size2 = 400;
char* b = new char[size1];
for(int j=0;j<size1-10;j+=10){
memcpy(b+j, "LOL", 3);
memcpy(b+j+3, "WUT", 3);
sprintf((char*) (b+j+6), "%d", j);
}
char* nb = new char[size2];
memcpy(nb, b, size1);
//mymemcpy(nb, b, size1);
SecureWipeBuffer(b,size1);
SecureWipeBuffer(nb,size2);
*((int*)NULL) = 1;
return 0;
}
Если вы замените memcpy
на mymemcpy
или используете меньшие размеры, проблема исчезнет, поэтому я думаю, что встроенная memcpy делает что-то, что оставляет часть скопированных данных в памяти.
Я предполагаю, что это просто показывает, что очистка конфиденциальных данных из памяти практически невозможна, если она не разработана во всей системе с нуля.
Ответ 2
Несмотря на то, что отображается в coredump, пароль фактически не находится в памяти
больше после очистки буферов. Проблема состоит в том, что memcpy
ing
достаточно длинная строка утечки пароля в регистры SSE, и те
это то, что появляется в coredump.
Когда аргумент size
для memcpy
больше определенного
threshold - 80 байт на mac - тогда инструкции SSE используются для выполнения
копирование памяти. Эти инструкции быстрее, потому что они могут копировать 16
байты одновременно параллельно, а не идти по-символу,
байт по-байтам или слово за словом. Здесь ключевая часть исходного кода
Libc на mac:
LAlignedLoop: // loop over 64-byte chunks
movdqa (%rsi,%rcx),%xmm0
movdqa 16(%rsi,%rcx),%xmm1
movdqa 32(%rsi,%rcx),%xmm2
movdqa 48(%rsi,%rcx),%xmm3
movdqa %xmm0,(%rdi,%rcx)
movdqa %xmm1,16(%rdi,%rcx)
movdqa %xmm2,32(%rdi,%rcx)
movdqa %xmm3,48(%rdi,%rcx)
addq $64,%rcx
jnz LAlignedLoop
jmp LShort // copy remaining 0..63 bytes and done
%rcx
- регистр индекса цикла, %rsi
- это регистр адресов s ource,
и %rdi
является регистром адресов адресации d. Каждый бегает вокруг цикла,
64 байта копируются из исходного буфера в 4 16-байтных регистра SSE
xmm{0,1,2,3}
; то значения в этих регистрах копируются в
целевого буфера.
В этом исходном файле больше материала, чтобы убедиться, что копии
только по выровненным адресам, чтобы заполнить часть оставшейся копии
после выполнения 64-байтовых кусков и для обработки случая, когда источник и
перекрытие пункта назначения.
Однако регистры SSE не очищаются после использования! Это означает, что 64 байта
буфера, который был скопирован, все еще присутствует в регистрах xmm{0,1,2,3}
.
Здесь есть модификация программы Расмуса, которая показывает это:
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <emmintrin.h>
inline void SecureWipeBuffer(char* buf, size_t n){
volatile char* p = buf;
asm volatile("rep stosb" : "+c"(n), "+D"(p) : "a"(0) : "memory");
}
int main(){
const size_t size1 = 200;
const size_t size2 = 400;
char* b = new char[size1];
for(int j=0;j<size1-10;j+=10){
memcpy(b+j, "LOL", 3);
memcpy(b+j+3, "WUT", 3);
sprintf((char*) (b+j+6), "%d", j);
}
char* nb = new char[size2];
memcpy(nb, b, size1);
SecureWipeBuffer(b,size1);
SecureWipeBuffer(nb,size2);
/* Password is now in SSE registers used by memcpy() */
union {
__m128i a[4];
char c;
};
asm ("MOVDQA %%xmm0, %0": "=x"(a[0]));
asm ("MOVDQA %%xmm1, %0": "=x"(a[1]));
asm ("MOVDQA %%xmm2, %0": "=x"(a[2]));
asm ("MOVDQA %%xmm3, %0": "=x"(a[3]));
for (int i = 0; i < 64; i++) {
char p = *(&c + i);
if (isprint(p)) {
putchar(p);
} else {
printf("\\%x", p);
}
}
putchar('\n');
return 0;
}
На моем mac это печатает:
0\0LOLWUT130\0LOLWUT140\0LOLWUT150\0LOLWUT160\0LOLWUT170\0LOLWUT180\0\0\0
Теперь, исследуя дамп ядра, пароль появляется только один раз,
и как точная строка 0\0LOLWUT130\0...180\0\0\0
. Сальник ядра должен
содержат копию всех регистров, поэтому эта строка есть
значения регистров xmm{0,1,2,4}
.
Итак, пароль уже не находится в ОЗУ после вызова
SecureWipeBuffer
, он выглядит только как, потому что он фактически находится в некоторых
регистры, которые появляются только в coredump. Если вас беспокоит
memcpy
с уязвимостью, которая может быть использована при замораживании RAM,
больше не беспокойтесь. Если вам нужна копия пароля в реестрах,
используйте модифицированный memcpy
, который не использует регистры SSE2, или очищает их
когда это будет сделано. И если вы действительно параноики об этом, продолжайте проверять свои
coredumps, чтобы убедиться, что компилятор не оптимизирует ваш
код очистки пароля.
Ответ 3
Строковые литералы будут храниться в памяти и не управляться классом SecByteBlock.
Этот другой вопрос SO делает достойную работу, объясняя это:
Является строковым литералом в С++, созданным в статической памяти?
Вы можете попробовать и проверить, могут ли совпадения grep обрабатываться строковыми литералами, видя, сколько совпадений вы получаете. Вы также можете распечатать ячейки памяти буферов SecByteBlock и попытаться выяснить, соответствуют ли они местоположениям в дампе ядра, которые соответствуют вашему маркеру.
Ответ 4
Не проверяя детали memcpy_s
, я подозреваю, что то, что вы видите, это временный буфер стека, используемый memcpy_s
для копирования небольших буферов памяти. Вы можете проверить это, запустив в отладчике и посмотрев, появляется ли LOLWUT
при просмотре памяти стека.
[Реализация reallocate
в Crypto ++ использует memcpy_s
при изменении размеров распределений памяти, поэтому вы можете найти некоторое количество строк LOLWUT
в памяти. Кроме того, тот факт, что многие разные строки LOLWUT
перекрываются в этом дампе, позволяют предположить, что это временный буфер, который используется повторно.]
Пользовательская версия memcpy
, которая представляет собой простой цикл, не требует временного хранения за пределами счетчиков, поэтому это будет, безусловно, более безопасным, чем реализация memcpy_s
.
Ответ 5
Я бы предположил, что способ сделать это - зашифровать данные в памяти. Таким образом, данные всегда безопасны независимо от того, находится ли она в памяти или нет. Недостатком, конечно же, является накладные расходы с точки зрения шифрования/дешифрования данных каждый раз, когда к нему обращаются.