Вызов системных функций в Windows

Вызов системных функций через прерывание 2E




В одной из статей, опубликованной на этом сайте, рассматривался способ нахождения номеров функций в SDT (см. Номера системных функций в SDT Windows). Например, при вызове NtCreateFile из ntdll.dll в Windows 2000 будет выполняться следующий код:

public ZwCreateFile
ZwCreateFile proc near

mov     eax, 20h                ; номер в SDT
lea     edx, [esp+dword ptr  4] ; адрес первого аргумента
int     2Eh                     ; шлюз
retn    2Ch

ZwCreateFile endp

Перед вызовом ZwCreateFile все аргументы были переданы в стек командой push, а затем следовал call, который привел к появлению в стеке еще одной переменной - адреса возврата. Именно поэтому в качестве адреса первого аргумента в edx записывается [esp+dword ptr 4], а не просто [esp].

Итак, чтобы вызвать функцию напрямую без использования ntdll.dll следует выполнить следующую последовательность действий:

  1. Передать все аргументы функции в стек
  2. Записать в eax номер функции в SDT
  3. Записать в edx адрес первого аргумента
  4. Вызывать прерывание 2Eh
  5. Т.к. все Native API функции используют соглашение __stdcall, то необходимо очистить стек от переданных аргументов перед вызывом прерывания

Для примера вызовем ZwOpenKey, имеющей следующий прототип:

NTSYSAPI
NTSTATUS
NTAPI
ZwOpenKey(
    OUT PHANDLE KeyHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes
    );

Открытый ключ необходимо закрыть используя ZwClose следующего прототипа:

NTSYSAPI
NTSTATUS
NTAPI
ZwClose(
    IN HANDLE Handle
    );

Рассмотрите код функции, которая открывает ключ HKEY_LOCAL_MACHINE\Software и закрывает его. Заметьте, что имя ключа, которое необходимо передать функции записывается в пространстве имен ядра, т.е. "\Registry\MACHINE\Software":

VOID
SdtSimpleTestNtOpenKeyNtCloseKeyWindowsXP(
    )
{
    UNICODE_STRING        KeyName;
    OBJECT_ATTRIBUTES    ObjectAttributes;
    NTSTATUS            Status;
    ACCESS_MASK            DesiredAccess = GENERIC_READ;
    HANDLE                KeyHandle;

    //
    //  Будем открывать ключ HKEY_LOCAL_MACHINE\SOFTWARE.
    //  RtlInitUnicodeString - это макрос препроцессора
    //
    RtlInitUnicodeString( &KeyName, L"\\REGISTRY\\MACHINE\\SOFTWARE" );

    //
    //  Заполним OBJECT_ATTRIBUTES.
    //  InitializeObjectAttributes - это макрос препроцессора
    //
    InitializeObjectAttributes( &ObjectAttributes, &KeyName, OBJ_CASE_INSENSITIVE, NULL, NULL );

//  
//  Прототип вызываемой функции такой:
//   
//     NTSYSAPI
//     NTSTATUS
//     NTAPI
//     ZwOpenKey(
//         OUT PHANDLE KeyHandle,
//         IN ACCESS_MASK DesiredAccess,
//         IN POBJECT_ATTRIBUTES ObjectAttributes
//         );

    __asm{
        lea eax, ObjectAttributes    ; Адрес ObjectAttributes
        push eax                     ; Передача в стек
        mov eax, DesiredAccess       ; Доступ (втрой аргумент)
        push eax                     ; Передача в стек
        lea eax, KeyHandle           ; Адрес KeyHandle (в функцию уже пришёл адресом)
        push eax                     ; Передача в стек
        
        ; Далее код взят из ntdll.dll Windows 2000
        mov eax, 119d                ; Номер функции в SDT (должен лежать в eax) (в XP это 119)
        lea edx, [esp]               ; В Windows 2000 здесь стояло [esp + DWORD PTR 4]
                                     ; Это сохранение адреса первого аргумента (должен лежать в edx)
        int 2Eh                      ; Вызов прерывание - обрабатывать его будет ядро. 
                                     ; Это шлюз к ядру
        mov Status, eax              ; По завершению обработки в eax лежит возвращенное 
                                     ; функцией значение (NTSTATUS)
                        
        ; Далее, поскольку функция __stdcall (сама не чистит стек после выполнения), следует убрать
        ; из стека "запушенные" туда данные. Запушивали 3 аргумента => pop сделать 3 раза
        pop eax
        pop eax
        pop eax

        ; Освободить стек можно по-другому, например add esp, 12d
    }

    //
    //  Теперь в Status лежит возвращенное функцией значение
    //
    
    if ( Status != 0 )
        return;


//
//  Теперь следует закрыть описатель. Вызовем ZwClose. Прототип у нее такой:
//
//     NTSYSAPI
//     NTSTATUS
//     NTAPI
//     ZwClose(
//         IN HANDLE Handle
//         );
    
    __asm{
        mov eax, KeyHandle         ; Значение параметра
        push eax                   ; Передача его в стек

        mov eax, 25d               ; Далее в eax записывается номер функции в SDT
        lea edx, [esp]             ; В edx - адрес аргумента

        int 2Eh                    ; Вызов шлюза
        mov Status, eax            ; Сохранение возвращенного результата

        pop eax                    ; Освобождение стека
    }
}

Эта функция успешно открывает и закрывает ключ реестра при выполнении в Windows XP не вызывая ни одной API функции и не используя ни одной библиотеки. Почему только в Windows XP? Потому что перед вызовом прерывания int 2Eh в eax записывался номер функции SDT именно для этой ОС. Для запуска функции на другой ОС, следует изменить номера (для этого используйте таблицу, полученную программой sdt.exe, таблица также опубликована в одной из статей).

Существенной проблемой является определение версии Windows без использования API. Но если посмотреть внимательно на таблицу номеров SDT, то можно заметить, что от версии к версии ОС содержит всё больше функций. Например, функция с номером 390 есть только в Windows Vista и ее нет в предыдущих версиях, 295 - в Windows 2003, 282 - в Windows XP, 247 - в Windows 2000. Очевидно, что следующая версия Windows будет иметь ещё больше функций и метод будет работать и дальше.

Теперь необходимо определить наличие функции с определенным номером в системе. Если функция отсутствует, то после вызова int 2Eh в eax будет значение 0xc000001c (STATUS_INVALID_SYSTEM_SERVICE). Но если функция присутствует, то она будет выполняться и может сгенерировать исключение, которое сложно обработать без использования API. Чтобы функция не выполнялась, запишем в edx в качестве адреса первого параметра функции NULL. При этом после выполнения прерывания int 2Eh исключения не возникнет, но eax будет содержать 0xC0000005 (STATUS_ACCESS_VIOLATION), а указатель команды (регистр EIP) изменится не на 4, а на 8 (небольшая особенность при возникновении исключительной ситуации).

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

BOOL
SdtTestServicePresent(
    IN ULONG Number 
    )
{
    NTSTATUS Status = 0;

    __asm{
        mov eax, Number            ; Далее в eax записывается номер функции в SDT
        mov edx, 0

        int 2Eh                    ; Вызов шлюза
        mov Status, eax            ; Сохранение возвращенного результата. При AV
                                   ; EIP изменяется на 8 а не 4 байта, поэтому эта
                                   ; команда выполнена не будет
    }

    // STATUS_INVALID_SYSTEM_SERVICE = 0xc000001c
    if ( Status == 0xc000001c )
        return FALSE;

    return TRUE;
}

Тогда, определить наличие ОС можно с помощью такой функции:

SDT_SYSTEM
SdtTestGetOsVersion()
{

    // Последние номера в SDT:
    // 2000:  247
    // XP:    282
    // 2003:  295 
    // Vista: 390

    if ( SdtTestServicePresent( 390 ) )
        return SystemWindowsVista;

    if ( SdtTestServicePresent( 295 ) )
        return SystemWindows2003;

    if ( SdtTestServicePresent( 282 ) )
        return SystemWindowsXP;

    if ( SdtTestServicePresent( 247 ) )
        return SystemWindows2000;

    return SystemWindowsUnknown;
}

Далее эту технику можно применять при разработке ПО и доставить огромное наслаждение хакерам, отлаживающим вашу программу :). Чтобы было удобнее вызывать функции из ntdll.dll, объявим следующие типы данных, функции, макросы:

typedef struct  
{
    ULONG Windows2000;
    ULONG WindowsXP;
    ULONG Windows2003;
    ULONG WindowsVista;
    ULONG Reserved;
} SDT_NUMBER;


ULONG
SdtNumber(
    SDT_SYSTEM System,
    SDT_NUMBER *Numbers
    )
{
    return ((ULONG*) Numbers)[System];
}


#define PROLOG_CODE( Win2000, WinXP, Win2003, WinVista ) \
    SDT_NUMBER Numbers = { Win2000, WinXP, Win2003, WinVista, -1 }; \
    ULONG Number = SdtNumber( System, &Numbers );

#define EPILOG_CODE( PopBytes ) \
    __asm mov eax, Number              \
    __asm lea edx, [esp]              \
    __asm int 2Eh                      \
    __asm mov Status, eax              \
    __asm add esp, PopBytes

#define PUSH_POINTER( Pointer ) \
    __asm lea eax, Pointer            \
    __asm push eax

#define PUSH_VALUE( Value ) \
    __asm mov eax, Value \
    __asm push eax

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

#define ZwOpenKeyCall( System, Status, KeyHandle, DesiredAccess, ObjectAttibutes ) { \
    PROLOG_CODE( 103, 119, 125, 189 ) \
    PUSH_POINTER(ObjectAttributes) \
    PUSH_VALUE( DesiredAccess ) \
    PUSH_POINTER( KeyHandle ) \
    EPILOG_CODE( 12 ) }


#define ZwCloseHandleCall( System, Status, Handle ) { \
    PROLOG_CODE( 24,25,27,48 ) \
    PUSH_VALUE( Handle ) \
    EPILOG_CODE( 4 ) }


/*NTSYSAPI
NTSTATUS
NTAPI
ZwEnumerateKey(
    IN HANDLE KeyHandle,
    IN ULONG Index,
    IN KEY_INFORMATION_CLASS KeyInformationClass,
    OUT PVOID KeyInformation,
    IN ULONG Length,
    OUT PULONG ResultLength
    );*/

#define ZwEnumerateKeyCall( System, Status, \
    KeyHandle, Index, KeyInformationClass, KeyInformation, \
    Length, ResultLength ) { \
    PROLOG_CODE( 60,71,75,133) \
    PUSH_POINTER( ResultLength ) \
    PUSH_VALUE( Length ) \
    PUSH_POINTER( KeyInformation ) \
    PUSH_VALUE( KeyInformationClass ) \
    PUSH_VALUE( Index ) \
    PUSH_VALUE( KeyHandle ) \
    EPILOG_CODE( 24 ) }

Впринципе, макросов должно хватить для объявления любой Native API функции таким вот образом (готовым к вызову в коде других функций).

Продемонстрируем пример использования функций. Например, перечислим подключи в ключе реестра HKEY_LOCAL_MACHINE\Software:

VOID
SdtTestEnumerateSoftwareSubkeys(
    )
{
    SDT_SYSTEM System = SdtTestGetOsVersion();
    NTSTATUS Status;
    HANDLE hKey;
    OBJECT_ATTRIBUTES ObjectAttributes;
    UNICODE_STRING KeyName;
    LPWSTR szKey = L"\\REGISTRY\\MACHINE\\SOFTWARE";


    RtlInitUnicodeString( &KeyName, szKey );
    InitializeObjectAttributes( &ObjectAttributes, &KeyName, OBJ_CASE_INSENSITIVE, NULL, NULL );

    _tprintf( _T("Key: %S\n"), szKey );

    ZwOpenKeyCall( System, Status, hKey, GENERIC_READ, ObjectAttributes );

    if ( Status == 0 )
    {
        UCHAR Buffer[1024];
        PKEY_BASIC_INFORMATION pKeyInfo = (PKEY_BASIC_INFORMATION) Buffer;
        ULONG i = 0;
        

        while ( Status == 0 )
        {
            ULONG uSize = sizeof( Buffer );
            ULONG uRetSize = 0;

            ZeroMemory( Buffer, sizeof(Buffer) );

            ZwEnumerateKeyCall( System, Status, hKey, i, KeyBasicInformation, Buffer, uSize, uRetSize );

            if ( Status == 0 )
            {
                
                _tprintf( _T("Subkey[%i]: %S\n"), i, pKeyInfo->Name );
            }

            i++;

        }

        ZwCloseHandleCall( System, Status, hKey );
    }
    else
    {
        _tprintf( _T("-Can't open key. Status = 0x%.8X\n"), Status );
    }

}

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

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

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

Исходники, код, приведенный в этой статье: sdttest.c



Автор: Fur
Дата: 07 августа 2007


При копировании материалов хорошим тоном будет указание авторства и ссылка на сайт.