За останні два тижні ми бачили низку статей, у яких розповідалося про те, що було описано як «злом головного пароля» в популярному менеджері паролів з відкритим кодом KeePass.
Помилка була визнана достатньо важливою, щоб отримати офіційний ідентифікатор уряду США (він відомий як CVE-2023-32784, якщо ви хочете його вистежити), і враховуючи, що головний пароль до вашого менеджера паролів є майже ключем до всього вашого цифрового замку, ви можете зрозуміти, чому ця історія викликала багато хвилювання.
Хороша новина полягає в тому, що зловмисник, який хотів використати цю помилку, майже напевно повинен був заразити ваш комп’ютер зловмисним програмним забезпеченням, і, отже, міг би стежити за вашими натисканнями клавіш і запущеними програмами в будь-якому випадку.
Іншими словами, помилку можна вважати легкокерованим ризиком, поки творець KeePass не випустить оновлення, яке має з’явитися найближчим часом (очевидно, на початку червня 2023 року).
Як особа, яка виявила помилку, подбає про вказати:
Якщо ви використовуєте повне шифрування диска з надійним паролем і ваша система [вільна від зловмисного програмного забезпечення], все має бути добре. Ніхто не зможе дистанційно викрасти ваші паролі через Інтернет лише за допомогою цього висновку.
Пояснення ризиків
Коротко кажучи, помилка зводиться до труднощів переконатися, що всі сліди конфіденційних даних видаляються з пам’яті, коли ви закінчите з ними.
Ми ігноруватимемо тут проблеми того, як уникнути наявності секретних даних у пам’яті взагалі, навіть ненадовго.
У цій статті ми просто хочемо нагадати програмістам у всьому світі, що код, схвалений рецензентом, який піклується про безпеку, з коментарем на кшталт «здається, правильно прибирає за собою»…
…насправді взагалі може не очищатися повністю, а потенційний витік даних може бути неочевидним із безпосереднього вивчення самого коду.
Простіше кажучи, уразливість CVE-2023-32784 означає, що головний пароль KeePass можна відновити із системних даних навіть після завершення роботи програми KeyPass, оскільки достатньо інформації про ваш пароль (хоча насправді не сам необроблений пароль, на якому ми зосередимося увімкнено за мить) може залишитися в системних файлах підкачки чи сплячому режимі, де виділена системна пам’ять може бути збережена на потім.
На комп’ютері з ОС Windows, де BitLocker не використовується для шифрування жорсткого диска, коли систему вимкнено, це дасть шахраю, який викрав ваш ноутбук, шанс завантажитися з USB- чи компакт-диска та навіть відновити ваш головний пароль. хоча сама програма KeyPass ніколи не зберігає його на диску.
Довготривалий витік пароля в пам’яті також означає, що пароль теоретично можна відновити з дампа пам’яті програми KeyPass, навіть якщо цей дамп було захоплено задовго після того, як ви ввели пароль, і задовго після того, як KeePass самому більше не було потреби тримати його поруч.
Зрозуміло, ви повинні припустити, що зловмисне програмне забезпечення, яке вже є у вашій системі, може відновити майже будь-який введений пароль за допомогою різноманітних методів стеження в режимі реального часу, якщо вони були активні під час введення. Але ви можете розумно очікувати, що ваш час, підданий небезпеці, буде обмежений коротким періодом набору тексту, а не розтягнутим на багато хвилин, годин чи днів після цього, або, можливо, довше, зокрема після того, як ви вимкнете комп’ютер.
Що залишається позаду?
Тому ми вирішили поглянути на високий рівень того, як секретні дані можуть залишатися в пам’яті способами, які неочевидні з коду.
Не хвилюйтеся, якщо ви не програміст – ми зробимо це простим і пояснимо по ходу роботи.
Ми почнемо з вивчення використання та очищення пам’яті в простій програмі на C, яка імітує введення та тимчасове збереження пароля, виконавши такі дії:
- Виділення виділеної частини пам'яті спеціально для збереження пароля.
- Вставка відомого текстового рядка тому ми можемо легко знайти його в пам'яті, якщо потрібно.
- Додавання 16 псевдовипадкових 8-бітових символів ASCII з діапазону AP.
- Роздрукування імітований буфер пароля.
- Звільнення пам'яті в надії стерти буфер пароля.
- Вихід Програма.
У значно спрощеному вигляді код 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 stem32cmd. exe.D 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 dowsSystem32Dr 00F513D0: 69 76 65 72 73 5C 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_ 00F 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 net ExplzV.< .K.. Повний рядок був: unlikelytextJHKNEJJCPOMDJHAN 00F51390: 75 6E 6C 69 6B 65 6C 79 74 65 78 74 4A 48 4B 4E unlikelytextJHKN 00F513A0: 45 4A 4A 43 50 4F 4 D 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 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem 32Dr 00F513D0: 69 76 65 72 73 5C 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 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 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 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr 00F513D0: 69 76 65 72 73 5C 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 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 4 E 47 3D 49 6E 74 65 72 ILE_STRING=Inter 00F51410: 6E 65 74 20 45 78 70 6C 4D 00 00 4D AC 4B 00 00 net 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 КБ.
Але, як ви бачите, пам’ять, яку ми повертаємо, автоматично очищається (встановлюється на нуль), тому ми не можемо бачити, що там було раніше, і цього разу програма аварійно завершує роботу, коли ми намагаємося виконати наше використання після звільнення трюк, тому що Windows виявляє, що ми намагаємося заглянути в пам’ять, якою ми більше не володіємо:
C:UsersduckKEYPASS> unl2 Викидання «нового» буфера на початку 0000000000EA0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .. .............. 0000000000EA0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ Повний рядок: unlikelytextIBIPJPPHEOPOIDLL 0000EA75: 6 6E 69C 6 65B 6 79C 74 65 78 74 49 42 49 50 0000000000 unlikelytextIBIP 0010EA4: 50A 50 48 45 4 50F 4 49F 44 4 4C 00C 00 00 00 0000000000 JPPHEOPOIDLL .... 0020EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0030EA00 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0040EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0050EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ............... 0060EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0070EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................. 0080EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ............. ... Очікування [ENTER] для звільнення буфера... Дамп буфера після free() 0000EAXNUMX: [Програма завершилася тут, оскільки Windows виявила наше використання після звільнення]
Оскільки пам’ять, яку ми звільнили, потребуватиме перерозподілу VirtualAlloc()
перш ніж його можна буде використовувати знову, ми можемо припустити, що його буде обнулено перед переробкою.
Однак, якщо ми хочемо переконатися, що він порожній, ми можемо викликати спеціальну функцію Windows RtlSecureZeroMemory()
безпосередньо перед звільненням, щоб гарантувати, що Windows спочатку запише нулі в наш буфер.
Відповідна функція RtlZeroMemory()
, якщо вам було цікаво, робить подібне, але без гарантії фактичної роботи, оскільки компіляторам дозволено видалити його як теоретично зайвий, якщо вони помітять, що буфер не використовується згодом.
Як ви бачите, нам потрібно дуже ретельно використовувати правильні функції Windows, якщо ми хочемо мінімізувати час, протягом якого секрети, збережені в пам’яті, можуть лежати в майбутньому.
У цій статті ми не розглядатимемо, як запобігти випадковому збереженню секретів у файлі підкачки, заблокувавши їх у фізичній оперативній пам’яті. (Підказка: VirtualLock()
насправді недостатньо самого по собі.) Якщо ви хочете дізнатися більше про низькорівневий захист пам’яті Windows, повідомте нам про це в коментарях, і ми розглянемо це в наступній статті.
Використання автоматичного керування пам'яттю
Одним із чудових способів уникнути необхідності самостійного розподілу, керування та звільнення пам’яті є використання мови програмування, яка піклується про malloc()
та free()
або VirtualAlloc()
та VirtualFree()
, автоматично.
Мова сценаріїв, наприклад Perl, Python, Lua, JavaScript та інші позбавляються найпоширеніших помилок безпеки пам’яті, які заважають коду C і C++, відстежуючи використання пам’яті у фоновому режимі.
Як ми вже згадували раніше, наш погано написаний зразок коду C, наведений вище, зараз працює нормально, але лише тому, що це все ще надпроста програма зі структурами даних фіксованого розміру, де ми можемо перевірити, чи не перезапишемо наш 128- байтового буфера, і що існує лише один шлях виконання, який починається з malloc()
і закінчується відповідним free()
.
Але якщо ми оновимо його, щоб дозволити генерацію паролів змінної довжини, або додамо додаткові функції в процес генерації, тоді ми (або будь-хто, хто буде підтримувати код наступним) може легко закінчитися переповненням буфера, помилками використання після звільнення або пам’яттю, яка ніколи не звільняється, тому секретні дані залишаються довго після того, як вони більше не потрібні.
У такій мові, як Lua, ми можемо дозволити середовищу виконання Lua, яке виконує те, що на жаргоні називається автоматичний збір сміття, займаються отриманням пам’яті від системи та поверненням її, коли виявляє, що ми припинили її використання.
Програма C, яку ми перерахували вище, стає набагато простішою, коли розподіл пам’яті та її де-розподілення подбали за нас:
Ми виділяємо пам'ять для зберігання рядка s
просто призначивши рядок 'unlikelytext'
до цього
Пізніше ми можемо або прямо натякнути Lua, що нас більше не цікавить s
присвоївши йому значення nil
(усі nils
по суті є тим самим об’єктом Lua), або припинити використання s
і зачекайте, поки Lua визначить, що він більше не потрібен.
У будь-якому випадку пам’ять, яку використовує s
зрештою буде відновлено автоматично.
І щоб запобігти переповненню буфера або неправильному керуванню розміром під час додавання до текстових рядків (оператор Lua ..
, вимовляється concat, по суті додає два рядки разом, наприклад +
у Python), кожного разу, коли ми розширюємо або скорочуємо рядок, Lua чарівним чином виділяє простір для абсолютно нового рядка, замість того, щоб змінювати або замінювати вихідний у його існуючому місці пам’яті.
Цей підхід є повільнішим і призводить до піків використання пам’яті, які є вищими, ніж у C через проміжні рядки, виділені під час маніпулювання текстом, але він набагато безпечніший щодо переповнення буфера.
Але цей тип автоматичного керування рядками (відомий на жаргоні як незмінність, оскільки рядки ніколи не отримують мутований, або змінені на місці, коли вони були створені), створює нові проблеми з кібербезпекою.
Ми запустили програму Lua вище на Windows, до другої паузи, безпосередньо перед виходом програми:
C:UsersduckKEYPASS> lua s1.lua Повний рядок: unlikelytextHLKONBOJILAGLNLN Очікування [ENTER] перед звільненням рядка... Очікування [ENTER] перед виходом...
Цього разу ми взяли дамп пам’яті процесу, ось так:
C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 – утиліта створення дампа процесів Sysinternals Copyright (C) 2009-2022 Mark Russinovich та Andrew Richards 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: unlikelytextALJBNGOAPLLBDEB 006D8B3C: unlikelytextALJBNGOA 006D8B7C: unlikelytextALJBNGO 006D8BFC: unlikelytextALJBNGOAPLLBDEBJ 006D8CBC: unlikely lytextALJBN 006D8D7C: малоймовірний текстALJBNGOAP 006D903C: малоймовірний текстALJBNGOAPL 006D90BC: малоймовірний текстALJBNGOAPLL 006D90FC: малоймовірний текстALJBNG 006D913C: малоймовірний текстALJBNGOAPLLB 006D91BC: малоймовірний текстALJB 006D91FC: unlikelytextALJBNGOAPLLBD 006D923C : unlikelytextALJBNGOAPLLBDE 006DB70C: unlikelytextALJ 006DBB8C: unlikelytextAL 006DBD0C: unlikelytextA
Ось і ось, у той час, коли ми схопили свій дамп пам’яті, хоча ми закінчили з рядком s
(і сказав Луа, що нам це більше не потрібно, сказавши s = nil
), усі рядки, які код створив на цьому шляху, все ще присутні в оперативній пам’яті, ще не відновлені чи видалені.
Дійсно, якщо ми відсортуємо наведені вище результати за самими рядками, а не за порядком, у якому вони з’явилися в оперативній пам’яті, ви зможете уявити, що відбувалося під час циклу, коли ми об’єднували по одному символу в наш рядок пароля:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | сортувати /+10 006DBD0C: unlikelytextA 006DBB8C: unlikelytextAL 006DB70C: unlikelytextALJ 006D91BC: unlikelytextALJB 006D8CBC: unlikelytextALJBN 006D90FC: unlikelytextALJBNG 006D8B7C: unlikelytextALJBNGO 006D 8B3C: unlikelytextALJBNGOA 006D8D7C: unlikelytextALJBNGOAP 006D903C: unlikelytextALJBNGOAPL 006D90BC: unlikelytextALJBNGOAPLL 006D913C: unlikelytextALJBNGOAPLLB 006D91FC: unlikelytextALJBNGOAPLLBD 006 D923C: малоймовірний текстALJBNGOAPLLBDE 006D8AFC: малоймовірний текстALJBNGOAPLLBDEB 006D8BFC : unlikelytextALJBNGOAPLLBDEBJ
Усі ці тимчасові, проміжні рядки все ще присутні, тож навіть якщо ми успішно стерли остаточне значення 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... не знайдено.
Але Vdohney, першовідкривач CVE-2023-32784, помітив, що коли ви вводите головний пароль, KeePass надає вам візуальний зворотний зв’язок, створюючи та відображаючи рядок-заповнювач, що складається з символів Unicode “blob” до довжини вашого пароль:
У широкосимвольних текстових рядках у Windows (які складаються з двох байтів на символ, а не лише з одного байта кожен, як у ASCII), символ «блоб» кодується в оперативній пам’яті як шістнадцятковий байт 0xCF
подальшою 0x25
(що випадково є знаком відсотка в ASCII).
Таким чином, навіть якщо KeePass дуже обережно ставиться до необроблених символів, які ви вводите під час введення самого пароля, у вас можуть залишитися рядки символів «ляпки», які можна легко виявити в пам’яті під час повторного запуску, наприклад CF25CF25
or CF25CF25CF25
...
…і, якщо так, найдовша серія символів-кляп, яку ви знайшли, ймовірно, видасть довжину вашого пароля, що буде скромною формою витоку інформації про пароль, якщо нічого іншого.
Ми використовували наступний сценарій Lua для пошуку ознак залишкових рядків-заповнювачів пароля:
Вихід був несподіваним (ми видалили послідовні рядки з однаковою кількістю blobs або з меншою кількістю blobs, ніж у попередньому рядку, щоб заощадити місце):
C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [ продовжується подібним чином для 8 blobs, 9 blobs і т.д. ] [ до двох останніх рядків рівно по 16 blobs кожен ] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [ усі збіги, що залишилися, мають одну крапку] 0123B058: *
На близьких одна до одної, але постійно зростаючих адресах пам’яті ми знайшли систематичний список з 3 blob-ів, потім 4 blob-ів і так далі до 16 blob-ів (довжина нашого пароля), за якими йшло багато випадково розкиданих екземплярів одиничних blob-рядків. .
Таким чином, ці рядки-заповнювачі «ляпки» справді, здається, просочуються в пам’ять і залишаються позаду, щоб отримати витік довжини пароля, довго після того, як програмне забезпечення KeePass завершить роботу з вашим головним паролем.
Наступний крок
Ми вирішили копати далі, як і Вдохней.
Ми змінили наш код зіставлення шаблонів, щоб виявити ланцюжки символів-блобів, за якими йде будь-який один символ 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()
що ваші дані безпечні та добре керовані. Іноді вам може знадобитися вжити додаткових заходів обережності, щоб уникнути залишення секретних даних, які лежать навколо, і ці заходи обережності залежать від операційної системи. - Якщо ви тестувальник якості або рецензент коду, завжди думайте «за лаштунками». Навіть якщо код керування пам’яттю виглядає охайним і добре збалансованим, будьте в курсі того, що відбувається за лаштунками (оскільки оригінальний програміст міг не знати про це), і приготуйтеся виконувати певну роботу в стилі пентестування, таку як моніторинг виконання та пам’ять скидання, щоб переконатися, що захищений код дійсно поводиться так, як він повинен.
КОД ЗІ СТАТТІ: UNL1.C
#включати #включати #включати void hexdump(unsigned char* buff, int len) { // Буфер друку у 16-байтових фрагментах для (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Показати 16 байт як шістнадцяткові значення для (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Повторіть ці 16 байтів як символи для (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("Викидання 'нового' буфера при запуску"); hexdump(buff,128); // Використовувати псевдовипадкову адресу буфера як випадкове початкове значення srand((unsigned)buff); // Пароль починається з фіксованого тексту, доступного для пошуку strcpy(buff,"unlikelytext"); // Додаємо 16 псевдовипадкових літер по одній для (int i = 1; i <= 16; i++) { // Виберіть букву від A (65+0) до P (65+15) char ch = 65 + (rand() & 15); // Потім змініть рядок бафа на місці strncat(buff,&ch,1); } // Повний пароль тепер у пам'яті, тому надрукуйте // його як рядок і покажіть весь буфер... printf("Повний рядок був: %sn",buff); hexdump(buff,128); // Призупинити дамп оперативної пам'яті процесу зараз (спробуйте: 'procdump -ma') puts("Очікування [ENTER] для звільнення буфера..."); getchar(); // Формально звільнити () пам'ять і показати буфер // знову, щоб побачити, чи щось залишилося... free(buff); printf("Викидання буфера після free()n"); hexdump(buff,128); // Пауза для повторного скидання RAM для перевірки відмінностей puts("Очікування [ENTER] для виходу з main()..."); getchar(); повернути 0; }
КОД ЗІ СТАТТІ: UNL2.C
#включати #включати #включати #включати void hexdump(unsigned char* buff, int len) { // Буфер друку у 16-байтових фрагментах для (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Показати 16 байт як шістнадцяткові значення для (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Повторіть ці 16 байтів як символи для (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("Викидання 'нового' буфера при запуску"); hexdump(buff,128); // Використовувати псевдовипадкову адресу буфера як випадкове початкове значення srand((unsigned)buff); // Пароль починається з фіксованого тексту, доступного для пошуку strcpy(buff,"unlikelytext"); // Додаємо 16 псевдовипадкових літер по одній для (int i = 1; i <= 16; i++) { // Виберіть букву від A (65+0) до P (65+15) char ch = 65 + (rand() & 15); // Потім змініть рядок бафа на місці strncat(buff,&ch,1); } // Повний пароль тепер у пам'яті, тому надрукуйте // його як рядок і покажіть весь буфер... printf("Повний рядок був: %sn",buff); hexdump(buff,128); // Призупинити дамп оперативної пам'яті процесу зараз (спробуйте: 'procdump -ma') puts("Очікування [ENTER] для звільнення буфера..."); getchar(); // Формально звільнити () пам'ять і показати буфер // знову, щоб побачити, чи щось залишилося... VirtualFree(buff,0,MEM_RELEASE); printf("Викидання буфера після free()n"); hexdump(buff,128); // Пауза для повторного скидання RAM для перевірки відмінностей puts("Очікування [ENTER] для виходу з main()..."); getchar(); повернути 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') -- Призупинити для створення дампу процесу RAM print('Очікування [ENTER] перед звільненням рядка...') io.read() - - Видалити рядок і позначити змінну як невикористану s = nil -- Знову вивести ОЗП, щоб знайти відмінності print('Очікування [ENTER] перед виходом...') io.read()
КОД ЗІ СТАТТІ: FINDIT.LUA
-- читати у файлі дампа local f = io.open(arg[1],'rb'):read('*a') -- шукати текст маркера, після якого один -- або більше випадкових символів ASCII локально b,e ,m = 0,0,nil while true do -- шукати наступний збіг і запам'ятовувати зсув b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- вийти, коли більше не буде збігається, якщо не b, то розрив 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 і 'ЗНАЙДЕНО' або 'не знайдено','.n') io.write ('Пошук SIXTEENPASSCHARS як UTF-16... ') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 і 'ЗНАЙДЕНО' або 'не знайдено' ','.n')
КОД ЗІ СТАТТІ: FINDBLOBS.LUA
-- читання у файлі дампа, указаному в командному рядку local f = io.open(arg[1],'rb'):read('*a') -- Шукайте один або більше блоків пароля, а потім будь-який інший блок -- Зауважте, що blob-символи (●) кодуються в широкі символи 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, тоді розірвати кінець -- CMD.EXE не може надрукувати краплі, тому ми перетворюємо їх на зірки. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) кінець
КОД ЗІ СТАТТІ: ПОШУКKP.LUA
-- читання у файлі дампа, указаному в командному рядку local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- Тепер ми хочемо, щоб один або більше blob (CF25) супроводжувався кодом -- для A..Z з наступним 0 байтом для перетворення ACSCII на UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- вийти, коли більше немає збігів, якщо ні b, тоді розірвати кінець -- CMD.EXE не може друкувати blobs, тому ми перетворюємо їх на зірки. -- Щоб заощадити місце, ми пригнічуємо послідовні збіги, якщо m ~= p, тоді print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m кінець кінець
- Розповсюдження контенту та PR на основі SEO. Отримайте посилення сьогодні.
- PlatoAiStream. Web3 Data Intelligence. Розширення знань. Доступ тут.
- Карбування майбутнього з Адріенн Ешлі. Доступ тут.
- Купуйте та продавайте акції компаній, які вийшли на IPO, за допомогою 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
- Здатний
- МЕНЮ
- вище
- абсолют
- AC
- доступ
- рахунки
- набувати
- придбання
- активний
- фактичний
- насправді
- доданий
- Додатковий
- адреса
- адреси
- Додає
- після
- потім
- знову
- ВСІ
- виділено
- виділяє
- розподіл
- асигнувань
- дозволяти
- тільки
- по
- вже
- Також
- змінений
- хоча
- завжди
- an
- та
- Ендрю
- відповідь
- будь-який
- все
- щось критичне
- з'являтися
- з'явився
- підхід
- затверджений
- ЕСТЬ
- навколо
- стаття
- статті
- AS
- At
- автор
- автоматичний
- автоматичний
- автоматично
- доступний
- уникнути
- уникати
- знати
- геть
- назад
- фон
- фонове зображення
- BE
- оскільки
- стає
- було
- перед тим
- початок
- за
- за лаштунками
- нижче
- Краще
- Біт
- Блокувати
- блоки
- border
- обидва
- дно
- марка
- Новинка
- Перерва
- коротко
- приносити
- буфера
- переповнення буфера
- Помилка
- помилки
- будувати
- але
- by
- C + +
- call
- покликання
- CAN
- Може отримати
- який
- випадок
- спійманий
- CD
- Центр
- звичайно
- ланцюга
- шанс
- змінилися
- характер
- символи
- контроль
- Перевірки
- китайський
- Вибирати
- ясно
- очевидно
- близько
- код
- color
- COM
- приходить
- майбутній
- коментар
- коментарі
- загальний
- повний
- комплекс
- комп'ютер
- Вважати
- значний
- вважається
- Складається
- будівництво
- зміст
- зміст
- безперестанку
- триває
- контроль
- конвертувати
- авторське право
- Відповідний
- може
- обкладинка
- тріщина
- створювати
- створений
- творець
- критичний
- Кібербезпека
- НЕБЕЗПЕЧНО
- Небезпечний
- дані
- витоку даних
- Днів
- угода
- вирішене
- присвячених
- описаний
- DID
- Відмінності
- різний
- трудність
- DIG
- цифровий
- прямий
- Прямий доступ
- безпосередньо
- дисплей
- показ
- має
- do
- документація
- робить
- Ні
- справи
- зроблений
- Не знаю
- вниз
- управляти
- два
- дамп
- під час
- e
- кожен
- Раніше
- легко
- екосистема
- або
- ще
- повідомлення електронної пошти
- дозволяє
- заохочення
- шифрування
- кінець
- закінчується
- досить
- забезпечувати
- забезпечення
- Що натомість? Створіть віртуальну версію себе у
- вхід
- Весь
- запис
- Навколишнє середовище
- Еквівалент
- помилка
- по суті
- оцінка
- і т.д.
- Ефір (ETH)
- Навіть
- врешті-решт
- постійно збільшується
- Кожен
- все
- точно
- приклад
- Крім
- Збудження
- виконання
- існувати
- існуючий
- вихід
- Вихід
- очікувати
- Пояснювати
- Експлуатувати
- піддаватися
- продовжити
- додатково
- витяг
- факт
- false
- захоплюючий
- риси
- зворотний зв'язок
- менше
- боротьба
- філе
- Файли
- фільтрувати
- остаточний
- в кінці кінців
- знайти
- виявлення
- знахідки
- кінець
- Перший
- фіксованою
- Сфокусувати
- потім
- після
- для
- форма
- Формально
- формат
- майбутній
- знайдений
- чотири
- Безкоштовна
- від
- Повний
- повністю
- функція
- Функції
- далі
- майбутнє
- генерує
- покоління
- отримати
- отримання
- GitHub
- Давати
- даний
- дає
- дає
- Go
- йде
- буде
- добре
- Уряд
- захоплення
- великий
- гарантувати
- було
- Ручки
- сталося
- Відбувається
- відбувається
- Жорсткий
- Мати
- має
- головні болі
- важкий
- висота
- тут
- HEX
- на вищому рівні
- вище
- число переглядів
- тримати
- Hole
- надія
- ГОДИННИК
- hover
- Як
- How To
- HTTPS
- полювання
- i
- ідентифікатор
- if
- негайно
- важливо
- in
- includes
- У тому числі
- інформація
- повідомив
- замість
- зацікавлений
- Проміжний
- інтернет
- в
- питання
- IT
- ЙОГО
- сам
- жаргон
- червень
- просто
- тільки один
- тримати
- ключ
- Знати
- відомий
- корейський
- мова
- мови
- портативний комп'ютер
- останній
- пізніше
- вести
- Веде за собою
- витік
- Витоку
- УЧИТЬСЯ
- вивчення
- найменш
- догляд
- залишити
- довжина
- лист
- бібліотека
- життя
- як
- Ймовірно
- обмеженою
- Лінія
- ліній
- список
- Перераховані
- ll
- загрузка
- місцевий
- розташування
- каротаж
- Довго
- довгостроковий
- довше
- подивитися
- виглядає як
- подивився
- шукати
- ВИГЛЯДИ
- серія
- удача
- підтримує
- зробити
- шкідливих програм
- управляти
- вдалося
- управління
- менеджер
- управляє
- Маніпуляція
- багато
- Маржа
- позначити
- маркер
- майстер
- матч
- узгодження
- макс-ширина
- Може..
- засоби
- пам'ять
- згаданий
- Microsoft
- може бути
- протокол
- скромний
- модифікований
- змінювати
- момент
- моніторинг
- більше
- найбільш
- багато
- множинний
- нерозбавлений
- Необхідність
- необхідний
- мережу
- ніколи
- проте
- Нові
- новини
- наступний
- приємно
- немає
- нормальний
- нічого
- Зверніть увагу..
- зараз
- номер
- номера
- об'єкт
- Очевидний
- of
- від
- офіційний
- Офіційно
- зсув
- Старий
- on
- один раз
- ONE
- тільки
- з відкритим вихідним кодом
- операційний
- операційна система
- операційні системи
- оператор
- варіант
- or
- порядок
- оригінал
- Інше
- інші
- інакше
- наші
- себе
- з
- вихід
- над
- загальний
- власний
- сторінка
- Паніка
- частина
- Пароль
- Password Manager
- Паролі
- шлях
- Викрійки
- Пол
- пауза
- Платити
- відсотків
- може бути
- period
- постійно
- персонал
- особисті дані
- фізичний
- картина
- частин
- місце
- заповнювач
- Чума
- plato
- Інформація про дані Платона
- PlatoData
- Plenty
- точка
- точок
- популярний
- положення
- це можливо
- Пости
- потенціал
- точно
- представити
- досить
- запобігати
- попередній
- price
- друк
- друк
- ймовірно
- проблеми
- процес
- програма
- Програміст
- Програмісти
- Програмування
- програми
- виражений
- put
- Python
- Питання та відповіді
- питання
- піднімається
- Оперативна пам'ять
- випадковий
- діапазон
- швидше
- Сировина
- необроблені дані
- RE
- досяг
- Читати
- читання
- готовий
- реальний
- справжнє життя
- реального часу
- насправді
- визнати
- Відновлювати
- відновлюється
- пов'язаний
- решті
- запам'ятати
- віддалений
- видаляти
- повторювати
- повторний
- ПОВТОРНО
- звітом
- представлений
- повага
- відповідно
- REST
- результати
- повертати
- повернення
- показувати
- позбавитися
- право
- Risk
- ризики
- Кімната
- прогін
- біг
- моніторинг часу виконання
- s
- сейф
- безпечніше
- то ж
- Незадоволений
- зберегти
- приказка
- сканування
- розсіяний
- сцени
- Пошук
- Грати короля карти - безкоштовно Nijumi логічна гра гри
- другий
- seconds
- секрет
- розділ
- безпечний
- безпеку
- побачити
- насіння
- бачачи
- здається
- бачив
- бачить
- Серія
- серйозний
- комплект
- установка
- Короткий
- Незабаром
- Повинен
- Показувати
- показаний
- підпис
- Ознаки
- аналогічний
- Аналогічно
- простий
- спрощений
- спростити
- просто
- один
- Розмір
- сон
- невеликий
- Підлий
- відстеження
- So
- Софтвер
- solid
- деякі
- що в сім'ї щось
- Скоро
- Source
- вихідні
- Простір
- спеціальний
- спеціально
- зазначений
- швидкість
- Зірки
- старт
- почалася
- Починаючи
- починається
- введення в експлуатацію
- Як і раніше
- вкрав
- Стоп
- зупинений
- зберігати
- зберігати
- Історія
- рядок
- сильний
- Вивчення
- Успішно
- такі
- достатній
- передбачуваний
- сюрприз
- здивований
- дивно
- виживати
- SVG
- обмін
- система
- Systems
- Приймати
- прийняті
- приймає
- взяття
- говорити
- технічно
- методи
- тимчасовий
- тест
- Тести
- ніж
- Що
- Команда
- Джерело
- їх
- Їх
- самі
- потім
- теорія
- Там.
- отже
- вони
- річ
- думати
- це
- ті
- хоча?
- думка
- час
- назва
- до
- разом
- прийняли
- інструмент
- топ
- трек
- Відстеження
- перехід
- прозорий
- правда
- намагатися
- Опинився
- два
- тип
- типово
- розуміти
- Unicode
- до
- невикористаний
- небажаний
- Оновити
- оновлений
- URL
- us
- нас уряд
- Використання
- USB
- використання
- використання після безкоштовно
- використовуваний
- користувач
- використовує
- використання
- утиліта
- значення
- Цінності
- різноманітність
- перевірити
- версія
- дуже
- через
- вразливість
- W
- чекати
- Очікування
- хотіти
- хотів
- було
- годинник
- шлях..
- способи
- we
- тижня
- ДОБРЕ
- були
- Що
- коли
- Чи
- який
- в той час як
- ВООЗ
- хто б не
- всі
- чому
- волі
- виграти
- windows
- протирати
- з
- без
- цікаво
- слова
- Work
- працював
- робочий
- працює
- турбуватися
- б
- дав би
- запис
- лист
- письмовий
- ще
- ви
- вашу
- себе
- зефірнет
- нуль