Фундаментальные основы хакерства. Распределение динамической памяти и указатели - Мобил Гуру
Интернет

Фундаментальные основы хакерства. Распределение динамической памяти и указатели

Как работа­ет выделе­ние и осво­бож­дение памяти? Каким обра­зом устро­ены обыч­ные и умные ука­зате­ли в C++? Как рас­познать опе­рато­ры работы с памятью, исполь­зуя дизас­сем­блер, не понима­ющий их истинную при­роду? 

Что­бы во всем этом разоб­рать­ся, нам пред­сто­ит по бай­тикам разоб­рать механиз­мы рас­пре­деле­ния динами­чес­кой памяти при­ложе­ния (ины­ми сло­вами, кучи) двух самых популяр­ных ком­пилято­ров и выявить раз­личия в их работе. Поэто­му в статье нас ждет мно­жес­тво дизас­сем­блер­ных лис­тингов и кода на C++.

Пят­надцать лет назад эпи­чес­кий труд Кри­са Кас­пер­ски «Фун­дамен­таль­ные осно­вы хакерс­тва» был нас­толь­ной кни­гой каж­дого начина­юще­го иссле­дова­теля в области компь­ютер­ной безопас­ности. 

Одна­ко вре­мя идет, и зна­ния, опуб­ликован­ные Кри­сом, теря­ют акту­аль­ность. 

Ука­затель this — это нас­тоящий золотой клю­чик или, если угод­но, спа­сатель­ный круг, поз­воля­ющий не уто­нуть в бур­ном оке­ане ООП. Имен­но бла­года­ря this мож­но опре­делять при­над­лежность вызыва­емой фун­кции к тому или ино­му клас­су. 

Пос­коль­ку все невир­туаль­ные фун­кции объ­екта вызыва­ются непос­редс­твен­но — по фак­тичес­кому адре­су, объ­ект как бы рас­щепля­ется на сос­тавля­ющие его фун­кции еще на ста­дии ком­пиляции. Не будь ука­зате­лей this, вос­ста­новить иерар­хию фун­кций было бы прин­ципи­аль­но невоз­можно!

Та­ким обра­зом, пра­виль­ная иден­тифика­ция this очень важ­на. Единс­твен­ная проб­лема: как отли­чить его от ука­зате­лей на мас­сивы и струк­туры? Ведь экзем­пляр клас­са иден­тифици­рует­ся по ука­зате­лю this (если на выделен­ную память ука­зыва­ет this, это экзем­пляр клас­са), одна­ко сам this по опре­деле­нию — это ука­затель, ссы­лающий­ся на экзем­пляр клас­са. Зам­кну­тый круг! К счастью, есть одна лазей­ка... Код, манипу­лиру­ющий ука­зате­лем this, весь­ма спе­цифи­чен, что и поз­воля­ет отли­чить thisот всех осталь­ных ука­зате­лей.

Во­обще‑то у каж­дого ком­пилято­ра свой почерк, который нас­тоятель­но рекомен­дует­ся изу­чить, дизас­сем­бли­руя собс­твен­ные прог­раммы на C++, но сущес­тву­ют и уни­вер­саль­ные рекомен­дации, при­мени­мые к боль­шинс­тву реали­заций. 

Пос­коль­ку this — это неяв­ный аргу­мент каж­дой фун­кции — чле­на клас­са, то логич­но отло­жить раз­говор о его иден­тифика­ции до раз­дела «Иден­тифика­ция аргу­мен­тов фун­кций». Здесь же мы обсу­дим, как реали­зуют переда­чу ука­зате­ля this самые популяр­ные ком­пилято­ры.

Здесь мы, конеч­но, говорим об архи­тек­туре x64. На 32-бит­ной плат­форме парамет­ры, выров­ненные до 32-бит­ного раз­мера, переда­ются через стек. С дру­гой сто­роны, на 64-бит­ной плат­форме дела обсто­ят инте­рес­нее: пер­вые четыре целочис­ленных аргу­мен­та переда­ются в регис­трах RCXRDXR8R9

Если целочис­ленных аргу­мен­тов боль­ше, осталь­ные раз­меща­ются в сте­ке. Аргу­мен­ты, име­ющие зна­чения с пла­вающей запятой, переда­ются в регис­трах XMM0XMM1XMM2XMM3. При этом 16-бит­ные аргу­мен­ты переда­ются по ссыл­ке. Замечу, все это каса­ется сог­лашения о вызовах в опе­раци­онных сис­темах Microsoft (Microsoft ABI), в Unix-подоб­ных сис­темах дела обсто­ят по‑дру­гому. Но не будем рас­пылять на них свое вни­мание.

Оба про­тес­тирован­ных мною ком­пилято­ра, Visual C++ 2019 и C++ Builder 10.3, незави­симо от сог­лашения вызова фун­кции (__cdecl__clrcall__stdcall__fastcall__thiscall) переда­ют ука­затель this в регис­тре RCX, что соот­ветс­тву­ет его при­роде: this — целочис­ленный аргу­мент.

Опе­рато­ры new и delete тран­сли­руют­ся ком­пилято­ром в вызовы биб­лиотеч­ных фун­кций, которые могут быть рас­позна­ны точ­но так же, как и обыч­ные биб­лиотеч­ные фун­кции. Авто­мати­чес­ки рас­позна­вать биб­лиотеч­ные фун­кции уме­ет, в час­тнос­ти, IDA Pro, сни­мая эту заботу с плеч иссле­дова­теля. 

Одна­ко IDA Pro есть не у всех и далеко не всег­да в нуж­ный момент находит­ся под рукой, да к тому же не все биб­лиотеч­ные фун­кции она зна­ет, а из тех, что зна­ет, не всег­да узна­ет new и delete... Сло­вом, при­чин иден­тифици­ровать их вруч­ную пре­дос­таточ­но.

Ре­али­зация new и delete может быть любой, но Windows-ком­пилято­ры в боль­шинс­тве сво­ем ред­ко реали­зуют фун­кции работы с кучей самос­тоятель­но. Зачем это? Нам­ного про­ще обра­тить­ся к услу­гам опе­раци­онной сис­темы. 

Одна­ко наив­но ожи­дать вмес­то new появ­ление вызова HeapAlloc, а вмес­то delete — HeapFree. Нет, ком­пилятор не так прост! Раз­ве он может отка­зать себе в удо­воль­ствии «выреза­ния мат­решек»? Опе­ратор new тран­сли­рует­ся в фун­кцию new, вызыва­ющую для выделе­ния памяти mallocmalloc же, в свою оче­редь, обра­щает­ся к HeapAlloc (или ее подобию — в зависи­мос­ти от реали­зации биб­лиоте­ки работы с памятью) — сво­еоб­разной «обер­тке» одно­имен­ной Win32 API-про­цеду­ры. Кар­тина с осво­бож­дени­ем памяти ана­логич­на.

Уг­лублять­ся в деб­ри вло­жен­ных вызовов слиш­ком уто­митель­но. Нель­зя ли new и delete иден­тифици­ровать как‑нибудь ина­че, с мень­шими тру­дозат­ратами и без лиш­ней голов­ной боли? Разуме­ется, мож­но! Давай вспом­ним все, что мы зна­ем о new:

  • new при­нима­ет единс­твен­ный аргу­мент — количес­тво бай­тов выделя­емой памяти, при­чем этот аргу­мент в подав­ляющем боль­шинс­тве слу­чаев вычис­ляет­ся еще на ста­дии ком­пиляции, то есть явля­ется кон­стан­той;
  • ес­ли объ­ект не содер­жит ни дан­ных, ни вир­туаль­ных фун­кций, его раз­мер равен еди­нице (минималь­ный блок памяти, выделя­емый толь­ко для того, что­бы было на что ука­зывать ука­зате­лю this); отсю­да будет очень мно­го вызовов типа

    mov ecx, 1 ; size

    call XXX

    где XXX и есть адрес new! Вооб­ще же, типич­ный раз­мер объ­ектов сос­тавля­ет менее сот­ни бай­тов... ищи час­то вызыва­емую фун­кцию с аргу­мен­том‑кон­стан­той мень­ше ста бай­тов;

  • фун­кция new — одна из самых популяр­ных биб­лиотеч­ных фун­кций, ищи фун­кцию с «тол­пой» перек­рес­тных ссы­лок;

  • са­мое харак­терное: new воз­вра­щает ука­затель this, а this очень лег­ко иден­тифици­ровать даже при бег­лом прос­мотре кода (обыч­но он воз­вра­щает­ся в регис­тре RCX);

  • воз­вра­щен­ный new резуль­тат всег­да про­веря­ется на равенс­тво нулю (опе­рато­рами типа test RCXRCX), и, если он дей­стви­тель­но равен нулю, конс­трук­тор (если он есть) не вызыва­ется.

«Родимых пятен» у new более чем дос­таточ­но для быс­трой и надеж­ной иден­тифика­ции, тра­тить вре­мя на ана­лиз кода этой фун­кции совер­шенно ни к чему! Единс­твен­ное, о чем сле­дует пом­нить: new исполь­зует­ся не толь­ко для соз­дания новых экзем­пля­ров объ­ектов, но и для выделе­ния памяти под мас­сивы (струк­туры) и изредка — под оди­ноч­ные перемен­ные (типа int *x = new int, что вооб­ще маразм, но некото­рые так дела­ют). 

К счастью, отли­чить два этих спо­соба очень прос­то — ни у мас­сивов, ни у струк­тур, ни у оди­ноч­ных перемен­ных нет ука­зате­ля this!

Слож­нее иден­тифици­ровать delete. Каких‑либо харак­терных приз­наков эта фун­кция не име­ет. Да, она при­нима­ет единс­твен­ный аргу­мент — ука­затель на осво­бож­даемый реги­он памяти, при­чем в подав­ляющем боль­шинс­тве слу­чаев это ука­затель this. Но помимо нее, this при­нима­ют десят­ки, если не сот­ни дру­гих фун­кций! 

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

mov rcx, [rsp+58h+block] ; block

call operator delete(void *,unsigned __int64)

В дан­ном слу­чае IDA без замеша­тель­ств рас­позна­ла delete.

К тому же delete ничего не воз­вра­щает, но мало ли фун­кций пос­тупа­ют точ­но так же? Единс­твен­ная зацеп­ка — вызов delete сле­дует за вызовом дес­трук­тора (если он есть), но, пос­коль­ку конс­трук­тор как раз и иден­тифици­рует­ся как фун­кция, пред­шес­тву­ющая delete, обра­зует­ся зам­кну­тый круг!

Ни­чего не оста­ется, кро­ме как ана­лизи­ровать содер­жимое фун­кции: delete рано или поз­дно вызыва­ет HeapFree (хотя тут воз­можны и вари­анты: так, Borland/Embarcadero содер­жит биб­лиоте­ки, работа­ющие с кучей на низ­ком уров­не и осво­бож­дающие память вызовом VirtualFree). 

К счастью, IDA Pro в боль­шинс­тве слу­чаев опоз­нает delete и самос­тоятель­но нап­рягать­ся не при­ходит­ся.

А что про­изой­дет, если IDA не рас­позна­ет delete? Код будет выг­лядеть при­мер­но так:

mov rcx, [rsp+58h+block] ; block

call XXX

cmp [rsp+58h+block], 0

jnz short loc_1400010B0

Нег­лубокий ана­лиз показы­вает: в пер­вой строч­ке в регистр RCX, оче­вид­но для переда­чи в качес­тве парамет­ра, помеща­ется блок памяти. Похоже, это ука­затель на сущ­ность. А пос­ле вызова XXX выпол­няет­ся срав­нение это­го бло­ка памяти с нулем и, если блок не обну­лен, про­исхо­дит переход по адре­су. 

Таким нес­ложным обра­зом мы можем лег­ко иден­тифици­ровать delete, даже если IDA его не опре­деля­ет.

Фундаментальные основы хакерства. Распределение динамической памяти и указатели

0 коммент.:

Отправка комментария