Showing posts with label beginners. Show all posts
Showing posts with label beginners. Show all posts

Tuesday, August 17, 2010

DLL_PROCESS_DETACH и С++

При разработке dll приходится мириться с тем фактом, что как правило программы прекращают свое существование, используя для этого вызов функции ExitProcess. Причем многие из них могут вызывать эту функцию в тот момент, когда ваша dll еще загружена.

Функция ExitProcess, в свою очередь, известна тем, что сначала останавливает все потоки, кроме вызывающего (используя NtTerminateProcess), а потом вызывает EntryPoint для всех dll с флагом DLL_PROCESS_DETACH. В С++ обычно точка входа называется DllMainCRTStartup. DllMainCRTStartup вызывает функцию DllMain и после нее все деструкторы глобальных объектов, расположенных в DLL.

Разумеется, что после удаления некоторого количества потоков, отличного от нуля, выполнение любого кода становится чрезвычайно опасным мероприятием, чреватым дедлоками и крешами. Некоторые комрады также утверждают, что после вызова ExitProcess перестают работать критические секции, и случаются многие другие Ужасные Вещи.

Поэтому, популярный паттерн использования DLL_PROCESS_DETACH выглядит так:
BOOL WINAPI DllMain(
IN HINSTANCE hinstDll,
IN DWORDfdwReason,
LPVOID lpvReserved
)
{
....

case DLL_PROCESS_DETACH:

if( lpvReserved == NULL )
{
// FreeLibrary is called and it is safe to free resources
// All critical sections and other primitives should work fine
FreeResources1();
FreeResources2();
...
FreeResourcesN();
}
// else { ExitProcess is called, process is terminating, so do nothing }
break;
}
_Winnie C++ Colorizer

Параметр lpvReserved описан в MSDN следующим образом:
If fdwReason is DLL_PROCESS_DETACH, lpvReserved is NULL if FreeLibrary has been called or the DLL load failed and non-NULL if the process is terminating.
(с) MSDN


В случае использования С++, код нашей идеальной Dll должен выглядеть так:
static std::auto_ptr<CMyDll> g_myDll;

BOOL WINAPI DllMain(
IN HINSTANCE hinstDll,
IN DWORD fdwReason,
LPVOID lpvReserved
)
{
....

case DLL_PROCESS_DETACH:

if( lpvReserved == NULL )
{
g_myDll.reset(0);
}
g_myDll.release();
break;
}
_Winnie C++ Colorizer

В приведенном коде нет особых проблем, но нельзя упускать тот факт, что наша идеальная dll может подключать и, как правило, подключает множество статических библиотек. Которые вполне могут содержать глобальные переменные-объекты.

Выводы крайне тривиальны: использование глобальных переменных-объектов - есть зло, особенно в статических библиотеках. Но если вы уже разрабатываете статическую библиотеку на С++ с глобальными переменными внутри, стоит предоставить функцию для их корректного фатального освобождения, с семантикой, аналогичной g_myDll.release().

Tuesday, December 23, 2008

открываем файлы

Почему-то IFS DDK не достаточно освещает один забавный момент, связанный с восприятием реальности NT в области открытия файлов. Вообще, его стоило бы добавить к разделу "ZwCreateFile" или к разделу Using Files In A Driver".

Пусть у нас в системе есть некий виртуальный девайс-диск, например "\device\my_super_disk", и на него существует символическая ссылка "\DosDevices\X:".
Допустим он даже примаунчен, что значит, что к нему "снизу" присоединилось соответствующее устройство файловой системы (не то чтобы у них там какой-то "стек", на самом деле диск может, вобщем-то, и не знать, в каком он состоянии находится, ему это без разницы).

Так вот, как мне кажется, у пользователя существует целых ТРИ принципиально разных способа открытия файлов используя "\DosDevices\X". Пользователь может:
1) просто открыть файл, используя в качестве имени что-то вроде "\DosDevices\X:\MyDir\MyFile.txt". Все просто и хорошо изучено.
Что изучено плохо:
- если устройство не примаунчено, NT попытается его примаунтить прямо в месте вызова;
- если вызывать IoDeleteDevice на my_super_disk в процессе маунта, возможны bsod'ы из-за специфики реализации некоторых микрософтовских фильтров(sr.sys);
2) открыть устройство для "прямого" доступа. Делаем тот же ZwCreateFile, но используем в качестве имени "\DosDevices\X:", и, обязательно указываем FILE_READ_DATA в DesiredOptions(или любой другой "неизбранный" флаг - см ниже).
Тоже простой и хорошо изученный способ. Интересности:
- если устройство не примаунчено, NT тоже попытается его примаунтить прямо в месте вызова;
- несмотря на то что my_super_disk открыт вроде бы как "напрямую" (c) MSDN, запросы будут ходить через устройство файловой системы, со всеми вытекающими. Сможете послать ей чего-нибудь;
3) открыть устройство для "по-настоящему-прямого" доступа. Открываем файл примерно как в пункте 2, за исключением того, что в DesiredOptions не ставим никаких других флагов, кроме избранных: ACCESS_SYSTEM_SECURITY, FILE_READ_ATTRIBUTES, SYNCHRONIZE, READ_CONTROL, WRITE_OWNER, WRITE_DAC. Например, указываем только SYNCHRONIZE.
Интересности:
- если устройство не примаунчено, NT не будет пытается его примаунтить;
- если устройство и примаунчено, запросы все равно не будут заходить в файловую систему;
- у вашего файла будет pFileObject->FsContext будет равен 0;
- идеально подходит для того, чтобы пообщаться с my_super_disk напрямую.

Интересно, МСДН как бы намекает обо всем этом в описании IoGetDeviceObjectPointer:
"To get a pointer to the highest-level driver in the file system driver stack, a driver must ensure that the file system is mounted; if it is not, this routine traverses the storage device stack. To ensure that the file system is mounted on the storage device, the driver must specify an appropriate access mask, such as FILE_READ_DATA or FILE_WRITE_ATTRIBUTES, in the DesiredAccess parameter. Specifying FILE_READ_ATTRIBUTES does not cause the file system to be mounted. "
Но - уныло и поверхностно.

Friday, December 7, 2007

С++ фабрики и вопросы экологии

Итак, очередной выпуск журнала "Кодим Вместе".
Предлагаю немного отвлечься от насущных вопросов, и заняться чем-нибудь более медитативным и расслабляющим. Например, всем вместе реализовать известный дивный программистский паттерн - фабрику.
На С++, конечно же, на чем же еще?

Итак, наш первый вариант:
//------------------------------------------------------------------
[Фабрика I] Вариант первый - брутальный

struct MegaClass
{
};
class SomeFactory
{
public:
MegaClass * CreateSmth()
{
return new MegaClass();
}
};
_Winnie C++ Colorizer
Тоже ничего себе способ, довольно часто встречается, особенно среди любителей managed сред. Недостатки способа очевидны -
1. Можно легко допустить, чтобы кто-то прострелил себе ногу -

{
factory.CreateSmth();
} // утечка на пустом месте
Особенно если метод называется CreateMyMegaSuperObjectAndDoTenAnotherActions, принимает N параметров, и, кроме этого, еще делает M неочевидных действий;

2. Нечитабельно - факт передачи владения неочевиден.
//------------------------------------------------------------------
[Фабрик II] Вариант на "мягкую" четверку

#include <memory>
struct MegaClass
{
};
class SomeFactory
{
public:
std::auto_ptr<megaclass> CreateSmth()
{
return std::auto_ptr<megaclass>(new MegaClass()); // он
}
};
Почему на четверку? Ибо налицо факт явного использования расширения C++. Стандарт C++ утверждает, что временный объект пользовательского типа представляет собой modifyable rvalue. В терминах стандарта, modifyable rvalue - это такая специальная сущность, у которой можно вызывать не константные методы, но которую никак нельзя связывать с не константными ссылками.
Т.е:

// пусть есть некоторое fnc
void fnc(A & a) { /* определенно что-то делаем тут*/}

// тогда, для него
A().fnc(); // так делать можно
fnc(A()); // а так - не везде :)
Несложно заметить, что в этом варианте как раз и имеется попытка связывания временного обекта

std::auto_ptr<megaclass>(new MegaClass())

с не константной ссылкой - аргументом оператора присваивания (или конструктора копирования) класса std::auto_ptr

template<class>
auto_ptr(auto_ptr<_other>&amp; _Right);
auto_ptr<_ty>&amp; operator=(auto_ptr<_ty>&amp; _Right);

Утешает только то, что в микрософтских компиляторах это расширение не отключается даже по /Za. А вот другие компиляторы вполне могут воспринимать такие творения более агрессивно:

/*
"ComeauTest.c", line 11: error: class "std::auto_ptr" has no suitable
copy constructor
return std::auto_ptr(new MegaClass());
^
*/

Посему - минус бал за непереносимость.

//------------------------------------------------------------------
[Фабрика III] Вариант - на пятерку

#include <memory>
struct MegaClass
{
};
class SomeFactory
{
public:
void CreateSmth(std::auto_ptr<MegaClass> * pResult)
{
pResult->reset(new MegaClass());
}
};

Этот вариант лишен всех вышеописанных недостатков.
Делайте поменьше вредных выбросов.

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

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