Биб­лиотеч­ный код может сос­тавлять льви­ную долю прог­раммы. Ясное дело, ничего инте­рес­ного этот код не содер­жит. Поэто­му его ана­лиз с пол­ной уве­рен­ностью мож­но опус­тить. Незачем тра­тить на него наше дра­гоцен­ное вре­мя. Одна­ко как быть, если дизас­сем­блер неп­равиль­но рас­познал име­на фун­кций? Что, при­дет­ся изу­чать мно­готон­ный лис­тинг самос­тоятель­но? У хакеров есть про­верен­ные методы решения подоб­ных проб­лем — об этих методах мы сегод­ня и погово­рим.

Фундаментальные основы хакерства

Пят­надцать лет назад эпи­чес­кий труд Кри­са Кас­пер­ски «Фун­дамен­таль­ные осно­вы хакерс­тва» был нас­толь­ной кни­гой каж­дого начина­юще­го иссле­дова­теля в области компь­ютер­ной безопас­ности. Одна­ко вре­мя идет, и зна­ния, опуб­ликован­ные Кри­сом, теря­ют акту­аль­ность. Редак­торы «Хакера» попыта­лись обно­вить этот объ­емный труд и перенес­ти его из вре­мен Windows 2000 и Visual Studio 6.0 во вре­мена Windows 10 и Visual Studio 2019.

Ссыл­ки на дру­гие статьи из это­го цик­ла ищи на стра­нице авто­ра.

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

Ана­лиз дизас­сем­блер­ного лис­тинга — дело дру­гое. Име­на фун­кций, за ред­кими исклю­чени­ями, в нем отсутс­тву­ют, и опре­делить, printf это или что‑то дру­гое, «на взгляд» невоз­можно. При­ходит­ся вни­кать в алго­ритм... Лег­ко ска­зать — вни­кать! Та же printf пред­став­ляет собой слож­ный интер­пре­татор стро­ки спе­цифи­като­ров — с ходу в нем не раз­берешь­ся! А ведь есть и более монс­тру­озные фун­кции. Самое обид­ное — алго­ритм их работы не име­ет никако­го отно­шения к ана­лизу иссле­дуемой прог­раммы. Тот же new может и выделять память из Windows-кучи, и реали­зовы­вать собс­твен­ный менед­жер, но нам‑то от это­го что? Дос­таточ­но знать, что это имен­но new, то есть фун­кция выделе­ния памяти, а не free или, ска­жем, fopen.

До­ля биб­лиотеч­ных фун­кций в прог­рамме в сред­нем сос­тавля­ет от пятиде­сяти до девянос­та про­цен­тов. Осо­бен­но она велика у прог­рамм, сос­тавлен­ных в визу­аль­ных сре­дах раз­работ­ки, исполь­зующих авто­мати­чес­кую генера­цию кода (нап­ример, Microsoft Visual Studio, Delphi). При­чем биб­лиотеч­ные фун­кции под­час нам­ного слож­нее и запутан­нее три­виаль­ного кода самой прог­раммы. Обид­но, льви­ная доля уси­лий на ана­лиз тра­тит­ся впус­тую... Как бы опти­мизи­ровать это?

 

На помощь вновь приходит IDA

Уни­каль­ная спо­соб­ность IDA раз­личать стан­дар­тные биб­лиотеч­ные фун­кции мно­жес­тва ком­пилято­ров выгод­но отли­чает ее от боль­шинс­тва дру­гих дизас­сем­бле­ров, это­го делать не уме­ющих. К сожале­нию, IDA (как и все, соз­данное челове­ком) далека от иде­ала: каким бы обширным ни был спи­сок под­держи­ваемых биб­лиотек, кон­крет­ные вер­сии кон­крет­ных пос­тавщи­ков или моделей памяти могут отсутс­тво­вать. И даже из тех биб­лиотек, что ей извес­тны, рас­позна­ются не все фун­кции (о при­чинах будет рас­ска­зано чуть поз­же).

