В одной из статей, опубликованной на этом сайте, рассматривался способ
нахождения номеров функций в SDT
(см.
Номера системных функций в SDT в Windows 2000 / XP / 2003 / VISTA).
Например, при вызове 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