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

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

Пят­надцать лет назад эпи­чес­кий труд Кри­са Кас­пер­ски «Фун­дамен­таль­ные осно­вы хакерс­тва» был нас­толь­ной кни­гой каж­дого начина­юще­го иссле­дова­теля в области компь­ютер­ной безопас­ности. Одна­ко вре­мя идет, и зна­ния, опуб­ликован­ные Кри­сом, теря­ют акту­аль­ность. Редак­торы «Хакера» попыта­лись обно­вить этот объ­емный труд и перенес­ти его из вре­мен 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("DERIVEDn");
}

Пос­ледняя вер­сия 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

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

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

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

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

Крис Касперски

Крис Касперски

Известный российский хакер. Легенда ][, ex-редактор ВЗЛОМа. Также известен под псевдонимами мыщъх, nezumi (яп. 鼠, мышь), n2k, elraton, souriz, tikus, muss, farah, jardon, KPNC.

Юрий Язев

Юрий Язев

Широко известен под псевдонимом yurembo. Программист, разработчик видеоигр, независимый исследователь. Старый автор журнала «Хакер».

Check Also

Куча приключений. Изучаем методы heap exploitation на виртуалке c Hack The Box

В этой статье я расскажу об алгоритмах управления памятью в Linux, техниках heap exploitat…

Оставить мнение