mirror of
https://github.com/reactos/reactos.git
synced 2024-11-01 04:11:30 +00:00
49da1cdbeb
Make sure RtlRemovePrivileges gets compiled, when DLL_EXPORT_VERSION is 0x600+ Enable all existing functions from rtl_vista. Hack CsrNewThread export to exist on later versions, too
591 lines
19 KiB
C
591 lines
19 KiB
C
/*
|
|
* COPYRIGHT: See COPYING in the top level directory
|
|
* PROJECT: ReactOS system libraries
|
|
* FILE: lib/rtl/priv.c
|
|
* PURPOSE: Security related functions and Security Objects
|
|
* PROGRAMMER: Eric Kohl
|
|
* Pierre Schweitzer (pierre@reactos.org)
|
|
*/
|
|
|
|
/* INCLUDES *****************************************************************/
|
|
|
|
#include <rtl.h>
|
|
|
|
#define NDEBUG
|
|
#include <debug.h>
|
|
|
|
/* FUNCTIONS ***************************************************************/
|
|
|
|
/*
|
|
* @implemented
|
|
*/
|
|
NTSTATUS
|
|
NTAPI
|
|
RtlpOpenThreadToken(IN ACCESS_MASK DesiredAccess,
|
|
OUT PHANDLE TokenHandle)
|
|
{
|
|
NTSTATUS Status;
|
|
|
|
Status = ZwOpenThreadToken(NtCurrentThread(), DesiredAccess,
|
|
TRUE, TokenHandle);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
Status = ZwOpenThreadToken(NtCurrentThread(), DesiredAccess,
|
|
FALSE, TokenHandle);
|
|
}
|
|
|
|
return Status;
|
|
}
|
|
|
|
/*
|
|
* @implemented
|
|
*/
|
|
NTSTATUS
|
|
NTAPI
|
|
RtlImpersonateSelf(IN SECURITY_IMPERSONATION_LEVEL ImpersonationLevel)
|
|
{
|
|
HANDLE ProcessToken;
|
|
HANDLE ImpersonationToken;
|
|
NTSTATUS Status;
|
|
OBJECT_ATTRIBUTES ObjAttr;
|
|
SECURITY_QUALITY_OF_SERVICE Sqos;
|
|
|
|
PAGED_CODE_RTL();
|
|
|
|
Status = ZwOpenProcessToken(NtCurrentProcess(),
|
|
TOKEN_DUPLICATE,
|
|
&ProcessToken);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DPRINT1("NtOpenProcessToken() failed (Status %lx)\n", Status);
|
|
return Status;
|
|
}
|
|
|
|
Sqos.Length = sizeof(SECURITY_QUALITY_OF_SERVICE);
|
|
Sqos.ImpersonationLevel = ImpersonationLevel;
|
|
Sqos.ContextTrackingMode = 0;
|
|
Sqos.EffectiveOnly = FALSE;
|
|
|
|
InitializeObjectAttributes(&ObjAttr,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
NULL);
|
|
|
|
ObjAttr.SecurityQualityOfService = &Sqos;
|
|
|
|
Status = ZwDuplicateToken(ProcessToken,
|
|
TOKEN_IMPERSONATE,
|
|
&ObjAttr,
|
|
Sqos.EffectiveOnly, /* why both here _and_ in Sqos? */
|
|
TokenImpersonation,
|
|
&ImpersonationToken);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DPRINT1("NtDuplicateToken() failed (Status %lx)\n", Status);
|
|
NtClose(ProcessToken);
|
|
return Status;
|
|
}
|
|
|
|
Status = ZwSetInformationThread(NtCurrentThread(),
|
|
ThreadImpersonationToken,
|
|
&ImpersonationToken,
|
|
sizeof(HANDLE));
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DPRINT1("NtSetInformationThread() failed (Status %lx)\n", Status);
|
|
}
|
|
|
|
ZwClose(ImpersonationToken);
|
|
ZwClose(ProcessToken);
|
|
|
|
return Status;
|
|
}
|
|
|
|
/*
|
|
* @implemented
|
|
*/
|
|
NTSTATUS
|
|
NTAPI
|
|
RtlAcquirePrivilege(IN PULONG Privilege,
|
|
IN ULONG NumPriv,
|
|
IN ULONG Flags,
|
|
OUT PVOID *ReturnedState)
|
|
{
|
|
PRTL_ACQUIRE_STATE State;
|
|
NTSTATUS Status, IntStatus;
|
|
ULONG ReturnLength, i, OldSize;
|
|
SECURITY_QUALITY_OF_SERVICE Sqos;
|
|
OBJECT_ATTRIBUTES ObjectAttributes;
|
|
HANDLE ImpersonationToken = 0, ProcessToken;
|
|
|
|
DPRINT("RtlAcquirePrivilege(%p, %u, %u, %p)\n", Privilege, NumPriv, Flags, ReturnedState);
|
|
|
|
/* Validate flags */
|
|
if (Flags & ~(RTL_ACQUIRE_PRIVILEGE_PROCESS | RTL_ACQUIRE_PRIVILEGE_IMPERSONATE))
|
|
{
|
|
return STATUS_INVALID_PARAMETER;
|
|
}
|
|
|
|
/* If user wants to acquire privileges for the process, we have to impersonate him */
|
|
if (Flags & RTL_ACQUIRE_PRIVILEGE_PROCESS)
|
|
{
|
|
Flags |= RTL_ACQUIRE_PRIVILEGE_IMPERSONATE;
|
|
}
|
|
|
|
/* Allocate enough memory to hold: old privileges (fixed buffer size, might not be enough)
|
|
* new privileges (big enough, after old privileges memory area)
|
|
*/
|
|
State = RtlAllocateHeap(RtlGetProcessHeap(), 0, sizeof(RTL_ACQUIRE_STATE) + sizeof(TOKEN_PRIVILEGES) +
|
|
(NumPriv - ANYSIZE_ARRAY) * sizeof(LUID_AND_ATTRIBUTES));
|
|
if (!State)
|
|
{
|
|
return STATUS_NO_MEMORY;
|
|
}
|
|
|
|
/* Only zero a bit of the memory (will be faster that way) */
|
|
State->Token = 0;
|
|
State->OldImpersonationToken = 0;
|
|
State->Flags = 0;
|
|
State->OldPrivileges = NULL;
|
|
|
|
/* Check whether we have already an active impersonation */
|
|
if (NtCurrentTeb()->IsImpersonating)
|
|
{
|
|
/* Check whether we want to impersonate */
|
|
if (Flags & RTL_ACQUIRE_PRIVILEGE_IMPERSONATE)
|
|
{
|
|
/* That's all fine, just get the token.
|
|
* We need access for: adjust (obvious...) but also
|
|
* query, to be able to query old privileges
|
|
*/
|
|
Status = RtlpOpenThreadToken(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &State->Token);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
RtlFreeHeap(RtlGetProcessHeap(), 0, State);
|
|
return Status;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Otherwise, we have to temporary disable active impersonation.
|
|
* Get previous impersonation token to save it
|
|
*/
|
|
Status = RtlpOpenThreadToken(TOKEN_IMPERSONATE, &State->OldImpersonationToken);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
RtlFreeHeap(RtlGetProcessHeap(), 0, State);
|
|
return Status;
|
|
}
|
|
|
|
/* Remember the fact we had an active impersonation */
|
|
State->Flags |= RTL_ACQUIRE_PRIVILEGE_IMPERSONATE;
|
|
|
|
/* Revert impersonation (ie, give 0 as handle) */
|
|
Status = ZwSetInformationThread(NtCurrentThread(),
|
|
ThreadImpersonationToken,
|
|
&ImpersonationToken,
|
|
sizeof(HANDLE));
|
|
}
|
|
}
|
|
|
|
/* If we have no token yet (which is likely) */
|
|
if (!State->Token)
|
|
{
|
|
/* If we are asked to use process, then do */
|
|
if (Flags & RTL_ACQUIRE_PRIVILEGE_PROCESS)
|
|
{
|
|
Status = ZwOpenProcessToken(NtCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
|
|
&State->Token);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Otherwise, we have to impersonate.
|
|
* Open token for duplication
|
|
*/
|
|
Status = ZwOpenProcessToken(NtCurrentProcess(), TOKEN_DUPLICATE, &ProcessToken);
|
|
|
|
InitializeObjectAttributes(&ObjectAttributes,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
NULL);
|
|
|
|
ObjectAttributes.SecurityQualityOfService = &Sqos;
|
|
Sqos.Length = sizeof(SECURITY_QUALITY_OF_SERVICE);
|
|
Sqos.ImpersonationLevel = SecurityDelegation;
|
|
Sqos.ContextTrackingMode = 1;
|
|
Sqos.EffectiveOnly = FALSE;
|
|
|
|
/* Duplicate */
|
|
Status = ZwDuplicateToken(ProcessToken,
|
|
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | TOKEN_IMPERSONATE,
|
|
&ObjectAttributes,
|
|
FALSE,
|
|
TokenImpersonation,
|
|
&ImpersonationToken);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
ZwClose(ProcessToken);
|
|
goto Cleanup;
|
|
}
|
|
|
|
/* Assign our duplicated token to current thread */
|
|
Status = ZwSetInformationThread(NtCurrentThread(),
|
|
ThreadImpersonationToken,
|
|
&ImpersonationToken,
|
|
sizeof(HANDLE));
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
ZwClose(ImpersonationToken);
|
|
ZwClose(ProcessToken);
|
|
goto Cleanup;
|
|
}
|
|
|
|
/* Save said token and the fact we have impersonated */
|
|
State->Token = ImpersonationToken;
|
|
State->Flags |= RTL_ACQUIRE_PRIVILEGE_IMPERSONATE;
|
|
|
|
ZwClose(ProcessToken);
|
|
}
|
|
}
|
|
|
|
/* Properly set the privileges pointers:
|
|
* OldPrivileges points to the static memory in struct (= OldPrivBuffer)
|
|
* NewPrivileges points to the dynamic memory after OldPrivBuffer
|
|
* There's NO overflow risks (OldPrivileges is always used with its size)
|
|
*/
|
|
State->OldPrivileges = (PTOKEN_PRIVILEGES)State->OldPrivBuffer;
|
|
State->NewPrivileges = (PTOKEN_PRIVILEGES)(State->OldPrivBuffer + (sizeof(State->OldPrivBuffer) / sizeof(State->OldPrivBuffer[0])));
|
|
|
|
/* Assign all the privileges to be acquired */
|
|
State->NewPrivileges->PrivilegeCount = NumPriv;
|
|
for (i = 0; i < NumPriv; ++i)
|
|
{
|
|
State->NewPrivileges->Privileges[i].Luid.LowPart = Privilege[i];
|
|
State->NewPrivileges->Privileges[i].Luid.HighPart = 0;
|
|
State->NewPrivileges->Privileges[i].Attributes = SE_PRIVILEGE_ENABLED;
|
|
}
|
|
|
|
/* Start privileges adjustements */
|
|
OldSize = sizeof(State->OldPrivBuffer);
|
|
do
|
|
{
|
|
ReturnLength = sizeof(State->OldPrivBuffer);
|
|
Status = ZwAdjustPrivilegesToken(State->Token, FALSE, State->NewPrivileges,
|
|
OldSize, State->OldPrivileges, &ReturnLength);
|
|
/* This is returned when OldPrivileges buffer is too small */
|
|
if (Status == STATUS_BUFFER_TOO_SMALL)
|
|
{
|
|
/* Try to allocate a new one, big enough to hold data */
|
|
State->OldPrivileges = RtlAllocateHeap(RtlGetProcessHeap(), 0, ReturnLength);
|
|
if (State->OldPrivileges)
|
|
{
|
|
DPRINT("Allocated old privileges: %p\n", State->OldPrivileges);
|
|
OldSize = ReturnLength;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
/* If we failed, properly set status: we failed because of the lack of memory */
|
|
Status = STATUS_NO_MEMORY;
|
|
}
|
|
}
|
|
|
|
/* If we failed to assign at least one privilege */
|
|
if (Status == STATUS_NOT_ALL_ASSIGNED)
|
|
{
|
|
/* If there was actually only one privilege to acquire, use more accurate status */
|
|
if (NumPriv == 1)
|
|
{
|
|
Status = STATUS_PRIVILEGE_NOT_HELD;
|
|
}
|
|
}
|
|
|
|
/* Fail if needed, otherwise return our state to caller */
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
else
|
|
{
|
|
*ReturnedState = State;
|
|
break;
|
|
}
|
|
} while (TRUE);
|
|
|
|
DPRINT("RtlAcquirePrivilege succeed!\n");
|
|
|
|
return Status;
|
|
|
|
Cleanup:
|
|
/* If we allocated our own buffer for old privileges, release it */
|
|
if (State->OldPrivileges && (PVOID)State->OldPrivBuffer != (PVOID)State->OldPrivileges)
|
|
{
|
|
RtlFreeHeap(RtlGetProcessHeap(), 0, State->OldPrivileges);
|
|
}
|
|
|
|
/* Do we have to restore previously active impersonation? */
|
|
if (State->Flags & RTL_ACQUIRE_PRIVILEGE_IMPERSONATE)
|
|
{
|
|
IntStatus = ZwSetInformationThread(NtCurrentThread(), ThreadImpersonationToken,
|
|
&State->OldImpersonationToken, sizeof(HANDLE));
|
|
/* If this ever happens, we're in a really bad situation... */
|
|
if (!NT_SUCCESS(IntStatus))
|
|
{
|
|
RtlRaiseStatus(IntStatus);
|
|
}
|
|
}
|
|
|
|
/* Release token */
|
|
if (State->Token)
|
|
{
|
|
ZwClose(State->Token);
|
|
}
|
|
|
|
/* And free our state buffer */
|
|
RtlFreeHeap(RtlGetProcessHeap(), 0, State);
|
|
|
|
DPRINT("RtlAcquirePrivilege() failed with status: %lx\n", Status);
|
|
|
|
return Status;
|
|
}
|
|
|
|
/*
|
|
* @implemented
|
|
*/
|
|
VOID
|
|
NTAPI
|
|
RtlReleasePrivilege(IN PVOID ReturnedState)
|
|
{
|
|
NTSTATUS Status;
|
|
PRTL_ACQUIRE_STATE State = (PRTL_ACQUIRE_STATE)ReturnedState;
|
|
|
|
DPRINT("RtlReleasePrivilege(%p)\n", ReturnedState);
|
|
|
|
/* If we had an active impersonation before we acquired privileges
|
|
* Or if we have impersonated, quit it
|
|
*/
|
|
if (State->Flags & RTL_ACQUIRE_PRIVILEGE_IMPERSONATE)
|
|
{
|
|
/* Restore it for the current thread */
|
|
Status = ZwSetInformationThread(NtCurrentThread(), ThreadImpersonationToken,
|
|
&State->OldImpersonationToken, sizeof(HANDLE));
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
RtlRaiseStatus(Status);
|
|
}
|
|
|
|
/* And close the token if needed */
|
|
if (State->OldImpersonationToken)
|
|
ZwClose(State->OldImpersonationToken);
|
|
}
|
|
else
|
|
{
|
|
/* Otherwise, restore old state */
|
|
Status = ZwAdjustPrivilegesToken(State->Token, FALSE,
|
|
State->OldPrivileges, 0, NULL, NULL);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
RtlRaiseStatus(Status);
|
|
}
|
|
}
|
|
|
|
/* If we used a different buffer for old privileges, just free it */
|
|
if ((PVOID)State->OldPrivBuffer != (PVOID)State->OldPrivileges)
|
|
{
|
|
DPRINT("Releasing old privileges: %p\n", State->OldPrivileges);
|
|
RtlFreeHeap(RtlGetProcessHeap(), 0, State->OldPrivileges);
|
|
}
|
|
|
|
/* Release token and free state */
|
|
ZwClose(State->Token);
|
|
RtlFreeHeap(RtlGetProcessHeap(), 0, State);
|
|
}
|
|
|
|
/*
|
|
* @implemented
|
|
*/
|
|
NTSTATUS
|
|
NTAPI
|
|
RtlAdjustPrivilege(IN ULONG Privilege,
|
|
IN BOOLEAN Enable,
|
|
IN BOOLEAN CurrentThread,
|
|
OUT PBOOLEAN Enabled)
|
|
{
|
|
TOKEN_PRIVILEGES NewState;
|
|
TOKEN_PRIVILEGES OldState;
|
|
ULONG ReturnLength;
|
|
HANDLE TokenHandle;
|
|
NTSTATUS Status;
|
|
|
|
PAGED_CODE_RTL();
|
|
|
|
DPRINT("RtlAdjustPrivilege() called\n");
|
|
|
|
if (CurrentThread)
|
|
{
|
|
Status = ZwOpenThreadToken(NtCurrentThread(),
|
|
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
|
|
FALSE,
|
|
&TokenHandle);
|
|
}
|
|
else
|
|
{
|
|
Status = ZwOpenProcessToken(NtCurrentProcess(),
|
|
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
|
|
&TokenHandle);
|
|
}
|
|
|
|
if (!NT_SUCCESS (Status))
|
|
{
|
|
DPRINT1("Retrieving token handle failed (Status %lx)\n", Status);
|
|
return Status;
|
|
}
|
|
|
|
OldState.PrivilegeCount = 1;
|
|
|
|
NewState.PrivilegeCount = 1;
|
|
NewState.Privileges[0].Luid.LowPart = Privilege;
|
|
NewState.Privileges[0].Luid.HighPart = 0;
|
|
NewState.Privileges[0].Attributes = (Enable) ? SE_PRIVILEGE_ENABLED : 0;
|
|
|
|
Status = ZwAdjustPrivilegesToken(TokenHandle,
|
|
FALSE,
|
|
&NewState,
|
|
sizeof(TOKEN_PRIVILEGES),
|
|
&OldState,
|
|
&ReturnLength);
|
|
ZwClose (TokenHandle);
|
|
if (Status == STATUS_NOT_ALL_ASSIGNED)
|
|
{
|
|
DPRINT1("Failed to assign all privileges\n");
|
|
return STATUS_PRIVILEGE_NOT_HELD;
|
|
}
|
|
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DPRINT1("NtAdjustPrivilegesToken() failed (Status %lx)\n", Status);
|
|
return Status;
|
|
}
|
|
|
|
if (OldState.PrivilegeCount == 0)
|
|
{
|
|
*Enabled = Enable;
|
|
}
|
|
else
|
|
{
|
|
*Enabled = (OldState.Privileges[0].Attributes & SE_PRIVILEGE_ENABLED);
|
|
}
|
|
|
|
DPRINT("RtlAdjustPrivilege() done\n");
|
|
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
#if (NTDDI_VERSION >= NTDDI_VISTA) || (DLL_EXPORT_VERSION >= _WIN32_WINNT_VISTA)
|
|
|
|
/**
|
|
* @brief
|
|
* Removes all privileges in the specified access token.
|
|
*
|
|
* @param[in] TokenHandle
|
|
* A handle to the access token that contains the privileges to be removed.
|
|
*
|
|
* @param[in] PrivilegesToKeep
|
|
* A pointer to an array of privilege values (defined as SE_XXX_PRIVILEGE) that specify
|
|
* the privileges to keep in the token.
|
|
*
|
|
* @param[in] PrivilegeCount
|
|
* Specifies the number of entries in the PrivilegesToKeep array.
|
|
*
|
|
* @return
|
|
* Returns STATUS_SUCCESS if privileges removed successfully.
|
|
* STATUS_INVALID_PARAMETER is returned if input privilege value greater than
|
|
* SE_MAX_WELL_KNOWN_PRIVILEGE. STATUS_NOT_ALL_ASSIGNED is returned if The token does
|
|
* not have one or more of the privileges specified in the PrivilegesToKeep parameter,
|
|
* and no privileges were removed. A failure NTSTATUS code is returned otherwise.
|
|
*/
|
|
NTSTATUS
|
|
NTAPI
|
|
RtlRemovePrivileges(
|
|
_In_ HANDLE TokenHandle,
|
|
_In_reads_opt_(PrivilegeCount) _When_(PrivilegeCount != 0, _Notnull_)
|
|
PULONG PrivilegesToKeep,
|
|
_In_ ULONG PrivilegeCount)
|
|
{
|
|
NTSTATUS Status;
|
|
UINT64 PrivilegesToKeepBitmap;
|
|
ULONG i, ReturnLength;
|
|
UCHAR Buffer[sizeof(TOKEN_PRIVILEGES) +
|
|
sizeof(LUID_AND_ATTRIBUTES) * (SE_MAX_WELL_KNOWN_PRIVILEGE - SE_MIN_WELL_KNOWN_PRIVILEGE)];
|
|
PTOKEN_PRIVILEGES Privileges;
|
|
|
|
C_ASSERT(SE_MAX_WELL_KNOWN_PRIVILEGE < 64);
|
|
|
|
DPRINT("RtlRemovePrivileges(%p, %p, %u)\n", TokenHandle, PrivilegesToKeep, PrivilegeCount);
|
|
|
|
/* Save privileges that should be keep */
|
|
PrivilegesToKeepBitmap = 0;
|
|
if (PrivilegeCount)
|
|
{
|
|
for (i = 0; i < PrivilegeCount; i++)
|
|
{
|
|
if (PrivilegesToKeep[i] > SE_MAX_WELL_KNOWN_PRIVILEGE)
|
|
{
|
|
return STATUS_INVALID_PARAMETER;
|
|
}
|
|
PrivilegesToKeepBitmap |= (1ULL << PrivilegesToKeep[i]);
|
|
}
|
|
}
|
|
|
|
/* Get token privileges information */
|
|
Status = ZwQueryInformationToken(TokenHandle,
|
|
TokenPrivileges,
|
|
Buffer,
|
|
sizeof(Buffer),
|
|
&ReturnLength);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
return Status;
|
|
}
|
|
|
|
/* Remove all privileges that we don't need to keep */
|
|
Privileges = (PTOKEN_PRIVILEGES)Buffer;
|
|
for (i = 0; i < Privileges->PrivilegeCount; i++)
|
|
{
|
|
LARGE_INTEGER Privilege = *(LARGE_INTEGER*)&Privileges->Privileges[i].Luid;
|
|
ASSERT(Privilege.QuadPart <= SE_MAX_WELL_KNOWN_PRIVILEGE);
|
|
if (PrivilegesToKeepBitmap & (1ULL << Privilege.QuadPart))
|
|
{
|
|
PrivilegesToKeepBitmap &= ~(1ULL << Privilege.QuadPart);
|
|
}
|
|
else
|
|
{
|
|
Privileges->Privileges[i].Attributes = SE_PRIVILEGE_REMOVED;
|
|
}
|
|
}
|
|
|
|
if (PrivilegesToKeepBitmap)
|
|
{
|
|
Status = STATUS_NOT_ALL_ASSIGNED;
|
|
}
|
|
else
|
|
{
|
|
Status = ZwAdjustPrivilegesToken(TokenHandle,
|
|
FALSE,
|
|
(PTOKEN_PRIVILEGES)Buffer,
|
|
sizeof(Buffer),
|
|
NULL,
|
|
NULL);
|
|
}
|
|
|
|
return Status;
|
|
}
|
|
|
|
#endif /* (NTDDI_VERSION >= NTDDI_VISTA) */
|