• Открыта запись на вторую часть курса по анонимности и безопасности в сети интернет "Paranoid II" от команды codeby. Анонимные роутеры, Подъём, настройка и администрирование Tor-ноды, Работа с железом ПК, Удаление аппаратных закладок, Минимизация рисков, Авторские разработки и многое другое. Подробнее ...

Статья Windows Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]

g00db0y

g00db0y

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


В предыдущей части мы нашли адрес Kernel32.dll, проходя через LDR структуру. В этой части мы сконцентрируемся на нахождении адреса функций (известных как DLL символы) в Kernel32.dll и вызовем их путем предоставления аргументов. Одним из методов вызова этих символов является использование LoadLibraryA с целью нахождения адреса этих символов от Kernel32.dll. Однако, мы этого делать не будем, т.к. это слишком легко. Основная цель моей серии блогов является помочь людям понять, как именно работает код сборки, чтобы было легко писать нулевой шелл-код для любой функции, которая им нужна. В этой части я объясню, как написать шелл-код для GetComputerNameA, и мы пройдемся по нашему исполняемому файлу в дизассемблере x32dbg, чтобы посмотреть имя хоста компьютера, на котором он запущен.

_start:

Итак, все, что у нас есть прямо сейчас, это адрес Kernel32.dll, 74F50000. Нашим следующим шагом будет нахождение адреса GetComputerNameA в Kernel32.dll и его вызов, предоставляя все необходимые параметры. В предыдущем посте я написал код на С, чтобы найти адрес этой функции из Kernel32.dll и мы знаем, что адрес в моей системе - 74F69AC0. Теперь, если мы вычтем адрес нашего kernel32.dll из 74F69AC0, мы получим 19AC0. Дело в том, что если бы я мог просто добавить это значение к адресу нашего kernel32.dll и попытаться запустить его, он запустился бы в моей системе, но, скорее всего, в любой другой системе он не заработает. Ситуация следующая, Microsoft продолжает обновлять kernel32.dll, и из-за этого в DLL могут быть добавлены новые функции/символы. По этой причине адрес нашей функции будет колебаться в зависимости от обновлений kernel32.dll. Поэтому мы не можем просто добавить этот адрес и попытаться заставить его работать. Таким образом, нам нужно будет динамически пройтись по всем символам внутри kernel32.dll, сравнить имя функции GetComputerNameA со всеми другими функциями внутри kernel32.dll в цикле, и когда мы нажимаем на эту строку сравнения, не смотря на адрес, который мы получили для этой строки, он должен быть нашим адресом, который мы можем добавить к адресу нашего kernel32.dll, чтобы получить базовый адрес нашей функции. Звучит просто? Давайте продолжим!

Теперь, чтобы по-настоящему понять структуру Kernel32.dll, давайте запустим и просмотрим содержимое DLL. Kernel32.dll находится в C:\Windows\System32, что мы можем увидеть ниже:

Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]



Как только мы открываем kernel32.dll, мы видим внутренние компоненты PE (переносимого исполняемого файла). Если вы кликните по IMAGE_DOS_HEADER с левой стороны, вы увидите то, что называется RVA (Relative Virtual Address), данные с некоторым содержимым в нем и описание с правой стороны.

Теперь, прежде чем мы углубимся в PEview, давайте сначала четко разберемся, что означают эти термины. RVA обозначает Относительный виртуальный адрес (Relative Virtual Address). Этот адрес не зависит от ОЗУ (RAM) и фиксируется до тех пор, пока DLL не будет перекомпилирована или изменена. Внутри Kernel32.dll есть несколько RVA. Например, если скажем, что RVA функции xyz в kernel32.dll равно 200, то базовый адрес функции становится 74F50000 + 200 = 74f50200. Это было просто теоретическое представление о том, как найти базовый адрес. Давайте сначала пройдемся по PEview и объясним, как найти базовый адрес GetComputerNameA. Обычно любой двоичный файл содержит много информации, особенно если это исполняемый файл. DLL и EXE-файлы входят в семейство исполняемых файлов. Теперь, начало любого исполняемого файла всегда содержит тонны информации относительно того, является ли это файлом DOS или файлом ELF, его MZ (Волшебное число (Magic number) для исполняемого файла) или любым другим форматом. Мы можем видеть, что у нас MZ на RVA00000000, что означает, что это исполняемый файл. Точно так же он содержит другую информацию, такую как идентификатор OEM, информация OEM и множество метаданных. Все это нам не интересно. Наша область интересов начинается с RVA0000003C или 0x3C вкратце. Если вы хотите понять, как работает PE Header, вы можете прочитать об этом .

