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

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

Пе­ред тобой уже во вто­рой раз обновлен­ная вер­сия цик­ла «Фун­дамен­таль­ные осно­вы хакерс­тва». В 2018 году текст Кри­са Кас­пер­ски был переде­лан для соот­ветс­твия новым вер­сиям Windows и Visual Studio, а теперь обновлен с уче­том отладки прог­рамм для 64-раз­рядной архи­тек­туры.

Чи­тай так­же улуч­шенные вер­сии прош­лых ста­тей цик­ла:

  1. Учим­ся ана­лизи­ровать прог­раммы для x86-64 с нуля
  2. Ис­поль­зуем отладчик для ана­лиза 64-раз­рядных прог­рамм в Windows

Все новые вер­сии ста­тей дос­тупны без плат­ной под­писки.

Цикл «Фун­дамен­таль­ные осно­вы хакерс­тва» со все­ми обновле­ниями опуб­ликован в виде кни­ги, ку­пить ее по выгод­ной цене ты можешь на сай­те изда­тель­ства «Солон‑пресс».

Ко­неч­но, у нас есть дизас­сем­блер, с помощью которо­го мы наш­ли эти адре­са (под­робнее об этом в статье «Учим­ся ана­лизи­ровать прог­раммы для x86-64 с нуля»). Это уда­лось во мно­гом бла­года­ря тому, что пре­пари­руемая нами прог­рамма очень малень­кая и нам не сос­тавило тру­да разоб­рать­ся в ее дизас­сем­бли­рован­ном лис­тинге. А если бы иссле­дуемая прог­рамма весила сот­ни мегабайт?

Так­же мы выяс­нили, что PE-файл может быть заг­ружен по адре­су, отлично­му от того, для которо­го он был соз­дан (это свой­ство называ­ется переме­щаемостью), при этом сис­тема авто­мати­чес­ки кор­ректи­рует все ссыл­ки на абсо­лют­ные адре­са, заменяя их новыми зна­чени­ями. В резуль­тате образ фай­ла в памяти не будет соот­ветс­тво­вать тому, что записа­но на дис­ке. И это про­исхо­дит пос­ле каж­дой перезаг­рузки сис­темы, а порой даже переза­пус­ка при­ложе­ния. Вся­кий раз PE-файл помеща­ется по новому адре­су.

Вдо­бавок к это­му если рань­ше (до «Вис­ты») сис­темный заг­рузчик мог переме­щать толь­ко DLL (в то же вре­мя, если ему не уда­валось раз­местить в памяти по задан­ным адре­сам .exe, Windows выдава­ла ошиб­ку заг­рузки модуля), то теперь исполня­емые фай­лы тоже под­верже­ны переме­щению.

Меж­ду тем ошиб­ка заг­рузки модуля про­исхо­дила доволь­но ред­ко, потому что, как мы прек­расно зна­ем, для каж­дого про­цес­са Windows выделя­ет незави­симое вир­туаль­ное адресное прос­транс­тво. Во вре­мена 32-бит­ной Windows это было 2 Гбайт ядер­ного прос­транс­тва и 2 Гбайт поль­зователь­ско­го. То есть по фак­ту для про­цес­са выделя­лось толь­ко 2 Гбайт, а 2 Гбайт ядер­ного прос­транс­тва были общи­ми для всех про­цес­сов, к которым код из поль­зователь­ско­го режима дос­тупа не имел.

При вклю­чении режима PAE поль­зователь­ско­му прос­транс­тву дос­тавалось 3 Гбайт и, соот­ветс­твен­но, 1 Гбайт — ядер­ному. PAE в про­цес­сорах x86 стал нужен для работы сис­темы DEP, пре­пятс­тву­ющей выпол­нению кода в сек­ции дан­ных. DEP авто­мати­чес­ки вклю­чена во всех более поз­дних про­цес­сорах. Если поль­зователь­ское прос­транс­тво обо­соб­лено для кон­крет­ного про­цес­са, то прос­транс­тво ядра общее для всех при­виле­гиро­ван­ных механиз­мов, выпол­няющих­ся в нулевом коль­це.

Для x86-64 кар­тина в целом ана­логич­на. Адресное прос­транс­тво замет­но уве­личи­лось, теоре­тичес­ки до 16 Эбайт. Но так как сов­ремен­ные про­цес­соры фак­тичес­ки исполь­зуют толь­ко 48 бит для адре­сации прос­транс­тва, реаль­но исполь­зует­ся лишь малая часть: 8 Тбайт для поль­зователь­ско­го режима и 248 Тбайт для ядер­ного. Конеч­но, пока эти раз­меры кажут­ся заоб­лачны­ми — при­мер­но как 4 Гбайт в кон­це 1980-х!

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

Сна­чала вос­поль­зуем­ся ути­литой dumpbin из штат­ной пос­тавки Visual Studio, на этот раз с ее помощью най­дем базовый адрес модуля — тот, с которым работа­ют Hiew (или дру­гой шес­тнад­цатерич­ный редак­тор) и дизас­сем­блер:

dumpbin /headers passcompare1.exe
OPTIONAL HEADER VALUES
...
140000000 image base (0000000140000000 to 0000000140007FFF)

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

0:000> lmf m passcompare1
start end module name
00007ff7`159b0000 00007ff7`159b8000 passCompare1 passCompare1.exe

Да­лее нам нуж­но най­ти адрес инс­трук­ции, которую надо изме­нить. Для это­го пер­вым делом ищем рас­положе­ние эта­лон­ного пароля (он находит­ся в сек­ции .rdata), поэто­му вос­поль­зуем­ся коман­дой !dh passCompare1, которая выведет све­дения о сек­циях. Сло­жим адрес заг­рузки модуля и вир­туаль­ный адрес сек­ции .rdata.

Та­ким обра­зом, в моем слу­чае сек­ция .rdata начина­ется с адре­са 0x7FF7159B2000. Нем­ного прок­рутив вывод отладчи­ка вниз, я вижу, что пароль рас­полага­ется по адре­су 0x7ff7159b2280. Теперь нам нужен адрес рас­положе­ния инс­трук­ции в памяти. Не нап­рягая мозг, лег­ким дви­жени­ем руки пос­тавим бряк на пароль: ba r4 7ff7159b2280. Про­дол­жим отладку и вве­дем любой пароль, пос­ле всплы­тия отладчи­ка по коман­де gu сде­лаем выход из текущей фун­кции. Мы попада­ем на срав­нива­ющую инс­трук­цию TEST EAX, EAX, которую нам надо заломить, а сле­ва в пер­вом стол­бце видим ее адрес: 0x7ff7159b10c3. Если поп­робовать най­ти его в фай­ле, то Hiew ска­жет, что такой адрес отсутс­тву­ет.

Но теперь, ког­да есть все необ­ходимые зна­чения, нет­рудно пос­читать, что адрес 0x7ff7159b10c3 будет соот­ветс­тво­вать адре­су

адрес инструкции в файле на диске == адрес инструкции в памяти – (адрес загрузки модуля – базовый адрес модуля):
0x7ff7159b10c3 – (0x7ff7159b0000 0x140000000) == 0x7ff7159b10c3 7FF5D59B0000 == 1400010C3

Для про­вер­ки заг­лянем в дизас­сем­блер­ный лис­тинг и с удов­летво­рени­ем обна­ружим, что это как раз тот адрес, инс­трук­цию по которо­му мы пра­вили:

00000001400010C3: 85 C0 test eax,eax
00000001400010C5: 74 58 je 000000014000111F

Все вер­но, пос­мотри, как хорошо это сов­пада­ет с дам­пом отладчи­ка:

00007ff7`159b10c3 85c0 test eax, eax
00007ff7`159b10c5 7458 je passCompare1!main+0xaf (7ff7159b111f)

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

Перемещаемость DLL

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

Для раз­нооб­разия сле­дующие при­меры откомпи­лиру­ем для плат­формы IA-32. На самом деле 32-бит­ных при­ложе­ний раз­работа­но так мно­го, что их при­дет­ся ана­лизи­ровать еще мно­го лет.

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

Единс­твен­ная проб­лема — как отли­чить дей­стви­тель­ные непос­редс­твен­ные сме­щения от кон­стант, сов­пада­ющих с ними по зна­чению? Не дизас­сем­бли­ровать же, в самом деле, DLL, что­бы разоб­рать­ся, какие имен­но ячей­ки в ней необ­ходимо «под­кру­тить»? Вер­но, куда про­ще перечис­лить их адре­са в спе­циаль­ной таб­лице, рас­положен­ной непос­редс­твен­но в заг­ружа­емом фай­ле и носящей гор­дое имя «таб­лицы переме­щаемых эле­мен­тов». За ее фор­мирова­ние отве­чает ком­понов­щик.

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

fixupdemo.c:
__declspec(dllexport) void meme(int x)
{
static int a=0x666;
a=x;
}

От­компи­лиру­ем коман­дой cl fixupdemo.c /LD и тут же дизас­сем­бли­руем его:

DUMPBIN /DISASM fixupdemo.dll > fixupdemo-disasm.txt
DUMPBIN /SECTION:.data /RAWDATA fixupdemo.dll > fixupdemo-data.txt
10001000: 55 push ebp
10001001: 8B EC mov ebp,esp
10001003: 8B 45 08 mov eax,dword ptr [ebp+8]
10001006: A3 30 60 00 10 mov dword ptr ds:[10006030h],eax
1000100B: 5D pop ebp
1000100C: C3 ret
RAW DATA #3
10006000: 00 00 00 00 00 00 00 00 00 00 00 00 63 28 00 10 ............c(..
10006010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
10006020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
10006030: 66 06 00 00 E3 11 00 10 FF FF FF FF 00 00 00 00 f...a...yyyy....

Су­дя по коду, запись содер­жимого EAX всег­да про­исхо­дит в ячей­ку 0x10006030. Но не торопись с вывода­ми! Давай пос­мотрим содер­жимое таб­лицы переме­щаемых эле­мен­тов:

DUMPBIN /RELOCATIONS fixupdemo.dll > fixupdemo-relocations.txt
BASE RELOCATIONS #4
1000 RVA, 164 SizeOfBlock
7 HIGHLOW 10006030
1C HIGHLOW 10005004
23 HIGHLOW 10008A50
32 HIGHLOW 10008A50
3A HIGHLOW 10008A51

Таб­лица переме­щаемых эле­мен­тов‑то не пус­та! И пер­вая же ее запись ука­зыва­ет на ячей­ку 0x100001007, получен­ную алгебра­ичес­ким сло­жени­ем сме­щения 0x7 с RVA-адре­сом 0x1000 и базовым адре­сом заг­рузки 0x10000000 (получи его с помощью DUMPBIN самос­тоятель­но). Смот­рим — ячей­ка 0x100001007 при­над­лежит инс­трук­ции MOV [0x10006030],EAX и ука­зыва­ет на самый стар­ший байт непос­редс­твен­ного сме­щения. Вот это самое сме­щение и кор­ректи­рует заг­рузчик в ходе под­клю­чения динами­чес­кой биб­лиоте­ки (разуме­ется, если в этом есть необ­ходимость).

Же­лаешь про­верить? Пожалуй­ста, соз­дадим две копии одной DLL (нап­ример, с помощью коман­ды copy fixupdemo.dll fixupdemo2.dll) и заг­рузим их пооче­ред­но сле­дующей прог­раммой:

fixupload.c:
#include <windows.h>
main()
{
void (*demo) (int a);
HMODULE h;
if ((h=LoadLibrary("fixupdemo.dll")) &&
(h=LoadLibrary("fixupdemo2.dll")) &&
(demo=(void (*)(int a))GetProcAddress(h,"meme")))
demo(0x777);
}

Сра­зу же из коман­дной стро­ки откомпи­лиру­ем: cl fixupload.c.

Пос­коль­ку по одно­му и тому же адре­су две раз­личные DLL не заг­рузишь (отку­да же сис­теме знать, что это одна и та же DLL!), заг­рузчи­ку при­ходит­ся при­бегать к ее переме­щению. Заг­рузим откомпи­лиро­ван­ную прог­рамму в отладчик и уста­новим точ­ку оста­нова на фун­кцию LoadLibraryA коман­дой bp KernelBase!LoadLibraryA.

К сло­ву, коман­да bp поз­воля­ет уста­новить точ­ку оста­нова по адре­су, опре­делен­ному фун­кци­ей. Уста­нов­ка точ­ки оста­нова на пер­вую коман­ду необ­ходима, что­бы про­пус­тить startup-код и попасть в тело фун­кции main. Как лег­ко убе­дить­ся, исполне­ние прог­раммы начина­ется отнюдь не с main, а со слу­жеб­ного кода, в котором очень лег­ко уто­нуть. Но отку­да взя­лась загадоч­ная бук­ва А на кон­це име­ни фун­кции? Ее про­исхожде­ние тес­но свя­зано с вве­дени­ем в Windows под­дер­жки уни­кода.

При­мени­тель­но к LoadLibrary — теперь имя биб­лиоте­ки может быть написа­но на любом язы­ке. Зву­чит заман­чиво, но не ухуд­шает ли это про­изво­дитель­ность? Разуме­ется, ухуд­шает, еще как! В боль­шинс­тве слу­чаев впол­не дос­таточ­но ста­рой доб­рой кодиров­ки ASCII. Так какой же смысл бро­сать дра­гоцен­ные так­ты про­цес­сора на ветер? Ради про­изво­дитель­нос­ти было решено пос­тупить­ся раз­мером, соз­дав отдель­ные вари­анты фун­кций для работы с уни­кодом и ASCII-сим­волами. Пер­вые получи­ли суф­фикс W (от Wide — широкий), а вто­рые — А (от ASCII). Эта тон­кость скры­та от прик­ладных прог­раммис­тов. Какую имен­но фун­кцию вызывать — W или А, реша­ет ком­пилятор, но при работе с отладчи­ком необ­ходимо ука­зывать точ­ное имя фун­кции — самос­тоятель­но опре­делить суф­фикс он не в сос­тоянии. Камень прет­кно­вения в том, что некото­рые фун­кции, нап­ример ShowWindows, вооб­ще не име­ют суф­фиксов — ни А, ни W — и их биб­лиотеч­ное имя сов­пада­ет с канони­чес­ким. Как же быть?

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

>DUMPBIN /IMPORTS fixupload.exe > fixupload-imports.exe
...
175 GetVersionExA
1C2 LoadLibraryA
CA GetCommandLineA
174 GetVersion
7D ExitProcess
29E TerminateProcess
F7 GetCurrentProcess
...
Developer Command Prompt for VS2017 с введенными командами
Developer Command Prompt for VS2017 с вве­ден­ными коман­дами

Из при­веден­ного выше фраг­мента вид­но, что LoadLibrary все‑таки име­ет суф­фикс А, а вот фун­кции ExitProcess и TerminateProcess не име­ют суф­фиксов, пос­коль­ку вооб­ще не работа­ют со стро­ками.

Но вер­немся к нашим баранам, от которых нам приш­лось так далеко отой­ти. Итак, мы пос­тавили бряк на LoadLibraryA и про­дол­жили выпол­нение прог­раммы, она момен­таль­но сно­ва оста­нав­лива­ется на точ­ке оста­нова.

Содержимое отладчика в момент останова на функции LoadLibraryA
Со­дер­жимое отладчи­ка в момент оста­нова на фун­кции LoadLibraryA

На­жима­ем сочета­ние Shift-F11 для выхода из LoadLibraryA (ана­лизи­ровать ее, в самом деле, ни к чему) и ока­зыва­емся в лег­ко узна­ваемом теле фун­кции main:

0040100b ff1504504000 call dword ptr [fixupload+0x5004 (00405004)]
00401011 8945f8 mov dword ptr [ebp-8], eax ss:002b:0019ff38=00401055
00401014 837df800 cmp dword ptr [ebp-8], 0
00401018 7437 je fixupload+0x1051 (00401051)
0040101a 6840604000 push offset fixupload+0x6040 (00406040)
0040101f ff1504504000 call dword ptr [fixupload+0x5004 (00405004)]
00401046 6877070000 push 777h
0040104b ff55fc call dword ptr [ebp-4]

Толь­ко пос­ле воз­вра­щения из LoadLibraryA отладчик не под­ста­вил в мес­то вызова фун­кции ее сим­воль­ное имя стро­кой выше выделен­ной:

call dword ptr [fixupload+0x5004 (00405004)]

За­пом­ним ее как вызов LoadLibraryA.

Об­рати вни­мание на содер­жимое регис­тра EAX (для это­го слу­жит коман­да r <имя регистра>) — фун­кция воз­вра­тила в нем адрес заг­рузки (на моем компь­юте­ре рав­ный 0x10000000). Про­дол­жая трас­сиров­ку (кноп­ка F10), дож­дись выпол­нения вто­рого вызова LoadLibraryA.

Не прав­да ли, на этот раз адрес заг­рузки изме­нил­ся? На моем компь­юте­ре он равен 0x001d0000.

Приб­лижа­емся к вызову фун­кции demo. В отладчи­ке это выг­лядит так:

push 777h
call dword ptr [ebp-4]

Вто­рая инс­трук­ция ни о чем не говорит, но вот аргу­мент 0x777 в пер­вой инс­трук­ции опре­делен­но что‑то нам напоми­нает. Смот­ри исходный текст fixupload.c. Не забудь перес­тавить палец с кла­виши F10 на кла­вишу F8, что­бы вой­ти внутрь фун­кции.

001d1000 55 push ebp
001d1001 8bec mov ebp, esp
001d1003 8b4508 mov eax, dword ptr [ebp+8]
001d1006 a330601d00 mov dword ptr [fixupdemo2!meme+0x5030 (001d6030)], eax
001d100b 5d pop ebp
001d100c c3 ret

Вот оно! Сис­темный заг­рузчик скор­ректи­ровал адрес ячей­ки сог­ласно базово­му адре­су заг­рузки самой DLL. Это, конеч­но, хорошо, да вот проб­лема — в ори­гиналь­ной DLL нет ни такой ячей­ки, ни даже пос­ледова­тель­нос­ти A3 30 60 1D 00, в чем лег­ко убе­дить­ся, про­изве­дя кон­текс­тный поиск. Допус­тим, воз­намери­лись бы мы затереть эту коман­ду NOP’ами. Как най­ти это мес­то в ори­гиналь­ной DLL?

Об­ратим свой взор выше, на коман­ды, заведо­мо не содер­жащие переме­щаемых эле­мен­тов:

001d1000 55 push ebp
001d1001 8bec mov ebp, esp
001d1003 8b4508 mov eax, dword ptr [ebp+8]

От­чего бы не поис­кать пос­ледова­тель­ность 55 8B EC 8B 45 08 A3? В дан­ном слу­чае это сра­бота­ет, смот­ри, как хорошо сов­пада­ет:

10001000: 55 push ebp
10001001: 8B EC mov ebp,esp
10001003: 8B 45 08 mov eax,dword ptr [ebp+8]
10001006: A3 30 60 00 10 mov dword ptr ds:[10006030h],eax

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

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

модифицированный загрузчиком адрес – (базовый адрес загрузки рекомендуемый адрес загрузки): 0x1d6030 – (0x001d0000 0x10000000) == 0x1d6030 FFFFFFFFF01D0000 == 0x10006030
Найденная в Hiew инструкция
Най­ден­ная в Hiew инс­трук­ция

Учи­тывая обратный порядок сле­дова­ния бай­тов, получа­ем, что инс­трук­ция mov dword ptr ds:[10006030h],eax в машин­ном коде дол­жна выг­лядеть так: A3 30 60 00 10. Ищем ее в Hiew, и чудо — она есть!

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

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

    Подписаться

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