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

       

Способ 0. Бряк на оригинальный пароль.


Используя поставляемую вместе с "Айсом" утилиту "wldr" загрузим ломаемый нами файл, указав его имя в командной строке, например, так:

>wldr simple.exe

Да, я знаю, что wldr – 16-разрядный загрузчик, и NuMega рекомендует использовать его 32-разрядную версию loadrer32, специально разработанную для Win 9x\NT. Это так, но loader32 частенько глючит (в частности не всегда останавливается на первой строчке запускаемой программы), а wldr успешно работает и 32-разрядными приложениями, единственный присущий ему недостаток – отсутствие поддержки длинных имен файлов.

Если отладчик настроен корректно, на экране появится черное текстовое окно, обычно вызывающее большое удивление у начинающих – это в нашу-то это эпоху визуальщины серый текст и командный язык a la command.com.!

А почему бы и нет? Набрать на клавиатуре нужную команду куда быстрее, чем отыскать ее в длинной веренице вложенных меню, мучительно вспоминая где же вы ее в последний раз видели. К тому же язык – это естественное средство выражения мыслей, а меню – оно годится разве что для выбора блюд в ресторане. Вот хороший пример – попробуйте с помощью проводника Windows вывести на печать список файлов такой-то директории. Не получается? А в MS-DOS это было так просто dir >PRN и никаких лаптей!

Если в окне кода видны одни лишь инструкции "INVALID" (а оно так и будет) не пугайтесь – просто Windows еще не успела спроецировать исполняемый файл в память и выделилавыделить ему страницы. Стоит нажать <F10> (аналог команды "P" – трассировка без заходов в функцию) или <F8> (аналог команды "T" – трассировка с заходами в функции) как все сразу же станет на свои места.

001B:00401277  INVALID

001B:00401279  INVALID

001B:0040127B  INVALID

001B:0040127D  INVALID

:P

001B:00401285  PUSH    EBX

001B:00401286  PUSH    ESI

001B:00401287  PUSH    EDI

001B:00401288  MOV     [EBP-18],ESP

001B:0040128B  CALL    [KERNEL32!GetVersion]




001B:00401291  XOR     EDX,EDX

001B:00401293  MOV     DL,AH

001B:00401295  MOV     [0040692C],EDX

Обратите внимание: в отличие от дизассемблера DUMPBIN, Айс распознает имена системных функций, чем существенно упрощает анализ. Впрочем, анализировать всю программу целиком,

нет никакой нужды. Давайте попробуем наскоро найти защитный механизм, и, не вникая в подробности его функционирования, напрочь отрубить защиту. Легко сказать, но сделать еще проще! Вспомним: по какому адресу расположен в памяти оригинальный пароль. Э… что-то плохо у нас с этим получается – то ли память битая, то ли медведь на лапоть наступил, но точный адрес никак не хочет вспоминаться. Не хочет – не надо. Найдем-ка мы его самостоятельно!

В этом нам поможет команда "map32" выдающая карту памяти выбранного модуля (наш модуль называется "simple" – по имени исполняемого файла за вычетом расширения).

:map32 simple

Owner     Obj Name  Obj#  Address        Size      Type

simple    .text     0001  001B:00401000  00003F66  CODE  RO

simple    .rdata    0002  0023:00405000  0000081E  IDATA RO

simple    .data     0003  0023:00406000  00001E44  IDATA RW

           ^^^^           ^^^^^^^^^^^^^

Вот он, адрес начала секции  ".data". То, что пароль находится в секции ".data", надеюсь, читатель все еще помнит. Даем команду "d 23:406000" (возможно предварительно придется создать окно командой "wc" – если окна данных нет) и, нажав, <ALT-D> для перехода в это окно, прокрутим его содержимое <стрелкой вниз> или кирпичом на <Page Down>. Впрочем, кирпич излишен, – долго искать не придется:

0023:00406040 6D 79 47 4F 4F 44 70 61-73 73 77 6F 72 64 0A 00  myGOODpassword..

0023:00406050 57 72 6F 6E 67 20 70 61-73 73 77 6F 72 64 0A 00  Wrong password..

0023:00406060 50 61 73 73 77 6F 72 64-20 4F 4B 0A 00 00 00 00  Password OK.....

0023:00406070 47 6E 40 00 00 00 00 00-40 6E 40 00 01 01 00 00  Gn@.....@n@.....



0023: 00406080 00 00 00 00 00 00 00 00-00 10 00 00 00 00 00 00  ................

0023:00406090 00 00 00 00 00 00 00 00-00 00 00 00 02 00 00 00  ................

0023:004060A0 01 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

0023:004060B0 00 00 00 00 00 00 00 00-00 00 00 00 02 00 00 00  ................

Есть контакт! Задумаемся еще раз (второй раз за этот день) чтобы проверить корректность введенного пользователем пароля защита, очевидно должна сравнить его с оригинальным. А раз так – установив точку останова на чтение памяти по адресу 0x406040, мы поймаем "за хвост" сравнивающий механизм. Сказано – сделано.

:bmpm 406040

Теперь нажимаем <CTRL-D> для выхода из отладчика (или отдаем команду "x") и вводим любой пришедший на ум пароль, например, "KPNC++". Отладчик "всплывает" незамедлительно:

001B:004010B0  MOV     EAX,[EDX]

001B:004010B2  CMP     AL,[ECX]

001B:004010B4  JNZ     004010E4                                      (JUMP ^)

001B:004010B6  OR      AL,AL

001B:004010B8  JZ      004010E0

001B:004010BA  CMP     AH,[ECX+01]

001B:004010BD  JNZ     004010E4

001B:004010BF  OR      AH,AH

Break due to BPMB #0023:00406040 RW DR3  (ET=752.27 milliseconds)

  MSR LastBranchFromIp=0040104E

    MSR LastBranchToIp=004010A0

В силу архитектурных особенностей процессоров Intel, бряк срабатывает после инструкции, выполнившей "поползновение", т.е. CS:EIP указывают на следующую выполняемую команду. В нашем случае – JNZ 004010E4, а к памяти, стало быть, обратилась инструкция CMP AL, [ECX]. А что находится в AL? Поднимаем взгляд еще строкой выше – "MOV EAX,[EDX]". Можно предположить, что EСX содержит указатель на строку оригинального пароля (поскольку он вызвал всплытие отладчика), а EDX в таком случае – указатель на введенный пользователем пароль. Проверим наше предположение.

:d edx

0023:00406040 6D 79 47 4F 4F 44 70 61-73 73 77 6F 72 64 0A 00  myGOODpassword..



:d edx

0023: 0012FF18 4B 50 4E 43 2B 2B 0A 00-00 00 00 00 00 00 00 00  KPNC++..........

И правда – догадка оказалась верна. Теперь вопрос – а как это заломить? Вот, скажем, JNZ можно поменять на JZ или, еще оригинальнее, заменить EDX на ECX – тогда оригинальный пароль будет сравниваться сам с собой! Погодите, погодите… не стоит так спешить! А что если мы находится не в теле защиты, а в библиотечной функции (действительно, мы находится в теле strcmp), – ее изменение приведет к тому, что программа любые строки будет воспринимать как идентичные. Любые – а не только строки пароля. Это не повредит нашему примеру, где strcmp вызывалась лишь однажды, но завалит нормальное полнофункциональное приложение. Что же делать?

Выйти из strcmp и подкорректировать тот самый "IF",

который анализирует правильный – не правильный пароль. Для этого служит команда "P RET" (трассировать пока не встреться ret – инструкция возврата из функции).

:P RET

001B:0040104E  CALL    004010A0

001B:00401053  ADD     ESP,08

001B:00401056  TEST    EAX,EAX

001B:00401058  JZ      00401069

001B:0040105A  PUSH    00406050

001B:0040105F  CALL    00401234

001B:00401064  ADD     ESP,04

001B:00401067  JMP     0040106B

Знакомые места! Помните, мы их посещали дизассемблером? Алгоритм действий прежний – запоминаем адрес команды "TEST" для последующей замены ее на "XOR" или записываем последовательность байт, идентифицирующую… эй, постойте, а где же наши байты – шестнадцатеричное представление команд? Коварный Айс по умолчанию их не выводит, и заставить его это делать помогает команда "CODE ON"

code on

001B:0040104E  E84D000000          CALL    004010A0

001B:00401053  83C408              ADD     ESP,08

001B:00401056  85C0                TEST    EAX,EAX

001B:00401058  740F                JZ      00401069

001B:0040105A  6850604000          PUSH    00406050

001B:0040105F  E8D0010000          CALL    00401234

001B:00401064  83C404              ADD     ESP,04



001B:00401067  EB02                JMP     0040106B

Вот, теперь совсем другое дело! Но можно ли быть уверенным, что эти байтики по этим самым адресам будут находиться в исполняемом файле? Вопрос не так глуп, каким кажется на первый взгляд. Попробуйте сломать описанным выше методом пример "crackme0x03". На первый взгляд он очень похож на simple.exe, - даже оригинальный пароль располагается по тому же самому адресу. Ставим на него бряк, дожидаемся всплытия отладчика, выходим из сравнивающей процедуры и попадаем на точно такой же код, который уже встречался нам ранее.

001B:0042104E  E87D000000          CALL    004210D0

001B:00421053  83C408              ADD     ESP,08====

001B:00421056  85C0                TEST    EAX,EAX

001B:00421058  740F                JZ      00421069

Сейчас мы запустим HIEW, перейдем по адресу 0x421053 и… эй, постой, HIEW ругается и говорит, что в файле нет такого адреса! Последний байт заканчивается на 0x407FFF. Быть может, мы находимся в теле системной функции Windows? Но нет – системные функции Windows расположены значительно выше – начиная с адреса 0x80000000.

Фокус весь в том, что PE-файл может быть загружен по адресу отличному от того, для которого он был создан (это свойство называется перемещаемостью), - при этом система автоматически корректирует все ссылки на абсолютные адреса, заменяя их новыми значениями. В результате – образ файла в памяти не будет соответствовать тому, что записано на диске. Хорошенькое начало! Как же теперь найти место, которое нужно править?

Задачу несколько облегчает тот факт, что системный загрузчик умеет перемещать только DLL, а исполняемые файлы всегда пытается загрузить по "родному" для них адресу. Если же это невозможно – загрузка прерывается с выдачей сообщения об ошибке. Выходит, мы имеем дело с DLL, загруженной исследуемой нами защитой. Хм… вроде бы не должно быть здесь никаких DLL – да и откуда бы им взяться?

Что ж, изучим листинг 2 на предмет выяснения: как же он работает.



#include <stdio.h>

#include <windows.h>

__declspec(dllexport) void Demo()

^^^^^^^^^^^^^^^^^^^^^

{

#define PASSWORD_SIZE 100

#define PASSWORD      "myGOODpassword\n"

int count=0;

char buff[PASSWORD_SIZE]="";

for(;;)

{

printf("Enter password:");

fgets(&buff[0],PASSWORD_SIZE-1,stdin);

if (strcmp(&buff[0],PASSWORD))

printf("Wrong password\n");

else break;

if (++count>2) return -1;        

}

printf("Password OK\n");

}

main()

{

HMODULE hmod;

void (*zzz)();

if ((hmod=LoadLibrary("crack0~1.exe"))

&& (zzz=(void (*)())GetProcAddress(h,"Demo")))

zzz();

}

Листинг 2 Исходный текст защиты crackme 0x3

Какой, однако, извращенный способ вызова функции! Защита экспортирует ее непосредственно из самого исполняемого файла и этот же файл загружает как DLL (да, один и тот же файл может быть одновременно и исполняемым приложением и динамической библиотекой!).

"Все равно ничего не сходится", - возразит программист средней квалификации, - "всем же известно, что Windows не настолько глупа, чтобы дважды грузить один и тот же файл, - LoadLibrary всего лишь возвратит базовый адрес модуля crackme0x03, но не станет выделять для него память". А вот как бы не так! Хитрая защита обращается к файлу по его альтернативному короткому имени, вводя системный загрузчик в глубокое заблуждение!

Система выделяет память и возвращает базовый адрес загружаемого модуля в переменной hmod. Очевидно, код и данные этого модуля смещены на расстояние hmod – base, где base – базовый адрес модуля – тот, с которым работают HIEW и дизассемблер. Базовый адрес узнать нетрудно – достаточно вызвать тот же DUMPBIN с ключом "/HEADERS" (его ответ приведен в сокращенном виде)

>dumpbin /HEADERS crack0x03

OPTIONAL HEADER VALUES

...

400000 image base

^^^^^^^^^^^^^^^^^

...



Значит, базовый адрес – 0x400000

(в байтах). А опередить адрес загрузки  можно командой "mod -u" отладчика: (ключ u разрешает выводить только прикладные, т.е. не системные модули).

:mod -u

hMod Base     PEHeader Module Name      File Name

     00400000 004000D8 crack0x0         \.PHCK\src\crack0x03.exe

     00420000 004200D8 crack0x0         \.PHCK\src\crack0x03.exe

     ^^^^^^^^

     77E80000 77E800D0 kernel32         \WINNT\system32\kernel32.dll

     77F80000 77F800C0 ntdll            \WINNT\system32\ntdll.dll

Смотрите, загружено сразу две копии crack0x03, причем последняя расположена по адресу 0x420000, как раз что нам надо! Теперь нетрудно посчитать, что адрес 0x421056 (тот, что мы пытались последний раз найти в ломаемом файле) "на диске" будет соответствовать адресу 0x421056

– (0x42000

– 0x400000) == 0x421056 – 0x20000 == 0x401056. Смотрим:

00401056: 85C0                     test      eax,eax

00401058: 740F                     je       .000401069   -------- (1)

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

001B:00421056  85C0                TEST    EAX,EAX

001B:00421058  740F                JZ      00421069

Разумеется, описанная методика вычислений применима к любым DLL, а не только тем, что представляют собой исполняемый файл.

А вот, если бы мы пошли не путем адресов, а попытались найти в ломаемой программе срисованную с отладчика последовательность байт, включая и ту часть, которая входит в CALL 00422040 – интересно, нашли бы мы ее или нет?

001B:0042104E  E87D000000          CALL    004210D0

001B:00421053  83C408              ADD     ESP,08

001B:00421056  85C0                TEST    EAX,EAX

001B:00421058  740F                JZ      00421069

:Образ файла в памяти.

.0040104E: E87D000000              call     .0004010D0   -------- (1)

.00401053: 83C408                  add       esp,008 ;"•"

.00401056: 85C0                    test      eax,eax



.00401058: 740F                    je       .000401069   -------- (2)

:Образ файла на диске

Вот это новость – командам CALL 0x4210D0 и CALL 0x4010D0

соответствует один и тот же машинный код – E8 7D 00 00 00! Как же такое может быть?! А вот как –  аргумент операнд процессорной инструкции "0xE8" представляет собой не смещение подпрограммы, а разницу смещений подпрограммы и инструкции, следующей за командой call. Т.е. в первом случае: 0x421053

(смещение инструкции, следующей за CALL) + 0x0000007D

(не забываем об обратном порядке байтов в двойном слове) == 0x4210D0, - вот он, искомый адрес. Таким образом, при изменении адреса загрузки, коррекция кодов команд CALL не требуется.

"Оценка по аналогии основывается на предположении, что если два или более объекта согласуются друг с другом в некоторых отношениях, то они, вероятно, согласуются и в других отношениях"

Ганс Селье "От мечты к открытию"

Рассуждения по аналогии – опасная штука. Увлеченные стройностью аналогии мы подчас даже не задумываемся о проверке. Между тем, аналогии лгут чаще, чем этого хотелось бы.

В примере crack0x03 среди прочего кода есть и такая строка (найдите ее с помощью hiew):

004012C5: 89154C694000                 mov       [00040694C],edx

Легко видеть, что команда MOV обращается к ячейке не по относительному, а по абсолютному адресу. Вопрос: а) выясните, что произойдет при изменении адреса загрузки модуля; б) как вы думаете – будет ли теперь совпадать образ файла на диске и в памяти?

Заглянув отладчиком по адресу 0x4212C5 (0x4012C5 + 0x2000) мы увидим, что обращение идет совсем не к ячейке 0x42694C, а – 0x40694C! Наш модуль самым бессовестным образом вторгается в чужие владения, модифицируя их по своему усмотрению. Так и до краха системы докатиться недолго! В данном случае этого не происходит только потому, что искомая строка расположена в Startup-процедуре (стартовом коде) и выполняется лишь однажды – при запуске приложения, а из загруженного модуля не вызывается.



Другое дело, если бы функция Demo() обращалась к какой-нибудь статической переменной – компилятор, подставив ее непосредственное смещение, сделал бы модуль неперемещаемым! После сказанного становится непонятно: как же тогда ухитряются работать динамически подключаемые библиотеки (DLL), адрес загрузки которых заранее неизвестен? Поразмыслив некоторое время, мы найдем, по крайней мере, два решения проблемы:

Первое – вместо непосредственной адресации использовать относительную, например: [reg+offset_val], где reg – регистр, содержащий базовый адрес загрузки, а offset_val – смещение ячейки от начала модуля. Это позволит модулю грузится по любому адресу, но заметно снизит производительность программы уже хотя бы за счет потери одного регистра….

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

Единственная проблема – как отличить действительные непосредственные смещения от констант, совпадающих с ними по значению? Не дизассемблировать же в самом деле DLL, чтобы разобраться какие именно ячейки в ней необходимо "подкрутить"? Верно, куда проще перечислить их адреса в специальной таблице, расположенной непосредственно в загружаемом файле и носящей гордое имя "Таблицы перемещаемых элементов" или (Relocation

[Fix Up] table по-английски). За ее формирование отвечает линкер (он же – компоновщик) и такая таблица присутствует в каждой DLL.

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

::fixupdemo.c

__declspec(dllexport) void meme(int x)

{

static int a=0x666;

a=x;

}

> cl fixupdemo.c /LD

Листинг 3 Исходный текст fixupdemo.c

Откомпилируем и тут же дизассемблируем его: "DUMPBIN /DISASM fixupdemo.dll" и "DUMPBIN /SECTION:.data /RAWDATA".



  10001000: 55                 push        ebp

  10001001: 8B EC              mov         ebp,esp

  10001003: 8B 45 08           mov         eax,dword ptr [ebp+8]

  10001006: A3 30 50 00 10     mov         [10005030],eax

               ^^^^^^^^^^^                  ^^^^^^^^

  1000100B: 5D                 pop         ebp

  1000100C: C3                 ret

RAW DATA #3

10005000: 00 00 00 00 00 00 00 00 00 00 00 00 33 24 00 10  ............3$..

10005010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

10005020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

10005030: 66 06 00 00 E3 11 00 10 FF FF FF FF 00 00 00 00  f...у...    ....

          ^^^^^

Судя по коду, запись содержимого EAX всегда происходит в ячейку 0x10005030, но не торопите с выводами! "DUMPBIN /RELOCATIONS fixupdemo.dll":

BASE RELOCATIONS #4

    1000 RVA,      154 SizeOfBlock

       7 HIGHLOW

       ^       

      1C HIGHLOW

      23 HIGHLOW

      32 HIGHLOW

      3A HIGHLOW

Таблица перемещаемых элементов-то не пуста! И первая же ее запись указывает на ячейку 0x100001007, полученную алгебраическим сложением смещения 0x7 с RVA-адресом 0x1000 и базовым адресом загрузки 0x10000000 (получите его с помощью DUMPBIN самостоятельно). Смотрим – ячейка 0x100001007 принадлежит инструкции "MOV [0x10005030],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

Листинг 4 Исходный текст fixupload

Поскольку, по одному и тому же адресу две различные DLL не загрузишь (откуда же системе знать, что это одна и та же DLL!), загрузчику приходится прибегать к ее перемещению. Загрузим откомпилированную программу в отладчик и установим точку останова на функцию LoadLibraryA. Это, – понятное дело, – необходимо чтобы пропустить Startup-код и попасть в тело функции main. (Как легко убедиться исполнение программы начинается отнюдь не с main, а со служебного кода, в котором очень легко утонуть). Но откуда взялась загадочная буква 'A' на конце имени функции? Ее происхождение тесно связано с введением в Windows поддержки уникода – специальной кодировки, каждый символ в которой кодируется двумя байтами, благодаря чему приобретает способность выражать любой из 216 = 65.536 знаков, – количество достаточно для вмещения практически всех алфавитов нашего мира. Применительно к LoadLibrary – теперь имя библиотеки может быть написано на любом языке, а при желании и на любом количестве любых языков одновременно, например, на русско-француско-китайском. Звучит заманчиво, но не ухудшает ли это производительность? Разумеется, ухудшает, еще как – уникод требует жертв! Самое обидное – в подавляющем большинстве случаев вполне достаточно старой доброй кодировки ASCII (во всяком случае нам,

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


Как же быть?

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

> DUMPBIN /IMPORTS fixupload.exe > filename

> type filename

                 19D  HeapDestroy

                 1C2  LoadLibraryA

                  CA  GetCommandLineA

                 174  GetVersion

                  7D  ExitProcess

                 29E  TerminateProcess

...

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

Другой путь – заглянуть в SDK. Конечно, библиотечное имя функций в нем отсутствует, но в "Quick Info" мимоходом приводится информация и поддержке уникода (если таковая присутствует). А раз есть уникод – есть суффиксы 'W' и 'A', соответственно, наоборот – где нет уникода, нет и суффиксов. Проверим?

Вот так выглядит Quick Info от LoadLibrary:

QuickInfo

  Windows NT: Requires version 3.1 or later.

  Windows: Requires Windows 95 or later.

  Windows CE: Requires version 1.0 or later.

  Header: Declared in winbase.h.

  Import Library: Use kernel32.lib.

  Unicode: Implemented as Unicode and ANSI versions on Windows NT.

На чистейшем английском языке здесь сказано – "Реализовано как Unicode и ANSI версии на Windows NT". Стоп! С NT все понятно, а как насчет "народной" девяносто восьмой (пятой)? Беглый взгляд на таблицу экспорта KERNEL32.DLL показывает: такая функция там есть, но, присмотревшись повнимательнее, мы с удивлением обнаружим, что ее точка входа совпадает с точками входа десятка других функций!

    ordinal hint RVA      name

        556  1B3 00039031 LoadLibraryW

Третья колонка в отчете DUMPBIN это RVA-адрес – виртуальный адрес начала функции за вычетом базового адреса загрузки файла. Простой контекстный поиск показывает, что он встречается не единожды. Воспользовавшись программой-фильтром srcln (см. Приложения Исходные тексты)



для получения связного протокола, мы увидим следующее:

   21:        118    1 00039031 AddAtomW

  116:        217   60 00039031 DeleteFileW

  119:        220   63 00039031 DisconnectNamedPipe

  178:        279   9E 00039031 FindAtomW

  204:        305   B8 00039031 FreeEnvironmentStringsW

  260:        361   F0 00039031 GetDriveTypeW

  297:        398  115 00039031 GetModuleHandleW

  341:        442  141 00039031 GetStartupInfoW

  377:        478  165 00039031 GetVersionExW

  384:        485  16C 00039031 GlobalAddAtomW

  389:        490  171 00039031 GlobalFindAtomW

  413:        514  189 00039031 HeapLock

  417:        518  18D 00039031 HeapUnlock

  440:        541  1A4 00039031 IsProcessorFeaturePresent

  455:        556  1B3 00039031 LoadLibraryW

  508:        611  1E8 00039031 OutputDebugStringW

  547:        648  20F 00039031 RemoveDirectoryW

  590:        691  23A 00039031 SetComputerNameW

  592:        693  23C 00039031 SetConsoleCP

  597:        698  241 00039031 SetConsoleOutputCP

  601:        702  245 00039031 SetConsoleTitleW

  605:        706  249 00039031 SetCurrentDirectoryW

  645:        746  271 00039031 SetThreadLocale

  678:        779  292 00039031 TryEnterCriticalSection

Вот это сюрприз! Все уникодеовые  –

функции под одной крышей! Поскольку, трудно поверить в идентичность реализаций LoadLibraryW и, скажем, DeleteFileW, остается предположить, что мы имеем дело с "заглушкой", которая ничего не делает, а только возвращает ошибку. Следовательно, в 9x действительно, функция LoadLibraryW не реализована.

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

KERNEL32!LoadLibraryA                     

001B:77E98023  PUSH    EBP

001B:77E98024  MOV     EBP,ESP



001B:77E98026  PUSH    EBX

001B:77E98027  PUSH    ESI

001B:77E98028  PUSH    EDI

001B:77E98029  PUSH    77E98054

001B:77E9802E  PUSH    DWORD PTR [EBP+08]

Отдаем команду "P RET" для выхода из LoadLibraryA (анализировать ее, в самом деле, ни к чему) и оказываемся в легко узнаваемом теле функции main.

001B:0040100B  CALL    [KERNEL32!LoadLibraryA]

001B:00401011  MOV     [EBP-08],EAX           

001B:00401014  CMP     DWORD PTR [EBP-08],00

001B:00401018  JZ      00401051

001B:0040101A  PUSH    00405040

001B:0040101F  CALL    [KERNEL32!LoadLibraryA]

001B:00401025  MOV     [EBP-08],EAX

001B:00401028  CMP     DWORD PTR [EBP-08],00

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

Приблизившись к вызову функции demo (в отладчике это выглядит как PUSH 00000777\ CALL [EBP-04] – "EBP-04" ни о чем не говорит, но вот аргумент 0x777 определенно что-то нам напоминает, - см. исходный текст fixupload.c), не забудьте переменить руку с <F10> на <F8>, чтобы войти внутрь функции.

001B:00531000  55                  PUSH    EBP

001B:00531001  8BEC                MOV     EBP,ESP

001B:00531003  8B4508              MOV     EAX,[EBP+08]

001B:00531006  A330505300          MOV     [00535030],EAX

001B:0053100B  5D                  POP     EBP

001B:0053100C  C3                  RET

Вот оно! Системный загрузчик скорректировал адрес ячейки согласно базовому адресу загрузки самой DLL. Это, конечно, хорошо, да вот проблема – в оригинальной DLL нет ни такой ячейки, ни даже последовательности "A3 30 50 53 00", в чем легко убедиться контекстным поиском. Допустим, вознамерились бы мы затереть эту команду NOP-ми.


Как это сделать?! Вернее, как найти это место в оригинальной DLL?

Обратим свой взор выше, на команды, заведомо не содержащие перемещаемых элементов – PUSH EBP/MOV EBP, ESP/MOV EAX,[EBP+08]. Отчего бы не поискать последовательность "55  8B EC  xxx  A3"? В данном случае это сработает, но если бы перемещаемые элементы были густо перемешаны "нормальными" ничего бы не вышло. Опорная последовательность оказалась бы слишком короткой для поиска и выдала бы множество ложных срабатываний.

Более изящно и надежно вычислить истинное содержимое перемещаемых элементов, вычтя их низ разницу между действительным и рекомендуемым адресом загрузки. В данном случае: 0x535030 /модифицированный загрузчиком адрес/ – (0x530000 /базовый адрес загрузки/ - 0x10000000 /рекомендуемый адрес загрузки/) == 0x10005030. Учитывая обратный порядок следования байт, получаем, что инструкция MOV [10005030], EAX в машинном коде должна выглядеть так: "A3 30 50 00 10". Ищем ее HIEW-ом, и чудо – она есть!


Содержание раздела