Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6265
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

Глава 3. Объекты ядра.docx

27

#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002

Чтобы изменить сразу все флаги объекта, нужно объединить их побитовой операцией OR.

И, наконец, третий параметр функции SetHandleInformation dwFlags — указывает, в какое именно состояние следует перевести флаги. Например, чтобы установить флаг наследования для описателя объекта ядра:

SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);

а чтобы сбросить этот флаг:

SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);

Флаг HANDLE_FLAG_PROTECT_FROM_CLOSE сообщает системе, что дан-

ный описатель закрывать нельзя:

SetHandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE,

HANDLE_FLAG_PROTECT_FROM_CLOSE);

 

CloseHandle(hObj);

// Генерируется исключение

Если какой-нибудь поток попытается закрыть защищенный описатель, CloseHandle приведет к исключению. Необходимость в такой защите возникает очень редко. Однако этот флаг весьма полезен, когда процесс порождает дочерний, а тот в свою очередь — еще один процесс. При этом родительский процесс может ожидать, что его «внук» унаследует определенный описатель объекта, переданный дочернему. Но тут вполне возможно, что дочерний процесс, прежде чем породить новый процесс, закрывает нужный описатель. Тогда родительский процесс теряет связь с «внуком», поскольку тот не унаследовал требуемый объект ядра. Защитив описатель от закрытия, вы исправите ситуацию, и «внук» унаследует предназначенный ему объект.

У этого подхода, впрочем, есть один недостаток. Дочерний процесс, вызвав:

SetHandleInformation(hobj, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);

CloseHandle(hObj);

может сбросить флаг HANDLE_FLAG_PROTECT_FROM_CLOSE и закрыть за-

тем соответствующий описатель. Родительский процесс ставит на то, что дочерний не исполнит этот код. Но одновременно он ставит и на то, что дочерний процесс породит ему «внука», поэтому в целом ставки не слишком рискованны.

Для полноты картины стоит, пожалуй, упомянуть и функцию GetHandleInformation:

B00L GetHandleInformation(

HANDLE h0bject,

PDW0RD pdwFlags);

Эта функция возвращает текущие флаги для заданного описателя в переменной типа DWORD, на которую указывает pdwFlags. Чтобы проверить, является ли описатель наследуемым, сделайте так:

28 Часть I Материалы для обязательного чтения

DWORD dwFlags; GetHandleInformation(hObj, &dwFlags);

BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));

Именованные объекты

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

HANDLE CreateMutex(

PSECURITY_ATTRIBUTES psa,

B00L bInitialOwner,

PCTSTR pszName);

HANDLE CreateEvent(

PSECURITY_ATTRIBUTES psa,

B00L bManualReset,

B00L bInitialState,

PCTSTR pszName);

HANDLE CreateSemaphore(

PS_CORITY_ATTRIBUTES psa,

LONG lInitialCount,

LONG lMaximumCount,

PCTSTR pszName);

HANDLE CreateWaitableTimer(

PSECURITY_ATTRIBUTES psa,

B00L bManualReset,

PCTSTR pszName);

HANDLE CreateFileMapping(

HANDLE hFile,

PSECURITY_ATTRIBUTES psa,

DWORD flProtect,

DWORD dwMaximumSizeHigh,

DWORD dwMaximumSizeLow,

PCTSTR pszName);

HANDLE CreateJobObject(

PSECURITY_ATTRIBUTES psa,

PCT8TR pszName);

Последний параметр, pszName, у всех этих функций одинаков. Передавая в нем NULL, вы создаете безымянный (анонимный) объект ядра. В этом случае вы можете разделять объект между процессами либо через наследование

Глава 3. Объекты ядра.docx

29

(см. предыдущий раздел), либо с помощью DuplicateHandle (см. следующий раздел). А чтобы разделять объект по имени, вы должны присвоить ему какое-нибудь имя.

