Как программно предоставить или отозвать привилегию?

Автор: Александр Федотов
Опубликовано: 09.03.2002
Версия текста: 1.0

Демонстрационное приложение

Введение

При создании некоторых приложений для Windows NT вы можете столкнуться с задачей программного управления привилегиями пользователей. Например, чтобы служба, зарегистрированная под своей собственной учетной записью, могла запуститься, ее учетной записи должно быть предоставлено право входа в систему в качестве службы. Конечно, вы можете направить пользователя в Панель управления, чтобы он сделал это с использованием стандартного пользовательского интерфейса Windows NT, но будет гораздо лучше, если программа установки вашего продукта сделает это автоматически. В этой статье мы рассмотрим механизмы, предоставляемые Windows NT для программного управления привилегиями и правами пользователей.

Документация Microsoft различает понятия привилегия (privilege) и право пользователя (user right). Привилегии и права пользователей похожи в том смысле, что и те, и другие определяют, какие операции могут быть произведены пользователем. C другой стороны, права пользователей используются системой внутренне, они не поддерживаются функциями для работы с привилегиями, такими как LookupPrivilegeValue или PrivilegeCheck. Функции, которые мы будем обсуждать в этой статье, одинаково хорошо работают как с привилегиями, так и с правами пользователей, поэтому для целей этой статьи мы будем считать эти два понятия синонимами.

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

Программный интерфейс Local Security Authority

В операционной системе Windows NT всеми вопросами безопасности занимается защищенная подсистема, известная под названием Local Security Authority (LSA). Именно эта подсистема поддерживает базу данных учетных записей, содержащую в том числе и информацию о правах пользователей. По используемым структурам данных, программный интерфейс LSA больше похож на внутренний API Windows NT (native API), чем на Win32. Многие структуры данных, определяемые LSA, имеют прямые аналоги в native API и в программных интерфейсах режима ядра, используемых драйверами устройств.

Одной из таких структур данных является LSA_UNICODE_STRING, которая определяет строку, состоящую из символов Unicode. В отличие от обычных строк языка С, где конец обозначается нулевым символом, длина строки, определяемой этой структурой, задается явно.

typedef struct _LSA_UNICODE_STRING {
    USHORT Length;          // длина строки в байтах
    USHORT MaximumLength;   // размер буфера в байтах
    PWSTR  Buffer;          // указатель на буфер
} LSA_UNICODE_STRING, * PLSA_UNICODE_STRING;

Хочу обратить ваше внимание на то, что поля Length и MaximumLength содержат значения в байтах, а не в символах, как это принято в Win32. В приводимых ниже примерах мы будем пользоваться функцией InitUnicodeString (см. листниг 1), которая инициализирует эту структуру на основе обычной строки языка C, заканчивающейся нулевым символом (те, кто занимался разработкой драйверов устройств, должны сейчас вспомнить созвучную функцию, предоставляемую системой).

Листинг 1. Функция InitUnicodeString
VOID InitUnicodeString(
    OUT PLSA_UNICODE_STRING pUnicodeString,
    IN PCWSTR pSourceString
    )
{
    _ASSERTE(pUnicodeString != NULL);
    _ASSERTE(pSourceString != NULL);

    ULONG Length = wcslen(pSourceString) * sizeof(WCHAR);

    pUnicodeString->Length        = (USHORT)Length;
    pUnicodeString->MaximumLength = (USHORT)(Length + sizeof(WCHAR));
    pUnicodeString->Buffer        = (PWSTR)pSourceString;
}

Другой концепцией, заимствованной из native API, является возвращаемое значение типа NTSTATUS; подавляющее количество функций LSA имеют такое возвращаемое значение. NTSTATUS - это 32-битное значение, структура которого похожа на структуру HRESULT, широко используемого в COM. В отличие от HRESULT, который обозначает либо успешное завершение, либо ошибку, NTSTATUS имеет четыре градации успеха, закодированные в двух старших битах. Для проверки значения типа NTSTATUS служит макрос LSA_SUCCESS.

Чтобы по значению NTSTATUS получить привычный код ошибки Win32, LSA предоставляет функцию c неудобочитаемым названием LsaNtStatusToWinError:

ULONG LsaNtStatusToWinError(
    IN NTSTATUS Status
    );

Довольно общих сведений о программном интерфейсе LSA, приступим к решению нашей непосредственной задачи. Первое, что нам понадобится сделать - это получить доступ к объекту полититки безопасности на интересующем нас компьютере. Функция LsaOpenPolicy возвращает хэндл (handle) объекта политики безопасности для указанной машины:

NTSTATUS LsaOpenPolicy(
    IN PLSA_UNICODE_STRING SystemName,          // имя компьютера
    IN PLSA_OBJECT_ATTRIBUTES ObjectAttributes, // атрибуты
    IN ACCESS_MASK DesiredAccess,               // права доступа
    OUT PLSA_HANDLE PolicyHandle                // хэндл объекта политики
    );

Первый параметр функции задает имя компьютера, на котором открывается объект политики безопасности. Если этот параметр указан как NULL, то открывается объект политики локальной машины. Второй параметр содержит указатель на структуру, задающую атрибуты объекта (еще одна структура из native API). LsaOpenPolicy содержательно не использует этот параметр, так что структуру надо просто заполнить нулями. Параметр DesiredAccess указывает запрашиваемые права доступа к объекту политики, а последний параметр функции содержит указатель на переменную, в которую при успешном завершении заносится хэндл объекта политики.

Когда полученный хэндл больше не нужен, его следует закрыть функцией LsaClose:

NTSTATUS LsaClose(
    IN LSA_HANDLE ObjectHandle
    );

Предоставление привилегии

Для предоставления привилегий учетной записи служит функция LsaAddAccountRights, которая позволяет за один вызов добавить сразу несколько привилегий.

NTSTATUS LsaAddAccountRights(
    IN LSA_HANDLE PolicyHandle,         // хэндл политики
    IN PSID AccountSid,                 // SID учетной записи
    IN PLSA_UNICODE_STRING UserRights,  // массив привилегий
    IN ULONG CountOfRights              // количество привилегий
    );

Первый параметр функции задает хэндл объекта политики безопасности. Параметр AccountSid содержит так называемый идентификатор безопасности (security identifier, SID) учетной записи, которой предоставляются привилегии. Идентификатор безопасности - это двоичная строка, однозначно идентифицирующая учетную запись. Последние два параметра функции задают массив привилегий, которые предоставляются учетной записи. Каждая привилегия идентифицируется своим именем, которое обычно задается с помошью соответствующего макроса из winnt.h, например, SE_DEBUG_NAME.

В листинге 2 приведен исходный код функции GrantPrivilege, реализующей предоставление привилегии. Функция принимает на вход имя компьютера, которое может быть указано как NULL для локальной машины, имя или SID учетной записи и имя предоставляемой привилегии. При успешном завершении функция возвращает TRUE. В случае ошибки возвращаемое значение равно FALSE и код ошибки может быть получен с помощью GetLastError, как это принято в Win32.

Листинг 2. Функция GrantPrivilege
BOOL GrantPrivilege(
    IN PCTSTR pszMachineName,    // имя компьютера
    IN PSID pUserSid,            // SID учетной записи
    IN PCTSTR pszUserName,       // имя учетной записи
    IN PCTSTR pszPrivilegeName   // имя привилегии
    )
{
    _ASSERTE(pUserSid != NULL || pszUserName != NULL);
    _ASSERTE(pUserSid == NULL || IsValidSid(pUserSid));
    _ASSERTE(pszPrivilegeName != NULL);

    BYTE bSid[8 + 4 * SID_MAX_SUB_AUTHORITIES];

    if (pUserSid == NULL)
    {
        DWORD cbSid = sizeof(bSid);
        TCHAR szDomainName[DNLEN + 1];
        DWORD cchDomainName = countof(szDomainName);
        SID_NAME_USE Use;
        
        if (pszUserName[0] == _T('.') &&
            pszUserName[1] == _T('\\'))
            pszUserName += 2;

        // получаем SID учетной записи
        if (!LookupAccountName(pszMachineName, pszUserName, (PSID)bSid,
                                &cbSid, szDomainName, &cchDomainName, &Use))
            return FALSE;

        pUserSid = (PSID)bSid;
    }
    
    _ASSERTE(pUserSid != NULL);
    _ASSERTE(IsValidSid(pUserSid));
    
    USES_CONVERSION;

    NTSTATUS Status;
    LSA_HANDLE hPolicy;
    LSA_UNICODE_STRING SystemName;
    LSA_UNICODE_STRING UserRight;
    PLSA_UNICODE_STRING pSystemName = NULL;
    LSA_OBJECT_ATTRIBUTES ObjAttr;

    if (pszMachineName != NULL)
    {
        InitUnicodeString(&SystemName, T2CW(pszMachineName));
        pSystemName = &SystemName;
    }

    InitUnicodeString(&UserRight, T2CW(pszPrivilegeName));
    memset(&ObjAttr, 0, sizeof(ObjAttr));

    // открываем объект политики
    Status = LsaOpenPolicy(pSystemName, &ObjAttr, 
                           POLICY_LOOKUP_NAMES|POLICY_CREATE_ACCOUNT,
                           &hPolicy);
    if (!LSA_SUCCESS(Status))
        return SetLastError(LsaNtStatusToWinError(Status)), FALSE;

    // добавляем привилегию учетной записи
    Status = LsaAddAccountRights(hPolicy, pUserSid, &UserRight, 1);

    // закрываем хэндл политики
    LsaClose(hPolicy);

    if (!LSA_SUCCESS(Status))
        return SetLastError(LsaNtStatusToWinError(Status)), FALSE;

    return TRUE;
}

GrantPrivilege позволяет задать учетную запись как по имени, так и непосредственно идентификатором безопасности. Если учетная запись задана своим именем, функция получает ее SID посредством LookupAccountName. Затем она открывает объект политики безопасности на указанном компьютере, добавляет привилегию и закрывает хэндл объекта политики.

Необходимо заметить, что изменения в наборе привилегий вступят в силу только при новом входе пользователя в систему, то есть при создании новой логон-сессии. Набор привилегий существующих логон-сессий останется неизменным, так как список привилегий формируется LSA при создании логон-сессии и заносится в объект токена (token object), который представляет логон-сессию. Все последующие проверки привилегий используют объект токена, а не привилегии, записанные в базе данных учетных записей.

Отзыв привилегии

Отзыв привилегий осуществляется функцией LsaRemoveAccountRights:

NTSTATUS LsaRemoveAccountRights(
    IN LSA_HANDLE PolicyHandle,         // хэндл объекта политики
    IN PSID AccountSid,                 // SID учетной записи
    IN BOOLEAN AllRights,               // отозвать все привилегии
    IN PLSA_UNICODE_STRING UserRights,  // массив привилегий
    IN ULONG CountOfRights              // количество привилегий
    );

Параметры этой функции аналогичны параметрам LsaAddAccountRights. Дополнительный параметр AllRights позволяет отозвать все привилегии, предоставленные данной учетной записи.

Функция RevokePrivilege (листинг 3) реализует отзыв привилегии. Ее параметры имеют тот же смысл, что и соответствующие параметры GrantPrivilege.

Листинг 3. Функция RevokePrivilege
// из NTDDK
#define STATUS_SUCCESS                   ((NTSTATUS)0x00000000L)
#define STATUS_OBJECT_NAME_NOT_FOUND     ((NTSTATUS)0xC0000034L)

BOOL RevokePrivilege(
    IN PCTSTR pszMachineName,    // имя компьютера
    IN PSID pUserSid,            // SID учетной записи
    IN PCTSTR pszUserName,       // имя учетной записи
    IN PCTSTR pszPrivilegeName   // имя привилегии
    )
{
    _ASSERTE(pUserSid != NULL || pszUserName != NULL);
    _ASSERTE(pUserSid == NULL || IsValidSid(pUserSid));
    _ASSERTE(pszPrivilegeName != NULL);

    BYTE bSid[8 + 4 * SID_MAX_SUB_AUTHORITIES];

    if (pUserSid == NULL)
    {
        DWORD cbSid = sizeof(bSid);
        TCHAR szDomainName[DNLEN + 1];
        DWORD cchDomainName = countof(szDomainName);
        SID_NAME_USE Use;
        
        if (pszUserName[0] == _T('.') &&
            pszUserName[1] == _T('\\'))
            pszUserName += 2;

        // получаем SID учетной записи
        if (!LookupAccountName(pszMachineName, pszUserName, (PSID)bSid,
                                &cbSid, szDomainName, &cchDomainName, &Use))
            return FALSE;

        pUserSid = (PSID)bSid;
    }
    
    _ASSERTE(pUserSid != NULL);
    _ASSERTE(IsValidSid(pUserSid));
    
    USES_CONVERSION;

    NTSTATUS Status;
    LSA_HANDLE hPolicy;
    LSA_UNICODE_STRING SystemName;
    LSA_UNICODE_STRING UserRight;
    PLSA_UNICODE_STRING pSystemName = NULL;
    LSA_OBJECT_ATTRIBUTES ObjAttr;

    if (pszMachineName != NULL)
    {
        InitUnicodeString(&SystemName, T2CW(pszMachineName));
        pSystemName = &SystemName;
    }

    InitUnicodeString(&UserRight, T2CW(pszPrivilegeName));
    memset(&ObjAttr, 0, sizeof(ObjAttr));

    // открываем объект политики безопасности
    Status = LsaOpenPolicy(pSystemName, &ObjAttr, POLICY_LOOKUP_NAMES,
                           &hPolicy);
    if (!LSA_SUCCESS(Status))
        return SetLastError(LsaNtStatusToWinError(Status)), FALSE;

    // отзываем привилегию
    Status = LsaRemoveAccountRights(hPolicy, pUserSid, FALSE, &UserRight, 1);
    if (Status == STATUS_OBJECT_NAME_NOT_FOUND)
        Status = STATUS_SUCCESS;

    // закрываем хэндл политики
    LsaClose(hPolicy);

    if (!LSA_SUCCESS(Status))
        return SetLastError(LsaNtStatusToWinError(Status)), FALSE;

    return TRUE;
}

Если учетная запись не имеет привилегии, которую мы пытаемся отозвать, функция LsaRemoveAccountRights возвращает код ошибки STATUS_OBJECT_NAME_NOT_FOUND. Наша функция не считает такую ситуацию ошибочной, поэтому она отдельно проверяет код возврата LsaRemoveAccountRights на это значение и заменяет его значением STATUS_SUCCESS, индицирующим успешное завершение.

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

Заключение

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

Ссылки

  1. HOWTO: Manage User Privileges Programmatically in Windows NT, Q132958, Microsoft Knowledge Base.

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