Фундаментальные основы хакерства. Боремся с дизассемблерами и затрудняем реверс программ

  • Партнер

  • Содержание статьи

    • Самомодифицирующийся код в современных операционных системах
    • Архитектура памяти Windows
    • Использование функции WriteProcessMemory
    • Выполнение кода в стеке
    • «Подводные камни» перемещаемого кода
    • Самомодифицирующийся код как средство защиты приложений
    • Зашифрованный код — следующий уровень защиты приложений
    • Заключение

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

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

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

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

    Про­дол­жаем дер­жать обо­рону нашего при­ложе­ния от атак злоб­ных хакеров — от их попыток «за прос­то так» вос­поль­зовать­ся пло­дами нашего тру­да, от их подоз­ритель­ного инте­реса к нашим прог­раммам и скры­ваемым в них сек­ретам. Для это­го мы про­дол­жим соз­давать изощ­ренные сис­темы защиты, на сей раз — от дизас­сем­бли­рова­ния.

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

     

    Самомодифицирующийся код в современных операционных системах

    В эпо­ху рас­цве­та MS-DOS прог­раммис­ты широко исполь­зовали самомо­дифи­циру­ющий­ся код, без которо­го не обхо­дилась прак­тичес­ки ни одна мало‑маль­ски серь­езная защита. Да и не толь­ко защита — он встре­чал­ся в ком­пилято­рах, ком­пилиру­ющих код непос­редс­твен­но в память, в рас­паков­щиках исполня­емых фай­лов, в полимор­фных генера­торах и так далее.

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

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

    Во‑пер­вых, kernel32.dll экспор­тиру­ет фун­кцию WriteProcessMemory, пред­назна­чен­ную, как и сле­дует из ее наз­вания, для модифи­кации памяти про­цес­са. Во‑вто­рых, прак­тичес­ки все опе­раци­онные сис­темы, вклю­чая Windows и Linux, раз­реша­ют выпол­нение и модифи­кацию кода, раз­мещен­ного в сте­ке. Меж­ду тем сов­ремен­ные вер­сии ука­зан­ных опе­раци­онных сис­тем нак­ладыва­ют на стек огра­ниче­ния, мы под­робно погово­рим об этом чуть поз­днее.

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

     

    Архитектура памяти Windows

    Соз­дание самомо­дифи­циру­юще­гося кода тре­бует зна­ния некото­рых тон­костей архи­тек­туры Windows, не очень‑то хорошо осве­щен­ных в докумен­тации. Точ­нее, сов­сем не осве­щен­ных, но от это­го отнюдь не при­обре­тающих ста­тус «недоку­мен­тирован­ных осо­бен­ностей», пос­коль­ку, во‑пер­вых, они оди­нако­во реали­зова­ны на всех Windows-плат­формах, а во‑вто­рых, их активно исполь­зует ком­пилятор Visual C++ от Microsoft. Отсю­да сле­дует, что никаких изме­нений даже в отда­лен­ном будущем ком­пания не пла­ниру­ет; в про­тив­ном слу­чае код, сге­нери­рован­ный этим ком­пилято­ром, отка­жет в работе, а на это Microsoft не пой­дет (вер­нее, не дол­жна пой­ти, если верить здра­вому смыс­лу).

    В режиме обратной сов­мести­мос­ти для адре­сации четырех гигабайт вир­туаль­ной памяти, выделен­ной в рас­поряже­ние про­цес­са, Windows исполь­зует два селек­тора, один из которых заг­ружа­ется в сег­мен­тный регистр CS, а дру­гой — в регис­тры DS, ES и SS. Оба селек­тора ссы­лают­ся на один и тот же базовый адрес памяти, рав­ный нулю, и име­ют иден­тичные лимиты, рав­ные четырем гигабай­там. Помимо перечис­ленных сег­мен­тных регис­тров, Windows еще исполь­зует регистр FS, в который заг­ружа­ет селек­тор сег­мента, содер­жащего информа­цион­ный блок потока — TIB.

    Фак­тичес­ки сущес­тву­ет все­го один сег­мент, вме­щающий в себя и код, и дан­ные, и стек про­цес­са. Бла­года­ря это­му управле­ние коду, рас­положен­ному в сте­ке, переда­ется близ­ким (near) вызовом или перехо­дом, и для дос­тупа к содер­жимому сте­ка исполь­зование пре­фик­са SS совер­шенно необя­затель­но. Нес­мотря на то что зна­чение регис­тра CS не рав­но зна­чению регис­тров DS, ES и SS, коман­ды

    MOV dest,CS:[src]
    MOV dest,DS:[src]
    MOV dest,SS:[src]

    в дей­стви­тель­нос­ти обра­щают­ся к одной и той же ячей­ке памяти.

    Это точ­ный про­образ реали­зован­ной в про­цес­сорах на архи­тек­туре x86-64 RIP-отно­ситель­ной адре­сации памяти, в которой не исполь­зуют­ся сег­менты.

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

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

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

     

    Использование функции WriteProcessMemory

    Ес­ли тре­бует­ся изме­нить некото­рое количес­тво бай­тов сво­его (или чужого) про­цес­са, самый прос­той спо­соб сде­лать это — выз­вать фун­кцию WriteProcessMemory. Она поз­воля­ет модифи­циро­вать сущес­тву­ющие стра­ницы памяти, чей флаг супер­визора не взве­ден, то есть все стра­ницы, дос­тупные из коль­ца 3, в котором выпол­няют­ся прик­ладные при­ложе­ния. Совер­шенно бес­полез­но с помощью WriteProcessMemory пытать­ся изме­нить кри­тичес­кие струк­туры дан­ных опе­раци­онной сис­темы (нап­ример, page directory или page table) — они дос­тупны лишь из нулево­го коль­ца. Поэто­му ука­зан­ная фун­кция не пред­став­ляет никакой угро­зы для безопас­ности сис­темы и успешно вызыва­ется незави­симо от уров­ня при­виле­гий поль­зовате­ля.

    Про­цесс, в память которо­го про­исхо­дит запись, дол­жен быть пред­варитель­но открыт фун­кци­ей OpenProcess с атри­бута­ми дос­тупа PROCESS_VM_OPERATION и PROCESS_VM_WRITE. Час­то прог­раммис­ты, ленивые от при­роды, идут более корот­ким путем, уста­нав­ливая все атри­буты — PROCESS_ALL_ACCESS. И это впол­не закон­но, хотя спра­вед­ливо счи­тает­ся дур­ным сти­лем прог­рамми­рова­ния.

    Да­лее при­веден прос­той при­мер self-modifying_code, иллюс­три­рующий исполь­зование фун­кции WriteProcessMemory для соз­дания самомо­дифи­циру­юще­гося кода:

    #include <iostream>#include <Windows.h>using namespace std;int WriteMe(void* addr, int wb){ HANDLE h = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, true, GetCurrentProcessId()); return WriteProcessMemory(h, addr, &wb, 1, NULL);}int main(int argc, char* argv[]){ _asm { push 0x74 ; JMP -> JZ push offset Here call WriteMe add esp, 8 Here: JMP short here } cout << "#JMP SHORT $-2 was changed to JZ $-2n"; return 0;}

    Фун­кция WriteProcessMemory в рас­смат­рива­емой прог­рамме заменя­ет инс­трук­цию бес­конеч­ного цик­ла JMP short $-2 условным перехо­дом JZ $-2, который про­дол­жает нор­маль­ное выпол­нение прог­раммы. Неп­лохой спо­соб зат­руднить взлом­щику изу­чение при­ложе­ния, не прав­да ли? Осо­бен­но если вызов WriteMe не рас­положен воз­ле изме­няемо­го кода, а помещен в отдель­ный поток. Будет еще луч­ше, если модифи­циру­емый код впол­не естес­тве­нен сам по себе и внеш­не не вызыва­ет никаких подоз­рений. В этом слу­чае хакер может дол­го блуж­дать в той вет­ке кода, которая при выпол­нении прог­раммы вооб­ще не получа­ет управле­ния.

    Для ком­пиляции это­го при­мера уста­нови 32-бит­ный режим резуль­тиру­юще­го кода.

    Ре­зуль­тат выпол­нения при­ложе­ния self-modifying_code

    Ес­ли из ассем­блер­ной встав­ки убрать вызов фун­кции WriteMe, которая переза­писы­вает инс­трук­цию JMP на JZ, прог­рамма выпадет в бес­конеч­ный цикл.

    Вы­зов фун­кции заком­менти­рован

    Прог­рамма зацик­лена

    Об устройстве Windows: исторический нюанс

    Пос­коль­ку Windows для эко­номии опе­ратив­ной памяти раз­деля­ет код меж­ду про­цес­сами, воз­ника­ет воп­рос: а что про­изой­дет, если запус­тить вто­рую копию самомо­дифи­циру­ющей­ся прог­раммы? Соз­даст ли опе­раци­онная сис­тема новые стра­ницы или отош­лет при­ложе­ние к уже модифи­циру­емо­му коду? В докумен­тации на Windows NT ска­зано, что она под­держи­вает копиро­вание при записи (copy on write), то есть авто­мати­чес­ки дуб­лиру­ет стра­ницы кода при попыт­ке их модифи­циро­вать. Нап­ротив, Windows 9x не под­держи­вает такую воз­можность. Озна­чает ли это, что все копии самомо­дифи­циру­юще­гося при­ложе­ния будут вынуж­дены работать с одни­ми и теми же стра­ница­ми кода (а это неиз­бежно при­ведет к кон­флик­там и сбо­ям)?

    Нет, и вот почему: нес­мотря на то что копиро­вание при записи в Windows 9x не реали­зова­но, эту заботу берет на себя сама фун­кция WriteProcessMemory, соз­давая копии всех модифи­циру­емых стра­ниц, рас­пре­делен­ных меж­ду про­цес­сами. Бла­года­ря это­му самомо­дифи­циру­ющий­ся код оди­нако­во хорошо работа­ет как под Windows 9x, так и под Windows NT. Одна­ко сле­дует учи­тывать, что все копии при­ложе­ния, модифи­циру­емые любым иным путем (нап­ример, коман­дой mov нулево­го коль­ца), если их запус­тить под Windows 9x, будут раз­делять одни и те же стра­ницы кода со все­ми вытека­ющи­ми отсю­да пос­ледс­тви­ями.

    Те­перь об огра­ниче­ниях. Во‑пер­вых, исполь­зовать WriteProcessMemory разум­но толь­ко в ком­пилиру­ющих в память ком­пилято­рах или рас­паков­щиках исполня­емых фай­лов, а в защитах — нес­коль­ко наив­но. Мало‑маль­ски опыт­ный взлом­щик быс­тро обна­ружит под­вох, уви­дев эту фун­кцию в таб­лице импорта. Затем он уста­новит точ­ку оста­нова на вызов WriteProcessMemory и будет кон­тро­лиро­вать каж­дую опе­рацию записи в память. А это никак не вхо­дит в пла­ны раз­работ­чика защиты!

    Источник: xakep.ru

    Ответить

    Ваш адрес email не будет опубликован. Обязательные поля помечены *