На изображении выше показано, что смещение RVA по отношению к новому EXE Header (RVA of Offset to New EXE Header) имеет значение 0x3C и содержит другой адрес, т.е. F8. Теперь, если вы проверите левую часть в IMAGE_NT_HEADER-> Signature, это показывает, что в RVA F8 у нас есть IMAGE_NT_SIGNATURE, и это означает, что это отправная точка нашего PE Header. IMAGE_NT_HEADERS - это, в основном, структура Windows, в случай необходимости дополнительную информацию можно найти . Эта структура содержит 3 вещи. MSDN утверждает, что третье в этой структуре - IMAGE_OPTIONAL_HEADER, которая также является . Последний объект в этой структуре - IMAGE_DATA_DIRECTORY, и если вы посещаете для ознакомления с этой структурой, вы увидите, что он содержит виртуальный адрес (RVA) разных таблиц и их размер в структуре. Итак, самый простой способ объяснить это будет выглядеть примерно так.

RVA + size of that Table + size of address of the Size of Table = RVA of next Table



RVAРазмер таблицыИмя таблицы
10001500 (size of XYZ struct)XYZ
2504 (1000 + 1500 + 4)2000 (size of ABC struct)ABC
4508 (2504 + 2000 + 4)1900 (size of DEF struct)DEF
Диаграмма только для ознакомительных целей

Замечательно, вот, как мы подсчитываем RVA таблицы. Помните, что каждый адрес всегда будет иметь 4 байта, поэтому нам также нужно это добавить.

Все, что мы добавили - является просто случайными числа, которые были взяты для примера, чтобы вам было проще разобраться. Теперь, если мы углубимся в фактический адрес, вот как это будет происходить. Если вы переходите к PEview в IMAGE_OPTIONAL_HEADER, то первая таблица в IMAGE_DATA_DIRECTORY - это таблица EXPORT, а ее RVA - 170. В ней хранится RVA IMAGE_EXPORT_DIRECTORY, т.е. 972E0.

The IMAGE_EXPORT_DIRECTORY ответственна за хранение всей информации, относящейся к функциям в DLL. Как можно видеть из приведенного выше, изображение содержит три важные вводные:

  • RVA 972FC (972E0+0x1C) -> таблица адресов (Address Table) RVA содержит 97308, что является RVA EXPORT Address Table (ниже IMAGE_EXPORT_DIRECTORY)
  • RVA 97300 (972E0+0x20)-> Таблица указателей имен (Name Pointer Table) RVA содержит 98C14, что является RVA EXPORT Name Pointer Table (ниже EXPORT Address Table)
  • RVA 97304(972E0+0x24)-> таблица адресов RVA содержит 9A520, что является RVA EXPORT Ordinal Table (ниже EXPORT Name Pointer Table)

Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]


Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]


Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]



Прекрасно, позвольте мне объяснить, что означают эти таблицы.

  • Таблица экспортных адресов (Export Address Table) содержит RVAs, которые содержат RVA всех символов/функций в kernel32.dll;
  • Таблица экспортных указателей имен (Export Name Pointer Table) указывает на названия (строки) символов/функций в kernel32.dll и конечную порядковую величину функции;
  • Экспортная порядковая таблица (Export Ordinal Table) содержит нaчальную порядковую величину функции (которая всегда будет на единицу меньше конечной порядковой величины функции).
Поэтому для того, чтобы найти базовый адрес GetComputerNameA, нам нужно выполнить следующее:

  1. Найдите RVA of Offset to Exe Header (0x3C)(смещение RVA по отношению к Exe Header);
  2. Найдите RVA of Export Table (Таблицы экспорта) [170-F8(Start of PE Header) = 0x78];
  3. Найдите RVA of IMAGE_EXPORT_DIRECTORY (972E0) в RVA of Export Table;
  4. Найдите EXPORT Address Table’s RVA (Таблица экспортных адресов) (972E0 + 0x1C = 97308);
  5. Найдите EXPORT Name Pointer Table’s RVA (972E0 + 0x20 = 97300);
  6. Найдите (конечную стартовую величину) Ending Ordinal Value of GetComputerNameA in EXPORT Name Pointer Table путем организации циклов и сравнения строк в EXPORT Name Pointer Table (1DF);
  7. Вычтите 1 от конечной стартовой величины (Ending Ordinal Value), чтобы получить Стартовую порядковую величину (Starting Ordinal Value), ИЛИ организуйте цикл через Порядковую таблицу, чтобы найти Начальную Порядковую величину GetComputerNameA. (мы будем использовать метод вычитания, т.к. это легче выполнять в Asm) (1DF – 1 = 1DE);
  8. Добавьте EXPORT Address Table’s RVA к (Starting Ordinal Value * Size of Address [4]), чтобы получить RVA of RVA of GetComputerNameA (97308 + 1DE*4 = 97A80);
  9. Найдите RVA of GetComputerNameA в RVA of RVA of GetComputerNameA (RVA 97A80 contains 19AC0). Добавьте RVA of GetComputerNameA to the Base Address of Kernel32.dll (19AC0 + 74F50000 = 74F69AC0).
Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]


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

Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]



