• 15 апреля стартует «Курс «SQL-injection Master» ©» от команды The Codeby

    За 3 месяца вы пройдете путь от начальных навыков работы с SQL-запросами к базам данных до продвинутых техник. Научитесь находить уязвимости связанные с базами данных, и внедрять произвольный SQL-код в уязвимые приложения.

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

    Запись на курс до 25 апреля. Получить промодоступ ...

Статья Windows Shellcoding x86 – поиск Kernel32.dll – часть 1 [Перевод]

Доброго времени суток, codeby.
Первоисточник:
Перевод: Перевод выполнен от команды Codeby


Добро пожаловать в ад на земле

Абсолютно верно, вы обратились по адресу. Для человека, который изучал Linux Shellcoding, а потом начал работать в Windows, поверьте мне, это будет намного сложнее, чем вы себе можете это представить. Ядро Windows полностью отличается от ядра Linux. Как уже говорилось, ядро Linux намного легче в понимании, чем ядро Windows, потому что оно имеет открытый исходный код и у него намного меньше свойств по сравнению с ядром Windows. С другой стороны, у Windows было столько вправок и переделывании за последние годы, в результате чего многое изменилось. Мы сконцентрируемся только на Windows 10 x86, хотя более ранние версии не намного отличаются. Есть огромное количество блогов по PEB LDR, но нет ни одного, который бы логически объяснял его важность. Многие люди просто вскользь упоминают windbg и думают, что вы моментально поймете выходной буфер. Основная причина, почему я решил написать этот блог, это пройти от С до ASM и понять, как это работает в бэкенде, когда Shellcoding в ASM x86.

section .data
:

Прежде чем начать заниматься частью Shellcoding, я бы рекомендовал разобраться с С, чтобы понять, как работает память, т.к. все, что мы будем делать, будет в - памяти. Знание типов данных Windows, таких как LPWSTR, LPSTR и других, будет вам очень полезно, поскольку это поможет вам понять, что:

Standard C != Windows C programming

На следующем этапе, необходимо знать основы Assembly x86. ASM по умолчанию является таким же, как в Linux или Windows, за исключением syscalls или API calls. Поэтому знание того, как работают регистры, является чрезвычайно важным.

Самое главное, понимать, как разбирать двоичные файлы. Для этого я, в основном, использую x32dbg и windbgx86. Я бы использовал оба из них для отладки, потому что есть некоторые вещи, которые вы не можете сделать в x32dbg, но они выполнимы в windbgx86 и наоборот. Итак, мы будем переключаться с одного на другое.

section .text
:

Прежде чем начать заниматься Shellcoding, важно понимать, как все работает на более низком уровне. Мы начнем с очень простого примера поиска текущего имени хоста системы. Давайте посмотрим на пример ниже, написанный, с использованием Windows API, на C:

1573414132106.png



На изображении, приведенном выше, я создал две переменные compName и compNameSize. Это будут аргументы, предоставленные функции GetComputerNameA. Помните, что есть две похожие функции GetComputerNameA и GetComputerNameW. W обозначает Широкие символы Юникода, в то время, как A обозначает строки ANSI CHAR. Мы будем использовать ANSI на протяжении всей серии блогов. Итак, ниже указано то, что должен сказать о функции GetComputerNameA:

BOOL GetComputerNameA(LPSTR lpBuffer, LPDWORD nSize);

Приведенный выше код показывает, что GetComputerNameA принимает LPSTR, что означает длинную строку указателя, а LPDWORD означает Двойное слово длинного указателя (Long Pointer Double Word). Размер слова - 16 бит, поэтому DWORD составляет 32 бит на всех платформах. Теперь, если вы скомпилируете вышеуказанную программу, используя g++, вы должны увидеть что-то вроде этого:

1573414159791.png


В самом начале нашей программы мы имеем #include <windows.h>, что, в свою очередь, означает, что библиотека windows будет включена в код и это динамически соединит зависимости по умолчанию. Однако, мы не можем поступать таким же образом с ASM. В случае с ASM нам нужно найти адрес расположения функции GetComputerNameA, загрузить аргументы в стек и вызвать регистр, имеющий указатель на функцию. Важно знать, что большинство функций windows доступны из трех основных библиотек: NTDLL.DLL, Kernel32.DLL и Kernelbase.DLL. Таким образом, всякий раз, когда вы выполняете какой-либо двоичный файл, это будет минимальные библиотеки DLL, которые всегда будут загружаться. Теперь, чтобы загрузить функцию GetComputerNameA, нам нужно выяснить, в каком DLL находится эта функция, и найти там ее базовый адрес. Давайте загрузим любой двоичный файл x86 в x32dbg и посмотрим, что он нам даст. Я буду загружать вышеупомянутый exe, который мы скомпилировали, однако вы можете загрузить любой произвольный 32-битный исполняемый файл, так как мы будем проходить только через DLL, упомянутые выше. Открыв вышеприведенный exe-файл в x32dbg и перейдя в раздел Log section, мы увидим эти три загруженные DLL и их конкретные адреса:

1573414197085.png



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

