Статья SYSENTER – скрытый запуск Native-API

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

Оглавление:


1. Инструкция SYSENTER – общие сведения.
2. SSDT – System Service Descriptor Table.
3. Разбор таблицы-экспорта библиотеки Ntdll.dll.
4. Программа поиска нативных сервисов.
5. Вызов функций посредством SYSENTER.
6. Заключение.
-----------------------------------------------------------

Общие сведения

Жизненный цикл прикладных задач проходит на уровне подсистемы Win32, в состав которой входят библиотеки user32.dll, shell32.dll, kernel32.dll и другие с суффиксами(32). Однако имеются и глобальные вопросы, которые должна регистрировать в своих недрах ОС и вести над ними учёт. В таких случаях, коду нашего приложения необходимо перейти внутрь защищённого периметра, т.е. в старшую половину адресного пространства, выше 0x7FFFFFFF. Здесь на помощь приходят Native-функции с префиксами Nt и Zw, из ядерной библиотеки Ntdll.dll.

Исторически, для перехода из юзера в кернел был предусмотрен шлюз, который обслуживало прерывание INT-2Eh. Как это принято, обработчик любого прерывания находится в памяти ОЗУ, поэтому переход посредством шлюза работал очень медленно – для доступа к памяти требовалось ждать освобождения её шины, да и скорость самого обмена оставляет желать лучшего (см.тайминги памяти). В результате, переход в ядро превращался в рутину и срочно требовалось решение этой проблемы.

Позже, начиная с процессоров Pentium-II и системы Win-XP, для передачи управления в кернел была введена специальная инструкция процессора SYSENTER (опкод 0F34h). Она использует уже не память, а три MSR-регистра с номерами 174,175,176h. Поскольку обмен с регистрами происходит намного быстрее, нововведение позволило на порядок (~40%) повысить скорость выполнения задачи. Обратно юзеру, возвращает управление сопутствующая ей инструкция SYSEXIT.

Рассмотрим случай, когда наша программа планирует создать файл или открыть какое-нибудь устройство, типа жёсткий диск. Для этого предусмотрена функция CreateFile() из библиотеки kernel32.dll, которую (в зависимости от параметра dwCreationDistribution), можно использовать как для создания, так и для открытия существующего файла. Обработчик этой функции не создаёт файл, а лишь производит абсолютно не нужные нам различные проверки, после чего kernel32 передаёт управление в нативную функцию NtCreateFile() библиотеки ntdll.dll.

В свою очередь, эта функция так-же не оправдывает надежд и только делает вид, что занимается созданием долгожданного файла, а по факту – кладёт в регистр EAX номер-сервиса создания файла (в Win7 это код 42h), и посредством инструкции SYSENTER вручает его исполнительному ядру Ntoskrnl.exe. В технической литературе, номер системного сервиса назвали SSN или "System Service Number". Для каждой из задач типа создание файла/процесса/потока и прочее, в системе зарегистрирован свой сервис, а информация о них собрана в ядерной таблице SSDT – "System Service Descriptor Table". Рисунок ниже представляет сказанное в визуальной форме, где красной стрелкой выделен наш план:

Sysenter_01.png


Здесь видно, что вызов нами функции CreateFile() транзитом проходит через kernel32.dll и Ntdll.dll, после чего уходит в нёдра Ntoskrnl.exe. Прибывший верхом на инструкции SYSENTER код-сервиса в регистре EAX указывает ядру, какой именно сервис мы запрашиваем, и по этому коду ядро берёт из своей таблицы SSDT адрес обработчика этого сервиса и кол-во его аргументов. На выходе из обработчика, через инструкцию SYSEXIT в том-же регистре EAX система возвращает нам дескриптор Handle созданного файла, который в последующем мы можем использовать для обращения к нему.

Суть данной статьи в том, что если мы сможем пропарсить таблицу SSDT и вытащить из неё нужные нам номера системных сервисов SSN, это даст возможность вызывать нативные API-функции напрямую через SYSENTER, вообще не обращаясь к поднадзорным системой библиотекам kernel32 и Ntdll.dll. Основная проблема здесь в том, что SSDT находится в пространстве ядра и у нашего/смертного приложения нет прямого доступа к ней. Во-вторых – номера сервисов сильно отличаются от версии к версии Win, поэтому придётся искать их динамически в зависимости от того, в какую среду забросит судьба наш шелл-код.

Системные вызовы, для которых требуется переход в ядро можно условно разделить на 4 основных категорий. Во-всех этих случаях, ОС создаёт структуры (или делает отметки в уже существующих), чтобы в последующем пристально держать их под своим колпаком:

1. Контроль над процессом

• создать, загрузить, выполнить, прекратить процесс;
• получить/установить атрибуты процесса;
• выделить или освободить память;
• ждать события, подать сигнал о событии.

2. Управление файлами и устройствами
• создать/открыть, читать/писать, закрыть/удалить файл или устройство;
• получить/установить их атрибуты.

3. Общение
• создать/удалить коммуникационное соединение;
• отправить/получить сообщения;
• информация о статусе передачи;
• подключить или отключить удалённые устройства.

4. Информационное обслуживание
• получить/установить системные данные;
• получить/установить время или дату.

SSDT – System Service Descriptor Table

Системные сервисы работают под управлением их диспетчера, в прямом подчинении которого имеются две таблицы SSDT – затенённая (shadow) и открытая. Нас будет интересовать только открытая таблица, а она в свою очередь включает в себя две таблицы: KiServiceTable и KiArgumentTable. В первой, по именам отсортированы все имеющиеся в системе нативные API-функций, с указанием точек-входа в них. Вторая таблица жёстко привязана к первой, и хранит кол-во аргументов к каждой из перечисленных в первой таблице функций. Индекс (порядковый номер) каждой записи в этих таблицах и назвали SSN – именно он передаётся инструкции SYSENTER в регистре EAX. Вот-что думает на этот счёт отладчик WinDbg:

SSDT-table.png


Таким образом, никакого SSN в реале не существует – это лишь индекс в таблице KiServiceTable начиная с нуля. Например, если инструкции SYSENTER передать номер-сервиса EAX=1, это приведёт к вызову функции NtAccessCheck(); значение в регистре EAX=8 дёрнет функцию NtAddAtom() и т.д. Если в окне отладчика WinDbg прокрутить список дальше, то под порядковым номером 42h можно обнаружить подопытную нашу функцию NtCreateFile(). В таблице SSDT имеется поле с общим числом зарегистрированных в системе Nt-функций – в данном случае этот счётчик равен 191h=401 (см.третью сверху запись на рис.выше, система Win7 sp2).

Повторюсь, что номера системных сервисов отличаются не только в разных версиях Win-XP/Vista/7/8/10, но и внутри их сборок (build) и сервис-паков SP. Связано это с тем, что мелкософт без уведомления может запросто добавить или удалить какие-нибудь функции из этого списка (они это часто практикуют), в результате чего остальные записи дружненько съезжают с насиженных мест. Как говорится, проблемы индейцев шерифа не волнуют.

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

Закрылось ядро в себе – ну и ладно. Зато его хвост остался снаружи в виде библиотеки Ntdll.dll, которая проецируется системой в память любого пользовательского приложения (см.карту памяти в отладчике). Код именно этой библиотеки загружает в регистр EAX номер-сервиса, и посредством инструкции SYSENTER передаёт его ядру. Вот-она лазейка, за которую можно ухватиться и вытянуть все SSN-номера! Достаточно пропарсить экспорт этой библиотеки, по-ходу проверяя точки-входа во все функции, на загрузку некоторого значения (аля ssn) в регистр EAX.

На рисунке ниже представлен дизассемблерный листинг обработчиков где видно, что инструкция MOV EAX,xxx всегда будет иметь 1-байтный опкод В8h и дальше некоторое значение (выделены зелёным), которое есть ничто-иное, как номер сервиса SSN. На 32-битных системах, любой обработчик Nt-функции начинается с этой инструкции, а значит байт со-значением В8h у точки-входа, может послужить нам сигнатурой поиска:

HDasm.png


Функции с префиксами "Nt" и "Zw" имеют одинаковые точки-входа, хотя разные ординалы (порядковые номера) внутри библиотеки. Поэтому их сервисные номера SSN так-же будут одинаковы, что собственно и демонстрирует скрин выше (Zw' введена для драйверов). Всё-что нужно будет сделать нашему коду, это в цикле обойти всю таблицу-экспорта Ntdll.dll и прочитав из точек первые 4-байта, проверить младший из них на опкод В8h. Если совпадёт, значит это инструкция MOV EAX,ххх и следующие 2-байта будут SSN найденной функции. Всю цепочку вызовов Native-API более детально раскрывает отладчик:

WinDbg_Ntdll.png


Смотрим, что делает обработчик функции из Ntdll.dll, получив управление от kernel32.dll..
Сразу-же в регистр EAX пересылается номер запрашиваемого сервиса SSN (в данном случае 42h), после чего в регистр EDX заносится адрес стаба KiFastSystemCall с последующим его вызовов инструкцией CALL dword[EDX]. В результате, с загруженным регистром EAX мы попадаем в стаб SYSENTER, внутри которого регистр EDX перезаписывается новым значением, указывающим на предварительно оформленный нами стек, с аргументами вызываемой функции.

Последующий алгоритм нас уже мало интересует, поскольку без драйвера мы не сможем в него вмешаться. Тут на аппаратном уровне идёт переключение со-стека пользователя на стек ядра (его адрес хранится в сегменте TSS текущей задачи) и со-всем юзерским барахлом управление получает ядро Ntoskrnl.exe. Другими словами, мы можем модифицировать только то, что находится в нижней половине адресного пространства памяти, до 0х7FFFF000.

Разбор "таблицы-экспорта" библиотеки Ntdll.dll

Выше упоминалось, что Ntdll.dll уже висит в памяти нашего приложения, а значит мы имеем к ней полный доступ. Главное найти её базу в памяти, и здесь препятствует нам рандоммно меняющий эту базу системный механизм ASLR (Address Space Layout Randomization). Поэтому проще воспользоваться функцией GetModuleHandle(). Получив долгожданную базу, сразу-же наткнёмся на стандартную для файлов exe/sys/dll сигнатуру "MZ", и сместившись от неё на 0х3С получим указатель на РЕ-заголовок библиотеки. Дальше, не заморачиваясь с мелочами, прыгаем по смещению 0х78 от РЕ-заголовка на DATA-DIRECTORY, и взяв от туда первый-же указатель, становимся обладателем адреса таблицы-экспорта:

Data_Dir.png


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


C-подобный:
struct EXPORT_TABLE
  Flags                dd  0      ;//
  TimeStamp            dd  0      ;//
  Version              dd  0      ;//
  NameRVA              dd  0      ;// имя образа (exe/dll)
  OrdinalBase          dd  0      ;//
  NumOfFunction        dd  0      ;// всего функций-экспорта
  NumOfNamePointers    dd  0      ;// всего указателей на имена функций
  AddressTableRVA      dd  0      ;// указатель на таблицу адресов
  NamePointersRVA      dd  0      ;// указатель на таблицу имён
  OrdinalTableRVA      dd  0      ;// указатель на таблицу ординалов
ends

Первые три поля нам не интересны, а в четвёртом хранится указатель на строку с именем текущей библиотеки – в нашем случае это будет Ntdll.dll. В поле "OrdinalBase" зашито значение, с которого начинаются номера ординалов – в большинстве случаях это 1, но не факт. А вот дальше уже интересней.. Если комбинацией [Ctrl+Q] в "Total Commander" просмотреть файл Ntdll.dll, то можно найти в нём таблицу-экспорта с перечислением всех функций с их ординалами и точками-входа.

Как видим на рис.ниже, библиотека экспортирует 8 безликих функции, поэтому в "таблице-экспорта" имеется два счётчика с префиксами "Num". Я буду перебирать только именованные из них, так-что выбираю счётчик "NumOfNamePointers". Более того, либа выдаёт на экспорт не только нужные нам Nt-функции, но и огромное кол-во других, которые не зарегистрированны в таблице SSDT (не имеют SSN) – например с префиксами Rtl, Ldr, Dbg и т.д. Значит при поиске имён, нужен будет фильтр по маске (Nt).

Export.png


Дальше, в таблице-экспорта идут 3 сырых RVA-указателя на таблицы: Address, Name и Ordinal. Важно понимать, что в этих таблицах так-же лежат указатели, а не готовые имена/адреса/ординалы. В идеале, чтобы обойти всю таблицу-экспорта, мы должны в цикле передвигать всю/эту тройку, тогда получим такую-же распечатку, как в окне тотала выше.

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

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

C-подобный:
struct PE_HEADER
  Signature          dd  0    ;// PE = 50 45 00 00
  Machine            dw  0
  NumberOfSection    dw  0
  TimeStamp          dd  0
  SymbolPointer      dd  0
  SymbolSize         dd  0
  OptionalHdrSize    dw  0
  Flags              dw  0
;//--- OPTIONAL HEADER -------------------
  Magic              dw  0
  LinkerVersion      dw  0
  SizeOfCode         dd  0
  SizeOfInitData     dd  0
  SizeOfUnInitData   dd  0
  EntryPointRVA      dd  0
  CodeBaseRVA        dd  0
  DataBaseRVA        dd  0
  ImageBase          dd  0
  SectionAlign       dd  0
  FileAlign          dd  0
  OsVersion          dd  0
  ImageVersion       dd  0
  SubSysVersion      dd  0
  Reserved           dd  0
  ImageSize          dd  0
  HeaderSize         dd  0
  FileChecksum       dd  0
  SubSystem          dw  0
  DLLflags           dw  0
  StackReserve       dd  0
  StackCommit        dd  0
  HeapReserve        dd  0
  HeapCommit         dd  0
  LoaderFlag         dd  0
  NumberDataDir      dd  0
;//--- DATA DIRECTORY --------------------
  ExportRVA          dd  0,0
  ImportRVA          dd  0,0
  ResourceRVA        dd  0,0
  ExceptionRVA       dd  0,0
  SecurityRVA        dd  0,0
  FixUpRVA           dd  0,0
  DebugRVA           dd  0,0
  ArchitectureRVA    dd  0,0
  GlobalRVA          dd  0,0
  TlsRVA             dd  0,0
  LoadConfigRVA      dd  0,0
  BoundImportRVA     dd  0,0
  IatRVA             dd  0,0
  DelayImportRVA     dd  0,0
  COMdescriptor      dd  0,0
  Reserved1          dd  0,0
ends

struct SECTION_TABLE
  ObjectName           rb  8      ;// имя секции
  VirtualSize          dd  0      ;// размер в памяти
  VirtualOffsetRVA     dd  0      ;// адрес в памяти
  PhysicalOffsetRVA    dd  0      ;// адрес на диске
  Reserved             rb  12     ;//
  SectionFlags         dd  0      ;// флаги секции
ends

;//**************************************************
struct EXPORT_TABLE
  Flags                dd  0      ;//
  TimeStamp            dd  0      ;//
  Version              dd  0      ;//
  NameRVA              dd  0      ;// имя образа (exe/dll)
  OrdinalBase          dd  0      ;//
  NumOfFunction        dd  0      ;// всего функций
  NumOfNamePointers    dd  0      ;// всего указателей на имена функций
  AddressTableRVA      dd  0      ;// указатель на таблицу адресов
  NamePointersRVA      dd  0      ;// указатель на таблицу имён
  OrdinalTableRVA      dd  0      ;// указатель на таблицу ординалов
ends

struct IMPORT_TABLE
  ImportLookUp         dd  0
  TimeStamp            dd  0
  FotwardChain         dd  0
  NameRVA              dd  0
  AddressRVA           dd  0
ends

struct TLS_TABLE
  StartBlockVA         dd  0
  EndBlockVA           dd  0
  IndexVA              dd  0
  CallBackTableVA      dd  0
ends

struct RESOURCE_TABLE
  Flags                dd  0
  TimeStamp            dd  0
  Version              dd  0
  NameCount            dw  0
  IdCount              dw  0
ends

struct FIXUP_TABLE
  PageRVA              dd  0
  BlockSize            dd  0
  RecordOffset         dw  0
ends

•• Практика ••
1. Программа поиска SSN нативных функций

В первой демке приводится код, который соберёт все номера-сервисов SSN с указанием их функций, из уже загруженной в память библиотеки Ntdll.dll. Эти номера будут в точности совпадать с теми, что лежат в недоступной нам таблице SSDT. Данный код вернёт валидные данные только если будет запущен на любой 32-битной платформе, поскольку на x64 обработчики Nt-функций оформлены иначе и сигнатура В8h уже не сработает.

По этой причине, нужно будет при старте программы определить платформу 32 или 64 – проблему решает функция IsWow64Process(). Она возвращает 1, если 32-битный код запущен на 64-битной системе под оболочкой WOW64 (Windows on Windows), или нуль в противном случае. То-есть нуль будет означать, что система х32. Остальные моменты закомментированы в коде, поэтому повторяться не буду. Вот пример:


C-подобный:
format   pe console
include 'win32ax.inc'
include 'equates\pestruct.inc'    ;// подключаем РЕ-инклуд  
entry    start
;//----------
.data
counter    dd  0     ;// под счётчик найденных функций
pe         dd  0     ;// под указатель на РЕ-заголовок
base       dd  0     ;// под базу образа DLL
fnName     dd  0     ;// под указатель на имя функций
buff       db  0     ;// под всякое барахло..

;//----------
.code
start:
;// Обзовём окно и проверим на 64-бит систему ======================
        invoke  SetConsoleTitle,<' .:: Parse SSDT ::.',0>
        invoke  IsWow64Process,-1,buff
        cmp     [buff],1
        jne     @ok
       cinvoke  printf,<10,' ERROR! 64-bit system is not supported.',0>
        jmp     @exit

;// Получаем базу Ntdll.dll в памяти ===============================
@ok:    invoke  GetModuleHandle,<'ntdll.dll',0>
        mov     [base],eax

;// Собираем инфу из РЕ-заголовка библиотеки =======================
        xchg    esi,eax                         ;// ESI = база
        mov     esi,[esi+0x3c]                  ;// ESI = RVA-указатель на РЕ-заголовок
        add     esi,[base]                      ;//  ..(преобразовать в VA-адрес)
        mov     [pe],esi                        ;// запомнить адрес РЕ-заголовка

        mov     eax,[esi+PE_HEADER.ExportRVA]   ;// РЕ + 78h
        add     eax,[base]                      ;// EAX = указатель на таблицу-экспорта
        mov     ebx,[eax+EXPORT_TABLE.NameRVA]
        add     ebx,[base]                      ;// EВX = указатель на имя DLL-библиотеки

       cinvoke  printf,<10,' Module name....: %s',\
                        10,' Base address...: 0x%X',\
                        10,' Export table...: 0x%X',10,0>,ebx,[base],eax

;// Ищем функции и их SSN-номера в таблице-экспорта ===============
       cinvoke  printf,<10,' Find SYSENTER EAX-code...',10,\
                        10,' EAX  Native function',\
                        10,' ----------------------------',0>

        mov     esi,[pe]
        mov     ebx,[esi+PE_HEADER.ExportRVA]
        add     ebx,[base]                               ;// EBX = указатель на таблицу-экспорта
        mov     esi,[ebx+EXPORT_TABLE.NamePointersRVA]
        add     esi,[base]                               ;// ESI = адрес таблицы-указателей имён
        mov     ecx,[ebx+EXPORT_TABLE.NumOfNamePointers] ;// ECX = всего именованных функций в DLL

@@:     lodsd                      ;// EAX = RVA-указатель на очередное имя функции из ESI
        add     eax,[base]         ;//   ..делаем из него виртуальный адрес.
        cmp     word[eax],'Nt'     ;// проверить имя на маску "Nt"
        jne     @fuck              ;// если нет..
        mov     [fnName],eax       ;// иначе: запомнить указатель для вывода на консоль
        push    esi ecx            ;//
        invoke  GetProcAddress,[base],eax  ;// получить точку-входа в функцию
        pop     ecx esi            ;//
        mov     ebx,[eax]          ;// EBX = первые(4) опкода обработчика функции
        cmp     bl,0xb8            ;// мл.байт = B8h, значит это [mov eax,] – наш клиент!
        jne     @fuck              ;// если нет..
        shr     ebx,8              ;// иначе: сдвинуть на байт вправо (удалить опкод B8h)
                                   ;// EBX = номер SSN найденной функции!
        push    esi ecx
       cinvoke  printf,<10,' %03X  %s',0>,ebx,[fnName]  ;// вывести номер и имя функции
        pop     ecx esi
        inc     [counter]          ;// увеличить счётчик найденных

@fuck:  loop    @b                 ;// повторить цикл ЕСХ-раз..

       cinvoke  printf,<10,' -------------------------------',\
                        10,' Found services: %d',0>,[counter]

@exit: cinvoke  gets,buff
       cinvoke  exit,0
;//----------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import   msvcrt, printf,'printf',gets,'gets',exit,'exit'
include 'api\kernel32.inc'

EAX-code.png


Если вернуться к рисунку(2) то там упоминалось, что на данной системе Win7-SP2 в таблице SSDT зарегистрированы 191h=401 функций. Как показывает скрин, программа определила их все, значит алгоритм рабочий и нам остаётся лишь выбирать из этого списка нужные SSN для своей задачи. В качестве маски поиска, вместо "Nt" можно указать "Zw" и результат будет точно такой-же, т.к. точки-входа у этих функций совпадают.

2. Поиск SSN по фильтру, и вызов их посредством SYSENTER

Код следующей программы продемонстрирует, как из всей/этой портянки найти только SSN нужных нам API. В качестве примера, при помощи натива я создам файл на диске, запишу в него данные, после чего закрою дескриптор. Значит будут нужны номера сисколов: NtCreateFile(), NtWriteFile() и NtClose() – остальные отправляем в игнор.

Чтобы не сравнивать строки целиком (они обнаружат себя в секции-данных), имеет смысл вычислить их 16-битную контрольную сумму. То-есть просто складываем все коды ASCII-символов строки, и получаем 2-байтное число. Теперь взломщику придётся осуществлять только брутфорс, чтобы понять, какую именно из API-функций мы используем в своём коде.

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

1. В ядре нет привычных нам ANSI-строк – все строки только UNICODE. Поэтому всё-что планируется передать Native-функциям в аргументах, нужно переводить в юникод. Для этого, к каждому символу тупо добавляем нуль, в результате чего строка становится в 2-раза длиннее. В данном случае, это касается имени создаваемого файла в аргументе NtCreateFile(). Причём этой функции нужно указывать только полный путь, в формате: \??\C:\temp\example.txt

2. Буфер с именем файла обязательно должен быть выровнен на 4-байтную границу, иначе функция создания файла будет возвращать ошибку "Неверный аргумент" 0xC000000D. Более того, требуется указать длину Unicode-строки с именем файла, и что особенно важно – без терминального нуля! В своё время мне пришлось долго разбираться в этих мелочах, зато теперь этот опыт "записался в кэш".

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


Win_Native.png


4. Оформив в стеке подобающим образом все аргументы, нужно будет загнать в него и адрес-возврата, чтобы вдохнуть жизнь в код после инструкции SYSENTER. Так вот, этот адрес-возврата требуется помещать 2-раза, а не один. Я так и не понял, почему именно два, поскольку логическая ветвь уходит в ядро (просто примем это как должное). Взяв SYSENTER в свои руки, мы берём и всю ответственность за него, ..в том числе и очистку стека от аргументов, включая лишний адрес-возврата. Например если у NtCreateFile() 11-аргументов, то выталкивать из стека нужно 12.

Здесь перечислены лишь основные моменты программирования в Native-API, хотя в полной красе проблемы раскрываются только на практике. Чем созирцать это всё со-стороны, приведём наглядный пример, который из таблицы-экспорта Ntdll.dll вытащит номера SSN только нужных нам функций, снимая хэш с их имён и сравнивая его с заранее заготовленным. Код работоспособен на любой 32-битной системе Windows, поскольку вычисляет SSN динамически. Его можно воспринимать как скелет "дроппера" – программы, которая скрытно создаёт на машине жертвы какой-нибудь файл:


C-подобный:
format   pe console
include 'win32ax.inc'
include 'equates\pestruct.inc'
entry    start
;//----------
.data
struct OBJECT_ATTRIBUTES     ;// структура, для передачи атрибутов в NtCreateFile()
   Length                    dd  sizeof.OBJECT_ATTRIBUTES
   RootDirectory             dd  0
   ObjectName                dd  0
   Attributes                dd  40h
   SecurityDescriptor        dd  0
   SecurityQualityOfService  dd  0
ends

struct UNICODE_STRING          ;// структура описывает UNICODE-строку
   Length        dw  0
   MaxLength     dw  255
   Buffer        dd  0
ends

oa       OBJECT_ATTRIBUTES
us       UNICODE_STRING
iosb     dd  0,0,0             ;// Io_Status_Block. Сюда возвращается статус операций

create   dd  'N'+'t'+'C'+'r'+'e'+'a'+'t'+'e'+'F'+'i'+'l'+'e'  ;// контрольная сумма имени fn.
write    dd  'N'+'t'+'W'+'r'+'i'+'t'+'e'+'F'+'i'+'l'+'e'
close    dd  'N'+'t'+'C'+'l'+'o'+'s'+'e'
NtCreate dd  0                 ;// под SSN-номера сисколов
NtWrite  dd  0
NtClose  dd  0

Status   db  10,' Return code: %08Xh',0
base     dd  0
fHndl    dd  0

fName    db  '\??\D:\Sysenter.txt',0    ;// имя создаваемого файла
fLen     =   $ - fName                  ;// его длина
align    4                              ;// выравнивание на 4-байт границу
buff     db  0                          ;// здесь будет UNICODE-строка
;//----------
.code
start:
        invoke  SetConsoleTitle,<' .:: SYSENTER Example ::.',0>
        invoke  IsWow64Process,-1,buff
        cmp     [buff],1
        jne     @ok
       cinvoke  printf,<10,' ERROR! 64-bit system not support.',0>
        jmp     @exit

@ok:    invoke  GetModuleHandle,<'ntdll.dll',0>
        mov     [base],eax      ;// база библиотеки,
        xchg    esi,eax         ;//   ...в регистр ESI
        mov     esi,[esi+0x3c]  ;//
        add     esi,[base]      ;// ESI = РЕ-заголовок

        mov     ebx,[esi+PE_HEADER.ExportRVA]
        add     ebx,[base]
        mov     esi,[ebx+EXPORT_TABLE.NamePointersRVA]
        add     esi,[base]
        mov     ecx,[ebx+EXPORT_TABLE.NumOfNamePointers]

;//==== ESI = указатель на таблицу имён, ECX = их счётчик. ====================
;//==== Начинаем поиск нужных функций в таблице-экспорта Ntdll.dll ============
@findFunctionSSN:
        lodsd                   ;//
        add     eax,[base]      ;// указатель на очередное имя
        cmp     word[eax],'Nt'  ;// сравнить с маской
        jne     @fuck           ;// прокол..
        push    eax esi ecx     ;//
        invoke  GetProcAddress,[base],eax   ;// иначе: EAX = точка-входа
        pop     ecx esi ebx     ;// EBX = указатель на очередное имя
        mov     edx,[eax]       ;// EDX = первый DWORD из точки-входа
        cmp     dl,0xb8         ;// проверить на сигнатуру SSN
        jne     @fuck           ;// прокол..

        shr     edx,8           ;// иначе: EDX = SSN функции
        push    esi
        xchg    esi,ebx         ;// ESI = указатель на имя
        xor     ebx,ebx         ;// очистить регистры
        xor     eax,eax         ;//   ^^^^^
@hash:  lodsb                   ;// считаем контрольную сумму очередного имени,
        add     ebx,eax         ;//    ..в регистр EBX,
        or      al,al           ;//        ..пока не встретим нуль.
        jnz     @hash           ;//
        pop     esi

        cmp     ebx,[create]    ;// сравить полученную контр.сумму с валидной, из секции-данных
        jne     @f              ;// вниз, если промах..
        mov     [NtCreate],edx  ;// иначе: запомнить в переменной её SSN.

@@:     cmp     ebx,[write]     ;//
        jne     @f              ;//
        mov     [NtWrite],edx   ;//

@@:     cmp     ebx,[close]     ;//
        jne     @fuck           ;//
        mov     [NtClose],edx   ;//

@fuck:  loop    @findFunctionSSN  ;// продолжить поиск имён, по длинне ECX..
        nop

;//==== Нашли SSN требуемых функций! ======================================
;//==== Теперь воспользуемся ими для создания файла через SYSENTER ========
;//========================================================================
        mov     esi,fName     ;// ESI = адрсе строки с именем файла
        mov     edi,buff      ;// EDI = адрес приёмного буфера
        mov     ecx,fLen      ;// ECX = длина строки
        xor     eax,eax       ;//
@unicode:                     ;// конвертируем в UNICODE..
        lodsb                 ;// берём очередной байт из ESI
        stosw                 ;// записываем его как слово в EDI
        loop    @unicode      ;// повторить ECX-раз..

        mov     ecx,fLen      ;// ECX = длина строки
        dec     ecx           ;// Внимание! ..без терминального нуля!
        shl     ecx,1         ;// умножить длину на 2

        mov     [us.Length],cx       ;// отправляем длину в структуру UNICODE_STRING
        mov     [us.Buffer],buff     ;// туда-же адрес буфера со-строкой
        mov     [oa.ObjectName],us   ;// определяем struct.Unicode как имя в OBJECT_ATTRIBUTES

;//==== Теперь всё готово для вызова SYSENTER ==============================
;// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntcreatefile
        xor     eax,eax
        push    eax           ;// SecureLength       = 0
        push    eax           ;// SecureBuffer       = 0
        push    0x60          ;// Create Options     = Non Dir
        push    0x05          ;// Create Disposition = Max_Disposition
        push    eax           ;// Share Access       = 0
        push    0x80          ;// File attributes    = Normal
        push    eax           ;// Allocation size    = 0
        push    iosb          ;// IoStatusBlock
        push    oa            ;// ObjectAttributes
        push    0xC0100080    ;// Desired Access     = R/W
        push    fHndl         ;// Handle

        mov     eax,@f          ;// EAX = адрес-возврата
        push    eax eax         ;// Внимание!!! Отправляем его дважды в стек!
        mov     eax,[NtCreate]  ;// <----------- NtCreateFile
        mov     edx,esp         ;// EDX = указатель на аргументы
        sysenter                ;// Первый пошёл..
@@:     add     esp,4*12        ;// вытолкнуть аргументы из стека 11+1
       cinvoke  printf,Status,eax

;//==== Таким-же макаром записываем в созданный файл ========================
;//==== в данном случае сбрасывается строка с именем файла ==================
        xor     eax,eax
        push    eax           ;// Key           = 0
        push    eax           ;// ByteOffset    = 0
        push    fLen          ;// Length
        push    fName         ;// Buffer
        push    iosb          ;// IoStatusBlock
        push    eax           ;// ApcContext    = 0
        push    eax           ;// ApcRoutine    = 0
        push    eax           ;// Event         = 0
        push    [fHndl]       ;// FileHandle

        mov     eax,@f         ;// Retn-offset
        push    eax eax        ;// Retn-address
        mov     eax,[NtWrite]  ;// <----------- NtWriteFile
        mov     edx,esp        ;// Arguments
        sysenter
@@:     add     esp,4*10       ;// Clear Stack 9+1
       cinvoke  printf,Status,eax

;//==== Прихлопнем дескриптор открытого файла ===============================
        push    [fHndl]       ;// FileHandle

        mov     eax,@f        ;// Retn-offset
        push    eax eax       ;// Retn-address
        mov     eax,[NtClose] ;// <----------- NtClose
        mov     edx,esp       ;// Arguments
        sysenter
@@:     add     esp,4*2       ;// Clear Stack 1+1
       cinvoke  printf,Status,eax

@exit: cinvoke  gets,buff
       cinvoke  exit,0
;//----------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import   msvcrt, printf,'printf',gets,'gets',exit,'exit'
include 'api\kernel32.inc'

Как видим, простая задача по записи в файл разбухла до размеров слона, хотя её можно было-бы сжать в пару-тройку строк. Но на это и делается ставка, чтобы сбить с толку армию пионеров-взломщиков. Сейчас не просто разобраться, что именно делает этот код, ведь API-функций как-таковых нет. Если и поставить в отладчике точку-останова на обнажённую функцию GetProcAddress(), то это даст ~400 ложных срабатываний, по числу экспортируемых либой функций. Но несомненный плюс в том, что номера SSN вычисляются динамически и не зависят от конкретной платформы – лишь-бы она была 32-битная.

Заключение

Всё, о чём шла речь выше, применительно только к системам х32, хотя с незначительными правками может быть модернизирована и под х64. Во-первых, нужно будет изменить некоторые поля в РЕ-инклуде (см.выше). Во-вторых, точки-входа в нативные функции 64-битных библиотек Ntdll.dll имеют другой вид, а потому придётся в качестве сигнатуры SSN использовать уже не первый байт со-значением 0хВ8, а он смещён от начала и занимает позицию(4). То-есть можно так-же читать dword'ами, но проверять не младший, а старший байт - если в яблочко, значит в следующем dword'e будет лежать SSN найденной Nt-функции. Двоичный редактор HIEW в курсе дел..

Win10.png


Как-видим отличия не значительны, и SSN пересылается в регистр EAX не сразу, а ему предшествует инструкция mov r10,rcx. Таким образом, при желании можно воплотить идею и на х64, но оставим это дело до лучших времён, когда вдоволь напрактикуемся с х32. В скрепке лежат два рассмотренных исполняемых файла, чтобы у желающих была возможность погонять их в отладчике, а так-же лист со-списком моих/нативных функций, система Win-7 SP2. До скорого..
 

Вложения

Шикарная статья. Спасибо.

. Так вот, этот адрес-возврата требуется помещать 2-раза, а не один. Я так и не понял, почему именно два

Тоже не понял , тестил это когда-то (правда, для 64бит), можно вместо второго адреса возврата было чето другое передать, просто чтобы лишний push в стеке.
 
  • Нравится
Реакции: Mikl___ и Marylin

Под х64 да, это другой разговор, но вот касаемо WoW64. Если сделать так после
shr ebx,8
добавить
movzx ebx,bx , потому что некоторые апи выглядят как на скрине (мы очищаем старшую половину дворда), то под WoW64 должно работать.. По идее, NTDLL же у нас таже, 32 битная.
Потестил только что на Win10x64 (1909), 64 битной ХР и семерке (SP1), везде работает. Посмотри, если я не ошибся- можно убрать проверку на IsWoW64Process.
 

Вложения

  • нт.PNG
    нт.PNG
    7,8 КБ · Просмотры: 375
Посмотри, если я не ошибся- можно убрать проверку на IsWoW64Process.
К сожалению проверить на х64 сейчас не могу (бук не дома), позже проверю.. Хотя походу должно сработать, спасибо за наводку.
 
  • Нравится
Реакции: Mikl___
@Marylin проверил на разных ОС - все работает с 32 битного приложения, для парсинга сисколов писал выше (как делать на х64), для вызова - можно или использовать HG либо через гейт WOW64. Т.е. как то так (писал на масм).

Пример для NtProtectVM
Код:
.const
PAGE_EXECUTE_READWRITE equ 40h

.data
dwNum dd 0
oldProtect dd 0
pMem dd 0
sNum dd 0

.code
Syscall2 proc;wow64 syscall
push ebp
mov ebp,esp

mov eax,[ebp+8]                     ; first param
mov pMem,eax
mov eax,[ebp+12]                    ;  second
mov sNum,eax

push offset oldProtect
push PAGE_EXECUTE_READWRITE
mov dwNum,4096
push offset dwNum
push offset pMem
push 0FFFFFFFFh

call @f
@@:

mov eax,sNum
lea edx,[esp+4]
xor ecx,ecx ;тут не всегда так!

;assume fs:nothing
call dword ptr fs:[0C0h]

add esp,5*4 ; тут параметры
add esp,4

pop ebp
ret
Syscall2 endp

Там, где xor ecx,ecx , может быть другое значение в есх, это надо также парсить, но в большинстве случаев там 0. Хотя не всегда - скажем, на десятке всегда 0, а на семерке в некоторых случаях могут быть другие значения. Что это такое, индекс или что-то левое, пока не разобрался.
 
Так вот, этот адрес-возврата требуется помещать 2-раза, а не один.
Скорее всего из-за того, что нерассчитанно, что будет функция вызываться через sysenter. Максимум вызов Nt функции.
Поэтому ядро и думает, что параметры лежат чуть дальше.
Ладно, с sysenter и syscall (под x64) понятно всё. А вот если мы пишем программу, которая x86 и будет запускаться на Windows x64, тогда как сделать? (избегая call dword ptr fs:[0C0h] потому что могут похукать).
Вызвать syscall мы можем только в x64, как выполнить этот переход? Что-то слышал про heaven's gate, но конкретный примеров в интернете не нашёл... Разве, что только такая реализация, но она кривая. Да и хотелось бы объяснение.
Будет статейка насчёт этого? :) (интересует именно часть от пуша параметров для Nt функции в x86, до вызова syscall в x64 и возврата результата обратно в x86. Там есть какие-то прыжки типа jmp 33:<addr> и выход оттуда с retf)
 
Здраствуйте. Спасибо большое за статью. Пробую реализовать идею, описанную в данной статье для ОС Windows7 x64 (приложение пишу х64). На сколько я знаю, в папке system32 должна находится 64-разрядная ntdll.dll. Дизассемблировав ее в IDA, я увидел, что она x32. Такое возможно?
 
Здраствуйте. Спасибо большое за статью. Пробую реализовать идею, описанную в данной статье для ОС Windows7 x64 (приложение пишу х64). На сколько я знаю, в папке system32 должна находится 64-разрядная ntdll.dll. Дизассемблировав ее в IDA, я увидел, что она x32. Такое возможно?
не ту взял dll, For x86 and x64 Windows these are ntdll.dll located in the C:\Windows\SysWow64 and C:\Windows\System directories respectively
 
  • Нравится
Реакции: iskander23
Мы в соцсетях:

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