ХОРОШО! ЗДОРОВО! Теперь, возможно, вы захотите разобраться с вышеуказанными секторами, прежде чем идти дальше. Это может немного ввести вас в заблуждение, но если вы понимаете концепции должным образом, то сможете справиться с задачей без особых проблем. Теперь давайте приступим к написанию всего, что мы сделали выше в сборке. Сначала мы поместим имя функции в стек, который позже будем использовать для сравнения строк:

Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]



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

Код:
;;;;#push the function name on stack - GetComputerNameA
    mov edx, 0x41656d61     ;#ameA in reverse = Aema
    push edx                ;#Push to stack
    push 0x4e726574         ;#terN in reverse = Nret
    push 0x75706d6f         ;#ompu in reverse = upmo
    push 0x43746547         ;#GetC in reverse = CteG
    ;#Find the address of Export table and store it in ebx
    mov edx, [eax + 0x3c]       ;#Data at 0x3c is F8 which is moved to edx register: edx = F8
    add edx, eax                ;#add F8 to the base address of the kernel32.dll to get the RVA till the PE section: edx = 74F500F8
    mov edx, [edx + 0x78]       ;#add 0x78(170 - F8) to 74F500F8 to get RVA of Image Export Directory: edx = 972E0
    add edx, eax                ;#add 972E0 to the base address of the kernel32.dll(74F50000) to get the base address of Image Export Directory: edx = 74FE72E0
    ;#Find the address of the Export Name Pointer table and store it in ecx
    mov ecx, [edx + 0x20]       ;#add 74FE72E0 and 0x20(97300 - 972E0) to get the RVA of Export Name Pointer Table: ecx = 98C14
    add ecx, eax                ;#add 98C14 to the base address of kernel32.dll to get the base address of Export Name Pointer Table: ecx = 74FE8C14
    mov [ebp-4], ecx            ;#Moved the base address of Export Name Pointer Table to [ebp-4] variable on stack: [ebp-4] = 74FE8C14
    ;#Find the address of the Export Address table and store it in edx
    mov edx, [edx + 0x1c]       ;#add 74FE72E0 and 0x1c(972FC - 972E0) to get the RVA of Export Address Table: edx = 97308
    add edx, eax                ;#add 97308 to the base address of kernel32.dll to get the base address of Export Address Table: edx = 74FE7308
Следующим шагом обычно является прохождение цикла и сравнение строки в стеке со строками в Export Name Pointer Table. Но перед этим давайте рассмотрим пункт «Export Ordinal Table». Если мы снова перейдем к PEview, внутри Export Ordinal Table, мы увидим, что оно начинается с RVA 9A520, а порядковое значение начинается с 0004, т.е. AcquireSRWLockExclusive, и оно продолжает увеличиваться на единицу для каждой функции. Таким образом, если мы опустимся ниже до GetComputerNameA, конечное порядковое значение будет равно 1DF, и оно показывает, что начальное значение равно 1DE (в шестнадцатеричном формате) = 478 в десятичном виде. Поэтому мы можем просто запустить счетчик, который начинается с целого числа 4, и увеличивать его на единицу, пока строка GetComputerNameA не будет сопоставлена в Export Name Pointer Table [ebx-4]. Но здесь есть одна большая проблема. Все было бы хорошо, если и только если бы порядковые величины строились строго в хронологическом порядке. Мы можем видеть в Export Ordinal Table , что порядковое значение начинается с 4, но в Export Address Table оно начинается с 1. Если вы опуститесь ниже в Export Ordinal Table, вы поймете, что первые три значения Export Address Table (BaseThreadInitThunk, InterlockedPushListSList, Wow64Transition) были хаотично вставлены в Export Ordinal Table в случайных местах, и поэтому Export Ordinal Table начинается с 0004.

Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]


И, как вы можете видеть, он был случайно вставлен после 47-го значения в Export Name Pointer Table:

Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]


Однако, эти места фиксированы по всей ОС. Что нам нужно сделать, это вместо использования отдельного счетчика для порядковой величины применять тот же счетчик, который используется для запуска цикла, и как только мы найдем строку, мы просто исправим счетчик, добавив к нему 1 или 2 целых числа:

Код:
    ;;;;;#Find the address of GetComputerNameA in kernel32.dll
findproc:
    xor ecx, ecx                ;#empty ecx register for string comparison counter
    mov esi, esp                ;#move GetComputerNameA from stack to esi register
    mov edi, [ebp-4]            ;#move the base address of Export Name Pointer Table to edi: edi = 74FE8C14
    mov edi, [edi + ebx*4]      ;#base address of Export Name Pointer Table + Ordinal Value * 4: edi = RVA of the Function's Name(string) in Export Name Pointer Table
    add edi, eax                ;#add RVA of Name of Function(string) to base address of kernel32.dll to get the Name of the Function in Strings
    add cx, 8                   ;#move string length of GetComputerNameA to the cx register: len(GetComputerNameA) = 16 bytes = 8 WORD
    repe cmpsw                  ;#compare the number of words in cx register(8) in left to right order from edi register and esi register. Stores the output in ZF Flag
    jz findaddr                 ;#Jump to findaddr label(break loop) if ZF Flag is 1(TRUE), else continue
    inc ebx                     ;#increase the counter to calculate the ordinal value
    loop findproc               ;#loop findproc label
findaddr:
Если мы запустим это в x32dbg, то по окончанию цикла это будет выглядеть следующим образом:

Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]



Величина EBX, которая использовалась для запуска цикла, составляет 1DC вместо 1DE. Итак, нам нужно будет исправить счетчик, добавив 2, чтобы он стал 1DE, а затем мы можем приступить к извлечению адреса из него:

Код:
findaddr:
    add ebx, 2                  ;#patching the EBX counter to change 1DC to 1DE
    mov edi, [edx + ebx*4]      ;#74FE7308 + 1DE*4: edi = 19AC0
    add eax, edi                ;#add base address of kerneldll32 to above address to get GetComputerNameA address: eax = 74F69AC0

Итак, если вы производите запуск сейчас, то скорее всего вы должны увидеть название функции в вашем EAX регистре:

Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]


К сожалению, адрес моего Kernel32.dll изменился с 74F50000 на 76340000 при сборке этого руководства. Итак, мой последний адрес должен быть 74F5000000 + 19AC0 =74F69AC0, но вместо этого он стал 76340000 + 19AC0 = 76359AC0.


Превосходно! Теперь у нас есть адрес GetComputerNameA, осталось только выделить буфер для параметров этой функции. GetComputerNameA принимает :
  • Указатель на буфер с длиной MAX_COMPUTERNAME_LENGTH + 1 = 15 + 1 = 16 bytes
  • Указатель на DWORD, имеющего размер упомянутого выше буфера.
  • Итак, все, что нам нужно сделать, это выделить 16 байт в стеке, переместить DWORD в регистр, поместить указатель на этот буфер в стеке, а затем вставить указатель в регистр и после вызвать адрес, который мы сохранили выше в регистре EAX:
Код:
    ;;;;#Push parameters to stack - GetComputerNameA(LPSTR lpBuffer, LPDWORD nSize)
    sub esp, 16                 ;#allocate space for LPSTR lpBuffer = MAX_COMPUTERNAME_LENGTH + 1 = 15 + 1 = 16 bytes
    mov esi, esp                ;#move the stack pointer to lpBuffer in esi
    add ecx, 16                 ;#since ecx was xor'd above, its empty and we can use it to store the value 16
    push ecx                    ;#push DWORD nSize on stack
    push esp                    ;#push the pointer to nSize (LPDWORD nSize) on stack
    push esi                    ;#push the pointer to lpBuffer on stack
    call eax                    ;#Call the function GetComputerNameA
После выполнения всех упомянутых выше шагов, ваше имя хоста должно быть размещено в виде строки в регистр ESI, который использовался в качестве буфера выше:

Windows  Shellcoding x86 – функции вызова в Kernel32.dll – часть 2 [Перевод]



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

Вложения

Мы в соцсетях: