hex.pp.ua

Исследование CreateProcess

Дизассемблирование функции Win32 API CreateProcess




Приведу пример дизассемблирования функции Win32 API CreateProcess, чтобы дать представление о технике изучения подсистемы, а так же, чтобы показать как Win32 взаимодействует с исполнительной системой Windows NT.

Прототип функции из MSDN.

BOOL CreateProcess(
    LPCTSTR lpApplicationName,
                           // pointer to name of executable module
    LPTSTR lpCommandLine,  // pointer to command line string
    LPSECURITY_ATTRIBUTES lpProcessAttributes,  // process security attributes
    LPSECURITY_ATTRIBUTES lpThreadAttributes,   // thread security attributes
    BOOL bInheritHandles,  // handle inheritance flag
    DWORD dwCreationFlags, // creation flags
    LPVOID lpEnvironment,  // pointer to new environment block
    LPCTSTR lpCurrentDirectory,   // pointer to current directory name
    LPSTARTUPINFO lpStartupInfo,  // pointer to STARTUPINFO
    LPPROCESS_INFORMATION lpProcessInformation  // pointer to PROCESS_INFORMATION
  );

Нет смысла подробно описывать все параметры в функцию - это справочная информация. Сразу, в начальных строках, устанавливается обработчик исключений __except_handler3. (Структура в стеке соответствует структуре Visual C). Затем происходит интенсивная работа с dwCreationFlags. В любом случае очищается флаг CREATE_NO_WINDOW в dwCreationFlags (зачем - для меня это загадка). Проверяется недопустимая комбинация DETACH_PROCESS | CREATE_NEW_CONSOLE. Если эти биты установлены одновременно - выход с ошибкой. Происходит выбор одного из приоритетов нового процесса. (Очистка всех бит приоритета в dwCreationFlags , кроме одного) Если запрошен REAL_TIME приоритет, но он не может быть выделен процессу, то устанавливается HIGH_PRIORITY. Далее происходит достаточно громоздкая манипуляция с параметрами lpApplicationName, lpCommandLine, lpEnvironment. Анализ показал , что функция CreateProcessW действительно делает то, что написано в документации. Итак, будем считать, что командная строка и имя приложения уже разделены. Имеется полный путь в стиле DOS к образу. С помощью недокументированной функции ntdll.dll

NTSYSAPI
BOOLEAN
NTAPI
RtlDosPathNameToNtPathName_U (char* lpPath,
                              RTL_STRING *NtPath,
                              BOOLEAN AllocFlag,
                              RTL_STRING *Reserved);

В результате, будет получен путь вида \??\:\. Затем, заполняется документированная структура OBJECT_ATTRIBUTES, в поле ObjectName заносится указатель на полученный путь и вызывается недокументированная функция

NTSYSAPI
  IOSTATUS
  NTAPI
  NtOpenFile (OUT DWORD* Handle, IN ACCESS_MASK DesiredAccess,
          OBJECT_ATTRIBUTES* ObjAttr, PIO_STATUS_BLOCK IoStatusBlock,
          DWORD ShareAccess, DWORD OpenOptions);

с доступом SYNCHRONYZE | FILE_EXECUTE. Полученный дескриптор открытого файла используется для вызова другой недокументированной функции:

NTSYSAPI
  NTSTATUS
  NTAPI
  NtCreateSection(
     OUT PHANDLE SectionHandle,
     IN ACCESS_MASK DesiredAccess,
     IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
     IN PLARGE_INTEGER MaximumSize OPTIONAL,
     IN ULONG Protect,
     IN ULONG Attributes,
     IN HANDLE FileHandle OPTIONAL
     );

Большинство недокументированных системных вызовов доступны через соответствующие Win32 API вызовы. API функция CreateFileMapping является заглушкой для NtCreateSection. Фактически, никто не мешает использовать такие системные вызовы напрямую. (Кстати, избегая накладных расходов.) Интересно, как на основе своих входных параметров API функция генерирует параметр DesiredAccess для NtCreateSection.

  DesiredAccess = (flProtectLow == PAGE_READWRITE) ? STANDARD_RIGHTS_REQUIRED | 7 :
                         STANDART_RIGHTS_REQUIRED | 5;

DesiredAccess может принимать только два значения. Вызов же из CreateProcessW выглядит так:

NtCreateSection ( &SectionHandle, STANDARD_RIGHTS_REQUIRED | 0x1F,
                    NULL, &qwMaximumSize,
                    PAGE_EXECUTEREAD, SEC_IMAGE, NtFileHandle );

Таким образом получен спроецированный образ и можно закрыть файл - источник образа. Что и делается с помощью документированной функцией NtClose. Происходит анализ кода, который возвратила NtCreateSection. Здесь не рассматривается обработку ошибок, так как она очень громоздка, с множеством служебных функций. Отметим только следующий интересный факт. CMD и BAT файлы обрабатываются как ошибки. (После такой ошибки идет подготовка параметров для выполнения файла cmd с соответствующими параметрами и все повторяется сначала). Причем эти расширения (.cmd .bat) жестко зашиты в код. Примерно также обрабатываются приложения DOS и других подсистем. Происходит настройка новых параметров и возврат к началу. Итак, рассмотрим случай, когда ошибки не произошло и образ представляет собой выполняемый PE образ. Происходит вызов очень интересной недокументированной функции

  NTSYSAPI  NTSTATUS  NTAPI
       NtQuerySection(
            IN HANDLE SectionHandle,
            IN SECTIONINFOCLASS SectionInformationClass,
            OUT PVOID SectionInformation,
            IN ULONG SectionInformationLength,
            OUT PULONG ReturnLength OPTIONAL
            );

Существует несколько функций (недокументированных) NtQueryInformationXxxxx. Не смотря на то, что они не документированы, в NTDDK.H описаны некоторые прототипы и структуры информации, которые возвращают эти функции. Мэтт Пьетрик в своей статье журнала Microsoft Systems (есть в MSDN) подробно описал NtQueryInformationProcess на основе информации из NTDDK.H. К сожалению, информация о функции NtQuerySection отсутствует . Все такие функции имеют практически одинаковые прототипы и работают с объектами ОС.) Поэтому приведенная структура не полная и получена в результате дизассемблирования единственного вызова этой функции из NtCreateProcess. NtQuerySection возвращает два типа информации (SectionInformationClass может принимать значения 0 и 1). Размеры структур, представляющих классы 0 и 1 равны 16 и 58 байт соответственно. При вызове из CreateProcessW, параметр SectionInformation задает класс 1.

Struct SECTION_INFO_CLASS1 {
  DWORD EntryPoint;
  DWORD field_4;
  DWORD StackReserved;
  DWORD StackCommited;
  DWORD SubSystem;
  DWORD ImageVersionMinor;
  DWORD ImageVersionMajor;
  DWORD unknown1;
  DWORD Characteristics;
  DWORD Machine;
  DWORD Unknown[4];
  };

Видно, что эта информация получена из заголовка PE образа. На основе поля Characteristics Делается вывод о типе образа (выполняемый или нет). Затем идут проверки на тип машины (допустимый интервал определяется на основе содержимого 16р. слов по адресам 7FFE002C и 7FFE002E), развилка на основе анализа поля SubSystem, проверка на версию образа. И, наконец, вызов недокументированной функции

NtCreateProcess(
     OUT PHANDLE ProcessHandle,
     IN ACCESS_MASK DesiredAccess,
     IN POBJECT_ATTRIBUTES ObjectAttributes,
     IN HANDLE ParentProcess, //-1
     IN BOOLEAN InheritHandles,
     IN HANDLE SectionHandle,
     IN HANDLE DebugPort OPTIONAL, // NULL
     IN HANDLE ExceptionPort OPTIONAL //NULL
     );

При этом создается объект-процесс ОС Windows NT. Закрывается секция, так как она больше не нужна. И происходит установка атрибутов объекта с помощью вызовов недокументированной функции NtSetInformationProcess.

NTSYSAPI
NTSTATUS
NTAPI
NtSetInformationProcess(
    IN HANDLE ProcessHandle,
    PROCESSINFOCLASS ClassInfo,
    IN PVOID Information,
    IN ULONG Length,
);

В файле NTDDK.H есть перечисление _PROCESSINFOCLASS, которое описывает классы информации. Устанавливаются значения следующих классов информации : ProcessDefaultHardErrorMode, ProcessBasePriority. Для этих классов структура информации представляет собой просто 32р слова. Затем происходит вызов не документированной, но описанной Мэттом Пьетриком в статье Microsoft Systems функции:

NTSYSAPI
NTSTATUS
NTAPI
NtQueryInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);

Получаемая информация - это ProcessBasicInfo, описанная в файле NTDDK.H.

typedef struct _PROCESS_BASIC_INFORMATION {
    NTSTATUS ExitStatus;
    PPEB PebBaseAddress;
    KAFFINITY AffinityMask;
    KPRIORITY BasePriority;
    ULONG UniqueProcessId;
    ULONG InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;

Единственная необходимая информация для CreateProcessW это адрес PEB. Потому что сразу же после получения этой информации происходит вызов внутренней функции _BasePushProcessParameters. Полный анализ этой функции в рамках дипломной работы на выполнялся, но, судя по получаемым ею параметрам (см. исходный текст) , ее назначение - настроить адресное пространство порожденного только что процесса. Далее вызывается две внутренние служебные функции. Сначала происходит вызов _BaseCreateStack. _BaseCreateStack выделяет и настраивает стек процесса. Во-первых выбираются значения для резервирования и выделения стека. (reservrd и commited) Причем, если в качестве параметров SizeReserved и SizeCommited поступили нули - будут выбраны значения этих величин из PE заголовка образа процесса, из которого произошел вызов CreateProcess. Выполняется выравнивание этих значений и резервирование памяти в адресном пространстве порожденного процесса с помощью недокументированной функции NtAllocateVirtualMemory. (К счастью, документированная Win32 API функция VirtualAllocEx - это очень короткая заглушка для нее, и параметры этих двух функций совпадают). После чего, происходит два вызова, которые проще пояснить псевдокодом.

FreeReserved = SizeReserv - SizeCommited;
ReservedAddr += FreeReserved;
if(SizeReserved<=SizeCommited)
  fl=0;
else {
  ReservedAddr -= Delta;
  SizeCommited += Delta;
  fl = 1;
}
NtAllocateVirtualMemory(Han, &ReservedAddr, 0, SizeCommited, 1000, 4);

//[skipped]

NtProtectVirtualMemory
     (ProcHan, &ReservedAddr, Delta, PAGE_READWRITE | PAGE_GUARD, &OldProt);
                         /* См. заглушку  VirtualProtectEx */

Следовательно, происходит выделение памяти в зарезервированной области (в ее конце). Причем выделяется памяти больше на величину Delta. На этот участок (размером Delta) накладываются атрибуты PAGE_GUARD и PAGE_READWRITE. Т.е. в конечном счете получается следующая структура:

***Stack***
---------------¬-ReservedAddr
¦              ¦
¦              ¦
¦  RESERVED    ¦<- SizeReserved - (SizeCommited + Delta)
¦              ¦
¦--------------¦-CommitedAddr
¦  GUARD PAGE  ¦<- Delta
¦--------------¦
¦  READ_WRITE  ¦<- SizeCommited
¦              ¦
L----------------SS:ESP

Таким образом, для стека выделяется SizeCommited байт. Резервируется SizeReserved. И дальнейшее выделение памяти в зарезервированной области под стек, будет происходить при каждом обращении к сторожевой странице (при обращении к такой странице возникает исключение). Как видно из исходного кода, неправильная величина Delta может привести к плачевным последствиям. Так как это ключевая информация - посмотрим откуда выбирается значение Delta.

.text:77F04B99             mov     eax, large fs:18h
.text:77F04B9F             mov     ecx, [eax+30h]  ; PEB
.text:77F04BA2             mov     eax, [ecx+54h]  ; READ ONLY DATA
                         ; ReadOnlyStaticServerData
.text:77F04BA5             mov     edx, [eax+4]
[skipped]
.text:77F04BB1             mov     esi, [edx+128h] ; Delta

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

Typedef struct _StackInformation
{
     DWORD Reserved0;
     DWORD Reserved1;
     DWORD AddressOfTop;
     DWORD CommitAddress;
     DWORD ReservedAddress;
} StackInformation;

Информация из этой структуры используется в качестве параметров при вызове следующей интересной функции BaseInitializeContext.

BaseInitializeContext(PCONTEXT Context, // 0x200 bytes
PPEB Peb,
PVOID EntryPoint,
DWORD StackTop,
int Type // union (Process, Thread, Fiber)
);

У нее всего несколько параметров: адрес PEB, точка входа адрес стека и параметр, определяющий какой контекст создавать (нить, процесс, поток). Функция заполняет некоторые поля структуры CONTEXT (есть в NTDDK.H) Интерес представляет одно поле, в которое помещается точка старта. (одно из смещений BaseFiberStart, BaseProcessStartThunk, BaseThreadStartThunk). Это точка "рождения" потока. Первые коды, которые выполняются в новом контексте. На самом деле, код по всем трем смещениям очень короткий - это заполнение соответствующим образом стека и переход на одну из двух процедур. Эти процедуры _BaseProcessStart и _BaseThreadStart. Они также очень похожи. Исходный код всех этих функций приводить нет необходимости, отметим только особенности функции _BaseProcessStart.

В этой функции устанавливается первый обработчик исключений в цепочке (см. TEB). Именно этот обработчик вызывается, когда появляется диалог с кнопками OK и CANCEL, сообщающий о неправильном доступе к памяти. Этот обработчик снимает текущий процесс. Но в случае, если исключение произошло по вине потока сервера, снимается только поток.

Итак, после возврата из BaseInitializeContext, заполнена соответствующая структура. И эта структура используется как параметр к функции (конечно, не документированной) NtCreateThread. Прототип функции примерно такой:

NTSYSAPI
  NTSTATUS
  NTAPI
  NtCreateThread(
      OUT PHANDLE ThreadHandle,
      IN ACCESS_MASK DesiredAccess,
      IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
      IN HANDLE ProcessHandle,
      OUT PCLIENT_ID ClientID,
      IN PCONTEXT Context, /* see _BaseInitializeContext */
      IN StackInformation* StackInfo, /* see _BaseCreateStack */
      IN BOOLEAN CreateSuspended  /* ==1 */
  );

Наконец, после некоторых манипуляций с данными на основе поля SubSystem в PE образе, происходит обращение через LPC к Win32 серверу. Процессы должны создаваться только через подсистему Win32. Некоторую высокоуровневую информацию по этому поводу можно прочитать в книге Хелен Кастер.

Для выполнения функцией CreateProcess своей задачи необходимо запустить поток. (Конечно, если в параметре dwCreationFlags CreateProcessW не установлен флаг CREATE_SUSPEND). Запуск потока осуществляется вызовом к NtResumeThread. (Заглушка в Win32 - ResumeThread.) Все! Теперь осталось освободить память и правильно выйти.

При анализе работы CreateProcess не рассматривалось создание процессов из образов для других подсистем. Это связано с тем, что при создании, например, DOS процесса происходит инициализация VDM с помощью внутренних функций, которые сами по себе очень нетривиальны.

Таким образом на основании анализа работы функции подсистемы CreateProcess Win32 можно сделать выводы, распространяющиеся на подсистему в целом. Итак: подсистема обычно работает с исполнительной системой Windows NT. Подсистема использует множество недокументированных функций. Подсистема взаимодействует с своим сервером с помощью LPC. Многие Win32 API функции являются заглушками в Nt функции. Вся эта информация не является новой, но мы ее только что подтвердили с помощью дизассемблирования.



Автор: Gloomy aka Peter Kosyh ([email protected])
Дата: 2001


При копировании материалов хорошим тоном будет указание авторства и ссылка на сайт. По поводу рекламы обращайтесь на почту [email protected]