1573414211820.png



Если в поисковике вы ищете функцию GetComputerNameA, то там будет показано, что эту функцию загружает Kernel32.DLL. Также будет указан адрес 0x74F69AC0, где эта функция расположена. Все это хорошо в теории и отладке, а теперь давайте попытаемся сделать это через язык С, а затем через ASM. Вот следующие шаги, которые необходимо выполнять:

  1. Загрузите Kernel32.dll в память с функцией LoadLibraryA WinAPI
  2. Найдите адрес функци GetComputerNameA в пределах Kernel32.dll с использованием GetProcAddress
  3. Введите приведенное значение GetProcAddress в функцию WINAPI, которая принимает 2 аргумента (поскольку GetComputerNameA принимает лишь 2 аргумента).
  4. Создайте буфер для ComputerName и его длины

Выполните адрес как указатель на функцию.

1573414240529.png



Посещение страницы для LoadLibraryA, дает нам информацию о том, что он возвращает HMODULE, что, в свою очередь означает, что он возвращает дескриптор загруженному модулю. Таким образом, мы создали переменную hmod_libname. Точно так же GetProcAddress возвращает адрес для функции, загруженной из DLL. И нам нужно типизировать адрес, возвращенный из GetProcAddress, в функцию GetComputerNameA, чтобы он работал. Таким образом, мы создали typedef, который в основном является копией структуры функции GetComputerNameA. На изображении выше показано, как я загружаю библиотеку Kernel32.dll и использую GetProcAddress, чтобы найти базовый адрес функции GetComputerNameA и сохраняю адрес в GetComputerNameProc. Наконец, мы создаем две переменные CompName и CompNameSize и выполняем адрес, сохраненный в GetComputerNameProc, как указатель на функцию, используя (* GetComputerNameProc) и предоставляя ему необходимые переменные. Приведенный выше код также выводит на экран адрес функции GetComputerNameA. Давайте скомпилируем это и посмотрим, как это будет выглядеть:

1573414256488.png



Превосходно! Адрес 0x74F69AC0 – такой же, как мы обнаружили при отладке x32dbg обсуждаемой выше.


_start:

Восхитительно. А сейчас начнется самая забавная часть. Адреса всех библиотек DLL и их функций изменяются при перезагрузке и различаются в каждой системе. По этой причине мы не можем жестко закодировать любые адреса в нашем коде ASM. Однако главный вопрос остается. Как мы находим адрес самого kernel32.dll?

Помните, когда я сказал в начале, что Kernel32.dll, NTDLL.DLL and Kernelbase.dll загружаются в каждом exe? Да! Правда состоит в том, что эти DLL – довольно важная часть операционной системы и они загружаются каждый раз, когда что-либо выполняется. Таким образом, порядок загрузки этих DLL в память всегда один и тот же. Однако это может изменяться от системы к системе, что означает, что в ХР он может отличаться от того, который используется в Windows 10, но порядок загрузки всегда будет оставаться неизменным во всех версиях Windows 10.

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

  1. Найдите порядок загрузки Kernel32.dll
  2. Найдите адрес Kernel32.dll
  3. Найдите адрес GetComputerNameA
  4. Загрузите аргументы GetComputerNameA на стэк
  5. Вызовите указатель на функцию GetComputerNameA.

Звучит легко? Не так ли? Давайте пойдем дальше.

Найти адрес kernel32.dll не так-то и просто. Всякий раз, когда мы выполняем любой exe-файл, первое, что создается в ОС, это и .

Наш основной интерес находится здесь - это структура PEB (известная как LDR), поскольку именно здесь загружается вся информация, связанная с процессом. Здесь хранится все, от аргументов процесса до идентификатора процесса. В PEB есть структура, которая называется PEB_LDR_DATA, которая содержит три важные вещи. Это так называемые связанные списки или Linked Lists:

  1. InLoadOrderModuleList – порядок, в котором модули (exes или dlls) загружаются;
  2. InMemoryOrderModuleList – порядок, в котором модули (exes или dlls) хранятся в памяти;
  3. InInitializationOrderModuleList - порядок, в котором модули (exes или dlls) инициализируются в блоке среды процесса.
  4. Порядок относительно того, как модули загружаются в связанные списки (Linked Lists), исправлен. Это означает, что если мы сможем найти порядок kernel32.dll в приведенных выше списках, мы сможем найти адрес kernel32.dll и продолжить. Давайте сейчас запустим windbg x32. Если вы еще не установили windbg и его зависимости, вы можете найти небольшой блог по windbg , написанный ’ом. После того, как вы установили windbg, откройте любой exe, как мы делали ранее.
  5. Как только вы загрузите exe-файл в windbg, он сразу покажет вам определенные результаты. На данный момент мы проигнорируем результат и наберем .cls в командной строке ниже, чтобы очистить экран и начать заново. Теперь введите !рeb в командной строке и посмотрите, что мы здесь получим:
1573414327000.png



Как можно было видеть выше, мы получаем адрес LDR (PEB структура) - 779E0C40. Это очень важно, т.к. мы будем использовать этот адрес для вычисления адресов, чтобы продвинуться вперед. Далее, мы вводим команду dt nt!_TEB, чтобы обнаружить смещение структуры PEB.

1573414344484.png



Как мы можем видеть, _PEB имеет смещение 0x030. Аналогичным образом мы можем увидеть содержимое структуры _PEB, используя dt nt!_PEB

1573414361719.png


Смещение _PEB_LDR_DATA составляет 0x00c. Далее мы пытаемся найти, что находится внутри _PEB_LDR_DATA структуры. Мы можем сделать это следующим образом:

dt nt!_PEB_LDR_DATA.

1573414388906.png


Здесь мы видим, что смещение InLoadOrderModuleList составляет 0x00c, InMemoryOrderModuleList - 0x014, а InInitializationOrderModuleList - 0x01c. Также, если вы хотите видеть адреса, где находится каждый из этих списков, вы можете использовать адрес 779E0C40, который мы нашли выше (адрес LDR) вместе со следующей командой: dt nt!_PEB_LDR_DATA 779E0C40. Это покажет нам соответствующий начальный адрес и конечные адреса связанных списков, как показано ниже:


1573414418096.png


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

1573414434285.png


Итак, MSDN утверждает, что это тип LDR_DATA_TABLE_ENTRY, а не _LIST_ENTRY. Давайте попробуем просмотреть модули, загруженные в структуру, и мы также укажем начальный адрес этой структуры 0x7041e8, чтобы можно было видеть базовые адреса загруженных модулей. Помните, что 0x7041e8 является адресом этой структуры, поэтому первая запись будет на 8 байтов меньше этого адреса. Таким образом, наша команда будет выглядеть следующим образом:

dt nt!_LDR_DATA_TABLE_ENTRY 0x7041e8-8

1573414447751.png


Как видите, первое имя BaseDllName - это gethost.exe. Это exe, который я выполнил. Кроме того, вы можете увидеть, что адрес InMemoryOrderLinks теперь следующий - 0x7040e0. DllBase по смещению 0x018 содержит базовый адрес BaseDllName. Теперь наш следующий загруженный модуль должен быть на расстоянии 8 байтов от 0x7040e0, а именно 0x7040e0-8.

dt nt!_LDR_DATA_TABLE_ENTRY 0x7040e0-8

1573414461449.png


Великолепно! Итак, наш второй модуль - ntdll.dll, его адрес 0x778c000, а следующий модуль находится через 8 байт после 0x704690. Поэтому наша следующая команда будет выглядеть так:

dt nt!_LDR_DATA_TABLE_ENTRY 0x704690-8

1573414477508.png


Превосходно! Наш третий модуль - Kernel32.dll, его адрес - 0x74f50000, смещение составляет 0x018. Такой порядок загрузки модуля всегда будет фиксированным, по крайней мере для Windows 10,7, 8 и 8.1. Поэтому, когда мы пишем на ASM, мы можем пройти через всю структуру PEB LDR и найти адрес Kernel32.dll и загрузить его в наш шеллкод. Подобным образом вы можете найти адрес Kernelbase.dll, который является четвертым модулем.

1573414491411.png


Замечательно! Теперь давайте суммируем наши действия относительно того, что нам нужно делать:

  1. PEB расположен по смещению 0x030 от основного регистра сегмента файла;
  2. LDR находится по смещению PEB + 0x00C;
  3. InMemoryOrderModuleList находится по смещению LDR + 0x014;
  4. Первый модуль Entry - это сам exe;
  5. Второй модуль Entry - это ntdll.dll;
  6. Третий модуль Entry - это kernel32.dll;
  7. Четвертый модуль Entry - это Kernelbase.dll

Наша основная сфера интересов на данный момент это Kernel32.dll. Каждый раз, когда вы загружаете DLL, адрес сохраняется со смещением DllBase, равным 0x018. Наш начальный адрес связанных списков будет сохранен в смещении InMemoryOrderLinks, которое составляет 0x008. Таким образом, разница смещений будет DllBase - InMemoryOrderLinks = 0x018 - 0x008 = 0x10. Следовательно, смещение Kernel32.dll будет LDR + 0x10. Детальное понимание можно найти на изображении ниже, которое я позаимствовал .

1573414545674.png


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

1573414559242.png


Давайте скомпилируем это с NASM следующим образом и загрузим его в x32dbg. Вы можете скачать NASM, перейдя по этой ссылке .

1573414591309.png


По сути, после выполнения нашей последней инструкции должен загрузиться адрес Kernel32.dll в регистр EAX, и вы можете проверить то же самое как в x32dbg, так и в windbg, используя команду lmcommand: 74F50000, которая является адресом Kernel32.dll.

Теперь, когда у нас есть адрес kernel32.dll, следующий шаг - найти адрес GetComputerNameA с помощью LoadLibraryA и вызвать функцию. К сожалению, этот блог стал слишком большим, и я должен продолжить это в моем следующем посте. В следующем посте мы завершим наш полный код ASM для извлечения имени компьютера и выведении его на экран.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!