Тогда вместо NULL в параметре pszName нужно передать адрес строки с именем, завершаемой нулевым символом. Имя может быть длиной до MAX_PATH знаков (это значение определено как 260). К сожалению, Майкрософт ничего не сообщает о правилах именования объектов ядра. Например, создавая объект с именем JerfObj, вы никак не застрахованы от того, что в системе еще нет объекта ядра с таким именем. И что хуже, все эти объекты делят единое пространство имен. Из-за этого следующий вызов CreateSemaphore будет всегда возвращать

NULL:

HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("JeffObj"));

HANDLE hSem = CreateSemaphore(NULL, 1, 1, TEXT("JeffObj"));

DWORD dwErrorCode = GetLastError();

После выполнения этого фрагмента значение dwErrorCode будет равно 6 (ERROR_INVALID_HANDLE). Полученный код ошибки не слишком вразумителен, но другого не дано.

Итак, вы узнали, как давать объектам имена, а теперь посмотрим, как организовать совместное использование именованных объектов. Допустим, после запуска процесса А вызывается функция:

HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));

Этот вызов заставляет систему создать новенький, как с иголочки, объект ядра «мьютекс» и присвоить ему имя JeffMutex. Заметьте, что описатель hMutexProcessA в процессе А не является наследуемым, — он и не должен быть таковым при простом именовании объектов.

Спустя какое-то время некий процесс порождает процесс В. Необязательно, чтобы последний был дочерним от процесса А; он может быть порожден Explorer или любым другим приложением. (В этом, кстати, и состоит преимущество механизма именования объектов перед наследованием.) Когда процесс В приступает к работе, исполняется код:

HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));

При этом вызове система сначала проверяет, не существует ли уже объект ядра с таким именем. Если да, то ядро проверяет тип этого объекта. Поскольку мы пытаемся создать мьютекс и его имя тоже JeffMutex, система проверяет права доступа вызывающего процесса к этому объекту. Если у него есть все права, в таблице описателей, принадлежащей процессу В, создается новая запись, указывающая на существующий объект ядра. Если же вызывающий процесс не имеет полных прав на доступ к объекту или если типы двух объектов с одинаковыми именами не совпадают, вызов CreateMutex заканчивается неудачно и возвращает-

ся NULL.

30 Часть I Материалы для обязательного чтения

Примечание. Функции, создающие объекты ядра (например, CreateSemaphore), всегда возвращают описатели, обеспечивающие полный доступ. Чтобы ограничить доступ к созданному объекту, следует использовать расширенные версии этих функций (с суффиксом Ex в именах). Расширенные функции принимают дополнительный DWORD-параметр dwDesiredAccess. Например, можно разрешить либо запретить вызов ReleaseSemaphore на семафоре, соответственно указывая либо не указывая

SEMAPHORE_MODIFY_STATE при вызове CreateSemaphoreEx. Подробнее о правах, поддерживаемых объектами ядра, см. в документации Windows SDK (http://msdn2.microsoft.com/en-us/library/ms686670.aspx)

Однако, хотя процесс В успешно вызвал CreateMutex, новый объект-мьютекс он не создал. Вместо этого он получил свой описатель существующего объектамьютекса. Счетчик объекта, конечно же, увеличился на 1, и теперь этот объект не разрушится, пока его описатели не закроют оба процесса — А и В. Заметьте, что значения описателей объекта в обоих процессах скорее всего разные, но так и должно быть: каждый процесс будет оперировать с данным объектом ядра, используя свой описатель.

Примечание. Разделяя объекты ядра по именам, помните об одной крайне важной вещи, вызывая CreateMutex, процесс В передает ей атрибуты защиты и второй параметр. Так вот, эти параметры игнорируются, если объект с указанным именем уже существует! Приложение может определить, что оно делает: создает новый объект ядра или просто открывает уже существующий, — вызвав GetLastError сразу же после вызова одной из Create- функций:

HANDLE hMutex = CreateMutex(&sa, FALSE, TEXT("JeffObj")); if (GetLastError() == ERROR_ALREADY_EXISTS) {

//Открыт описатель существующего объекта;

//sa.lpSecurityDescriptor и второй параметр

//(FALSE) игнорируются.

}else {

//Создан совершенно новый объект;

//sa.lpSecurityDescriptor и второй параметр

//(FALSE) используются при создании объекта.

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

HANDLE OpenMutex(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

Глава 3. Объекты ядра.docx

31

HANDLE OpenEvent(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

HANDLE OpenSemaphore(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

HANDLE OpenWaitableTimer(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

HANDLE OpenFileMapping(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

HANDLE OpenJobObject(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

Заметьте: все эти функции имеют один прототип. Последний параметр, pszName, определяет имя объекта ядра. В нем нельзя передать NULL — только адрес строки с нулевым символом в конце. Эти функции просматривают единое пространство имен объектов ядра, пытаясь найти совпадение. Если объекта ядра с указанным именем нет, функции возвращают NULL, а GetLastError — код 2 (ERROR_FILE_NOT_FOUND). Но если объект ядра с заданным именем существует и если его тип идентичен тому, что вы указали, система проверяет, разрешен ли к данному объекту доступ запрошенного вида (через параметр dwDesiredAccess). Если такой вид доступа разрешен, таблица описателей в вызывающем процессе обновляется, и счетчик числа пользователей объекта возрастает на 1. Если вы присвоили параметру bInheritHandle значение TRUE, то получите наследуемый описатель.

Главное отличие между вызовом Create- и Open-функций в том, что при отсутствии указанного объекта Create-функция создает его, а Open-функция просто уведомляет об ошибке.

Как я уже говорил, Майкрософт ничего не сообщает о правилах именования объектов ядра. Но представьте себе, что пользователь запускает две программы от разных компаний и каждая программа пытается создать объект с именем «MyObject». Ничего хорошего из этого не выйдет. Чтобы избежать такой ситуации, я бы посоветовал создавать GUID и использовать его строковое представление как имя объекта.

32 Часть I Материалы для обязательного чтения

Именованные объекты часто применяются для того, чтобы не допустить запуска нескольких экземпляров одного приложения. Для этого вы просто вызываете одну из Create-функций в своей функции _tmain или _tWinMain и создаете некий именованный объект. Какой именно — не имеет ни малейшего значения. Сразу после Create-функции вы должны вызвать GetLastError. Если она вернет ERROR_ALREADY_EXISTS, значит, один экземпляр Вашего приложения уже выполняется и новый его экземпляр можно закрыть. Вот фрагмент кода, иллюстрирующий этот прием:

int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow) {

HANDLE h = CreateMutex(NULL, FALSE, TEXT("{FA531CC1-0497-11d3-A180-00105A276C3E}"));

if (GetLastError() == ERROR_ALREADY_EXISTS) {

//экземпляр этого приложения уже выполняется

//закрываем объект и возвращаем управление

CloseHandle(h);

return(0);

}

//запущен первый экземпляр этого приложения

//перед выходом закрываем объект

CloseHandle(h);

return(0);

}

Пространства имен Terminal Services

Службы терминалов (Terminal Services) несколько меняют описанный выше сценарий. Если на машине работают Terminal Services, существует множество пространств имен для объектов ядра. Объекты, которые должны быть доступны всем клиентам, используют одно глобальное пространство имен. (Такие объекты, как правило, связаны с сервисами, предоставляемыми клиентским программам.) В каждом клиентском сеансе формируется свое пространство имен, чтобы исключить конфликты между несколькими сеансами, в которых запускается одно и то же приложение. Ни из какого сеанса нельзя получить доступ к объектам другого сеанса, даже если у их объектов идентичные имена. Этот сценарий актуален не только для серверов, но и для клиентов, поскольку такие механизмы, как Remote Desktop и Fast User Switching также основаны на сеансах служб терминалов.

Примечание. Первый (неинтерактивный) сеанс создается службами, когда в систему еще не вошел ни один пользователь. В Windows Vista, в отличие от предыдущих версий Windows, после входа пользователя приложения запускаются в новом сеансе, отличном от Session 0 (сеанс 0

Глава 3. Объекты ядра.docx

33

принадлежит исключительно службам). Это позволит надежнее изолировать ключевые компоненты системы, которые обычно работают с высоким уровнем привилегий, от вредоносных программ, которые может запустить неосторожный пользователь.

Разработчикам служб следует учесть, что требование исполнения служб и их клиентских приложений в разных сеансах влияет на правила именования общих объектов ядра. Теперь объекты ядра, которые должны быть доступны пользовательским приложениям, необходимо создавать в глобальном пространстве имен. Сходные проблемы возникают при написании служб, взаимодействующих с приложениями, запущенными в разных сеансах, созданных посредством Fast User Switching. Подробнее об изоляции сеанса Session 0 и ее последствиях для разработчиков служб см. в статье «Impact of Session 0 Isolation on Services and Drivers in Windows Vista» (http://www.microsoft.com/whdc/system/vista/services.aspx)

Выяснить, в каком из сеансов Terminal Services работает ваш процесс, поможет функция ProcessIdToSessionId (объявленная в WinBase.h и доступная в биб-

лиотеке kernel32.dll):

DWORD processID = GetCurrentProcessId(); DWORD sessionID;

if (ProcessIdToSessionId(processID, &sessionID)) { tprintf(

TEXT("Process '%u' runsi*ferroinal Services session '%u'"), processID, sessionID);

}else {

//Вызов ProcessIdToSessionId закончится неудачей, если

//у вас нет права доступа к процессу, ID которого вы передаете.

//Здесь такой опасности нет, поскольку используется ID

//процесса-владельца.

tprintf(

TEXT("Unable to get Terminal Services session ID for process '%u'"), processID);

}

Именованные объекты ядра, относящиеся к какому-либо сервису, всегда находятся в глобальном пространстве имен, а аналогичный объект, связанный с приложением, Terminal Server по умолчанию помещает в пространство имен клиентского сеанса. Однако и его можно перевести в глобальное пространство имен, поставив перед именем объекта префикс <<Global\», как в примере ниже.

HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Global\\MyName");

Если вы хотите явно указать, что объект ядра должен находиться в пространстве имен клиентского сеанса, используйте префикс <Local\»:

HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Local\\MyName"));

34 Часть I Материалы для обязательного чтения

Майкрософт рассматривает префиксы Global и Local как зарезервированные ключевые слова, которые не должны встречаться в самих именах объектов (если они не находятся в отдельном пространстве имен). К числу таких слов Майкрософт относит и Session, хотя на сегодняшний день оно не связано ни с какой функциОнальностью. Например, допустимо использовать Session\<ID_текущего_сеанса>\, но создавать объекты с именами, содержащими префикс Session, запрещено: соответствующий вызов заканчивается неудачей, а

GetLastError возвращает ERROR_ACCESS_DENIED.

Примечание. Эти ключевые слова чувствительные к регистру.

Закрытые пространства имен

Доступ к объекту ядра можно защитить, передав при создании объекта указатель на структуру SECURITY_ATTRIBUTES. Однако в версиях Windows, предшествующих Vista, защитить общий именованный объект от несанкционированного доступа было невозможно. Любому процессу, даже обладающему минимальными привилегиями, было разрешено создавать именованные объекты. Так, в предыдущая программа-пример использовала именованный мьютекс, чтобы определить, работает ли она. При этом можно без труда написать приложение, которое создает объект ядра с тем же самым именем. Если такая программа стартует перед singleton-приложением, то запустить последнее уже не удастся: оно тут же завершится, так сочтет, что в системе уже работает экземпляр такого приложения. На этом приеме основаны некоторые DoS-атаки. Заметьте, что DoS-атаки по неименованным объектам ядра невозможны, поэтому в приложениях часто используются именно такие объекты, несмотря на то, что они не могут одновременно использоваться разными процессами.

Чтобы защитить объекты ядра, которые создает ваше приложение, от конфликтов имен и атак злоумышленников, следует, определив собственный префикс, создать закрытое пространство имен. Серверный процесс, ответственный за создание объектов ядра, определяет дескриптор границ (boundary descriptor), защищающий имя самого пространства имен.

Использование закрытых пространств имен для более безопасной реализации singleton-сценариев демонстрируется на примере приложения «Singleton» (файл 03-Singleton.exe, см.также листинг Singleton.cpp ниже). После запуска этого приложения открывается окно, показанное на рис. 3-5.

Глава 3. Объекты ядра.docx

35

Рис. 3-5. Первый экземпляр приложения Singleton

Если, не завершая первого экземпляра Singleton, попытаться запустить второй, откроется окно с сообщением о том, что обнаружен работающий экземпляр программы (рис. 3-6).

Рис. 3-6. Результат попытки запуска второго экземпляра Singleton

В листинге ниже на примере функции CheckInstances показано, как создать границу, связать с ней идентификатор защиты (security identifier, SID) группы Local Administrators и создать (или открыть) закрытое пространство имен, имя которого будет использовано в качествен префикса для имена объекта ядра (мьютекса). Дескриптор границы получает имя, и, что важнее, SID связанной с ним привилегированной группы. Таким образом Windows гарантирует, что только приложения, работающие в контексте пользователя из этой группы получат доступ к этому пространству имен и созданным в нем объектам ядра, имена которых содержат его имя в виде префикса.

Предположим, что вредоносная программа с низким уровнем привилегий, похитив имя и SID, создает идентичный дескриптор границы. Ее попытка создать или открыть закрытое пространство имен, защищенное привилегированной учетной записью, закончится неудачей, а GetLastError при этом вернет ERROR_ACCESS_DENIED. Если же у вредоносной программы окажется достаточно привилегий, чтобы проникнуть в закрытое пространство имен, это будет уже не важно, поскольку с такими привилегиями она сможет причинить куда больший вред, чем простой захват имени объекта ядра.

Singleton.cpp

/******************************************************************** Module: Singleton.cpp

36 Часть I Материалы для обязательного чтения

Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre

******************************************************************///

#include "stdafx.h" #include "resource.h"

 

 

#include "..\CommonFiles\CmnHdr.h"

/* См. Приложение А. */

#include <windowsx.h>

 

#include <Sddl.h>

// Необходимо для работы с SID

#include <tchar.h>

 

#include <strsafe.h>

 

/////////////////////////////////////////////////////////////////////

// Главный диалог

HWND g_hDlg;

//Мьютекс, граница и пространство имен, необходимые для

//обнаружения существующего экземпляра программы.

HANDLE

g_hSingleton = NULL;

HANDLE

g_hBoundary = NULL;

HANOLE

g_hNamespace = NULL;

 

 

// переменная, отслеживающая состояние пространства имен

BOOL g_bNamespaceOpened = FALSE;

// имена границы и закрытого пространства имен

PCTSTR

g_szBoundary = TEXT("3-Boundary");

PCTSTR

g_szNamespace = TEXT("3-Namespace");

#define DETAILS_CTRL GetDlgItem(g_hDlg, IDC_EDIT_DETAILS)

/////////////////////////////////////////////////////////////////////

// Adds а string to the "Details" edit control void AddText(PCTSTR pszFormat, …) {

va_list argList; va_stаrt(аrgList, pszFormat);

TCHAR 8z[20 * 1024];

Edit_GetText(DETAILS_CTRL, sz, _countof(sz)); _vstprintf_s(

_tcschr(sz, TEXT('\0')), _countof(sz) - _tcslen(sz), pszFormat, argList);

Edit_SetText(DETAILS_CTRL, sz);

Соседние файлы в предмете Программирование на C++