Wednesday, November 21, 2007

В очередной раз о Previous Mode

Как и в любой современной операционной системе, в NT есть механизмы, позволяющие ядру защититься от разбушевавшегося приложения, не заблокировав при этом деятельность более дружественных компонентов - драйверов, уровень доверия к которым у системы неизмеримо выше.
Если задаться целью, то можно сформулировать основные постулаты защиты ядра от юзермодного кода (UC) примерно так:
- UC не должен иметь доступа к адресному пространству режима ядра;
- UC не должен иметь доступа к дескрипторам режима ядра(kernel mode handles);
- UC обязан проходить проверку доступа (access check) при работе с объектами системы;

К драйверам режима ядра такие ограничения, естественно, не применимы. Исходя из этих постулатов, возникает вполне закономерный вопрос - каким же образом NT разграничивает доступ к системным сервисам для user mode и kernel mode вызовов?
Дело в том, что в NT с каждым потоком ассоциировано некоторое поле, указывающее системному сервису, в каком режиме находился вызывающий тред до вызова функции ядра - поле это называется Previous Mode, и находится в кернельном TEB'е (Thread Environment Block). Тип этого поля представляет собой enum, описанный в DDK следующим образом:
typedef enum _MODE {
KernelMode,
UserMode,
MaximumMode
} MODE;
т.е. в качестве значений PreviousMode можно использовать зарезервированые костанты KernelMode=0 или UserMode=1, получить же к нему доступ можно (схематично) вот таким образом:
PsGetCurrentThread()->KernelTEB->PreviousMode;
Именно благодаря этому полю любая функция из ntoskrnl знает, откуда пришел выполняющий ее тред и нужно ли проверять параметры ее вызова на предмет юзермодных ограничений.

Для того, чтобы лучше понять детали реализации этого решения Microsoft, имеет смысл воспользоваться следующей схемой:

На схеме представлены два типичных прецедента:
  1. вызов CreateFile из пользовательского приложения (MyApplication.EXE).
    Который раскрывается в цепочку вызовов:
    ntdll.NtCreateFile -> ntoskrnl.KiFastCallEntry -> ntoskrnl.NtCreateFile
  2. вызов ZwCreateFile из драйвера режима ядра (MyDriver.SYS), проходящий другой сложный и интересный путь:
    ntoskrnl.ZwCreateFile -> ntoskrnl.KiSystemService -> ntoskrnl.KiFastCallEntry -> ntoskrnl.NtCreateFile
Интересна функция ntoskrnl.KiFastCallEntry - именно она копирует аргументы и диспетчеризирует все вызовы по их индексу, используя SDT/SST таблицы.

А функция KiSystemService и есть та самая дивная функция, которая устанавливает PreviousMode равным KernelMode, указывая на то, что вызывающий компонент имеет право использовать все свои драйверные полномочия.

Таким образом, алгоритм ZwCreateFile можно схематично представить так:
1. SetPreviousMode(KernelMode);
2. GetCallAddressValueFromSST (that corresponding NtCreateFile in normal way);
3. Call it;
4. SetPreviousMode(OldMode);
Такая себе абстракция "четыре-в-одном".
А что, если нам бы захотелось вызвать NtCreateFile напрямую, без диспетчеризации по SST? Такое желание может возникнуть, к примеру, если мы вспомним о существовании целого класса руткитов, основанных на подмене записей SST таблицы. Чтобы противостоять этому классу руткитов, достаточно написать свою функцию следующего вида:
1. SetPreviousMode(KernelMode);
2. Call NtCreateFile;
3. SetPreviousMode(OldMode);
Однако проблема состоит в том, что функции SetPreviousMode попросту не существует.
Тем более приятно написать свою!
Поскольку вбивать смещения для всех сервиспаков по меньшей мере скучно, попытаемся программно проанализировать код. К примеру, вот так выглядят популярные реализации GetPreviousMode:
2K
nt!KeGetPreviousMode:
80465320 a1 24f1dfff mov eax,[ffdff124]
80465325 0f b6 80 34010000 movzx eax,byte ptr [eax+0x134]
8046532c c3 ret

XP
nt!KeGetPreviousMode:
804daae3 a1 24f1dfff mov eax,[ffdff124]
804daae8 0f b6 80 40010000 movzx eax,byte ptr [eax+0x140]
804daaef c3 ret

2003
nt!KeGetPreviousMode:
8083a3a7 64 a1 24010000 mov eax,fs:[00000124]
8083a3ad 0f b6 80 d7000000 movzx eax,byte ptr [eax+0xd7]
8083a3b4 c3 ret
И этой информации достаточно, чтобы написать свою функцию CreateSetPreviousModeStub, задачей которой будет анализ имеющейся функции KeGetPreviousMode и создание новой SetPreviousMode:

//Lets write our own SetPreviousMode!
// TARGET FUNCTION
// head
static const
unsigned char stub1[]={
0x55, // __asm push ebp
0x8B, 0xEC, // __asm mov ebp, esp
0x8B, 0x4D, 0x08 // __asm mov ecx, [ebp + 8]
};

// autogenerated - for example for 2003
// movzx eax,byte ptr [eax+0xd7]

// tail
static const
unsigned char stub2[]={
0x86, 0x88, 0x00, 0x00, 0x00, 0x00, // __asm xchg byte ptr [eax+...], cl
0x0F, 0xB6, 0xC1, // __asm movzx eax, cl
0x5D, // __asm pop ebp
0xC2, 0x04, 0x00 // __asm ret 4
};

typedef unsigned long (__stdcall *KeSetPreviousModePtr)(unsigned long lMode);

PVOID GetKernelApiAddr(PCHAR ApiName)
{
PVOID Addr = NULL;
ANSI_STRING ApiNameAnsi = {0};
UNICODE_STRING ApiNameUnicode={0};
ApiNameAnsi.Length = ApiNameAnsi.MaximumLength=strlen(ApiName);
ApiNameAnsi.Buffer = ApiName;
RtlAnsiStringToUnicodeString(&ApiNameUnicode,&ApiNameAnsi,TRUE);
Addr = MmGetSystemRoutineAddress(&ApiNameUnicode);
RtlFreeUnicodeString(&ApiNameUnicode);
return Addr;
}

#define FS_prefix 0x64
#define MOV_first_byte 0xA1

// our goal !!!
NTSTATUS CreateSetPreviousModeStub(KeSetPreviousModePtr* ppStub)
{
unsigned char * pData = GetKernelApiAddr("KeGetPreviousMode");
unsigned char * pFirstMov = pData;
int iGetThreadCmdSize = 5;
unsigned long lSecondCmd = 0;

if (!pData)
return STATUS_UNSUCCESSFUL;

if (*pData == FS_prefix)
{
++iGetThreadCmdSize;
++pFirstMov;
}
if (*pFirstMov != MOV_first_byte)
return STATUS_UNSUCCESSFUL;

lSecondCmd = *(unsigned long*)(pFirstMov+5);
lSecondCmd &= 0x00FFFFFF;

if (lSecondCmd!=0x80b60f)
return STATUS_UNSUCCESSFUL;

{
unsigned long MagicOffset = *(unsigned long*)(pFirstMov+8);
unsigned long lStubSize = sizeof(stub1)+ sizeof(stub2) + iGetThreadCmdSize;
unsigned char * pStub = ExAllocatePool(NonPagedPool, lStubSize);
if (!pStub)
return STATUS_NO_MEMORY;

memcpy(pStub,stub1, sizeof(stub1));
memcpy(pStub+sizeof(stub1),pData, iGetThreadCmdSize);
memcpy(pStub+sizeof(stub1)+iGetThreadCmdSize,stub2, sizeof(stub2));

// set magic offset
*(unsigned long *)(pStub+sizeof(stub1)+iGetThreadCmdSize+2) = MagicOffset;

*ppStub = pStub;
}
return STATUS_SUCCESS;
}
void FreeSetPreviousModeStub(KeSetPreviousModePtr pStub)
{
ExFreePool(pStub);
}
_Winnie C++ Colorizer

Итого - вызываем CreateSetPreviousModeStub и наслаждаемся.
Именно в такой последовательности.
Поддержка Vista

Vista:
81891203 64a124010000 mov eax,fs:[nt!KiInitialPCR+0x124 (00000124)]
81891209 8a80e7000000 mov al,[eax+0xe7]
8189120f c3 ret

остается вам в качестве домашнего задания.

No comments: