Распуши пингвина! Разбираем способы фаззинга ядра Linux - «Новости» » Самоучитель CSS
Меню
Наши новости
Учебник CSS

Невозможно отучить людей изучать самые ненужные предметы.

Введение в CSS
Преимущества стилей
Добавления стилей
Типы носителей
Базовый синтаксис
Значения стилевых свойств
Селекторы тегов
Классы
CSS3

Надо знать обо всем понемножку, но все о немногом.

Идентификаторы
Контекстные селекторы
Соседние селекторы
Дочерние селекторы
Селекторы атрибутов
Универсальный селектор
Псевдоклассы
Псевдоэлементы

Кто умеет, тот делает. Кто не умеет, тот учит. Кто не умеет учить - становится деканом. (Т. Мартин)

Группирование
Наследование
Каскадирование
Валидация
Идентификаторы и классы
Написание эффективного кода

Самоучитель CSS

Вёрстка
Изображения
Текст
Цвет
Линии и рамки
Углы
Списки
Ссылки
Дизайны сайтов
Формы
Таблицы
CSS3
HTML5

Новости

Блог для вебмастеров
Новости мира Интернет
Сайтостроение
Ремонт и советы
Все новости

Справочник CSS

Справочник от А до Я
HTML, CSS, JavaScript

Афоризмы

Афоризмы о учёбе
Статьи об афоризмах
Все Афоризмы

Видео Уроки


Наш опрос



Наши новости

       
2-08-2021, 00:00
Распуши пингвина! Разбираем способы фаззинга ядра Linux - «Новости»
Рейтинг:
Категория: Новости

нес­коль­ко экс­пло­итов для най­ден­ных багов), затем фаз­зил ту же сеть с внеш­ней сто­роны и, наконец, фаз­зил под­систе­му USB со сто­роны устрой­ств.

info


Статья написа­на редак­цией «Хакера» по мотивам док­лада «Фаз­зинг ядра Linux» Ан­дрея Конова­лова при учас­тии док­ладчи­ка и изло­жена от пер­вого лица с его раз­решения.



Ког­да я говорю об ата­ках на USB, мно­гие сра­зу вспо­мина­ют Evil HID — одну из атак типа BadUSB. Это ког­да под­клю­чаемое устрой­ство выг­лядит безобид­но, как флеш­ка, а на самом деле ока­зыва­ется кла­виату­рой, которая авто­мати­чес­ки откры­вает кон­соль и дела­ет что‑нибудь нехоро­шее.


В рам­ках моей работы по фаз­зингу такие ата­ки меня не инте­ресо­вали. Я искал в пер­вую оче­редь пов­режде­ния памяти ядра. В слу­чае ата­ки через USB сце­нарий похож на BadUSB: мы под­клю­чаем спе­циаль­ное USB-устрой­ство и оно начина­ет делать нехоро­шие вещи. Но оно не набира­ет коман­ды, при­киды­ваясь кла­виату­рой, а экс­плу­ати­рует уяз­вимость в драй­вере и получа­ет исполне­ние кода внут­ри ядра.


За годы работы над фаз­зингом ядра у меня ско­пилась кол­лекция ссы­лок и нарабо­ток. Я их упо­рядо­чил и прев­ратил в док­лад. Сей­час я рас­ска­жу, какие есть спо­собы фаз­зить ядро, и дам советы начина­ющим иссле­дова­телям, которые решат занять­ся этой темой.


 

Что такое фаззинг


Фаз­зинг — это спо­соб искать ошиб­ки в прог­раммах.


Как он работа­ет? Мы генери­руем слу­чай­ные дан­ные, переда­ем их на вход прог­рамме и про­веря­ем, не сло­малась ли она. Если не сло­малась — генери­руем новый ввод. Если сло­малась — прек­расно, мы наш­ли баг. Пред­полага­ется, что прог­рамма не дол­жна падать от неожи­дан­ного вво­да, она дол­жна этот ввод кор­рек­тно обра­баты­вать.


Кон­крет­ный при­мер: мы берем XML-пар­сер и скар­мли­ваем ему слу­чай­но сге­нери­рован­ные XML-фай­лы. Если он упал — мы наш­ли баг в пар­сере.


Фаз­зеры мож­но делать для любой шту­ки, которая обра­баты­вает вход­ные дан­ные. Это может быть при­ложе­ние или биб­лиоте­ка в прос­транс­тве поль­зовате­ля — юзер­спей­се. Это может быть ядро, может быть про­шив­ка, а может быть даже железо.


Ког­да мы начина­ем работать над фаз­зером для оче­ред­ной прог­раммы, нам нуж­но разоб­рать­ся со сле­дующи­ми воп­росами:




  1. Как прог­рамму запус­кать? В слу­чае при­ложе­ния в юзер­спей­се — запус­тить бинар­ник. А вот запус­тить ядро или час­ти про­шив­ки так прос­то не вый­дет.


  2. Что слу­жит вход­ными дан­ными? Для XML-пар­сера вход­ные дан­ные — XML-фай­лы. А, нап­ример, бра­узер и обра­баты­вает HTML, и исполня­ет jаvascript.


  3. Как вход­ные дан­ные прог­рамме переда­вать? В прос­тей­шем слу­чае дан­ные переда­ются на стан­дар­тный ввод или в виде фай­ла. Но прог­раммы могут получать дан­ные и через дру­гие каналы. Нап­ример, про­шив­ка может получать их от физичес­ких устрой­ств.


  4. Как генери­ровать вво­ды? «Вво­дом» будем называть набор дан­ных, передан­ный прог­рамме на вход. В качес­тве вво­да мож­но соз­давать мас­сивы ран­домных бай­тов, а мож­но делать что‑нибудь более умное.


  5. Как опре­делять факт ошиб­ки? Если прог­рамма упа­ла — это баг. Но сущес­тву­ют ошиб­ки, которые не при­водят к падению. При­мер: утеч­ка информа­ции. Такие ошиб­ки тоже хочет­ся находить.


  6. Как авто­мати­зиро­вать про­цесс? Мож­но запус­кать прог­рамму с новыми вво­дами вруч­ную и смот­реть, не упа­ла ли она. А мож­но написать скрипт, который будет делать это авто­мати­чес­ки.


Се­год­ня мы говорим о ядре Linux, так что в каж­дом из воп­росов мы можем мыс­ленно заменить сло­во «прог­рамма» на «ядро Linux». А теперь давай поп­робу­ем най­ти отве­ты.


 

Простой способ


Для начала при­дума­ем отве­ты поп­роще и раз­работа­ем пер­вую вер­сию нашего фаз­зера.


 

Запускаем ядро


Нач­нем с того, как ядро запус­кать. Здесь есть два спо­соба: исполь­зовать железо (компь­юте­ры, телефо­ны или одноплат­ники) или исполь­зовать вир­туаль­ные машины (нап­ример, QEMU). У каж­дого свои плю­сы и минусы.


Ког­да запус­каешь ядро на железе, то получа­ешь сис­тему в том виде, в котором она работа­ет в реаль­нос­ти. Нап­ример, там дос­тупны и работа­ют драй­веры устрой­ств. В вир­туал­ке дос­тупны толь­ко те фичи, которые она под­держи­вает.


С дру­гой сто­роны, железом гораз­до слож­нее управлять: раз­ливать ядра, перезаг­ружать в слу­чае падения, собирать логи. Вир­туал­ка в этом пла­не иде­аль­на.


Еще один плюс вир­туаль­ных машин — мас­шта­биру­емость. Что­бы фаз­зить на боль­шем количес­тве железок, их надо купить, что может быть дорого или логис­тичес­ки слож­но. Для мас­шта­биро­вания фаз­зинга в вир­туал­ках дос­таточ­но взять машину помощ­нее и запус­тить их сколь­ко нуж­но.


Учи­тывая осо­бен­ности каж­дого из спо­собов, вир­туал­ки выг­лядят как луч­ший вари­ант. Но давай для начала отве­тим на осталь­ные воп­росы. Гля­дишь, мы при­дума­ем спо­соб фаз­зить, который не при­вязан к спо­собу запус­ка ядра.


 

Разбираемся со вводами


Что явля­ется вход­ными дан­ными для ядра? Ядро обра­баты­вает сис­темные вызовы — сис­колы (syscall). Как передать их в ядро? Давай напишем прог­рамму, которая дела­ет пос­ледова­тель­ность вызовов, ском­пилиру­ем ее в бинарь и запус­тим. Всё: ядро будет интер­пре­тиро­вать наши вызовы.


Те­перь раз­берем­ся с тем, какие дан­ные переда­вать в сис­колы в качес­тве аргу­мен­тов и в каком поряд­ке сис­колы вызывать.


Са­мый прос­той спо­соб генери­ровать дан­ные — брать слу­чай­ные бай­ты. Этот спо­соб работа­ет пло­хо: обыч­но прог­раммы, вклю­чая то же ядро, ожи­дают дан­ные в более‑менее кор­рек­тном виде. Если передать им сов­сем мусор, даже эле­мен­тарные про­вер­ки на кор­рек­тность не прой­дут, и прог­рамма отка­жет­ся обра­баты­вать ввод даль­ше.


Спо­соб луч­ше: генери­ровать дан­ные на осно­ве грам­матики. На при­мере XML-пар­сера: мы можем за­ложить в грам­матику зна­ние о том, что XML-файл сос­тоит из XML-тегов. Таким обра­зом мы обой­дем эле­мен­тарные про­вер­ки и про­ник­нем глуб­же внутрь кода пар­сера.


Од­нако для ядра такой под­ход надо адап­тировать: ядро при­нима­ет пос­ледова­тель­ность сис­колов с аргу­мен­тами, а это не прос­то мас­сив бай­тов, даже сге­нери­рован­ных по опре­делен­ной грам­матике.


Пред­ставь прог­рамму из трех сис­колов: open, который откры­вает файл, ioctl, который совер­шает опе­рацию над этим фай­лом, и close, который файл зак­рыва­ет. Для open пер­вый аргу­мент — это стро­ка, то есть прос­тая струк­тура с единс­твен­ным фик­сирован­ным полем. Для ioctl, в свою оче­редь, пер­вый аргу­мент — зна­чение, которое вер­нул open, а тре­тий — слож­ная струк­тура с нес­коль­кими полями. Наконец, в close переда­ется все тот же резуль­тат open.


int fd = open("/dev/something", …);
ioctl(fd,SOME_IOCTL, &{0x10, ...});
close(fd);

Це­ликом эта прог­рамма — типич­ный ввод, который обра­баты­вает ядро. То есть вво­ды для ядра пред­став­ляют собой пос­ледова­тель­нос­ти сис­колов. При­чем их аргу­мен­ты струк­туриро­ваны, а их резуль­тат может переда­вать­ся от одно­го сис­кола к дру­гому.


Это все похоже на API некой биб­лиоте­ки — его вызовы при­нима­ют струк­туриро­ван­ные аргу­мен­ты и воз­вра­щают резуль­таты, которые могут переда­вать­ся в сле­дующие вызовы.


По­луча­ется, что, ког­да мы фаз­зим сис­колы, мы фаз­зим API, который пре­дос­тавля­ет ядро. Я такой под­ход называю API-aware-фаз­зинг.


В слу­чае ядра Linux, к сожале­нию, точ­ного опи­сания всех воз­можных сис­колов и их аргу­мен­тов нет. Есть нес­коль­ко попыток сге­нери­ровать эти опи­сания авто­мати­чес­ки, но ни одна из них не выг­лядит удов­летво­ритель­ной. Поэто­му единс­твен­ный спо­соб — это написать опи­сания руками.


Так и сде­лаем: выберем нес­коль­ко сис­колов и раз­работа­ем алго­ритм генери­рова­ния их пос­ледова­тель­нос­тей. Нап­ример, заложим в него, что в ioctl дол­жен переда­вать­ся резуль­тат open и струк­тура пра­виль­ного типа со слу­чай­ными полями.


 

[Не] автоматизируем


С авто­мати­заци­ей пока не будем замора­чивать­ся: наш фаз­зер в цик­ле будет генери­ровать вво­ды и переда­вать их ядру. А мы будем вруч­ную монито­рить лог ядра на пред­мет оши­бок типа kernel panic.


 

Готово


Всё! Мы отве­тили на все воп­росы и раз­работа­ли прос­той спо­соб фаз­зинга ядра.
































Как запус­кать ядро?В QEMU или на реаль­ном железе
Что будет вход­ными дан­ными?Сис­темные вызовы
Как вход­ные дан­ные переда­вать ядру?Че­рез запуск исполня­емо­го фай­ла
Как генери­ровать вво­ды?На осно­ве API ядра
Как опре­делять наличие багов?По kernel panic
Как авто­мати­зиро­вать?while (true) syscall(…)

Наш фаз­зер пред­став­ляет собой бинар­ник, который в слу­чай­ном поряд­ке вызыва­ет сис­колы с более‑менее кор­рек­тны­ми аргу­мен­тами. Пос­коль­ку бинар­ник мож­но запус­тить и на вир­туал­ке, и на железе, то фаз­зер получил­ся уни­вер­саль­ным.


Ход рас­сужде­ний был прос­тым, но сам под­ход работа­ет прек­расно. Если спе­циалис­та по фаз­зингу ядра Linux спро­сить: «Какой фаз­зер работа­ет опи­сан­ным спо­собом?», то он сра­зу ска­жет: Trinity! Да, фаз­зер с таким алго­рит­мом работы уже сущес­тву­ет. Одно из его пре­иму­ществ — он лег­ко перено­симый. Закинул бинарь в сис­тему, запус­тил — и все, ты уже ищешь баги в ядре.


 

Способ получше


Фаз­зер Trinity сде­лали дав­но, и с тех пор мысль в области фаз­зинга ушла даль­ше. Давай поп­робу­ем улуч­шить при­думан­ный спо­соб, исполь­зовав более сов­ремен­ные идеи.


 

Собираем покрытие


Идея пер­вая: для генера­ции вво­дов исполь­зовать под­ход coverage-guided — на осно­ве сбор­ки пок­рытия кода.


Как он работа­ет? Помимо генери­рова­ния слу­чай­ных вво­дов с нуля, мы под­держи­ваем набор ранее сге­нери­рован­ных «инте­рес­ных» вво­дов — кор­пус. И иног­да, вмес­то слу­чай­ного вво­да, мы берем один ввод из кор­пуса и его слег­ка модифи­циру­ем. Пос­ле чего мы исполня­ем прог­рамму с новым вво­дом и про­веря­ем, инте­ресен ли он. А инте­ресен ввод в том слу­чае, если он поз­воля­ет пок­рыть учас­ток кода, который ни один из пре­дыду­щих исполнен­ных вво­дов не пок­рыва­ет. Если новый ввод поз­волил прой­ти даль­ше вглубь прог­раммы, то мы добав­ляем его в кор­пус. Таким обра­зом, мы пос­тепен­но про­ника­ем все глуб­же и глуб­же, а в кор­пусе собира­ются все более и более инте­рес­ные прог­раммы.


Этот под­ход исполь­зует­ся в двух основных инс­тру­мен­тах для фаз­зинга при­ложе­ний в юзер­спей­се: AFL и libFuzzer.


нес­коль­ко экс­пло­итов для най­ден­ных багов), затем фаз­зил ту же сеть с внеш­ней сто­роны и, наконец, фаз­зил под­систе­му USB со сто­роны устрой­ств. info Статья написа­на редак­цией «Хакера» по мотивам док­лада «Фаз­зинг ядра Linux» Ан­дрея Конова­лова при учас­тии док­ладчи­ка и изло­жена от пер­вого лица с его раз­решения. Ког­да я говорю об ата­ках на USB, мно­гие сра­зу вспо­мина­ют Evil HID — одну из атак типа BadUSB. Это ког­да под­клю­чаемое устрой­ство выг­лядит безобид­но, как флеш­ка, а на самом деле ока­зыва­ется кла­виату­рой, которая авто­мати­чес­ки откры­вает кон­соль и дела­ет что‑нибудь нехоро­шее. В рам­ках моей работы по фаз­зингу такие ата­ки меня не инте­ресо­вали. Я искал в пер­вую оче­редь пов­режде­ния памяти ядра. В слу­чае ата­ки через USB сце­нарий похож на BadUSB: мы под­клю­чаем спе­циаль­ное USB-устрой­ство и оно начина­ет делать нехоро­шие вещи. Но оно не набира­ет коман­ды, при­киды­ваясь кла­виату­рой, а экс­плу­ати­рует уяз­вимость в драй­вере и получа­ет исполне­ние кода внут­ри ядра. За годы работы над фаз­зингом ядра у меня ско­пилась кол­лекция ссы­лок и нарабо­ток. Я их упо­рядо­чил и прев­ратил в док­лад. Сей­час я рас­ска­жу, какие есть спо­собы фаз­зить ядро, и дам советы начина­ющим иссле­дова­телям, которые решат занять­ся этой темой. Что такое фаззинг Фаз­зинг — это спо­соб искать ошиб­ки в прог­раммах. Как он работа­ет? Мы генери­руем слу­чай­ные дан­ные, переда­ем их на вход прог­рамме и про­веря­ем, не сло­малась ли она. Если не сло­малась — генери­руем новый ввод. Если сло­малась — прек­расно, мы наш­ли баг. Пред­полага­ется, что прог­рамма не дол­жна падать от неожи­дан­ного вво­да, она дол­жна этот ввод кор­рек­тно обра­баты­вать. Кон­крет­ный при­мер: мы берем XML-пар­сер и скар­мли­ваем ему слу­чай­но сге­нери­рован­ные XML-фай­лы. Если он упал — мы наш­ли баг в пар­сере. Фаз­зеры мож­но делать для любой шту­ки, которая обра­баты­вает вход­ные дан­ные. Это может быть при­ложе­ние или биб­лиоте­ка в прос­транс­тве поль­зовате­ля — юзер­спей­се. Это может быть ядро, может быть про­шив­ка, а может быть даже железо. Ког­да мы начина­ем работать над фаз­зером для оче­ред­ной прог­раммы, нам нуж­но разоб­рать­ся со сле­дующи­ми воп­росами: Как прог­рамму запус­кать? В слу­чае при­ложе­ния в юзер­спей­се — запус­тить бинар­ник. А вот запус­тить ядро или час­ти про­шив­ки так прос­то не вый­дет. Что слу­жит вход­ными дан­ными? Для XML-пар­сера вход­ные дан­ные — XML-фай­лы. А, нап­ример, бра­узер и обра­баты­вает HTML, и исполня­ет jаvascript. Как вход­ные дан­ные прог­рамме переда­вать? В прос­тей­шем слу­чае дан­ные переда­ются на стан­дар­тный ввод или в виде фай­ла. Но прог­раммы могут получать дан­ные и через дру­гие каналы. Нап­ример, про­шив­ка может получать их от физичес­ких устрой­ств. Как генери­ровать вво­ды? «Вво­дом» будем называть набор дан­ных, передан­ный прог­рамме на вход. В качес­тве вво­да мож­но соз­давать мас­сивы ран­домных бай­тов, а мож­но делать что‑нибудь более умное. Как опре­делять факт ошиб­ки? Если прог­рамма упа­ла — это баг. Но сущес­тву­ют ошиб­ки, которые не при­водят к падению. При­мер: утеч­ка информа­ции. Такие ошиб­ки тоже хочет­ся находить. Как авто­мати­зиро­вать про­цесс? Мож­но запус­кать прог­рамму с новыми вво­дами вруч­ную и смот­реть, не упа­ла ли она. А мож­но написать скрипт, который будет делать это авто­мати­чес­ки. Се­год­ня мы говорим о ядре Linux, так что в каж­дом из воп­росов мы можем мыс­ленно заменить сло­во «прог­рамма» на «ядро Linux». А теперь давай поп­робу­ем най­ти отве­ты. Простой способ Для начала при­дума­ем отве­ты поп­роще и раз­работа­ем пер­вую вер­сию нашего фаз­зера. Запускаем ядро Нач­нем с того, как ядро запус­кать. Здесь есть два спо­соба: исполь­зовать железо (компь­юте­ры, телефо­ны или одноплат­ники) или исполь­зовать вир­туаль­ные машины (нап­ример, QEMU). У каж­дого свои плю­сы и минусы. Ког­да запус­каешь ядро на железе, то получа­ешь сис­тему в том виде, в котором она работа­ет в реаль­нос­ти. Нап­ример, там дос­тупны и работа­ют драй­веры устрой­ств. В вир­туал­ке дос­тупны толь­ко те фичи, которые она под­держи­вает. С дру­гой сто­роны, железом гораз­до слож­нее управлять: раз­ливать ядра, перезаг­ружать в слу­чае падения, собирать логи. Вир­туал­ка в этом пла­не иде­аль­на. Еще один плюс вир­туаль­ных машин — мас­шта­биру­емость. Что­бы фаз­зить на боль­шем количес­тве железок, их надо купить, что может быть дорого или логис­тичес­ки слож­но. Для мас­шта­биро­вания фаз­зинга в вир­туал­ках дос­таточ­но взять машину помощ­нее и запус­тить их сколь­ко нуж­но. Учи­тывая осо­бен­ности каж­дого из спо­собов, вир­туал­ки выг­лядят как луч­ший вари­ант. Но давай для начала отве­тим на осталь­ные воп­росы. Гля­дишь, мы при­дума­ем спо­соб фаз­зить, который не при­вязан к спо­собу запус­ка ядра. Разбираемся со вводами Что явля­ется вход­ными дан­ными для ядра? Ядро обра­баты­вает сис­темные вызовы — сис­колы (syscall). Как передать их в ядро? Давай напишем прог­рамму, которая дела­ет пос­ледова­тель­ность вызовов, ском­пилиру­ем ее в бинарь и запус­тим. Всё: ядро будет интер­пре­тиро­вать наши вызовы. Те­перь раз­берем­ся с тем, какие дан­ные переда­вать в сис­колы в качес­тве аргу­мен­тов и в каком поряд­ке сис­колы вызывать. Са­мый прос­той спо­соб генери­ровать дан­ные — брать слу­чай­ные бай­ты. Этот спо­соб работа­ет пло­хо: обыч­но прог­раммы, вклю­чая то же ядро, ожи­дают дан­ные в более‑менее кор­рек­тном виде. Если передать им сов­сем мусор, даже эле­мен­тарные про­вер­ки на кор­рек­тность не прой­дут, и прог­рамма отка­жет­ся обра­баты­вать ввод даль­ше. Спо­соб луч­ше: генери­ровать дан­ные на осно­ве грам­матики. На при­мере XML-пар­сера: мы можем за­ложить в грам­матику зна­ние о том, что XML-файл сос­тоит из XML-тегов. Таким обра­зом мы обой­дем эле­мен­тарные про­вер­ки и про­ник­нем глуб­же внутрь кода пар­сера. Од­нако для ядра такой под­ход надо адап­тировать: ядро при­нима­ет пос­ледова­тель­ность сис­колов с аргу­мен­тами, а это не прос­то мас­сив бай­тов, даже сге­нери­рован­ных по опре­делен­ной грам­матике. Пред­ставь прог­рамму из трех сис­колов: open, который откры­вает файл, ioctl, который совер­шает опе­рацию над этим фай­лом, и close, который файл зак­рыва­ет. Для open пер­вый аргу­мент — это стро­ка, то есть прос­тая струк­тура с единс­твен­ным фик­сирован­ным полем. Для ioctl, в свою оче­редь, пер­вый аргу­мент — зна­чение, которое вер­нул open, а тре­тий — слож­ная струк­тура с нес­коль­кими полями. Наконец, в close переда­ется все тот же резуль­тат open. int fd = open(

Теги: CSS

Просмотров: 673
Комментариев: 0:   2-08-2021, 00:00
Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь. Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

 
Еще новости по теме:



Другие новости по теме: