• XSS.stack #1 – первый литературный журнал от юзеров форума

Введение в разработку шелл-кода для Windows

NokZKH

Переводчик
Забанен
Регистрация
09.02.2019
Сообщения
99
Реакции
121
Пожалуйста, обратите внимание, что пользователь заблокирован
Эта статья содержит обзор методов разработки шелл-кода и их специфических аспектов. Понимание этих концепций позволяет вам написать свой собственный шелл-код. Кроме того, вы можете изменить существующие эксплойты, содержащие уже созданный шелл-код, для выполнения необходимых вам пользовательских функций.

Вступление
Допустим, у вас есть рабочий эксплойт в Internet Explorer или Flash Player, который открывает calc.exe. Это не очень полезно, не так ли? Что вы действительно хотите - это выполнить некоторые удаленные команды или выполнить другие полезные функции.

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

Для тех, кто не знаком с этим термином, как говорит Википедия:

«В области компьютерной безопасности шелл-код - это небольшой фрагмент кода, используемый в качестве полезной нагрузки при использовании уязвимости программного обеспечения. Он называется «шеллкод», потому что он обычно запускает командную оболочку, из которой злоумышленник может управлять скомпрометированной машиной, но любой фрагмент кода, выполняющий аналогичную задачу, может называться шеллкодом… Шеллкод обычно пишется на машинном коде ».

Шелл-код - это фрагмент машинного кода, который мы можем использовать в качестве полезной нагрузки для эксплойта. Что это за «машинный код»? Давайте возьмем в качестве примера следующий код C:

1.png


Это переводится в ASM как следующий код:

2.png


Здесь важно отметить, что есть основная процедура и вызов функции printf.

Этот код собран в машинный код, как вы можете видеть выделенным в отладчике:

3.png


Таким образом, «55 8B EC 68 00 B0 33 01…» - это машинный код для нашего кода C.

Как шелл-код используется внутри эксплойта?
Возьмем в качестве примера простой эксплойт, уязвимость переполнения буфера в стеке.

4.png


Основная идея использования этой уязвимости заключается в следующем (обратите внимание, что целью данной статьи не является подробное описание работы эксплойтов переполнения буфера):

  1. Отправьте приложению строку размером более 20 байт, которая также содержит ваш шелл-код
  2. Стек поврежден из-за перезаписи за пределами статически выделенного буфера. Ваш шеллкод будет помещен в стек
  3. Ваша строка перезапишет часть важных данных в стеке (например, сохраненный EIP или указатель функции) с адресом пользовательской памяти
  4. Приложение перейдет к вашему шелл-коду из стека и начнет выполнять инструкции машинного кода внутри
Если вы сможете успешно использовать эту уязвимость, вы сможете запустить свой шелл-код, и вы действительно сделаете что-то полезное с этой уязвимостью, а не только аварийно завершите работу программы. Шелл-код может открыть оболочку, загрузить и выполнить файл, перезагрузить компьютер, включить RDP или любое другое действие.



Особенности Shellcode
Шелл-код - это не какой-либо машинный код. Есть несколько конкретных аспектов, которые мы должны учитывать при написании нашего собственного шелл-кода:

  1. Мы не можем использовать прямые смещения для строк
  2. Мы не знаем адреса функций (напр. Printf)
  3. Мы должны избегать некоторых конкретных байтов (например, NULL байтов)
Давайте кратко обсудим каждый из вышеперечисленных вопросов.

  • Прямые смещения в строки
Даже если в коде C / C ++ вы можете определить глобальную переменную, строку со значением «Hello, world!», Или вы можете напрямую поместить строку в качестве параметра функции, как в нашем примере «Hello, world», компилятор поместит эту строку в определенный раздел файла:

5.png


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

  • Адреса функций
В C / C ++ легко вызвать функцию. Мы указываем #include <> для использования определенного заголовка и вызова функции по его имени. В фоновом режиме проблему решают компилятор и компоновщик: они разрешают адреса функций (например, MessageBox из user32.dll), и мы можем легко вызывать эти функции по их именам.

6.png


В шеллкоде мы не можем этого сделать. Мы не знаем, загружена ли в память библиотека, содержащая нашу требуемую функцию, и не знаем адрес требуемой функции. DLL из-за ASLR (рандомизации размещения адресного пространства) не будет загружаться каждый раз по одному и тому же адресу. Кроме того, DLL может изменяться с каждым новым обновлением Windows, поэтому мы не можем полагаться на конкретное смещение в DLL.

Мы должны загрузить DLL в память и найти нужные функции прямо из шеллкода. К счастью, Windows API предлагает две полезные функции: LoadLibrary и GetProcAddress, которые мы можем использовать для поиска адресов наших функций.

  • Избегать пустых байтов
Нулевые байты имеют значение 0x00. В коде C / C ++ NULL-байт считается терминатором строки. Из-за этого присутствие этих байтов в шелл-коде может нарушить функциональность целевого приложения, и наш шелл-код может быть неправильно скопирован в память.

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

7.png


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

Кроме того, существуют конкретные случаи, когда шелл-код должен избегать символов, таких как \ r или \ n, или даже использовать только буквенно-цифровые символы.

