За последние две недели мы видели серию статей, посвященных тому, что было описано как «взлом мастер-пароля» в популярном менеджере паролей с открытым исходным кодом KeePass.
Ошибка была сочтена достаточно серьезной, чтобы получить официальный идентификатор правительства США (известный как CVE-2023-32784, если вы хотите найти его), и учитывая, что мастер-пароль к вашему менеджеру паролей является в значительной степени ключом ко всему вашему цифровому замку, вы можете понять, почему эта история вызвала такой ажиотаж.
Хорошей новостью является то, что злоумышленник, который хотел использовать эту ошибку, почти наверняка уже заразил ваш компьютер вредоносным ПО и, следовательно, в любом случае сможет следить за вашими нажатиями клавиш и запущенными программами.
Другими словами, баг можно считать легко управляемым риском, пока создатель KeePass не выпустит обновление, которое должно появиться в ближайшее время (в начале июня 2023 года, по-видимому).
Поскольку тот, кто обнаружит ошибку, заботится о указать:
Если вы используете полное шифрование диска с надежным паролем и ваша система [свободна от вредоносных программ], все будет в порядке. Никто не может украсть ваши пароли удаленно через Интернет только с помощью этого открытия.
Объяснение рисков
Грубо говоря, ошибка сводится к сложности обеспечения того, чтобы все следы конфиденциальных данных были удалены из памяти после того, как вы закончили с ними работать.
Мы проигнорируем здесь проблемы того, как вообще избежать хранения секретных данных в памяти, даже ненадолго.
В этой статье мы просто хотим напомнить программистам во всем мире, что код, одобренный рецензентом, заботящимся о безопасности, с комментарием типа «похоже, правильно очищает после себя»…
… на самом деле может вообще не очищаться полностью, а потенциальная утечка данных может быть не очевидна при непосредственном изучении самого кода.
Проще говоря, уязвимость CVE-2023-32784 означает, что мастер-пароль KeePass может быть восстановлен из системных данных даже после завершения работы программы KeyPass, поскольку достаточно информации о вашем пароле (хотя и не самого необработанного пароля, на котором мы сосредоточимся через мгновение) может остаться в системных файлах подкачки или спящих файлах, где выделенная системная память может быть сохранена на потом.
На компьютере с Windows, где BitLocker не используется для шифрования жесткого диска при выключении системы, это даст мошеннику, укравшему ваш ноутбук, шанс загрузиться с USB-накопителя или компакт-диска и даже восстановить ваш мастер-пароль. хотя сама программа KeyPass заботится о том, чтобы никогда не сохранять ее на диск навсегда.
Утечка долговременного пароля в памяти также означает, что пароль теоретически может быть восстановлен из дампа памяти программы KeyPass, даже если этот дамп был получен спустя много времени после того, как вы ввели пароль, и спустя много времени после того, как KeePass себе больше не нужно было держать его при себе.
Очевидно, вы должны исходить из того, что вредоносное ПО, уже находящееся в вашей системе, может восстановить почти любой введенный пароль с помощью различных методов отслеживания в реальном времени, если они были активны в то время, когда вы вводили пароль. Но вы могли бы разумно ожидать, что ваше время, подверженное опасности, будет ограничено кратким периодом набора текста, а не продлено на многие минуты, часы или дни после этого или, возможно, дольше, в том числе после выключения компьютера.
Что остается позади?
Поэтому мы решили рассмотреть на высоком уровне, как секретные данные могут оставаться в памяти способами, которые не очевидны непосредственно из кода.
Не беспокойтесь, если вы не программист — мы не будем усложнять и объяснять по ходу дела.
Мы начнем с рассмотрения использования и очистки памяти в простой программе на C, которая имитирует ввод и временное сохранение пароля, выполнив следующие действия:
- Выделение выделенного куска памяти специально для хранения пароля.
- Вставка известной текстовой строки поэтому мы можем легко найти его в памяти, если это необходимо.
- Добавление 16 псевдослучайных 8-битных символов ASCII из диапазона АП.
- Распечатка имитируемый буфер паролей.
- Освобождение памяти в надежде очистить буфер паролей.
- Выход программа.
Сильно упрощенный код C мог бы выглядеть примерно так, без проверки ошибок, с использованием некачественных псевдослучайных чисел из функции времени выполнения C rand()
и игнорирование любых проверок переполнения буфера (никогда не делайте этого в реальном коде!):
// Ask for memory char* buff = malloc(128); // Copy in fixed string we can recognise in RAM strcpy(buff,"unlikelytext"); // Append 16 pseudo-random ASCII characters for (int i = 1; i <= 16; i++) { // Choose a letter from A (65+0) to P (65+15) char ch = 65 + (rand() & 15); // Modify the buff string directly in memory strncat(buff,&ch,1); } // Print it out, so we're done with buff printf("Full string was: %sn",buff); // Return the unwanted buffer and hope that expunges it free(buff);
На самом деле код, который мы наконец использовали в наших тестах, включает в себя некоторые дополнительные фрагменты, показанные ниже, чтобы мы могли сбрасывать полное содержимое нашего временного буфера паролей по мере его использования для поиска нежелательного или остаточного содержимого.
Обратите внимание, что мы намеренно сбрасываем буфер после вызова free()
, что технически является ошибкой использования после освобождения, но мы делаем это здесь как хитрый способ увидеть, останется ли что-нибудь критическое после передачи нашего буфера обратно, что может привести к опасной дыре утечки данных в реальной жизни.
Мы также вставили два Waiting for [Enter]
подсказки в коде, чтобы дать себе возможность создавать дампы памяти в ключевых точках программы, предоставляя нам необработанные данные для последующего поиска, чтобы увидеть, что осталось после выполнения программы.
Чтобы сделать дампы памяти, мы будем использовать Microsoft Инструмент Sysinternals procdump
с -ma
вариант (сбросить всю память), что избавляет от необходимости писать собственный код для использования Windows DbgHelp
система и довольно сложная MiniDumpXxxx()
Функции.
Для компиляции кода C мы использовали нашу собственную маленькую и простую сборку бесплатного и открытого исходного кода Фабриса Беллара. Крошечный компилятор C, доступен для 64-битной Windows в исходный код и бинарная форма прямо с нашей страницы GitHub.
Копируемый и вставляемый текст всего исходного кода, изображенного в статье, появляется внизу страницы.
Вот что получилось, когда мы скомпилировали и запустили тестовую программу:
C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C Compiler - Copyright (C) 2001-2023 Fabrice Bellard Урезано Полом Даклином для использования в качестве инструмента обучения Версия petcc64-0.9.27 [0006] - Генерирует 64-битную версию Только PE -> unl1.c -> c:/users/duck/tcc/petccinc/stdio.h [. . . .] -> c:/users/duck/tcc/petcclib/libpetcc1_64.a -> C:/Windows/system32/msvcrt.dll -> C:/Windows/system32/kernel32.dll -------- ----------------------- размер файла virt раздел 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 байта) C:UsersduckKEYPASS> unl1.exe Сбрасывает «новый» буфер при запуске 00F51390: 90 57 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .W......P....... 00F513A0: 73 74 65 6D 33 32 5C 63 6D 64 2E 65 78 65 00 44 exe.D 32F00B513: 0 72 69 76 65 72 44 61 74 61D 3 43A 3C 5 57 69E riverData=C:Win 6F00C513: 0 64F 6 77 73C 5 53 79 73 74 65D 6 33 32C 5 44 dowsSystem72Dr 32F00D513: 0 69 76 65 72 73C 5 44 72 69 76 65 72 44 61 74 iversDriverData 61F00E513: 0 00 45 46 43F 5 34 33 37 32D 3 31 00 46 50 53F .EFC_5=4372.FPS_ 1F00 513F0: 42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF 00F51400: 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter 00F51410: 6E 65 74 20 45 78 70 6C 7A 56 F4 3C AC 4B 00 00 нетто ExplzV.< .K.. Полная строка была такой: вряд литекстJHKNEJJCPOMDJHAN 00F51390: 75 6E 6C 69 6B 65 6C 79 74 65 78 74 4A 48 4B 4E вряд литекстJHKN 00F513A0: 45 4A 4A 43 50 4F 4D 44 4 A 48 41 4E 00 65 00 44 EJJCPOMDJHAN.eD 00F513B0 : 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32D р 00F513D0: 69 76 65 72 73 5С 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_ 00F513F0: 42 52 4Ж 57 53 45 52 5Ж 41 50 50 5Ж 50 52 4F 46 BROWSER_APP_PROF 00F51400: 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter 00F51410: 6E 65 74 20 45 78 70 6C 7A 56 F 4 3C AC 4B 00 00 net ExplzV.<.K.. Ожидание освобождения буфера [ENTER]... Сброс буфера после free() 00F51390: A0 67 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .g......P...... 00F513A0: 45 4A 4A 43 50 4F 4D 44 4A 48 41 4E 00 65 00 44 EJJCPOMDJHAN.eD 00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6 00E RiverData=C:Win 513F0C64: 6 77F 73 5 53C 79 73 74 65 6 33D 32 5 44C 72 32 dowsSystem00Dr 513F0D69: 76 65 72 73 5 44C 72 69 76 65 72 44 61 74 61 00 iversDriverData 513F0E00: 45 46 43 5 34 33F 37 32 3 31 00D 46 50 53 5 4372 1F .EFC_00=513.FPS_ 0F42F52: 4 57 53F 45 52 5 41 50F 50 5 50 52F 4 46 00F 51400 BROWSER_APP_PROF 49F4: 45 5C 53 54F 52 49 4 47 3 E 49 6D 74 65E 72 00 51410 ILE_STRING=Inter 6F65: 74E 20 45 78 70 6 4 00C 00D 4 4 00D AC 00B XNUMX XNUMX нетто ExplM..MK. Ожидание выхода [ENTER] из main()... C:UsersduckKEYPASS>
В этом прогоне мы не удосужились получить какие-либо дампы памяти процесса, потому что сразу увидели из вывода, что этот код приводит к утечке данных.
Сразу после вызова функции библиотеки времени выполнения Windows C malloc()
, мы видим, что буфер, который мы возвращаем, содержит то, что выглядит как данные переменной среды, оставшиеся от кода запуска программы, причем первые 16 байтов, по-видимому, изменены, чтобы выглядеть как какой-то оставшийся заголовок распределения памяти.
(Обратите внимание, что эти 16 байт выглядят как два 8-байтовых адреса памяти, 0xF55790
и 0xF50150
, которые находятся сразу после и непосредственно перед нашим собственным буфером памяти соответственно.)
Когда предполагается, что пароль находится в памяти, мы можем четко видеть всю строку в буфере, как и следовало ожидать.
Но после звонка free()
, обратите внимание, как первые 16 байт нашего буфера были снова переписаны чем-то похожим на близлежащие адреса памяти, по-видимому, для того, чтобы распределитель памяти мог отслеживать блоки в памяти, которые он может повторно использовать…
… но остальная часть нашего «удаленного» текста пароля (последние 12 случайных символов EJJCPOMDJHAN
) остался позади.
Нам нужно не только управлять нашим собственным выделением и освобождением памяти в C, мы также должны убедиться, что мы выбираем правильные системные функции для буферов данных, если мы хотим точно управлять ими.
Например, переключившись на этот код, мы получаем немного больше контроля над тем, что находится в памяти:
Переключившись с malloc()
и free()
использовать низкоуровневые функции распределения Windows VirtualAlloc()
и VirtualFree()
напрямую, мы получаем лучший контроль.
Однако мы платим за скорость, потому что каждый вызов VirtualAlloc()
делает больше работы, чем вызов malloc()
, который работает, постоянно разделяя и подразделяя блок предварительно выделенной низкоуровневой памяти.
. VirtualAlloc()
повторно для небольших блоков также потребляет больше памяти в целом, потому что каждый блок, VirtualAlloc()
обычно потребляет кратное 4 КБ памяти (или 2 МБ, если вы используете так называемую большие страницы памяти), так что наш 128-байтовый буфер выше округляется до 4096 байтов, теряя 3968 байтов в конце блока памяти 4 КБ.
Но, как видите, память, которую мы возвращаем, автоматически очищается (устанавливается в ноль), поэтому мы не можем видеть, что там было раньше, и на этот раз программа вылетает, когда мы пытаемся выполнить нашу операцию use-after-free. трюк, потому что Windows обнаруживает, что мы пытаемся заглянуть в память, которая нам больше не принадлежит:
C:UsersduckKEYPASS> unl2 Сброс 'нового' буфера при запуске 0000000000 0000 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0010 00 00 00 00 00 00 00 00 00 00 00 00 .. ........... 00EA00: 00 00 0000000000 0020 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0030 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0040 00 00 00 00 00 00 00 00 00 00 00 00 ................. 00EA00: 00 00 0000000000 0050 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0060 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0070 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ Полная строка была такой: lineartextIBIPJPPHEOPOIDLL 00EA00: 00 0000000000E 0080C 00 00B 00 00C 00 00 00 00 00 00 00 00 00 lineartextIBIP 00EA00: 00A 0000000000 0000 75 6 6F 69 6F 65 6 79C 74C 65 78 74 49 JPPHEOPOIDLL.... 42EA49: 50 0000000000 0010 4 50 50 48 45 4 50 4 49 44 4 4 00 ................ 00EA00 00 : 0000000000 0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 0000000000 0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 0000000000 0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............... 00EA00: 0000000000 0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 0000000000 0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 0000000000 0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............ ... Ожидание [ENTER] для освобождения буфера... Сброс буфера после free() 00EA00: [Программа завершена здесь, потому что Windows перехватила наше использование после освобождения]
Потому что память, которую мы освободили, нужно будет перераспределить с помощью VirtualAlloc()
прежде чем его можно будет использовать снова, мы можем предположить, что он будет обнулен до того, как будет переработан.
Однако, если бы мы хотели убедиться, что он скрыт, мы могли бы вызвать специальную функцию Windows RtlSecureZeroMemory()
непосредственно перед его освобождением, чтобы гарантировать, что Windows сначала запишет в наш буфер нули.
Связанная функция RtlZeroMemory()
, если вам интересно, делает то же самое, но без гарантии фактической работы, потому что компиляторы могут удалить его как теоретически избыточный, если они заметят, что после этого буфер больше не используется.
Как видите, нам нужно проявлять большую осторожность, чтобы использовать правильные функции Windows, если мы хотим свести к минимуму время, в течение которого секреты, хранящиеся в памяти, могут откладываться на потом.
В этой статье мы не будем рассматривать, как предотвратить случайное сохранение секретов в вашем файле подкачки, заблокировав их в физической оперативной памяти. (Намекать: VirtualLock()
на самом деле недостаточно.) Если вы хотите узнать больше о низкоуровневой безопасности памяти Windows, сообщите нам об этом в комментариях, и мы рассмотрим это в следующей статье.
Использование автоматического управления памятью
Один из изящных способов избежать необходимости самостоятельно выделять, управлять и освобождать память — это использовать язык программирования, который позаботится об malloc()
и free()
или VirtualAlloc()
и VirtualFree()
автоматически.
Язык сценариев, например Perl, Питон, Lua, JavaScript и другие избавляются от наиболее распространенных ошибок безопасности памяти, которые мешают коду C и C++, отслеживая использование памяти для вас в фоновом режиме.
Как мы упоминали ранее, наш плохо написанный пример кода на C теперь работает нормально, но только потому, что это все еще очень простая программа со структурами данных фиксированного размера, где мы можем проверить путем проверки, что мы не перезапишем наши 128-байтовые файлы. байтовый буфер и что существует только один путь выполнения, начинающийся с malloc()
и заканчивается соответствующим free()
.
Но если мы обновим его, чтобы разрешить генерацию паролей переменной длины, или добавим дополнительные функции в процесс генерации, то мы (или тот, кто будет сопровождать код дальше) легко можем столкнуться с переполнением буфера, ошибками использования после освобождения или памятью, которая никогда не освобождается и, следовательно, оставляет секретные данные еще долго после того, как они больше не нужны.
В таком языке, как Lua, мы можем позволить среде выполнения Lua, которая делает то, что на жаргоне называется автоматический сбор мусора, иметь дело с получением памяти из системы и возвратом ее, когда обнаруживает, что мы перестали ее использовать.
Программа C, которую мы перечислили выше, становится намного проще, когда за нас позаботятся о выделении и освобождении памяти:
Выделяем память для хранения строки s
просто назначив строку 'unlikelytext'
к нему.
Позже мы можем либо явно намекнуть Lua, что нас больше не интересует s
присвоив ему значение nil
(все nils
по сути один и тот же объект Lua), или прекратите использовать s
и подождите, пока Lua обнаружит, что он больше не нужен.
В любом случае, память, используемая s
в конечном итоге будут восстановлены автоматически.
И чтобы предотвратить переполнение буфера или неправильное управление размером при добавлении к текстовым строкам (оператор Lua ..
, произносится конкат, по существу добавляет две строки вместе, например +
в Python) каждый раз, когда мы удлиняем или укорачиваем строку, Lua волшебным образом выделяет место для совершенно новой строки, а не модифицирует или заменяет исходную строку в существующей ячейке памяти.
Этот подход медленнее и приводит к более высоким пикам использования памяти, чем в C, из-за промежуточных строк, выделяемых во время манипуляций с текстом, но он намного безопаснее в отношении переполнения буфера.
Но такого рода автоматическое управление строками (известное на жаргоне как неизменность, потому что строки никогда не получаются мутировал, или изменены на месте, как только они были созданы), сами по себе создают новые головные боли в области кибербезопасности.
Мы запустили программу Lua выше в Windows, до второй паузы, непосредственно перед выходом программы:
C:UsersduckKEYPASS> lua s1.lua Полная строка: вряд литекстHLKONBOJILAGLNLN Ожидание [ENTER] перед освобождением строки... Ожидание [ENTER] перед выходом...
На этот раз мы взяли дамп памяти процесса, например:
C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - утилита дампа процессов Sysinternals Copyright (C) 2009-2022 Марк Руссинович и Эндрю Ричардс Sysinternals - www.sysinternals.com [00:00:00] Дамп 1 инициировано: C:UsersduckKEYPASSlua-s1.dmp [00:00:00] Запись дампа 1: Расчетный размер файла дампа составляет 10 МБ. [00:00:00] Дамп 1 завершен: 10 МБ записано за 0.1 секунды [00:00:01] Достигнуто количество дампов.
Затем мы запустили этот простой скрипт, который считывает файл дампа обратно, находит везде в памяти ту известную строку unlikelytext
появился и распечатывает его вместе с его расположением в файле дампа и сразу же следующими за ним символами ASCII:
Даже если вы раньше использовали языки сценариев или работали в какой-либо экосистеме программирования, в которой есть так называемые управляемые строки, где система отслеживает выделение и освобождение памяти для вас и обрабатывает их по своему усмотрению…
… вы можете быть удивлены, увидев вывод, который производит это сканирование памяти:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: маловероятнотекстALJBNGOAPLLBDEB 006D8B3C: маловероятнотекстALJBNGOA 006D8B7C: маловероятнотекстALJBNGO 006D8BFC: маловероятнотекстALJBNGOAPLLBDEBJ 006D8CBC: маловероятнотекстALJBN 006 8D7D006C: маловероятнотекстALJBNGOAP 903D006C: маловероятнотекстALJBNGOAPL 90D006BC: маловероятнотекстALJBNGOAPLL 90D006FC: маловероятнотекстALJBNG 913D006C: маловероятнотекстALJBNGOAPLB 91D006BC: маловероятнотекстALJB 91D006FC: маловероятнотекстALJBNGOAPLL БД 923D006C : маловероятнотекстALJBNGOAPLLBDE 70DB006C: маловероятнотекстALJ 8DBB006C: маловероятнотекстAL 0DBDXNUMXC: маловероятнотекстA
О чудо, в тот момент, когда мы загрузили дамп памяти, хотя закончили со строкой s
(и сказал Lua, что он нам больше не нужен, сказав s = nil
), все строки, созданные кодом по пути, все еще присутствовали в оперативной памяти, еще не восстановлены и не удалены.
Действительно, если мы отсортируем приведенный выше вывод по самим строкам, а не по порядку, в котором они появились в ОЗУ, вы сможете представить, что произошло во время цикла, когда мы объединяли по одному символу в нашу строку пароля:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10 006DBD0C: вряд литекстA 006DBB8C: маловероятнотекстAL 006DB70C: маловероятнотекстALJ 006D91BC: маловероятнотекстALJB 006D8CBC: маловероятнотекстALJBN 006D90FC: маловероятнотекстALJBNG 006D8B7C: маловероятнотекстALJBNGO 006D8B3C: маловероятнотекстALJBNGOA 006D8D7C: маловероятнотекстALJBNGOAP 006D903C: маловероятнотекстALJBNGOAPL 006D90BC: маловероятнотекстALJBNGOAPLL 006D913C: маловероятнотекстALJBNGOAPLLB 006D91FC: маловероятнотекстALJBNGOAPLLBD 006D923C: маловероятнотекстALJBNGOAPLLBDE 006D 8AFC: вряд ли текстALJBNGOAPLLBDEB 006D8BFC : маловероятнотекстALJBNGOAPLLBDEBJ
Все эти временные, промежуточные строки все еще там, так что даже если бы мы успешно стерли окончательное значение s
, мы по-прежнему будем передавать все, кроме последнего символа.
На самом деле, в этом случае, даже когда мы намеренно заставляли нашу программу избавиться от всех ненужных данных, вызывая специальную функцию Lua collectgarbage()
(большинство языков сценариев имеют что-то подобное), большая часть данных в этих надоедливых временных строках все равно застряла в оперативной памяти, потому что мы скомпилировали Lua для автоматического управления памятью, используя старый добрый malloc()
и free()
.
Другими словами, даже после того, как Lua сам восстановил свои временные блоки памяти, чтобы использовать их снова, мы не могли контролировать, как и когда эти блоки памяти будут использоваться повторно, и, следовательно, как долго они будут лежать внутри процесса с их левой стороной. над данными, ожидающими обнаружения, сброса или иной утечки.
Введите .NET
Но как насчет KeePass, с которого началась эта статья?
KeePass написан на C# и использует среду выполнения .NET, поэтому он позволяет избежать проблем с неправильным управлением памятью, которые приносят с собой программы C…
… но C# управляет своими собственными текстовыми строками, как это делает Lua, что вызывает вопрос:
Даже если программист избегал хранения всего мастер-пароля в одном месте после того, как он закончил с ним, могли ли злоумышленники, имеющие доступ к дампу памяти, все же найти достаточно оставшихся временных данных, чтобы все равно угадать или восстановить мастер-пароль, даже если эти злоумышленники получили доступ к вашему компьютеру через несколько минут, часов или дней после того, как вы ввели пароль в ?
Проще говоря, существуют ли обнаруживаемые призрачные остатки вашего мастер-пароля, которые сохраняются в оперативной памяти даже после того, как вы ожидаете, что они будут удалены?
Досадно, как пользователь Github Вдохней обнаружил, ответ (по крайней мере, для версий KeePass до 2.54) — «Да».
Чтобы было ясно, мы не думаем, что ваш фактический мастер-пароль можно восстановить в виде одной текстовой строки из дампа памяти KeePass, потому что автор создал специальную функцию для ввода мастер-пароля, которая делает все возможное, чтобы избежать сохранения полного пароль, где его можно было бы легко обнаружить и разнюхать.
Мы убедились в этом, установив наш мастер-пароль на SIXTEENPASSCHARS
, набрав его, а затем немедленно, вскоре и долго после этого делая дампы памяти.
Мы искали дампы с помощью простого Lua-скрипта, который везде искал этот текст пароля, как в 8-битном формате ASCII, так и в 16-битном формате UTF-16 (Windows widechar), например:
Результаты обнадежили:
C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Чтение файла дампа... ГОТОВО. Поиск SIXTEENPASSCHARS как 8-битного ASCII... не найден. Поиск SIXTEENPASSCHARS как UTF-16... не найден.
Но Вдохней, первооткрыватель CVE-2023-32784, заметил, что когда вы вводите свой мастер-пароль, KeePass дает вам визуальную обратную связь, создавая и отображая строку-заполнитель, состоящую из символов «BLOB» Unicode, включая длину вашего пароля. пароль:
В широкоформатных текстовых строках в Windows (которые состоят из двух байтов на символ, а не только из одного байта, как в ASCII), символ «блоб» кодируется в ОЗУ как шестнадцатеричный байт. 0xCF
последующей 0x25
(который просто является знаком процента в ASCII).
Таким образом, даже если KeePass уделяет большое внимание необработанным символам, которые вы вводите при вводе самого пароля, вы можете получить оставшиеся строки символов «блоб», которые легко обнаруживаются в памяти при повторяющихся запусках, таких как CF25CF25
or CF25CF25CF25
...
…и, если это так, самая длинная серия символов BLOB-объектов, которую вы нашли, вероятно, выдаст длину вашего пароля, что будет скромной формой утечки информации о пароле, по крайней мере.
Мы использовали следующий сценарий Lua для поиска признаков оставшихся строк-заполнителей пароля:
Вывод оказался неожиданным (для экономии места мы удалили последующие строки с тем же количеством BLOB-объектов или с меньшим количеством BLOB-объектов, чем в предыдущей строке):
C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [продолжается аналогично для 8 BLOB, 9 BLOB и т. д.] [до двух последних строк ровно по 16 BLOB в каждой] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [все остальные совпадения имеют длину в один блок] 0123B058: *
По близким друг к другу, но постоянно растущим адресам памяти мы обнаружили систематический список из 3 больших двоичных объектов, затем 4 двоичных объектов и так далее до 16 двоичных объектов (длина нашего пароля), за которыми следует множество случайно разбросанных экземпляров строк из одного двоичного объекта. .
Таким образом, эти строки-заполнители «блобов» действительно, кажется, просачиваются в память и остаются позади, чтобы слить длину пароля, спустя много времени после того, как программное обеспечение KeePass завершило работу с вашим мастер-паролем.
Следующий шаг
Решили копать дальше, как это сделал Вдохней.
Мы изменили наш код сопоставления шаблонов, чтобы обнаруживать цепочки символов blob, за которыми следует любой одиночный символ ASCII в 16-битном формате (символы ASCII представлены в UTF-16 как их обычный 8-битный код ASCII, за которым следует нулевой байт).
На этот раз, чтобы сэкономить место, мы подавили вывод для любого совпадения, которое точно соответствует предыдущему:
Сюрприз Сюрприз:
C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp 00BE581B: *I 00BE621B: **X 00BE6BD3: ***T 00BE769B: ****E 00BE822B: *****E 00BE8C6B: ******N 00BE974B: *******P 00BEA25B: ********A 00BEAD33: *********S 00BEB81B: **********S 00BEC383: ***********C 00BECEEB: ************H 00BEDA5B: *************A 00BEE623: **************R 00BEF1A3: ***************S 03E97CF2: *N 0AA6F0AF: *W 0D8AF7C8: *X 0F27BAF8: *S
Посмотрите, что мы получаем из области управляемой строковой памяти .NET!
Плотно сгруппированный набор временных «цепочек больших двоичных объектов», которые показывают последовательные символы в нашем пароле, начиная со второго символа.
За этими дырявыми строками следуют широко распространенные односимвольные совпадения, которые, как мы предполагаем, возникли случайно. (Файл дампа KeePass имеет размер около 250 МБ, поэтому в нем достаточно места для появления символов «кляксы», как будто по счастливой случайности.)
Даже если мы примем во внимание эти дополнительные четыре совпадения, а не отбросим их как вероятные несоответствия, мы можем предположить, что мастер-пароль является одним из:
?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS
Очевидно, что этот простой метод не находит первый символ в пароле, потому что первая «строка большого двоичного объекта» создается только после того, как этот первый символ был введен в
Обратите внимание, что этот список хороший и короткий, потому что мы отфильтровали совпадения, которые не заканчиваются символами ASCII.
Если бы вы искали символы в другом диапазоне, например, китайские или корейские, вы могли бы столкнуться с большим количеством случайных совпадений, потому что существует гораздо больше возможных символов для сопоставления…
… но мы подозреваем, что вы в любом случае достаточно близко подберетесь к своему мастер-паролю, а «строки больших двоичных объектов», относящиеся к паролю, похоже, сгруппированы в ОЗУ, по-видимому, потому, что они были выделены примерно в одно и то же время одной и той же частью среда выполнения .NET.
И вот, по общему признанию, длинная и содержательная в двух словах, увлекательная история CVE-2023-32784.
Что делать?
- Если вы являетесь пользователем KeePass, не паникуйте. Хотя это ошибка и технически уязвимость, которую можно использовать, удаленные злоумышленники, которые хотят взломать ваш пароль с помощью этой ошибки, должны сначала внедрить вредоносное ПО на ваш компьютер. Это дало бы им множество других способов напрямую украсть ваши пароли, даже если бы этой ошибки не существовало, например, путем регистрации нажатий клавиш при вводе. На данный момент вы можете просто следить за предстоящим обновлением и захватить его, когда оно будет готово.
- Если вы не используете полнодисковое шифрование, рассмотрите возможность его включения. Чтобы извлечь оставшиеся пароли из вашего файла подкачки или файла гибернации (дисковые файлы операционной системы, используемые для временного сохранения содержимого памяти во время большой нагрузки или когда ваш компьютер «спит»), злоумышленникам потребуется прямой доступ к вашему жесткому диску. Если у вас активирован BitLocker или его эквивалент для других операционных систем, они не смогут получить доступ к вашему файлу подкачки, файлу гибернации или любым другим личным данным, таким как документы, электронные таблицы, сохраненные электронные письма и т. д.
- Если вы программист, держите себя в курсе проблем управления памятью. Не думайте, что только потому, что каждый
free()
соответствует соответствующемуmalloc()
что ваши данные в безопасности и хорошо управляются. Иногда вам может потребоваться принять дополнительные меры предосторожности, чтобы не оставить секретные данные без присмотра, и эти меры предосторожности очень зависят от операционной системы. - Если вы QA-тестер или рецензент кода, всегда думайте «за кулисами». Даже если код управления памятью выглядит опрятным и хорошо сбалансированным, будьте в курсе того, что происходит за кулисами (поскольку первоначальный программист мог этого не знать), и будьте готовы выполнять некоторую работу в стиле пентестинга, такую как мониторинг во время выполнения и память. дамп, чтобы убедиться, что безопасный код действительно ведет себя так, как должен.
КОД ИЗ СТАТЬИ: UNL1.C
#включать #включать #включать void hexdump(unsigned char* buff, int len) { // Печатаем буфер порциями по 16 байт for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +я); // Показать 16 байтов в виде шестнадцатеричных значений for (int j = 0; j < 16; j = j+1) { printf("%02X",buff[i+j]); } // Повторить эти 16 байтов как символы for (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Освобождение памяти для хранения пароля и показ // того, что находится в буфере, если он официально "новый"... char* buff = malloc(128); printf("Сброс 'нового' буфера при запуске"); шестнадцатеричный дамп (бафф, 128); // Использовать псевдослучайный адрес буфера как случайное начальное число srand((unsigned)buff); // Начните пароль с фиксированного текста с возможностью поиска strcpy(buff,"unlikelytext"); // Добавляем 16 псевдослучайных букв по одной for (int i = 1; i <= 16; i++) { // Выбираем букву от A (65+0) до P (65+15) char ch = 65 + (ранд() и 15); // Затем измените строку buff вместо strncat(buff,&ch,1); } // Полный пароль теперь находится в памяти, поэтому выведите // его в виде строки и покажите весь буфер... printf("Полная строка была: %sn",buff); шестнадцатеричный дамп (бафф, 128); // Сделайте паузу для сброса оперативной памяти процесса сейчас (попробуйте: 'procdump -ma') puts("Ожидание [ENTER] для освобождения буфера..."); получитьсимвол(); // Формально освобождаем() память и снова показываем // буфер, чтобы посмотреть, не осталось ли чего-нибудь... free(buff); printf("Сброс буфера после free()n"); шестнадцатеричный дамп (бафф, 128); // Пауза для повторного сброса ОЗУ для проверки различий puts("Ожидание [ENTER] для выхода из main()..."); получитьсимвол(); вернуть 0; }
КОД ИЗ СТАТЬИ: UNL2.C
#включать #включать #включать #включать void hexdump(unsigned char* buff, int len) { // Печатаем буфер порциями по 16 байт for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +я); // Показать 16 байтов в виде шестнадцатеричных значений for (int j = 0; j < 16; j = j+1) { printf("%02X",buff[i+j]); } // Повторить эти 16 байтов как символы for (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Освобождаем память для хранения пароля и показываем, // что находится в буфере, когда он официально "новый"... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("Сброс 'нового' буфера при запуске"); шестнадцатеричный дамп (бафф, 128); // Использовать псевдослучайный адрес буфера как случайное начальное число srand((unsigned)buff); // Начните пароль с фиксированного текста с возможностью поиска strcpy(buff,"unlikelytext"); // Добавляем 16 псевдослучайных букв по одной for (int i = 1; i <= 16; i++) { // Выбираем букву от A (65+0) до P (65+15) char ch = 65 + (ранд() и 15); // Затем измените строку buff вместо strncat(buff,&ch,1); } // Полный пароль теперь находится в памяти, поэтому выведите // его в виде строки и покажите весь буфер... printf("Полная строка была: %sn",buff); шестнадцатеричный дамп (бафф, 128); // Сделайте паузу для сброса оперативной памяти процесса сейчас (попробуйте: 'procdump -ma') puts("Ожидание [ENTER] для освобождения буфера..."); получитьсимвол(); // Формально освобождаем() память и снова показываем буфер, // чтобы посмотреть, не осталось ли чего-нибудь позади... VirtualFree(buff,0,MEM_RELEASE); printf("Сброс буфера после free()n"); шестнадцатеричный дамп (бафф, 128); // Пауза для повторного сброса ОЗУ для проверки различий puts("Ожидание [ENTER] для выхода из main()..."); получитьсимвол(); вернуть 0; }
КОД ИЗ СТАТЬИ: S1.LUA
-- Начните с фиксированного текста с возможностью поиска s = 'unlikelytext' -- Добавьте 16 случайных символов от 'A' до 'P' для i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('Полная строка:',s,'n') -- Пауза для вывода ОЗУ процесса print('Ожидание [ENTER] перед освобождением строки...') io.read() - - Очистить строку и пометить переменную как неиспользуемую s = nil -- Снова выполнить дамп ОЗУ для поиска различий print('Ожидание [ENTER] перед выходом...') io.read()
КОД ИЗ СТАТЬИ: FINDIT.LUA
-- читать в файле дампа local f = io.open(arg[1],'rb'):read('*a') -- искать текст маркера, за которым следует один -- или несколько случайных символов ASCII local b,e ,m = 0,0,nil while true do -- найти следующее совпадение и запомнить смещение b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- выйти, когда больше не будет соответствует, если не b, то break end -- сообщает позицию и найденную строку print(string.format('%08X: %s',b,m)) end
КОД ИЗ СТАТЬИ: SEARCHKNOWN.LUA
io.write('Чтение файла дампа...') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Поиск SIXTEENPASSCHARS в виде 8-битного ASCII... ') local p08 = f:find('SIXTEENPASSCHARS') io.write(p08 and 'FOUND' или 'not found','.n') io.write («Поиск SIXTEENPASSCHARS в кодировке UTF-16...») не найдено','.n')
КОД ИЗ СТАТЬИ: FINDBLOBS.LUA
-- читать в файле дампа, указанном в командной строке local f = io.open(arg[1],'rb'):read('*a') -- искать один или несколько блоков паролей, за которыми -- Обратите внимание, что символы больших двоичных объектов (●) кодируются в широкоформатные символы Windows -- как коды UTF-16 с прямым порядком байтов, которые выводятся как CF 25 в шестнадцатеричном формате. local b,e,m = 0,0,nil while true do -- Нам нужен один или несколько BLOB-объектов, за которыми следует любой не-BLOB-объект. -- Мы упрощаем код, ища явный CF25 -- за которым следует любая строка, содержащая только CF или 25, -- таким образом, мы найдем CF25CFCF или CF2525CF, а также CF25CF25. -- Мы отфильтруем "ложные срабатывания" позже, если они есть. -- Нам нужно написать '%%' вместо x25, потому что символ x25 -- (знак процента) является специальным поисковым символом в Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- выйти, когда совпадений больше нет, если не b, то break end -- CMD.EXE не может печатать капли, поэтому мы конвертируем их в звезды. print(string.format('%08X:%s',b,m:gsub('xCF%%','*'))) конец
КОД ИЗ СТАТЬИ: SEARCHKP.LUA
-- прочитать файл дампа, указанный в командной строке local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- Теперь нам нужен один или несколько больших двоичных объектов (CF25), за которыми следует код -- для A..Z, за которым следует 0 байт для преобразования ACSCII в UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- выйти, когда совпадений больше нет, если не b, то разорвать конец -- CMD.EXE не может печатать большие двоичные объекты, поэтому мы преобразуем их в звезды. -- Для экономии места мы подавляем последовательные совпадения, если m ~= p then print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m конец конец
- SEO-контент и PR-распределение. Получите усиление сегодня.
- ПлатонАйСтрим. Анализ данных Web3. Расширение знаний. Доступ здесь.
- Чеканка будущего с Эдриенн Эшли. Доступ здесь.
- Покупайте и продавайте акции компаний PREIPO® с помощью PREIPO®. Доступ здесь.
- Источник: https://nakedsecurity.sophos.com/2023/05/31/serious-security-that-keepass-master-password-crack-and-what-we-can-learn-from-it/
- :имеет
- :является
- :нет
- :куда
- ][п
- $UP
- 1
- 10
- 12
- 15%
- 20
- 200
- 2023
- 24
- 250
- 27
- 31
- 3d
- 49
- 50
- 67
- 70
- 72
- 77
- 8
- 9
- a
- в состоянии
- О нас
- выше
- Absolute
- AC
- доступ
- Учетная запись
- приобретать
- приобретение
- активный
- фактического соединения
- на самом деле
- добавленный
- дополнительный
- адрес
- адреса
- Добавляет
- После
- потом
- снова
- Все
- выделено
- выделяет
- распределение
- ассигнования
- позволять
- в одиночестве
- вдоль
- уже
- причислены
- изменен
- Несмотря на то, что
- всегда
- an
- и
- Эндрю
- ответ
- любой
- все
- что-нибудь критическое
- появиться
- появившийся
- подхода
- утвержденный
- МЫ
- около
- гайд
- статьи
- AS
- At
- автор
- автоматический
- Автоматический
- автоматически
- доступен
- избежать
- избегать
- знать
- прочь
- назад
- фон
- Фоновое изображение
- BE
- , так как:
- становится
- было
- до
- начало
- за
- за кулисами
- ниже
- Лучшая
- Немного
- Заблокировать
- Блоки
- граница
- изоферменты печени
- Дно
- марка
- Новостройка
- Ломать
- кратко
- приносить
- буфер
- переполнение буфера
- Ошибка
- ошибки
- строить
- но
- by
- C + +
- призывают
- вызова
- CAN
- Может получить
- заботится
- случаев
- пойманный
- CD
- Центр
- конечно
- цепи
- шанс
- менялась
- персонаж
- символы
- контроль
- Проверки
- китайский
- Выберите
- Очистить
- явно
- Закрыть
- код
- цвет
- COM
- выходит
- приход
- комментарий
- Комментарии
- Общий
- полный
- комплекс
- компьютер
- Рассматривать
- значительный
- считается
- Состоящий из
- строительство
- содержание
- содержание
- беспрестанно
- продолжается
- контроль
- конвертировать
- авторское право
- соответствующий
- может
- чехол для варгана
- трещина
- Создайте
- создали
- создатель
- критической
- Информационная безопасность
- ОПАСНО!
- опасно
- данным
- утечка данных
- Дней
- сделка
- решенный
- преданный
- описано
- DID
- Различия
- различный
- Трудность
- КОПАТЬ
- Интернет
- направлять
- Прямой доступ
- непосредственно
- Дисплей
- отображать
- распорядиться
- do
- Документация
- приносит
- не
- дело
- сделанный
- Dont
- вниз
- управлять
- два
- дамп
- в течение
- e
- каждый
- Ранее
- легко
- экосистема
- или
- еще
- Писем
- позволяет
- поощрение
- шифрование
- конец
- окончания поездки
- достаточно
- обеспечивать
- обеспечение
- Enter
- входящий
- Весь
- запись
- Окружающая среда
- Эквивалент
- ошибка
- по существу
- По оценкам,
- и т.д
- Эфир (ETH)
- Даже
- со временем
- постоянно растет
- Каждая
- многое
- точно,
- пример
- Кроме
- Возбуждение
- выполнение
- существовать
- существующий
- Выход
- Выход
- ожидать
- Объяснять
- Эксплуатировать
- подвергаться
- продлить
- дополнительно
- извлечение
- факт
- ложный
- увлекательный
- Особенности
- Обратная связь
- меньше
- борьба
- Файл
- Файлы
- фильтр
- окончательный
- в заключение
- Найдите
- обнаружение
- находит
- конец
- First
- фиксированной
- Фокус
- следует
- после
- Что касается
- форма
- Формально
- формат
- предстоящий
- найденный
- 4
- Бесплатно
- от
- полный
- полностью
- функция
- Функции
- далее
- будущее
- генерирует
- поколение
- получить
- получающий
- GitHub
- Дайте
- данный
- дает
- Отдаете
- Go
- идет
- будет
- хорошо
- Правительство
- захват
- большой
- гарантия
- было
- Ручки
- произошло
- Случай
- происходит
- Жесткий
- Есть
- имеющий
- головные боли
- тяжелый
- высота
- здесь
- HEX
- на высшем уровне
- высший
- Хиты
- держать
- Отверстие
- надежды
- ЧАСЫ
- зависать
- Как
- How To
- HTTPS
- Охота
- i
- идентификатор
- if
- немедленно
- важную
- in
- включает в себя
- В том числе
- информация
- сообщил
- вместо
- заинтересованный
- Intermediate
- Интернет
- в
- вопросы
- IT
- ЕГО
- саму трезвость
- жаргон
- июнь
- всего
- только один
- Сохранить
- Основные
- Знать
- известный
- Корейский
- язык
- Языки
- портативный компьютер
- Фамилия
- новее
- вести
- Лиды
- утечка
- Утечки
- УЧИТЬСЯ
- изучение
- наименее
- уход
- оставил
- Длина
- письмо
- Библиотека
- ЖИЗНЬЮ
- такое как
- Вероятно
- Ограниченный
- линия
- линий
- Список
- Включенный в список
- ll
- загрузка
- локальным
- расположение
- каротаж
- Длинное
- долгосрочный
- дольше
- посмотреть
- выглядит как
- смотрел
- искать
- ВЗГЛЯДЫ
- серия
- удачи
- поддерживает
- сделать
- вредоносных программ
- управлять
- управляемого
- управление
- менеджер
- управляет
- Манипуляция
- многих
- Маржа
- отметка
- маркер
- мастер
- Совпадение
- согласование
- макс-ширина
- Май..
- означает
- Память
- упомянутый
- Microsoft
- может быть
- минут
- скромный
- модифицировало
- изменять
- момент
- Мониторинг
- БОЛЕЕ
- самых
- много
- с разными
- неразбавленный
- Необходимость
- необходимый
- сеть
- никогда
- Тем не менее
- Новые
- Новости
- следующий
- хороший
- нет
- "обычные"
- ничего
- Уведомление..
- сейчас
- номер
- номера
- объект
- Очевидный
- of
- от
- Официальный представитель в Грузии
- Официально
- смещение
- Старый
- on
- консолидировать
- ONE
- только
- с открытым исходным кодом
- операционный
- операционная система
- операционные системы
- оператор
- Опция
- or
- заказ
- оригинал
- Другое
- Другое
- в противном случае
- наши
- себя
- внешний
- выходной
- за
- общий
- собственный
- страница
- Паника
- часть
- Пароль
- Password Manager
- пароли
- путь
- шаблон
- Пол
- Пауза
- ОПЛАТИТЬ
- процент
- возможно
- период
- постоянно
- личного
- личные данные
- физический
- картина
- штук
- Часть
- заполнитель
- Чума
- Платон
- Платон Интеллектуальные данные
- ПлатонДанные
- Много
- Точка
- пунктов
- Популярное
- должность
- возможное
- Блог
- потенциал
- Точно
- представить
- довольно
- предотвращать
- предыдущий
- цена
- Печать / PDF
- печать
- вероятно
- проблемам
- процесс
- FitPartner™
- Программист
- Программисты
- Программирование
- Программы
- выраженный
- положил
- Питон
- Вопросы и ответы
- вопрос
- повышения
- Оперативная память
- случайный
- ассортимент
- скорее
- Сырье
- необработанные данные
- RE
- достиг
- Читать
- Reading
- готовый
- реальные
- реальная жизнь
- реального времени
- на самом деле
- признать
- Recover
- Я выздоровела
- Связанный
- осталось
- помнить
- удаленные
- удаление
- повторять
- повторный
- НЕОДНОКРАТНО
- отчету
- представленный
- уважение
- соответственно
- ОТДЫХ
- Итоги
- возвращают
- возвращение
- показывать
- избавиться
- правую
- Снижение
- рисках,
- Комната
- Run
- Бег
- мониторинг времени выполнения
- s
- безопасный
- безопаснее
- то же
- довольный
- Сохранить
- поговорка
- сканирование
- рассеянный
- Сцены
- Поиск
- поиск
- Во-вторых
- секунды
- Secret
- Раздел
- безопасный
- безопасность
- посмотреть
- семя
- видя
- казаться
- видел
- видит
- Серии
- серьезный
- набор
- установка
- Короткое
- вскоре
- должен
- показывать
- показанный
- подпись
- Признаки
- аналогичный
- Аналогичным образом
- просто
- упрощенный
- упростить
- просто
- одинарной
- Размер
- спать
- небольшой
- Подлый
- шпионящий
- So
- Software
- твердый
- некоторые
- удалось
- Скоро
- Источник
- исходный код
- Space
- особый
- специально
- указанный
- скорость
- Звезды
- Начало
- и политические лидеры
- Начало
- начинается
- ввод в эксплуатацию
- По-прежнему
- украли
- Stop
- остановившийся
- магазин
- хранить
- История
- строка
- сильный
- Кабинет
- Успешно
- такие
- достаточный
- предполагаемый
- сюрприз
- удивлен
- удивительный
- выживать
- SVG
- обмен
- система
- системы
- взять
- приняты
- принимает
- с
- говорить
- технически
- снижения вреда
- временный
- тестXNUMX
- тестов
- чем
- который
- Ассоциация
- Источник
- их
- Их
- сами
- тогда
- теория
- Там.
- следовательно
- они
- задача
- think
- этой
- те
- хоть?
- мысль
- время
- Название
- в
- вместе
- приняли
- инструментом
- топ
- трек
- Отслеживание
- переход
- прозрачный
- правда
- стараться
- Оказалось
- два
- напишите
- типично
- понимать
- юникода
- до
- неиспользованный
- нежелательный
- Обновление ПО
- обновление
- URL
- us
- правительство США
- Применение
- USB
- использование
- использовать после освобождения
- используемый
- Информация о пользователе
- использования
- через
- утилита
- ценностное
- Наши ценности
- разнообразие
- проверить
- версия
- очень
- с помощью
- уязвимость
- W
- ждать
- Ожидание
- хотеть
- стремятся
- законопроект
- Смотреть
- Путь..
- способы
- we
- Недели
- ЧТО Ж
- были
- Что
- когда
- будь то
- который
- в то время как
- КТО
- кто бы ни
- все
- зачем
- будете
- выиграть
- окна
- вытирать
- без
- интересно
- слова
- Работа
- работавший
- работает
- работает
- беспокоиться
- бы
- даст
- записывать
- письмо
- письменный
- еще
- являетесь
- ВАШЕ
- себя
- зефирнет
- нуль