Про аналіз вихідного коду і автоматичної генерації експлойтів

    Останнім часом про аналіз захищеності вихідного коду не писав тільки ледачий. Воно й зрозуміло, адже той хлопець з Gartner, як запропонував розглядати аналіз вихідного коду в якості нового хайпа кілька років тому, так досі і не дав відмашку на те, щоб припинити це робити. А, враховуючи поточний напрямок моєї роботи (участь у розробці PT Application Inspector , далі AI), і той факт, що останнім часом придатних статей на тему аналізу вихідного коду в общем-то не було, якось навіть дивно, що до сьогоднішнього дня в цьому блозі не було жодної брудної подробиці на цю животрепетну тему. Що ж, исправляюсь.
 
Власне все, що можна було сказати про наш підхід до автоматизації аналізу захищеності вихідного коду в AI вже сказали Сергій Плехов та Олексій Москвін у доповіді " Проблеми автоматичної генерації експлойтів по вихідного коду " на PHDays IV. Тим, хто не був присутній на доповіді і не дивився його запис — вкрай рекомендую зробити це раніше, ніж читати статтю далі. Однак, в кінці доповіді від Івана Новикова aka d0znpp прозвучало відразу кілька запитань на тему «у чому кейс?», «Чим ваш підхід відрізняється від того ж RIPS?» І «як ви тоді отримуєте точки входу?» В контексті твердження про те, що без розгортання програми неможливо отримати зовнішні дані, необхідні для побудови експлоїта (такі наприклад, як ім'я привілейованого користувача і його пароль, маршрути до точок входу і т.п). Хочу відразу обмовитися, що тут має місце деяка (внесена безумовно з нашого боку) термінологічна плутанина: назва «проблеми автоматичного виведення множин векторів атак по вихідного коду» набагато більш точно відображало б суть вирішених у ході роботи над AI завдань. Називати те, що виходить на виході AI експлоїтом дійсно не цілком коректно. Хоча б тому, що це крутіше, ніж просто експлоїт в традиційному розумінні цього терміну :) І далі я постараюся розкрити цю думку і доповнити своїх колег більш розгорнутою відповіддю на задані Іваном питання.
 
 У чому кейс?
У першу чергу, кейс полягає в пошуку недоліків у коді і підтвердження їх уразливості до тих чи інших класах атак. Завдання по автоматичній генерації експлойтів в рамках даного кейса зводиться до висновку мінімального вектора атаки, що підтверджує існування уразливості. При цьому, під вектором мається на увазі не конкретний HTTP-запит, а деяка сукупність факторів, що призводить систему в стан уразливості і дозволяє провести на неї успішну атаку. Скажу навіть більше: у загальному випадку, висловити вектор атаки у вигляді тільки HTTP запиту не представляється можливим. По-перше, тому що даний вектор може вимагати виконання декількох запитів. По-друге (і це головне), тому що вектор може включати в себе умови на будь-які властивості оточення, які неможливо описати в контексті запиту HTTP. Проте, в рамках розглянутого кейса ми повинні: а) вивести всі подібні умови; б) якимось чином оформити їх в результатах аналізу. Саме це і призвело до настільки мудрому визначенню вектора. Наведу простий приклад (тут і далі розглядається код на C # під ASP.NET Web Forms):
 
 
var settings = Settings.ReadFromFile("settings.xml");
string str1;
if (settings["key1"] == "validkey")
{
    Response.Write(Request.Params["parm"]);
}
else
{
    Response.Write("Wrong key!");
}

Очевидно, що в даному випадку вразливість до атаки XSS залежить від значення параметра key1 в конфігураційному файлі settings.xml. І, якщо ми по-чесному прочитаємо його (тобто фактично, а не символьно здійснимо виклик Settings.ReadFromFile («settings.xml») і присвоїмо змінної settings отриманий результат), то далі ми підемо тільки по одному з двох можливих шляхів виконання, що неминуче приведе нас до пропуску уразливості в тому випадку, якщо параметр key1 в файлі не буде встановлено в значення «validkey». Виконуючи ж перший виклик символьно, ми прийдемо в підсумку до наступної формули, що і буде шуканим вектором:
 
 
Settings.ReadFromFile("setings.xml")["key1"] == "validkey" -> {Request.Params["Parm"] = <script>alert(0)</script>}

Ми також можемо вивести з цього і HTTP-експлоїт:
 
 
GET http://host.domain/path/to/document.aspx?parm=%3Cscript%3Ealert%280%29%3C%2fscript%3E HTTP/1.1

який, тим не менш, не є самодостатнім і залежить від умов, що накладаються на оточення веб-додатки.
 
Отримання будь-яких значень з бази даних, файлової системи або будь-якого іншого зовнішнього джерела, призводить до простої дилеми: або ми отримуємо зовнішні дані і маємо можливість будувати повноцінні експлоїти ( там, де це теоретично можливо), але пропускаємо при цьому потенційні уразливості через втрату шляхів виконання, або обробляємо виклики до зовнішніх джерел символьно і, тим самим, покриваємо всі можливі множини значень і шляхи виконання, які можуть мати місце в результаті таких викликів. І оскільки перед нами стояло завдання не створити універсальний аттакер-всемогутер, а максимально автоматизувати людську рутину з аналізу захищеності коду, то отримання векторів у вигляді доведених логічних формул, що вимагають подальшої роботи людини в плані побудови повноцінних експлойтів, переважніше автоматизованого отримання таких експлойтів на шкоду основному функціоналу.
 
Проте ж можливі ситуації, де без читання зовнішніх даних дійсно нікуди: це і визначення маршрутів до точок входу в веб-додаток в тому випадку, якщо вони визначені в зовнішніх файлах конфігурації, а не в коді програми, і підключення додаткових файлів з вихідним кодом через їх перерахування в файлах конфігурації (актуально для динамічних мов), і ряд аналогічних завдань. Але тут і питань немає: раз треба — значить, схрестивши пальці, читаємо. Де можемо, звичайно.
 
Підсумовуючи: зараз, немає ніяких перешкод до того, щоб навчити AI читати дані з БД і використовувати їх в ході символьного виконання аналізованого коду. Однак, це зажадає розгортання, як мінімум, БД веб-додатки з одного боку і істотно просадили можливості аналізатора по виявленню вразливостей з іншого, не давши при цьому будь-яких відчутних переваг в рамках поставленого перед нами завдання, яку я описував вище.
 
 Чим ваш підхід відрізняється від RIPS?
Наскільки я можу судити про підході прийнятому в RIPS, AI'шний — відрізняється трохи більше, ніж усім. Починаючи з того, що в RIPS реалізований класичний static-taint-analysis через розмітку тегами шляхів в графі потоку даних з емуляцією ряду стандартних бібліотечних функцій, а похід AI передбачає побудову моделі (по одній на кожну точку входу) у вигляді системи логічних тверджень, що описують стан додатки в кожному вузлі CFG та умови його досяжний, що дає можливість вирішувати будь-які шляхи в ньому (включаючи if'и, умовні return'и, обробку виключень і т.п) ще й з частковим виконанням реального коду замість його емуляції там, де це дає кращий результат у порівнянні з символьним виконанням. І закінчуючи (але не обмежуючись) тим, що RIPS тупо обламується на кастомних фільтруючих функціях, в той час, як AI намагається з ними працювати (причому досить успішно в більшості реальних випадків).
 
Напевно, краще показати на прикладі. Припустимо, у нас є наступний фрагмент вихідного коду [1] :
 
 
string name = Request.Params["name"];
string key1 = Request.Params["key1"];
string parm = Request.Params["parm"];
 
byte[] data;
if (string.IsNullOrEmpty(parm))
{
    data = new byte[0];
}
else
{
    data = Convert.FromBase64String(parm);
}
 
string str1;
if (name + "in" == "admin")
{
    if (key1 == "validkey")
    {
        str1 = Encoding.UTF8.GetString(data);
    }
    else
    {
        str1 = "Wrong key!";
        Response.Write(str1);
        return;
    }
}
else
{
    str1 = "Wrong role!";
}
 
Response.Write("<a href='http://host.domain' onclick='" + CustomSanitize(str1) + "'>Click me</a>");

Очевидно, що тут двічі має місце виконання потенційно-небезпечній операції (далі PVO — Potentially Vulnerable Operation) — виклику методу Response.Write, що здійснює запис в потік формованого сервером відповіді на HTTP-запиту. У першому випадку методу передається константа «Wrong Key!», Що не представляє для нас ніякого інтересу. Зате в другому, у відповідь відправляється результат виклику методу CustomSanitize з аргументом, значення якого обчислюється з значень параметрів отриманого запиту. Але якими вони повинні бути, щоб ми отримали можливість прокинути в str1 значення, достатню для підтвердження можливості проведення атаки XSS через ін'єкцію елементів розмітки HTML? Давайте розглянемо, як приблизно може виглядати процес пошуку відповіді на це питання [2] .
 
Для початку, виведемо умова досяжності другого Response.Write. Незважаючи на те, що сам він не вкладено в будь конструкції, що впливають на потік управління, в попередніх йому блоках коду має місце повернення із загальної для всього коду функції, умова досяжності якого одночасно є умовою недосяжності нашої PVO. Очевидно, що умовою виконання оператора return буде логічне вираз: (name == «adm» && key1! = «Validkey»). Отже, умовою його недосяжності буде вираз: (name! = «Adm» | | name == «adm» && key1 == «validkey»). Оскільки цей return — єдиний оператор, що впливає на досяжність другого Response.Write, останній вираз і буде умовою досяжності PVO.
 
Фактично, вираз (name! = «Adm» | | name == «adm» && key1 == «validkey») дає нам два взаємовиключних умови формування шляху до PVO на графі потоку управління. Розглянемо можливі значення str1 при виконанні кожного з них. При (name! = «Adm») змінна str1 отримує константне значення «Wrong role!», Що безумовно не може привести нас до успішної атаці. Але при (name == «adm» && key1 == «validkey») в str1 потрапляє результат виклику методу Encoding.UTF8.GetString з аргументом data, який у свою чергу може приймати два значення: new byte [0] при string.IsNullOrEmpty (parm) і Convert.FromBase64String (parm) при! string.IsNullOrEmpty (parm). Відкидаючи нецікаві з т.ч. експлуатовані уразливості значення і розкручуючи значення всіх змінних аж до їх taint-джерел, отримуємо наступну формулу:
 
 
(Request.Params["name"] == "adm" && Request.Params["key1"] == "validkey" && !string.IsNullOrEmpty(Request.Params["parm"])) -> Response.Write("<a href='http://host.domain' onclick='" + CustomSanitize(Convert.FromBase64String(Request.Params["parm"])) + "'>Click me</a>")

Графічне представлення моделі виконання, побудованої в даному випадку, буде мати вигляд (клікабельно):
 
 
Таким чином, значення параметрів запиту name і key1 у нас вже є і все, що залишилося зробити — це знайти таке значення Request.Params [«parm»], при якому кінцеве значення виразу CustomSanitize (Convert.FromBase64String (Request.Params [«parm »])) дасть нам експлуатацію уразливості, що приводить до XSS.
 
І ось тут виникає проблема, впоратися з якою традиційні засоби статанализа не в змозі. Метод Convert.FromBase64String є бібліотечним і може бути описаний в базі знань аналізатора, як має зворотну функцію Convert.ToBase64String, з чого ми можемо зробити висновок, що результат виконання CustomSanitize повинен потрапити на вхід Convert.ToBase64String. Але що робити з CustomSanitize, який не є бібліотечним, ніде не описаний і представляє з себе на даному етапі аналізу чорний перчений ящик? Добре, якщо нам доступні вихідні коди цього методу — в цьому випадку, ми можемо «провалитися» в його тіло і продовжити символьне виконання коду чином, аналогічним описаному вище. Але що ж робити, якщо ісходников ні? Відповідь пролунала в попередньому реченні: забути на деякий час про те, наш аналіз є статичним і працювати з даним методом, як з чорним ящиком. У нас є вже виведене раніше вираз Convert.ToBase64String (CustomSanitize (Request.Params [«parm»])), є безліч можливих векторів XSS (нехай це буде {`<script> alert (0) </ script>`, `'onmouseover =' a [alert]; a [0]. call (a [1], 1)` і `« onmouseover = »a [alert]; a [0]. apply (a [1], [1 ]) `}) — так чому б не профаззіть цю формулу, специфікуючи символьну переменнную Request.Params [« parm »] значеннями векторів і безпосередньо виконуючи вийшло вираз?
 
Припустимо, CustomSanitize видаляє виключно символи кутових дужок. Тоді, в результаті фаззінга, отримуємо три значення:
 
 
scriptalert(0)/script
'onmouseover='a[alert];a[0].call(a[1],1)
"onmouseover="a[alert];a[0].apply(a[1],[1])

з яких два останніх представляються гідними розгляду як векторів атаки. Отже, ми знаємо повне вираження, передане як аргумент PVO. Ми знаємо точне місце, в яке потрапить значення символьної змінної Request.Params [«parm»] при її специфікації значеннями векторів. Що ще нам потрібно для того, щоб вибрати з цих двох той вектор, використання якого призведе до ін'єкції? Ті, хто уважно слухали вебінар " Як розробити захищене веб-додаток і не зійти при цьому з розуму? " або розібралися з алгоритмом детектування XSS в IRV , відразу дадуть відповідь, що більше нам зовсім нічого не потрібно :)
 
Т.ч. кінцевим результатом аналізу цього коду є контекстний (визначальний значення символьних змінних в контексті виконання PVO) експлоїт:
 
 
Request.Params["name"] = "adm"
Request.Params["key1"] = "validkey"
Request.Params["parm"] = "'onmouseover='a[alert];a[0].call(a[1],1)"

з якого вже можна вивести і HTTP (визначає вимоги до фактичних параметрах HTTP-запиту) експлоїт:
 
 
GET http://host.domain/path/to/document.aspx?name=adm&key1=validkey&parm=%27onmouseover%3D%27a%5Balert%5D%3Ba%5B0%5D.call%28a%5B1%5D%2C1%29 HTTP/1.1

У AI, якщо цікаво, це виглядає так (клікабельно):
 
 ÐšÐ¾Ð½ÑÐ¾Ð»ÑŒÐ½Ð°Ñ версияGUI версия
Зрозуміло, в жорстокій реальності все злегка складніше: навіть змінений фільтрує функцією вектор може «вистрілити», що разом з появою регулярних виразів в таких функціях призводить до необхідності маніпулювати описують їх кінцевими автоматами замість константних значень; той факт, що вхідний параметр запиту може встромлятися в довільну граматичну конструкцію вихідного мови призводить до необхідності парсинга та / або евристичного виведення властивостей острівних мов і т.д. і т.п. Але це вже теми для окремих (і, ймовірно, трохи більше наукових) статей. Зауважу лише, що в рамках нашого завдання, ці проблеми також були успішно вирішені.
 
 Як ви отримуєте точки входу?
Я навмисно опустив у всіх прикладах питання отримання "/ path / to / document.aspx" (Тобто маршруту до точки входу в веб-додаток), тому що дана задача не має універсального рішення і вимагає опису специфіки різних фреймворків в базі знань аналізатора. Для ASP.NET Webforms, наприклад, точками входу є методи-обробники т.зв. postback'ов елементів управління веб-форм (що вимагає розбору. aspx файлів і зв'язування їх з відповідними codebehind-файлами). У ASP.NET MVC маршрути задаються через наповнення колекції RouteCollection прямо в коді ініціалізації програми. Не можна також забувати і про можливість появи в WebConfig СЕКЦІЮ urlMappings, urlrewritingnet і їм подібних, що також впливають на маршрутизацію HTTP-запитів до додатка. Та й розробнику нічого не заважає визначити власний HTTP-обробник, який реалізує кастомний логіку роутінга, реверс якої є алгоритмічно нерозв'язною завданням. У цьому випадку, нам нічого не залишається, крім як розглядати в якості точок входу все public і protected методи в разі Java / C # або всі. Php-файли у випадку з PHP, змирившись із зростанням ймовірності зловити false-positive на недосяжне зовні коді. Однак живцем, особисто я поки таких. NET додатків не зустрічав, а існуючий зоопарк PHP-фреймворків хоч і вселяє, але цілком формалізується в базі знань аналізатора, в тому числі і в частині, що стосується отримання маршрутів до точок входу. Екзотику типу опису правил роутінга в БД, як уже напевно зрозуміло, ми поки обробляємо згаданим вище прямому перебором всіх потенційних точок входу (що, до речі, дає зовсім не такі погані результати, як може здатися на перший погляд).
 
 That's all
Сподіваюся, що на поставлені питання відповісти все ж вдалося. Але якщо раптом виникли нові, або залишилися незрозумілі моменти — welcome, як кажуть :)
 
     
  1. Відразу обмовлюся, що приклад безумовно синтетичний і покликаний продемонструвати, швидше, можливі проблеми на шляху аналізу коду середньостатистичної паршивості, ніж якийсь реальний приклад з живої системи. Якщо хтось із читачів захоче запропонувати свій варіант фрагмента коду, то можна буде розглянути процес аналізу і на ньому — взагалі не питання
  2.  
  3. Опис покрокового виконання процесу аналізу навіть такого простого коду вилилося б в багатосторінкову ланцюжок перетворень одноманітних логічних формул, тому викладки тут не наводяться. Зацікавлені можуть ознайомитися з досить докладним описом підходу і окремих його етапів у записі доповіді, згаданого на початку статті.
  4.  
    
Джерело: Хабрахабр

0 коментарів

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