Linux против Windows шеллкоды
Проще написать шеллкод для Linux, хотя бы базовый. Это связано с тем, что в Linux можно очень легко использовать системные вызовы (системные «функции»), такие как us write, execve или send, с прерыванием 0x80 (воспринимайте это как «вызов функции»). Вы можете найти список системных вызовов здесь.

Например, шеллкод «Hello, world» в Linux требует следующих шагов:

  1. Укажите номер системного вызова (например, «запись»)
  2. Укажите параметры системного вызова (например, stdout, «Hello, world», length)
  3. Прервите 0x80, чтобы выполнить системный вызов
Это приведет к вызову: write (stdout, «Hello, world», length).

В Windows это сложнее. Есть более необходимые шаги для создания надежного шелл-кода.

  1. Получить базовый адрес kernel32.dll
  2. Найти адрес функции GetProcAddress
  3. Используйте GetProcAddress, чтобы найти адрес функции LoadLibrary
  4. Используйте LoadLibrary для загрузки DLL (например, user32.dll)
  5. Используйте GetProcAddress, чтобы найти адрес функции (например, MessageBox)
  6. Укажите параметры функции
  7. Вызвать функцию
Заключение
Это первая часть из серии статей о том, как написать шелл-код Windows для начинающих. Это введение требуется для того, чтобы понять, что такое шелл-код, каковы ограничения и каковы различия между шелл-кодом Windows и Linux.

Вторая часть будет содержать краткое введение в язык ассемблера, формат файлов PE (Portable Executable) и PEB (Process Environment Block). Позже вы увидите, как это поможет вам написать собственный шелл-код.

Источник: https://xss.pro
Переводчик статьи - https://xss.pro/members/177895/
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Если вы пропустили первую часть этой серии, где вы можете прочитать о том, что такое шелл-код и как он работает, вы можете найти его здесь: Часть I. В этой части я расскажу необходимую информацию, чтобы можно было правильно написать шелл-код для платформы Windows: блок Process Environment, формат переносимых исполняемых файлов и краткое введение в сборку x86 Assembly. Эта статья не будет охватывать все аспекты этих концепций, но этого должно быть достаточно для правильного понимания шелл-кодов.

Блок технологической среды
В операционной системе Windows PEB - это структура, доступная для каждого процесса по фиксированному адресу в памяти. Эта структура содержит полезную информацию о процессе, такую как: адрес, по которому исполняемый файл загружается в память, список модулей (DLL), флаг, указывающий, выполняется ли отладка процесса, и многие другие.

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

Как мы обсуждали в первой статье, библиотеки DLL (из-за ASLR) будут загружаться по разным адресам памяти, поэтому мы не можем использовать фиксированные адреса памяти в нашем шелл-коде. Но мы можем использовать эту структуру, найденную по фиксированному адресу памяти, чтобы найти расположение DLL в памяти.

Если вы знакомы с C / C ++, довольно легко понять, какую информацию содержит эта структура и где. Официальная документация Microsoft показывает следующие поля:

image12.png


Как видите, некоторые поля, называемые «зарезервированы», не описаны, но некоторые другие поля задокументированы.

Для тех, кто не знаком с C / C ++, вы должны понимать это: BYTE означает ... байт, PVOID - это указатель (адрес памяти) - так что это 4 байта в системе x86 (32-битная система), а PPEB_LDR_DATA - это указатель на пользовательскую структуру с именем PEB_LDR_DATA. Для первого поля зарезервировано два байта (поскольку Reserved1 [2] является массивом из двух байтов), флаг BeingDebugged составляет 1 байт, за которым следует еще один байт (Reserved2). Reserved3 [2] является массивом с 2 указателями (таким образом, 2 * 4 байта = 8 байтов), а Ldr является указателем - 4 байта.

Из этой структуры мы будем использовать указатель Ldr, который мы можем найти по смещению 12 (или 0xC) внутри структуры (2 байта Reserved1 + 1 байт BeingDebugged + 1 байт Reserved2 + 8 байт Reserved3).

PEB_LDR_DATA содержит следующую информацию:

image17.png


Мы будем действовать как прежде. Мы можем получить доступ к полю InMemoryOrderModuleList со смещением 20 (0x14 в шестнадцатеричном виде: 8 байтов зарезервировано1 + 3 * 4 байта зарезервировано2). Это поле даст информацию об использовании загруженных библиотек DLL.

Здесь все немного сложнее. Мы можем получить информацию о загруженных DLL, используя структуру под названием LDR_DATA_TABLE_ENTRY. Официальная документация Microsoft не публикует полное содержание структуры, но мы можем найти больше информации здесь:

image20.png


Структура LIST_ENTRY представляет собой простой двойной связанный список, содержащий указатель на следующий элемент (Flink) и указатель на предыдущий элемент (Blink), каждый из которых имеет 4 байта:

image15.png


Поле InMemoryOrderModuleList является указателем на поле LIST_ENTRY структуры LDR_DATA_TABLE_ENTRY. Это НЕ указатель на начало структуры LDR_DATA_TABLE_ENTRY, это указатель на поле InMemoryOrderLinks структуры! Как видите, Flink и Blink являются указателями на структуру LIST_ENTRY.

Давайте рассмотрим шаг за шагом:
  1. Прочитайте структуру PEB
  2. Перейти к смещению 0xC на указатель Ldr
  3. Перейти к смещению 0x14 в поле InMemoryOrderModuleList
В этот момент мы помещаем элемент InMemoryOrderLinks первого загруженного в память модуля. Этот модуль является исполняемым файлом (например, calc.exe). Мы хотим перемещаться по всем загруженным DLL. InMemoryOrderLinks, являясь структурой LIST_ENTRY, где первые 4 байта являются указателем Flink, а следующие 4 байта являются указателем Blink, позволяют нам перейти ко второму загруженному модулю через первые 4 байта. Нам нужно сделать это еще раз, и мы можем получить доступ к информации о третьем загруженном модуле.

Список InMemoryOrderModuleList предлагает нам список всех загруженных модулей в следующем порядке:
  1. calc.exe (the executable)
  2. ntdll.dll
  3. kernel32.dll
Как мы обсуждали в первой статье, нам нужен доступ к kernel32.dll для доступа к таким функциям, как GetProcAddress и LoadLibrary, которые помогут нам вызывать любую функцию Windows API.

Чтобы закончить нашу цель, мы должны прочитать поле DllBase (место памяти, куда DLL загружена в память) из текущей структуры LDR_DATA_TABLE_ENTRY. DllBase хранится в смещении 0x18 в структуре, но мы должны позаботиться о том, чтобы уже иметь смещение 0x8 (поле InMemoryOrderLinks), поэтому мы просто перепрыгиваем 0x10 байт, чтобы получить DllBase.

Вот перспектива всех необходимых шагов, необходимых для поиска адреса памяти kernel32.dll:

image6.png


Мои навыки рисования не удивительны, но я надеюсь, что вы сможете понять, как это работает. Просто помните, что навигация по модулям осуществляется с помощью указателя «Flink», представляющего первые 4 байта, на которых мы размещены.

Не бойся этого! Как вы увидите, это можно сделать всего за 8 (более или менее) строк кода сборки.

Формат переносимых исполняемых файлов
Portable Executable - это формат файла, используемый исполняемыми файлами и динамическими библиотеками (DLL) в системе Windows. Формат описывает содержимое этих файлов: заголовки и разделы, содержащие весь код и данные, используемые PE-файлами.

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

Очень короткая иллюстрация на PE-файлах:

image2.jpg


Как вы можете видеть на этом рисунке, PE-файл содержит:

  • заголовок DOS (старая операционная система Microsoft)
  • заглушка DOS - небольшая программа, которая печатает «Эта программа не может быть запущена в режиме DOS»
  • PE заголовки (различная полезная информация)
  • таблица разделов (заголовки разделов)
  • разделы (разделы кода и данных)
Более подробный обзор покажет нам, что содержит PE-файл, открытый в шестнадцатеричном редакторе:

image9.jpg


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

Давайте начнем с заголовка DOS. Заголовок DOS представлен в виде следующей структуры:

image4.png


Вы можете найти полную структуру и все другие необходимые структуры в заголовочных файлах «WinNT.h» компиляторов C / C ++.

Все PE файлы (EXE или DLL) будут начинаться с этой структуры. Итак, после того, как мы найдем модуль в памяти, по этому адресу памяти мы найдем эту структуру. Вы можете распознать его по его первым двум байтам: «MZ», это поле e_magic, представляющее «подпись» заголовка DOS.

Единственное, что нам нужно знать, это поле e_lfanew структуры. Это поле находится со смещением 0x3C и показывает местоположение PE-заголовка.

Заголовок PE - это структура, которая содержит следующую информацию:

image11.png


Он содержит подпись PE (вы можете увидеть строку «PE», если откроете файл PE с помощью редактора), FileHeader, структуру, содержащую информацию, такую как количество секций (код и данные), тип «машина» (x86 , x64, ARM…) и «характеристики», определяющие среди прочих сведений, является ли файл исполняемым (.exe) или динамически подключаемой библиотекой (.dll).

OptionalHeader - это структура, содержащая более полезную для нас информацию.

image16.png


Содержит такую информацию как:

  • AddressOfEntryPoint - где exe / dll начинает выполнять код
  • ImageBase - где DLL должна быть загружена (если возможно) в память
  • DataDirectory - информация, такая как импортированные и экспортированные функции
Нас интересует только последнее поле, DataDirectory, потому что нам нужно получить экспортированные функции. Вот как работает DLL: она содержит различные функции, и эти функции экспортируются, поэтому другое приложение может просто загрузить DLL в память, найти экспортированные функции и вызвать их. Например, «MessageBox» - это экспортированная функция из «user32.dll» (на самом деле существует две версии: ASCII и Unicode).

Поле DataDirectory структуры представляет собой массив структур IMAGE_DATA_DIRECTORY. IMAGE_DATA_DIRECTORY - это следующее:

image5.png


Таким образом, в конце структуры OptionalHeader есть структуры IMAGE_DATA_DIRECTORY (на самом деле 16). Для нашей области важно понимать, что первым является каталог данных «каталога экспорта».

Чтобы перейти в каталог экспорта, нам просто нужно следовать полю VirtualAddress структуры, которое указывает на начало каталога экспорта. DWORD представляет тип на 4 байта, а WORD - всего 2 байта. Если вы суммируете все размеры элементов до массива DataDirectory, вы заметите, что существует 120 байтов (0x78) байтов от начала PE-заголовка до начала массива DataDirectory. Таким образом, по смещению 0x78 мы найдем виртуальный адрес (поле VirtualAddress) в каталоге экспорта.

Каталог экспорта имеет следующую структуру:

image10.png


Из этой структуры мы будем использовать следующие поля:

  • AddressOfFunctions - адрес массива «указателей на функции»
  • AddressOfNames - адрес массива «указателей на имена функций»
  • AddressOfNameOrdinals - адрес массива ординалов (16-битные целые числа)
Давайте возьмем в качестве примера DLL с тремя функциями.

  • AddressOfFunctions = 0x11223344 —-> [0x11111111, 0x22222222, 0x33333333] - 0x11223344 - указатель на массив, содержащий адреса функций: 0x11111111, 0x22222222 и 0x33333333 - адреса функций.
  • AddressOfNames = 0x12345678 -> [0xaaaaaaaa -> «func0», 0xbbbbbbbb -> «func1», 0xcccccccc -> «func2»] - 0x12345678 - это указатель на массив указателей на имена функций: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Кроме того, Строка, представляющая имя экспортируемой функции и т. Д.
  • AddressOfNameOrdinals = 0xabcdef -> [0x00, 0x01, 0x02] - 0xabcdef - указатель на массив целых чисел (в два байта), представляющий смещения каждой функции в массиве AddressOfFunctions.
Чтобы получить адрес функции по ее имени, мы проверяем имена, анализируя массив AddressOfNames. Первая функция (func0) будет иметь порядковый номер 0, вторая функция (func1) будет иметь порядковый номер 1, а третья функция (func2) будет иметь порядковый номер 2. Поэтому, если мы ищем функцию func2, мы получим доступ к элементу 2 ( начиная с 0) массива AddressOfFunctions.

Вкратце, это так: function_address = AddressOfFunctions [Ordinal (имя_функции)].

Не пугайтесь этого, как вы увидите, все это можно сделать в 15-20 строках кода сборки.

Язык ассемблера
Даже если можно написать шелл-код на C / C ++, как вы можете видеть в этой статье, если вы хотите правильно понять, что делает, как это работает и как вы можете его изменить, вы должны понимать и писать ассемблерный код ,

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

Чтобы избежать различных сложностей, я напишу все примеры ниже с использованием встроенного ассемблера на компиляторе Microsoft Visual C ++ Express Edition. Однако вам может быть удобнее использовать ассемблер, такой как MASM, NASM или YASM.

Давайте начнем с «переменных». Процессор использует разные регистры (считают их переменными) для хранения временных данных. У них разные цели, но здесь мы будем рассматривать их как «глобальные переменные». Для лучшего описания вы можете прочитать эту статью.

Существует несколько регистров общего назначения: EAX, EBX, ECX, EDX, ESI и EDI. Каждый из них может хранить 4 байта данных. Кроме того, младшие 2 байта из них могут упоминаться как AX, BX, CX, DX, SI и DI. Последний байт доступен как: AL, BL, CL, DL. Следующая картина описывает это:

image3.jpg


Допустим, выполнение нашей программы начинается с адреса 0x12345678. Существует специальный регистр, который содержит текущий адрес выполнения, называемый EIP (указатель инструкции). После выполнения инструкции этот регистр будет автоматически изменен на адрес следующей инструкции.

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

Инструкции:

  • mov destination, source - «переместит» значение из источника в место назначения, повлияет на источник
  • add destination, источник - добавит источник в пункт назначения или пункт назначения = пункт назначения + источник
  • sub destination, source - вычтет источник из пункта назначения или destination = destination - source
  • inc destination - увеличит значение пункта назначения на 1
  • dec destination - уменьшит значение пункта назначения на 1
Несколько примеров:

image8.png


Вы можете проверить это на Visual C ++, как показано на следующем рисунке:

image19.png


Можно разместить точку остановки отладчика Visual C ++, нажав левую серую линию. Когда вы запустите программу, она остановится на указанной точке. Теперь внизу вы увидите окно «Watch1». Это место, где вы можете добавить имена регистров, чтобы увидеть их значения. Так что добавляйте EAX, EBX и так далее и смотрите их.

image14.png


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

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

Полезные инструкции:
  • JMP адрес / метка - безусловно перейдет на метку или адрес памяти
  • cmp destination, source - будет сравнивать назначение с источником путем вычитания (без влияния на операнды) источника из назначения. «Результат» не будет сохранен, просто помните, что если источник совпадает с пунктом назначения, будет установлен флаг, называемый «нулевой флаг». Этот флаг будет использоваться позже при следующих условных переходах
  • jz address / label - будет переходить к указанной метке или адресу, если установлен «нулевой флаг» (jz = Jump if Zero), поэтому, если у нас была инструкция «cmp», где параметры равны, флаг был установлен и код перейдет на указанный адрес / метку. Если нет, ничего не произойдет, выполнение перейдет к следующей инструкции
  • jnz адрес / метка - в противоположность jz (jnz = переход, если не ноль), код будет переходить на указанный адрес, если флаг нуля не был установлен, поэтому, если команда cmp операнды где-то отличаются.
Есть много других доступных инструкций перехода, но этого должно быть достаточно для начала. В качестве примера вы можете попробовать следующий код:

image7.png


Теперь мы можем перейти к важной части программирования ASM: стеку. Стек - это место в памяти, где вы можете хранить данные. Думайте об этом как о пространстве памяти, куда вы можете поместить данные, как вы можете поместить пластины друг над другом, и вы можете получить их только сверху.

Есть две полезные инструкции для работы со стеком:

  • push value - поместит значение в стек
  • pop register - возьмет значение из вершины стека и сохранит его в указанном регистре.
Есть два регистра, которые «указывают на стек»

  • Регистр ESP (указатель стека) - указывает на вершину стека
  • Регистр EBP (Base Pointer) - указывает на «базу» стека. Мы не будем освещать это здесь
Когда мы работаем со стеком, происходит несколько важных вещей. Допустим, ESP, вершина стека, имеет значение 0x11223344. Если мы передадим некоторые данные (4 байта) с помощью инструкции «push 0xaaaaaaaaa», значение 0xaaaaaaaaa будет помещено в верхнюю часть стека, а значение ESP уменьшится с 4 байтами. Таким образом, мы можем сказать, что стек увеличивается до более низких адресов. После инструкции толчка ESP будет 0x11223340

Если мы получаем данные из стека, все происходит по-другому: данные удаляются из стека (фактически, они все еще там, для оптимизации), и значение ESP увеличивается на 4 байта.

Это может выглядеть сложно, но это не так. Пример:

image1.png


Думая о математике стека, мы можем предположить, что если мы поместим в стек 0x20 байтов (используя 8 pushinstructions, 0x20 = 32), мы можем легко очистить стек, просто изменив значение ESP: добавьте ESP, 0x20. Это проще, чем 8 популярных инструкций.

Теперь мы можем вызывать функции. Есть два очень распространенных метода для вызова функций, называемые stdcall и cdecl. Windows API использует соглашение о вызовах stdcall (метод), и мы обсудим только это. Тем не менее, они похожи, вы можете найти более подробную информацию здесь.

Давайте возьмем следующую функцию в качестве примера:

image13.png


Мы хотим вызвать function(0x11, 0x22). Нам нужно знать следующие вещи:
  1. положить значения параметров в стек справа налево
  2. используйте инструкцию «call function» для вызова функции
  3. инструкция вызова автоматически поместит в стек адрес следующей инструкции после самой инструкции вызова (и значение ESP также уменьшится)
  4. мы можем увидеть результат функции в регистре EAX
image18.png


Таким образом, после выполнения этой функции у нас будет значение 0x33 (0x11 + 0x22 = 0x33) в регистре EAX.

Итак, тезисы - это основы. Тем не менее, мы также будем использовать некоторые другие инструкции в нашем шеллкоде, такие как:
  • xor destination, source - это бинарная операция, но мы просто будем использовать ее как «xor eax, eax». Результатом этой инструкции является изменение значения eax на 0. Мы используем его, чтобы избежать байтов NULL
  • lea destination, source (Load Effective Address) - используется для помещения в место назначения адреса памяти, указанного источником.
  • lodsd - положить в регистр EAX значение по адресу, указанному в регистре ESI
  • xchg destination, source - обменивается значениями операндов: источник будет иметь значение назначения, а назначение будет содержать значение источника
ASM - сложный язык, но если вы будете его шаг за шагом, его легко понять.

Заключение
Даже если мы еще не написали шелл-код, мы изучили всю необходимую информацию, чтобы иметь возможность писать шелл-код. Мы узнали, что такое PEB и как он может нам помочь, как выглядит PE-файл и даже написать несколько основных строк кода ASM.

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

Источник: https://xss.pro
Переводчик статьи - https://xss.pro/members/177895/
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Если вы пропустили первые две части этой статьи, вы можете найти в части I, что такое шелл-код, как он работает и каковы его ограничения, а во второй части вы можете прочитать о структуре PEB (Process Environment Block), PE ( .exe, .dll) формат файла, и вы можете пройти краткое введение в ASM. Эта информация понадобится вам для правильного понимания шелл-кодов Windows.

В этой последней части введения разработки шеллкода мы напишем простой шеллкод SwapMouseButton, шеллкод, который поменяет местами левую и правую кнопки мыши. Мы начнем с существующего шелл-кода: «Allwin URLDownloadToFile + WinExec + ExitProcess Shellcode». Название шеллкода говорит нам о нескольких вещах, например, таких как:
  1. URLDownloadToFile Функция Windows API для загрузки файла
  2. WinExec для запуска файла (исполняемый файл: .exe)
  3. ExitProcess завершит процесс, выполняющий шеллкод
Используя этот пример, мы будем вызывать функцию SwapMouseButton и функцию ExitProcess. Я уверен, что легко понять, что делают эти функции.

image5.png


Как видите, каждая функция имеет только один параметр:
  • Параметр fSwap может быть ИСТИНА или ЛОЖЬ. Если это ИСТИНА, кнопки мыши меняются местами, иначе они восстанавливаются.
  • uExitCode представляет код завершения процесса. Каждый процесс должен возвращать значение при выходе (ноль, если все было в порядке, любое другое значение в противном случае). Это «возврат 0» основной функции.
Обзор программы
Итак, нам нужно вызвать две функции. В C ++ мы можем сделать это довольно просто:

image7.png


Компилятор знает, как связаться с библиотекой «user32» и найти функцию. Но мы должны сделать это вручную в шелл-коде. Нам нужно вручную загрузить библиотеку «user32», найти адрес функции «SwapMouseButton» и вызвать ее.

image4.png


Но здесь компилятор знает адрес функций «LoadLibrary» и «GetProcAddress». В шеллкоде мы должны найти их программно.

Обратите внимание, что нам не нужно вызывать функцию «ExitProcess» в C ++, потому что при «возврате 0» из функции «main» программа будет завершена, но из шелл-кода мы вызываем ее, чтобы убедиться, что программа завершается корректно и не падает.

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

Необходимые шаги следующие:
  1. Найти, где kernel32.dll загружается в память
  2. Найти свою таблицу экспорта
  3. Найти функцию GetProcAddress, экспортированную kernel32.dll
  4. Использовать GetProcAddress, чтобы найти адрес функции LoadLibrary
  5. Использовать LoadLibrary для загрузки библиотеки user32.dll
  6. Найти адрес функции SwapMouseButton в user32.dll
  7. Вызвать функцию SwapMouseButton
  8. Найти адрес функции ExitProcess
  9. Вызвать функцию ExitProcess
Для написания нашего шелл-кода мы будем использовать Visual Studio 2015 (вы можете использовать любую другую версию или ассемблер, такой как masm, nasm и т. Д.). В Visual Studio мы можем использовать «__asm {}» для прямой записи кода ASM.

Пожалуйста, убедитесь, что вы правильно прочитали и поняли эту часть.

image2.png


Найти базовый адрес kernel32.dll

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

image6.png


(Строки 1-2) Давайте посмотрим, что он делает. Он устанавливает регистр ecx на ноль и использует его во второй инструкции. Но почему? Помните, когда мы говорили об избежании пустых байтов? Инструкция «mov eax, fs: [30]» будет собрана в следующей последовательности кода операции: «64 A1 30 00 00 00», поэтому мы имеем нулевые байты, а инструкция «mov eax, fs: [ecx + 0x30]» будет собрана в «64 8B 41 30». Таким образом, можно избежать байтов NULL.

(Строки 3-4). Теперь у нас есть указатель PEB в регистре eax. Как мы видим в предыдущем сообщении в блоге, по смещению 0xC мы можем найти Ldr, мы следуем этому указателю, а в Ldr при смещении 0x14 у нас есть список модулей «в порядке памяти».

(Строки 5-7) Теперь мы размещены в модуле «program.exe», в «InMemoryOrderLinks». Здесь первым элементом является «Flink», указатель на следующий модуль. Вы можете видеть, что мы поместили этот указатель в регистр ESI. Инструкция «lodsd» будет следовать указателю, указанному в регистре esi, и мы получим результат в регистре eax. Это означает, что после инструкции lodsd у нас будет второй модуль, ntdll.dll, в регистре eax. Мы помещаем этот указатель в esi путем обмена значениями eax и esi и снова используем инструкцию lodsd для доступа к третьему модулю: kernel32.dll.

(Строка 8) На данный момент в регистре eax есть указатель на «InMemoryOrderLinks» файла kernel32.dll. Добавление 0x10 байт даст нам указатель «DllBase», адрес памяти, куда загружен kernel32.dll. Целевая добыча!

Найти таблицу экспорта kernel32.dll
Мы нашли kernel32.dl в памяти. Теперь нам нужно проанализировать этот PE-файл и найти таблицу экспорта. Это не очень сложно:

image12.png


(Строки 1-2) Мы знаем, что можем найти указатель «e_lfanew» со смещением 0x3C, поскольку размер заголовка MS-DOS составляет 0x40 байт, а последние 4 байта являются указателем «e_lfanew». Мы добавляем это значение к базовому адресу, потому что указатель относительно базового адреса (это смещение).

(Строки 3-4). По смещению 0x78 PE-заголовка мы можем найти «DataDirectory» для экспорта. Мы знаем это, потому что размер всех PE-заголовков (Signature, FileHeader и OptionalHeader) перед DataDirectory составляет ровно 0x78 байт, а экспорт является первой записью в таблице DataDirectory. Опять же, мы добавляем это значение в регистр edx, и теперь мы помещаемся в таблицу экспорта из kernel32.dll.

(Строки 5-7) В структуре IMAGE_EXPORT_DIRECTORY со смещением 0x20 мы можем найти указатель на «AddressOfNames», чтобы мы могли получить экспортированные имена функций. Это необходимо, потому что мы пытаемся найти функцию по ее имени, даже если это возможно, используя некоторые другие методы. Мы сохраняем указатель в регистре esi и устанавливаем регистр ecx в 0 (вы увидите ниже, почему).

Найти имя функции GetProcAddress
Теперь мы находимся в «AddressOfNames», массиве указателей (относительно базы изображений, адреса, где kernel32.dll загружается в память). Таким образом, каждые 4 байта будут представлять указатель на имя функции. Мы можем найти имя функции и порядковый номер имени функции («число» функции GetProcAddress) следующим образом:

image9.png


(Строки 1-3) Первая строка «ничего не делает». Это метка, название места, куда мы будем переходить, чтобы прочитать имена функций, как вы увидите ниже. В строке 3 мы увеличиваем регистр ecx, который будет счетчиком наших функций и порядкового номера функции.

(Строки 4-5) В регистре esi есть указатель на имя первой функции. Инструкция lodsd поместит в eax смещение к имени функции (например, «ExportedFunction»), и мы добавим это к ebx (базовый адрес kernel32), чтобы найти правильный указатель. Обратите внимание, что инструкция «lodsd» также увеличивает значение регистра esi на 4! Это помогает нам, потому что нам не нужно увеличивать его вручную, нам просто нужно снова вызвать lodsd, чтобы получить следующий указатель на имя функции.

(Строки 6-11) Теперь в регистре eax есть правильный указатель на имя экспортируемой функции. Итак, есть строка, содержащая имя функции, нам нужно проверить, является ли эта функция «GetProcAddress». В строке 6 мы сравниваем имя экспортируемой функции с «0x50746547», это на самом деле «50 74 65 47», значения ascii означают «PteG». Вы можете догадаться, что обратным является «GetP», первые 4 байта «GetProcAddress», но процессоры x86 используют метод little-endian, который означает, что числа хранятся в памяти в обратном порядке их байтов! Итак, мы сравниваем, если первые 4 байта текущего имени функции - «GetP». Если это не так, инструкция jnz снова перейдет на нашу метку и продолжится со следующим именем функции. Если это так, мы также проверяем следующие 4 байта, они должны быть «rocA» и следующие 4 байта «ddre», чтобы быть уверенными, что мы не найдем другую функцию, которая начинается с «GetP».

Найти адрес функции GetProcAddress
На данный момент мы нашли только порядковый номер функции GetProcAddress, но мы можем использовать его для того, чтобы найти фактический адрес этой функции:

image11.png


(Строки 1-2) На данный момент в edx есть указатель на структуру IMAGE_EXPORT_DIRECTORY. По смещению 0x24 структуры мы можем найти смещение «AddressOfNameOrdinals». В строке 2 мы добавляем это смещение в регистр ebx, который является базой изображений для kernel32.dll, поэтому мы получаем действительный указатель на таблицу порядковых имен.

(Строки 3-4). Регистр esi содержит указатель на массив имени ординала. Этот массив содержит два байтовых числа. У нас есть порядковый номер имени (индекс) функции GetProcAddress в регистре ecx, поэтому мы получаем адрес функции порядковый номер (индекс). Это поможет нам получить адрес функции. Мы должны уменьшить число, потому что имя порядкового номера начинается с 0.

(Строки 5-6). По смещению 0x1c мы можем найти «AddressOfFunctions», указатель на массив указателей на функции. Мы просто добавляем базу изображений kernel32.dll, и мы помещаемся в начало массива.

(Строки 7-8) Теперь, когда у нас есть правильный индекс для массива «AddressOfFunctions» в ecx, мы просто находим указатель функции GetProcAddress (относительно базы изображений) в местоположении AddressOfFunctions [ecx]. Мы используем «ecx * 4», потому что каждый указатель имеет 4 байта и esi указывает на начало массива. В строке 8 мы добавляем базу изображений, чтобы в edx был указатель на функцию GetProcAddress. Целевая добыча!

Найти адрес функции LoadLibrary
Плохая новость в том, что до этого момента мы не делали ничего «полезного». Хорошей новостью является то, что мы сделали то, что было сложно, и теперь мы можем повеселиться!

image10.png


(Строки 1-3) Во-первых, мы устанавливаем ecx на ноль, потому что мы будем использовать его позже. Во-вторых, строки 2 и 3 мы сохраняем в стеке для будущего, ebx, который является базовым адресом kernel32, и edx, который является указателем на функцию GetProcAddress.

(Строки 4-10) Теперь мы должны сделать следующий вызов: GetProcAddress (kernel32, «LoadLibraryA»). У нас есть адрес kernel32, но как мы можем использовать строку? Мы снова будем использовать стек. Мы поместим строку «LoadLibraryA \ 0» в стек. Да, строка должна заканчиваться NULL, поэтому мы устанавливаем ecx в 0, а в строке 4 мы помещаем ее в стек. Мы помещаем строку «LoadLibraryA» в стек по 4 байта за раз в обратном порядке. Сначала мы помещаем «aryA», затем «Libr», а затем «Load», чтобы строка в стеке была «LoadLibraryA». Готово! Теперь, когда мы поместили данные в стек, регистр esp, указатель стека, будет указывать на начало нашей строки «LoadLibraryA». Теперь мы помещаем параметры функции в стек, от последнего до первого, поэтому сначала в строке 8 пишем esp, затем в строке 9 базовый адрес ebx, kernel32 и вызываем edx, который является указателем GetProcAddress. И это все!

Обратите внимание, что мы поместили в стек «LoadLibraryA», а не только «LoadLibrary». Это связано с тем, что kernel32.dll не экспортирует функцию «LoadLibrary», а экспортирует две функции: «LoadLibraryA», которая используется для строковых параметров ANSI, и «LoadLibraryW», которая используется для строковых параметров Юникода.

Загрузить библиотеку user32.dll
Ранее мы нашли адрес функции LoadLibrary, теперь мы будем использовать его для загрузки в память библиотеки «user32.dll», которая содержит нашу функцию SwapMouseButton.

image13.png


(Строки 1-3) Как вы видели, мы поместили в стек строку «LoadLibraryA» ранее. Таким образом, мы должны избавиться от этого. Самый простой способ, вместо трех «всплывающих окон», мы можем просто добавить 0xc (что означает 12 байт строки) в регистр esp, и все готово. Во второй строке мы также удаляем 0, помещенный в стек, перед вызовом функции, и регистр ecx будет установлен в 0. Теперь мы создадим резервную копию для будущего использования адреса функции LoadLibrary в стеке, потому что, как вы знаете, после вызова функции, возвращаемые данные будут сохранены в регистре eax.

(Строки 4-10) Мы хотим вызвать «LoadLibrary (« user32.dll »)». Итак, нам нужно снова поместить строку в стек. Теперь это немного сложнее, потому что длина строки не кратна 4 байтам, и мы не можем напрямую разместить ее с помощью нескольких инструкций push. Вместо этого мы сначала помещаем ecx, который равен 0, в стек, и мы используем регистр CX для размещения строки «ll». Регистр CX представляет половину регистра ecx, это самая низкая часть. Теперь мы можем поместить его в стек. В строках 7-8 мы помещаем строку «user32.d», так что теперь у esp есть строка «user32.dll». Мы помещаем этот параметр в стек, чтобы загрузить библиотеку, и он также вернет в eax базовый адрес библиотеки user32.dll, адрес, куда DLL загружается в память. Нам это понадобится позже.

Получить адрес функции SwapMouseButton
Мы загрузили в память библиотеку user32.dll, теперь мы хотим вызвать GetProcAddress, чтобы получить адрес функции SwapMouseButton.

image17.png


(Строки 1-2) Как и прежде, мы должны очистить стек. Во второй строке мы помещаем в регистр edx адрес функции GetProcAddress, который мы сохранили ранее. Как уже упоминалось, после вызова функции, eax, ecx и edx, вероятно, будут изменены, поскольку они не сохраняются.

(Строки 3-13) Мы хотим вызвать «GetProcAddress (user32.dll,« SwapMouseButton »)», поэтому мы снова должны поместить строку в стек. Сначала в строке 3-4 мы устанавливаем регистр ecx в 0 и помещаем его в стек. Во-вторых, мы помещаем в стек «Тону». Строка «ton» представляет последние 3 байта строки «SwapMouseButton», но мы также помещаем символ «a». Это трюк, который мы можем использовать, и в строке 7 мы вычитаем 0x61 из стека из того места, где мы поместили этот символ «a». «A» - это 0x61, и это означает, что мы преобразовали символ «a» в NULL. Теперь, как и раньше, мы помещаем оставшуюся часть строки в стек. Мы нажимаем на регистр eax, который содержит базовый адрес user32.dll, и вызываем функцию GetProcAddress. Пожалуйста, обратите внимание, что вы можете делать это, как хотите, возможно, есть более простые способы, так что просто развлекайтесь!

Вызов функции SwapMouseButton
Круто, у нас есть адрес функции SwapMouseButton, нам просто нужно вызвать ее, используя параметр «true».

image15.png


(Строки 1-5) Я знаю, что это скучно, но мы должны очистить стек. Мы хотим вызвать «SwapMouseButton (true)», а значит «SwapMouseButton (1)», поэтому нам нужно поместить значение «1» в стек. Мы просто устанавливаем регистр ecx на 0 и увеличиваем его. Мы помещаем его в стек и вызываем функцию SwapMouseButton. Если вы хотите восстановить функциональность мыши, просто удалите инструкцию «inc ecx».

Получить адрес функции ExitProcess
Фу, мы сделали свое дело, но мы хотим изящно выйти из процесса, поэтому нам нужно найти функцию ExitProcess в kernel32.dll.

image8.png


(Строки 1-3) Опять же, уберите значение «1» из стека. Мы также получаем из стека данные, которые мы резервировали в начале, адрес функции GetProcAddress в регистре edx и базовый адрес kernel32 в регистре ebx.

(Строки 4-11). Как вы уже знаете, мы помещаем в стек строку «ExitProcessa» и заменяем последний символ «a» байтом NULL. Мы помещаем параметры в стек и вызываем GetProcAddress, чтобы получить адрес функции ExitProcess.

Вызвать функцию ExitProcess

Наконец, мы вызываем функцию ExitProcess следующим образом: «ExitProcess (0)».

image1.png


(Строки 1-3) Мы должны поместить «0» в стек, поэтому мы просто устанавливаем ecx в 0, помещаем его в стек и вызываем функцию ExitProcess. И это все.

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

image3.png

image14.png


И вот как мы написали наш первый (полезный) шелл-код!

Тестирование шеллкода
Мы можем проверить шелл-код с помощью следующего кода:

image16.png


Заключение
Я надеюсь, вы шаг за шагом узнали, как работает шелл-код Windows, и теперь вы сможете настроить код ASM, который мы создали в этой статье. Даже если этот шеллкод не делает что-то очень полезное, это хорошая отправная точка, чтобы написать его самостоятельно.

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

Источник: https://xss.pro
Переводчик статьи - https://xss.pro/members/177895/
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх