Звідки беруться проломи в безпеці програм?

— У нас дірка у безпеці.
— Ну, хоч щось у нас у безпеці.
Анекдот

image
Якщо ви Windows-користувач, то вам повинні бути знайомі спливаючі кожен другий вівторок місяця віконця, рапортують про встановлення «критичних оновлень безпеки». Microsoft докладає чималих зусиль, постійно працюючи над виправленнями вразливостей у своїх операційних системах, але воно того варто: у світі, де число кібератак з кожним днем лише зростає, ні одна лазівка в обороні наших комп'ютерів не повинна залишатися відкритою для потенційних зловмисників.

Нещодавнє обговорення у списку розсилки, присвяченого 66192-й SVN ревізії ReactOS, показало як це легко — внести в код ядра критичну вразливість. Я буду використовувати цей випадок як приклад простий, але впливає на безпеку помилки, а також для наочного представлення деяких заходів, яких необхідно піддавати код ядра, якщо ви дійсно хочете отримати безпечну систему.

Відшукаємо вразливість



Давайте поглянемо на цей код і розберемося що до чого:

NTSTATUS
APIENTRY
NtUserSetInformationThread(IN HANDLE ThreadHandle,
IN USERTHREADINFOCLASS ThreadInformationClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength)
{
[...]
switch (ThreadInformationClass)
{
case UserThreadInitiateShutdown:
{
ERR("Shutdown ініціював\n");

if (ThreadInformationLength != sizeof(ULONG))
{
Status = STATUS_INFO_LENGTH_MISMATCH;
break;
}

Status = UserInitiateShutdown(Thread, (PULONG)ThreadInformation);
break;
}
[...]
}


Це невеликий шматочок функції NtUserSetInformationThread, що представляє собою системний виклик win32k.sys, який може бути (більш-менш) безпосередньо викликаний користувацькими програмами. Тут ThreadInformation — вказівник на якийсь блок з даними, а параметр ThreadInformationClass показує, як ці дані слід інтерпретувати. Якщо він дорівнює UserThreadInitiateShutdown, в блоці має бути 4-байтове ціле число. Кількість переданих байт зберігається в ThreadInformationLength, і, як нескладно помітити, код дійсно перевіряє, щоб там було саме «4», в іншому разі виконання перервано з помилкою STATUS_INFO_LENGTH_MISMATCH. Але зверніть увагу, що обидва цих параметра приходять безпосередньо з користувальницької програми, а значить, якась шкідлива закладка, викликаючи цю функцію, може передати їй що завгодно.

А тепер давайте подивимося, що відбувається з ThreadInformation, коли його передають у UserInitiateShutdown:

NTSTATUS
UserInitiateShutdown(IN PETHREAD Thread,
IN OUT PULONG pFlags)
{
NTSTATUS Status;
ULONG Flags = *pFlags;
[...]
*pFlags = Flags;
[...]
/* If the caller is not Winlogon, do some security checks */
if (PsGetThreadProcessId(Thread) != gpidLogon)
{
// FIXME: Play again with flags...
*pFlags = Flags;
[...]
}
[...]
*pFlags = Flags;

return STATUS_SUCCESS;
}


Оскільки досить велика частина цієї функції поки не реалізована, все, що відбувається вище — це лише кілька циклів читання та запису 4-байтового значення, на яке вказав користувач.

Так в чому тоді проблема?



Ну, одного тільки розіменування неперевіреного покажчика вистачає, щоб зробити можливу DoS-атаку (відмова від обслуговування) — шкідлива програма може банально вимкнути комп'ютер, не маючи на це прав. Наприклад, програма може просто передати нульовий (NULL) покажчик і таким чином скористається вразливістю. UserInitiateShutdown разыменует згаданий покажчик, що призведе до BSOD'у, зазвичай званого «bug check» серед розробників ядра. При цьому викликає має можливість писати в пам'ять (тут згадуємо, що це [i]довільний[/i] покажчик — він може навіть посилатися на область ядра!). На перший погляд, запис зчитаного з вказаної області пам'яті значення назад туди ж виглядає не так погано. Але в реальності може привнести достатньо проблем. Деякі ділянки пам'яті часто змінюються з високою інтенсивністю, і відновлення раніше зберігався там значення може, наприклад, знизити рівень ентропії генератора випадкових чисел якого-небудь криптоалгоритма, або переписати таблицю відображення сторінок пам'яті її старою версією, яка до цього моменту вже повинна була бути знищена, дозволяючи отримати доступ до більшої кількості пам'яті, що може бути використано для компрометації системи. Але це все просто приклади, які народилися по ходу — а цілеспрямовано атакуючих можуть бути місяці, щоб прийти до найкращого рішення, що дозволяє досягти поставлених цілей, і дрібний на перший погляд вада в безпеці, такий як цей, може виявитися для когось достатнім, щоб витягнути з вашої машини всі секрети і отримати повний контроль над нею. Звичайно, коли функція буде повністю реалізована, вона буде змінювати змінну Flags, перед тим, як записати її назад, надаючи можливість модифікувати довільний ділянку пам'яті (ядра), причому керованим чином — справжнє свято для хакера.

Знаючи все це, що можна виправити?



Для захисту від такого роду проблем в ядрі NT передбачені два механізми: probing (перевірка) і SEH (структурована обробка виключень, Structured Exception Handling). Перевірка пам'яті позбавляє від великої кількості проблем, дозволяючи переконатися, що отриманий від програми покажчик дійсно посилається на простір пам'яті користувача. Виконання такої перевірки для всіх параметрів-покажчиків дає впевненість, що програми рівня користувача не зможуть отримати доступ до пам'яті ядра таким способом. Проте це не рятує від нульових, або будь-яких інших недійсних покажчиків. І тут на допомогу приходить другий механізм, SEH: обернення кожного звернення до даних по сумнівним вказівниками (тобто отриманими від користувацьких програм) блок обробки виключень гарантує, що код збереже стійкість, навіть якщо вказівник недійсний. Код рівня ядра в цьому випадку надає обробник виключень, який викликається кожного разу, коли захищений код генерує виняток (таке, як доступ порушення внаслідок використання невірного покажчика). Обробник виключень збирає доступні дані (такі, як код винятку), виконує всі необхідні дії з очищення пам'яті і повертає, в більшості випадків, управління користувачеві, разом із кодом помилки.

Давайте подивимося на виправлені исходники (коміта r66223):

ULONG CapturedFlags = 0;

ERR("Shutdown ініціював\n");

if (ThreadInformationLength != sizeof(ULONG))
{
Status = STATUS_INFO_LENGTH_MISMATCH;
break;
}

/* Capture the caller value */
Status = STATUS_SUCCESS;
_SEH2_TRY
{
ProbeForWrite(ThreadInformation, sizeof(CapturedFlags), sizeof(PVOID));
CapturedFlags = *(PULONG)ThreadInformation;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;

if (NT_SUCCESS(Status))
Status = UserInitiateShutdown(Thread, &CapturedFlags);

/* Return the modified value to the caller */
_SEH2_TRY
{
*(PULONG)ThreadInformation = CapturedFlags;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;


Зауважте, всі звернення до небезпечного вказівником ThreadInformation виконуються тепер всередині блоків _SEH2_TRY. Що виникають у них винятку будуть контрольовано перехоплюватися кодом з блоку _SEH2_EXCEPT. Крім того, перед тим, як разыменовать курсор в перший раз, робиться виклик ProbeForWrite, який порушить виняток STATUS_ACCESS_VIOLATION або STATUS_DATATYPE_MISALIGNMENT, якщо виявиться недійсний (належить до області ядра, наприклад) покажчик, або захищена від запису пам'ять. В кінці зверніть увагу на введену змінну CapturedFlags, яка передається в UserInitiateShutdown. Подібна хитрість спрощує операції з небезпечним параметром: щоб не використовувати SEH щоразу при зверненні до pFlags всередині функції, це значення зберігається у довіреної область силами NtUserSetInformationThread, а потім записується назад у власну пам'ять, коли UserInitiateShutdown відпрацює. Так пропадає необхідність правити саму UserInitiateShutdown, оскільки тепер вона отримує на вхід безпечний покажчик з області ядра (покажчик на CapturedFlags). Результат усіх цих заходів — функція тепер може працювати з будь-яким набором даних, коректних, і не дуже, без ризику нашкодити системі. Справу зроблено!

Який урок з цього треба зробити?



Очевидно, підвищена пильність ще на етапі розробки дозволяє вчасно помітити рядки коду, здатні стати загрозою безпеки в подальшому. Не можна дозволяти, щоб їх ставало занадто багато, тому що, чесно кажучи, і без них проблем безпеки напевно і так буде багато. В перспективі, якщо все піде за планом, ми поступово будемо відшукувати їх і виправляти, випускаючи регулярні оновлення, на зразок тих, що приходять до вас по вівторках з Windows Update Center :)

Замітка на полях. Як справедливо зауважив Алекс Йонеску (Alex Ionescu), сама Windows має уразливість в цій же самій функції, NtUserSetInformationThread. Причому, за його словами досі не закриту і активно експлуатовану для всякого роду джейлбрейков пристроїв типу Surface RT. Вперше вона була описана ще в 2012 році відомим дослідником безпеки по імені Матеуш «jooro» Юрчик (Mateusz Jurczyk) (який, до речі, частенько тусується з нами в IRC ;]). Його статтю на цю тему ви знайдете в блозі: j00ru.vexillium.org/?p=1393

Примітки від перекладача:
Про опечатки, помилках і не неточності прошу повідомляти в особистих повідомленнях.
У перекладі брали участь: Postscripter, al-tarakanoff, Олексій Брагін, Мабу

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.