Впро­чем, нерас­познан­ная фун­кция — это пол­беды, неп­равиль­но рас­познан­ная фун­кция — мно­го хуже, ибо это при­водит к ошиб­кам (иног­да труд­ноуло­вимым) в ана­лизе иссле­дуемой прог­раммы или ста­вит иссле­дова­теля в глу­хой тупик. Нап­ример, вызыва­ется fopen и воз­вра­щен­ный ею резуль­тат спус­тя некото­рое вре­мя переда­ется free — с одной сто­роны: почему бы и нет? Ведь fopen воз­вра­щает ука­затель на струк­туру FILE, а free ее уда­ляет. А если free вов­се не free, а, ска­жем, fseek? Про­пус­тив опе­рацию позици­они­рова­ния, мы не смо­жем пра­виль­но вос­ста­новить струк­туру фай­ла, с которым работа­ет прог­рамма.

 

Опознание функций

Рас­познать ошиб­ки IDA будет лег­че, если пред­став­лять, как имен­но она выпол­няет рас­позна­вание. Мно­гие почему‑то счи­тают, что здесь задей­ство­ван три­виаль­ный под­счет CRC (кон­троль­ной сум­мы). Что ж, под­счет CRC — заман­чивый алго­ритм, но, увы, для решения дан­ной задачи он неп­ригоден. Основной камень прет­кно­вения — наличие непос­тоян­ных фраг­ментов, а имен­но переме­щаемых эле­мен­тов. И хотя при под­сче­те CRC переме­щаемые эле­мен­ты мож­но прос­то игно­риро­вать (не забывая про­делы­вать ту же опе­рацию и в иден­тифици­руемой фун­кции), раз­работ­чик IDA пошел дру­гим, более запутан­ным и вити­ева­тым, но и более про­изво­дитель­ным путем.

Клю­чевая идея зак­люча­ется в том, что незачем тра­тить вре­мя на вычис­ление CRC. Для пред­варитель­ной иден­тифика­ции фун­кции впол­не сой­дет и три­виаль­ное посим­воль­ное срав­нение, за вычетом переме­щаемых эле­мен­тов (они игно­риру­ются и в срав­нении не учас­тву­ют). Точ­нее говоря, не срав­нение, а поиск задан­ной пос­ледова­тель­нос­ти бай­тов в эта­лон­ной базе, орга­низо­ван­ной в виде дво­ично­го дерева. Вре­мя дво­ично­го поис­ка, как извес­тно, про­пор­циональ­но логариф­му количес­тва записей в базе. Здра­вый смысл под­ска­зыва­ет, что дли­на шаб­лона (ина­че говоря, сиг­натуры, то есть срав­нива­емой пос­ледова­тель­нос­ти) дол­жна быть дос­таточ­ной для однознач­ной иден­тифика­ции фун­кции. Одна­ко раз­работ­чик IDA по непонят­ным для авто­ров при­чинам решил огра­ничить­ся толь­ко пер­выми трид­цатью дву­мя бай­тами, что (осо­бен­но если вычесть про­лог, который у всех фун­кций прак­тичес­ки оди­наков) доволь­но мало.

И вер­но! Мно­гие фун­кции попада­ют на один и тот же лист дерева, воз­ника­ет кол­лизия — неод­нознач­ность отож­дест­вле­ния. Для раз­решения ситу­ации у всех «кол­лизи­онных» фун­кций под­счи­тыва­ется CRC16 с трид­цать вто­рого бай­та до пер­вого переме­щаемо­го эле­мен­та и срав­нива­ется с CRC16 эта­лон­ных фун­кций. Чаще все­го это сра­баты­вает, но, если пер­вый переме­щаемый эле­мент ока­жет­ся рас­положен­ным слиш­ком близ­ко к трид­цать вто­рому бай­ту, пос­ледова­тель­ность под­сче­та кон­троль­ной сум­мы ока­жет­ся слиш­ком корот­кой, а то и вов­се рав­ной нулю (может же быть трид­цать вто­рой байт переме­щаемым эле­мен­том, почему бы и нет?). В слу­чае пов­торной кол­лизии находим в фун­кци­ях байт, в котором все они отли­чают­ся, и запоми­наем его сме­щение в базе.

Все это (да прос­тит авто­ров раз­работ­чик IDA!) напоми­нает сле­дующий анек­дот. Пой­мали тузем­цы нем­ца, аме­рикан­ца и укра­инца и говорят им: мол, или отку­пай­тесь чем‑нибудь, или съедим. На откуп пред­лага­ется: мил­лион дол­ларов (толь­ко не спра­шивай­те, зачем тузем­цам мил­лион дол­ларов, — может, кос­тер жечь), сто щел­банов или съесть мешок соли. Ну, аме­рика­нец дос­тает сотовый, зво­нит кому‑то... Прип­лыва­ет катер с мил­лионом дол­ларов, и аме­рикан­ца бла­гопо­луч­но отпуска­ют. Немец в это вре­мя геро­ичес­ки съеда­ет мешок соли, и его полумер­тво­го спус­кают на воду. Укра­инец же ел соль, ел‑ел, две тре­ти съел, не выдер­жал и говорит: а, лад­но, чер­ти, бей­те щел­баны. Бьет вождь его, и толь­ко девянос­то уда­ров отщелкал, тот не выдер­жал и говорит: да нате мил­лион, подави­тесь! Так и с IDA, посим­воль­ное срав­нение не до кон­ца, а толь­ко трид­цати двух бай­тов, под­счет CRC не для всей фун­кции, а сколь­ко слу­чай на душу положит, наконец, пос­ледний клю­чевой байт — и тот‑то «клю­чевой», да не сов­сем. Дело в том, что мно­гие фун­кции сов­пада­ют байт в байт, но совер­шенно раз­личны по наз­ванию и наз­начению. Не веришь? Тог­да как тебе пон­равит­ся сле­дующее:

read: write:
sub rsp, 28h sub rsp, 28h
call _read call _write
add rsp, 28h add rsp, 28h
retn retn

Тут без ана­лиза переме­щаемых эле­мен­тов никак не обой­тись! При­чем это не какой‑то спе­циаль­но надуман­ный при­мер — подоб­ных фун­кций очень мно­го. В час­тнос­ти, биб­лиоте­ки от Embarcadero (в прош­лом от Borland) ими так и кишат. Поэто­му в былые вре­мена IDA час­то «спо­тыка­лась» и впа­дала в гру­бые ошиб­ки. Тем не менее сей­час IDA замет­но воз­мужала и уже не стра­дает дет­ски­ми боляч­ками. Для при­мера скор­мим ком­пилято­ру C++Builder такую фун­кцию:

void demo()
{
printf("DERIVED\n");
}

Пос­ледняя вер­сия IDA сей­час 7.4, меж­ду тем я исполь­зую IDA 7.2, и она чаще все­го успешно рас­позна­ет поч­ти любые фун­кции. В нашем слу­чае резуль­тат выг­лядит сле­дующим обра­зом:

.text:0000000140001000 void demo(void) proc near
.text:0000000140001000 sub rsp, 28h
.text:0000000140001004 lea rcx, _Format ; "DERIVED\n"
.text:000000014000100B call printf
.text:0000000140001010 add rsp, 28h
.text:0000000140001014 retn
.text:0000000140001014 void demo(void) endp

То есть дизас­сем­блер пра­виль­но рас­познал имя фун­кции. Но так быва­ет далеко не всег­да и не со все­ми биб­лиотеч­ными фун­кци­ями. А ког­да проб­лемы воз­ника­ют с ними, кодоко­пате­лю ана­лизи­ровать ста­новит­ся слож­новато. Быва­ет, сидишь, тупо уста­вив­шись в лис­тинг дизас­сем­бле­ра, и никак не можешь понять: что же этот фраг­мент дела­ет? И толь­ко потом обна­ружи­ваешь — одна или нес­коль­ко фун­кций опоз­наны неп­равиль­но!

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии