609 lines
14 KiB
C
609 lines
14 KiB
C
|
/*++
|
|||
|
|
|||
|
Module Name:
|
|||
|
|
|||
|
SeAstate.c
|
|||
|
|
|||
|
Abstract:
|
|||
|
|
|||
|
This Module implements the privilege check procedures.
|
|||
|
|
|||
|
Author:
|
|||
|
|
|||
|
Robert Reichel (robertre) 20-March-90
|
|||
|
|
|||
|
Environment:
|
|||
|
|
|||
|
Kernel Mode
|
|||
|
|
|||
|
Revision History:
|
|||
|
|
|||
|
v1: robertre
|
|||
|
new file, move Access State related routines here
|
|||
|
|
|||
|
--*/
|
|||
|
|
|||
|
#include "pch.h"
|
|||
|
|
|||
|
#pragma hdrstop
|
|||
|
|
|||
|
|
|||
|
#ifdef ALLOC_PRAGMA
|
|||
|
#pragma alloc_text(PAGE,SeCreateAccessState)
|
|||
|
#pragma alloc_text(PAGE,SeDeleteAccessState)
|
|||
|
#pragma alloc_text(PAGE,SeSetAccessStateGenericMapping)
|
|||
|
#pragma alloc_text(PAGE,SeAppendPrivileges)
|
|||
|
#pragma alloc_text(PAGE,SepConcatenatePrivileges)
|
|||
|
#endif
|
|||
|
|
|||
|
|
|||
|
//
|
|||
|
// Define logical sum of all generic accesses.
|
|||
|
//
|
|||
|
|
|||
|
#define GENERIC_ACCESS (GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE | GENERIC_ALL)
|
|||
|
|
|||
|
|
|||
|
//
|
|||
|
// The PRIVILEGE_SET data structure includes an array including ANYSIZE_ARRAY
|
|||
|
// elements. This definition provides the size of an empty PRIVILEGE_SET
|
|||
|
// (i.e., one with no privileges in it).
|
|||
|
//
|
|||
|
|
|||
|
#define SEP_PRIVILEGE_SET_HEADER_SIZE \
|
|||
|
((ULONG)sizeof(PRIVILEGE_SET) - \
|
|||
|
(ANYSIZE_ARRAY * (ULONG)sizeof(LUID_AND_ATTRIBUTES)))
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
#if 0
|
|||
|
NTSTATUS
|
|||
|
SeCreateAccessState(
|
|||
|
IN PACCESS_STATE AccessState,
|
|||
|
IN ACCESS_MASK DesiredAccess,
|
|||
|
IN PGENERIC_MAPPING GenericMapping OPTIONAL
|
|||
|
)
|
|||
|
|
|||
|
/*++
|
|||
|
Routine Description:
|
|||
|
|
|||
|
This routine initializes an ACCESS_STATE structure. This consists
|
|||
|
of:
|
|||
|
|
|||
|
- zeroing the entire structure
|
|||
|
|
|||
|
- mapping generic access types in the passed DesiredAccess
|
|||
|
and putting it into the structure
|
|||
|
|
|||
|
- "capturing" the Subject Context, which must be held for the
|
|||
|
duration of the access attempt (at least until auditing is performed).
|
|||
|
|
|||
|
- Allocating an Operation ID, which is an LUID that will be used
|
|||
|
to associate different parts of the access attempt in the audit
|
|||
|
log.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
AccessState - a pointer to the structure to be initialized.
|
|||
|
|
|||
|
DesiredAccess - Access mask containing the desired access
|
|||
|
|
|||
|
GenericMapping - Optionally supplies a pointer to a generic mapping
|
|||
|
that may be used to map any generic access requests that may
|
|||
|
have been passed in the DesiredAccess parameter.
|
|||
|
|
|||
|
Note that if this parameter is not supplied, it must be filled
|
|||
|
in at some later point. The IO system does this in IopParseDevice.
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
Error if the attempt to allocate an LUID fails.
|
|||
|
|
|||
|
Note that this error may be safely ignored if it is known that all
|
|||
|
security checks will be performed with PreviousMode == KernelMode.
|
|||
|
Know what you're doing if you choose to ignore this.
|
|||
|
|
|||
|
--*/
|
|||
|
|
|||
|
{
|
|||
|
|
|||
|
ACCESS_MASK MappedAccessMask;
|
|||
|
PSECURITY_DESCRIPTOR InputSecurityDescriptor = NULL;
|
|||
|
PAUX_ACCESS_DATA AuxData;
|
|||
|
|
|||
|
PAGED_CODE();
|
|||
|
|
|||
|
//
|
|||
|
// Don't modify what he passed in
|
|||
|
//
|
|||
|
|
|||
|
MappedAccessMask = DesiredAccess;
|
|||
|
|
|||
|
//
|
|||
|
// Map generic access to object specific access iff generic access types
|
|||
|
// are specified and a generic access mapping table is provided.
|
|||
|
//
|
|||
|
|
|||
|
if ( ((DesiredAccess & GENERIC_ACCESS) != 0) &&
|
|||
|
ARGUMENT_PRESENT(GenericMapping) ) {
|
|||
|
|
|||
|
RtlMapGenericMask(
|
|||
|
&MappedAccessMask,
|
|||
|
GenericMapping
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
RtlZeroMemory(AccessState, sizeof(ACCESS_STATE));
|
|||
|
|
|||
|
//
|
|||
|
// Assume RtlZeroMemory has initialized these fields properly
|
|||
|
//
|
|||
|
|
|||
|
ASSERT( AccessState->SecurityDescriptor == NULL );
|
|||
|
ASSERT( AccessState->PrivilegesAllocated == FALSE );
|
|||
|
|
|||
|
AccessState->AuxData = ExAllocatePool( PagedPool, sizeof( AUX_ACCESS_DATA ));
|
|||
|
|
|||
|
if (AccessState->AuxData == NULL) {
|
|||
|
return( STATUS_NO_MEMORY );
|
|||
|
}
|
|||
|
|
|||
|
AuxData = (PAUX_ACCESS_DATA)AccessState->AuxData;
|
|||
|
|
|||
|
SeCaptureSubjectContext(&AccessState->SubjectSecurityContext);
|
|||
|
|
|||
|
if (((PTOKEN)EffectiveToken( &AccessState->SubjectSecurityContext ))->TokenFlags & TOKEN_HAS_TRAVERSE_PRIVILEGE ) {
|
|||
|
AccessState->Flags = TOKEN_HAS_TRAVERSE_PRIVILEGE;
|
|||
|
}
|
|||
|
|
|||
|
if (SeTokenIsRestricted(EffectiveToken( &AccessState-SubjectSecurityContext))) {
|
|||
|
AccessState->Flags |= TOKEN_IS_RESTRICTED;
|
|||
|
}
|
|||
|
|
|||
|
AccessState->RemainingDesiredAccess = MappedAccessMask;
|
|||
|
AccessState->OriginalDesiredAccess = DesiredAccess;
|
|||
|
AuxData->PrivilegesUsed = (PPRIVILEGE_SET)((PUCHAR)AccessState +
|
|||
|
(FIELD_OFFSET(ACCESS_STATE, Privileges)));
|
|||
|
|
|||
|
ExAllocateLocallyUniqueId(&AccessState->OperationID);
|
|||
|
|
|||
|
if (ARGUMENT_PRESENT(GenericMapping)) {
|
|||
|
AuxData->GenericMapping = *GenericMapping;
|
|||
|
}
|
|||
|
|
|||
|
return( STATUS_SUCCESS );
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
#endif
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
SeCreateAccessState(
|
|||
|
IN PACCESS_STATE AccessState,
|
|||
|
IN PAUX_ACCESS_DATA AuxData,
|
|||
|
IN ACCESS_MASK DesiredAccess,
|
|||
|
IN PGENERIC_MAPPING GenericMapping OPTIONAL
|
|||
|
)
|
|||
|
|
|||
|
/*++
|
|||
|
Routine Description:
|
|||
|
|
|||
|
This routine initializes an ACCESS_STATE structure. This consists
|
|||
|
of:
|
|||
|
|
|||
|
- zeroing the entire structure
|
|||
|
|
|||
|
- mapping generic access types in the passed DesiredAccess
|
|||
|
and putting it into the structure
|
|||
|
|
|||
|
- "capturing" the Subject Context, which must be held for the
|
|||
|
duration of the access attempt (at least until auditing is performed).
|
|||
|
|
|||
|
- Allocating an Operation ID, which is an LUID that will be used
|
|||
|
to associate different parts of the access attempt in the audit
|
|||
|
log.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
AccessState - a pointer to the structure to be initialized.
|
|||
|
|
|||
|
AuxData - Supplies a buffer big enough for an AuxData structure
|
|||
|
so we don't have to allocate one.
|
|||
|
|
|||
|
DesiredAccess - Access mask containing the desired access
|
|||
|
|
|||
|
GenericMapping - Optionally supplies a pointer to a generic mapping
|
|||
|
that may be used to map any generic access requests that may
|
|||
|
have been passed in the DesiredAccess parameter.
|
|||
|
|
|||
|
Note that if this parameter is not supplied, it must be filled
|
|||
|
in at some later point. The IO system does this in IopParseDevice.
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
Error if the attempt to allocate an LUID fails.
|
|||
|
|
|||
|
Note that this error may be safely ignored if it is known that all
|
|||
|
security checks will be performed with PreviousMode == KernelMode.
|
|||
|
Know what you're doing if you choose to ignore this.
|
|||
|
|
|||
|
--*/
|
|||
|
|
|||
|
{
|
|||
|
|
|||
|
ACCESS_MASK MappedAccessMask;
|
|||
|
PSECURITY_DESCRIPTOR InputSecurityDescriptor = NULL;
|
|||
|
|
|||
|
PAGED_CODE();
|
|||
|
|
|||
|
//
|
|||
|
// Don't modify what he passed in
|
|||
|
//
|
|||
|
|
|||
|
MappedAccessMask = DesiredAccess;
|
|||
|
|
|||
|
//
|
|||
|
// Map generic access to object specific access iff generic access types
|
|||
|
// are specified and a generic access mapping table is provided.
|
|||
|
//
|
|||
|
|
|||
|
if ( ((DesiredAccess & GENERIC_ACCESS) != 0) &&
|
|||
|
ARGUMENT_PRESENT(GenericMapping) ) {
|
|||
|
|
|||
|
RtlMapGenericMask(
|
|||
|
&MappedAccessMask,
|
|||
|
GenericMapping
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
RtlZeroMemory(AccessState, sizeof(ACCESS_STATE));
|
|||
|
RtlZeroMemory(AuxData, sizeof(AUX_ACCESS_DATA));
|
|||
|
|
|||
|
//
|
|||
|
// Assume RtlZeroMemory has initialized these fields properly
|
|||
|
//
|
|||
|
|
|||
|
ASSERT( AccessState->SecurityDescriptor == NULL );
|
|||
|
ASSERT( AccessState->PrivilegesAllocated == FALSE );
|
|||
|
|
|||
|
AccessState->AuxData = AuxData;
|
|||
|
|
|||
|
SeCaptureSubjectContext(&AccessState->SubjectSecurityContext);
|
|||
|
|
|||
|
if (((PTOKEN)EffectiveToken( &AccessState->SubjectSecurityContext ))->TokenFlags & TOKEN_HAS_TRAVERSE_PRIVILEGE ) {
|
|||
|
AccessState->Flags = TOKEN_HAS_TRAVERSE_PRIVILEGE;
|
|||
|
}
|
|||
|
|
|||
|
AccessState->RemainingDesiredAccess = MappedAccessMask;
|
|||
|
AccessState->OriginalDesiredAccess = MappedAccessMask;
|
|||
|
AuxData->PrivilegesUsed = (PPRIVILEGE_SET)((ULONG_PTR)AccessState +
|
|||
|
(FIELD_OFFSET(ACCESS_STATE, Privileges)));
|
|||
|
|
|||
|
ExAllocateLocallyUniqueId(&AccessState->OperationID);
|
|||
|
|
|||
|
if (ARGUMENT_PRESENT(GenericMapping)) {
|
|||
|
AuxData->GenericMapping = *GenericMapping;
|
|||
|
}
|
|||
|
|
|||
|
return( STATUS_SUCCESS );
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
#if 0
|
|||
|
|
|||
|
|
|||
|
VOID
|
|||
|
SeDeleteAccessState(
|
|||
|
PACCESS_STATE AccessState
|
|||
|
)
|
|||
|
|
|||
|
/*++
|
|||
|
|
|||
|
Routine Description:
|
|||
|
|
|||
|
This routine deallocates any memory that may have been allocated as
|
|||
|
part of constructing the access state (normally only for an excessive
|
|||
|
number of privileges), and frees the Subject Context.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
AccessState - a pointer to the ACCESS_STATE structure to be
|
|||
|
deallocated.
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
None.
|
|||
|
|
|||
|
--*/
|
|||
|
|
|||
|
{
|
|||
|
PAUX_ACCESS_DATA AuxData;
|
|||
|
|
|||
|
PAGED_CODE();
|
|||
|
|
|||
|
AuxData = (PAUX_ACCESS_DATA)AccessState->AuxData;
|
|||
|
|
|||
|
if (AccessState->PrivilegesAllocated) {
|
|||
|
ExFreePool( (PVOID)AuxData->PrivilegesUsed );
|
|||
|
}
|
|||
|
|
|||
|
if (AccessState->ObjectName.Buffer != NULL) {
|
|||
|
ExFreePool(AccessState->ObjectName.Buffer);
|
|||
|
}
|
|||
|
|
|||
|
if (AccessState->ObjectTypeName.Buffer != NULL) {
|
|||
|
ExFreePool(AccessState->ObjectTypeName.Buffer);
|
|||
|
}
|
|||
|
|
|||
|
ExFreePool( AuxData );
|
|||
|
|
|||
|
SeReleaseSubjectContext(&AccessState->SubjectSecurityContext);
|
|||
|
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
#endif
|
|||
|
|
|||
|
VOID
|
|||
|
SeDeleteAccessState(
|
|||
|
PACCESS_STATE AccessState
|
|||
|
)
|
|||
|
|
|||
|
/*++
|
|||
|
|
|||
|
Routine Description:
|
|||
|
|
|||
|
This routine deallocates any memory that may have been allocated as
|
|||
|
part of constructing the access state (normally only for an excessive
|
|||
|
number of privileges), and frees the Subject Context.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
AccessState - a pointer to the ACCESS_STATE structure to be
|
|||
|
deallocated.
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
None.
|
|||
|
|
|||
|
--*/
|
|||
|
|
|||
|
{
|
|||
|
PAUX_ACCESS_DATA AuxData;
|
|||
|
|
|||
|
PAGED_CODE();
|
|||
|
|
|||
|
AuxData = (PAUX_ACCESS_DATA)AccessState->AuxData;
|
|||
|
|
|||
|
if (AccessState->PrivilegesAllocated) {
|
|||
|
ExFreePool( (PVOID)AuxData->PrivilegesUsed );
|
|||
|
}
|
|||
|
|
|||
|
if (AccessState->ObjectName.Buffer != NULL) {
|
|||
|
ExFreePool(AccessState->ObjectName.Buffer);
|
|||
|
}
|
|||
|
|
|||
|
if (AccessState->ObjectTypeName.Buffer != NULL) {
|
|||
|
ExFreePool(AccessState->ObjectTypeName.Buffer);
|
|||
|
}
|
|||
|
|
|||
|
SeReleaseSubjectContext(&AccessState->SubjectSecurityContext);
|
|||
|
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
VOID
|
|||
|
SeSetAccessStateGenericMapping (
|
|||
|
PACCESS_STATE AccessState,
|
|||
|
PGENERIC_MAPPING GenericMapping
|
|||
|
)
|
|||
|
|
|||
|
/*++
|
|||
|
|
|||
|
Routine Description:
|
|||
|
|
|||
|
This routine sets the GenericMapping field in an AccessState structure.
|
|||
|
It must be called before access validation is performed if the GenericMapping
|
|||
|
is not passed in when the AccessState structure is created.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
AccessState - a pointer to the ACCESS_STATE structure to be modified.
|
|||
|
|
|||
|
GenericMapping - a pointer to the GenericMapping to be copied into the AccessState.
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
|
|||
|
--*/
|
|||
|
{
|
|||
|
PAUX_ACCESS_DATA AuxData;
|
|||
|
|
|||
|
PAGED_CODE();
|
|||
|
|
|||
|
AuxData = (PAUX_ACCESS_DATA)AccessState->AuxData;
|
|||
|
|
|||
|
AuxData->GenericMapping = *GenericMapping;
|
|||
|
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
SeAppendPrivileges(
|
|||
|
PACCESS_STATE AccessState,
|
|||
|
PPRIVILEGE_SET Privileges
|
|||
|
)
|
|||
|
/*++
|
|||
|
|
|||
|
Routine Description:
|
|||
|
|
|||
|
This routine takes a privilege set and adds it to the privilege set
|
|||
|
imbedded in an ACCESS_STATE structure.
|
|||
|
|
|||
|
An AccessState may contain up to three imbedded privileges. To
|
|||
|
add more, this routine will allocate a block of memory, copy
|
|||
|
the current privileges into it, and append the new privilege
|
|||
|
to that block. A bit is set in the AccessState indicating that
|
|||
|
the pointer to the privilge set in the structure points to pool
|
|||
|
memory and must be deallocated.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
AccessState - The AccessState structure representing the current
|
|||
|
access attempt.
|
|||
|
|
|||
|
Privileges - A pointer to a privilege set to be added.
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
STATUS_INSUFFICIENT_RESOURCES - an attempt to allocate pool memory
|
|||
|
failed.
|
|||
|
|
|||
|
--*/
|
|||
|
|
|||
|
{
|
|||
|
ULONG NewPrivilegeSetSize;
|
|||
|
PPRIVILEGE_SET NewPrivilegeSet;
|
|||
|
PAUX_ACCESS_DATA AuxData;
|
|||
|
|
|||
|
PAGED_CODE();
|
|||
|
|
|||
|
AuxData = (PAUX_ACCESS_DATA)AccessState->AuxData;
|
|||
|
|
|||
|
if (Privileges->PrivilegeCount + AuxData->PrivilegesUsed->PrivilegeCount >
|
|||
|
INITIAL_PRIVILEGE_COUNT) {
|
|||
|
|
|||
|
//
|
|||
|
// Compute the total size of the two privilege sets
|
|||
|
//
|
|||
|
|
|||
|
NewPrivilegeSetSize = SepPrivilegeSetSize( Privileges ) +
|
|||
|
SepPrivilegeSetSize( AuxData->PrivilegesUsed );
|
|||
|
|
|||
|
NewPrivilegeSet = ExAllocatePoolWithTag( PagedPool, NewPrivilegeSetSize, 'rPeS' );
|
|||
|
|
|||
|
if (NewPrivilegeSet == NULL) {
|
|||
|
return( STATUS_INSUFFICIENT_RESOURCES );
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
RtlCopyMemory(
|
|||
|
NewPrivilegeSet,
|
|||
|
AuxData->PrivilegesUsed,
|
|||
|
SepPrivilegeSetSize( AuxData->PrivilegesUsed )
|
|||
|
);
|
|||
|
|
|||
|
//
|
|||
|
// Note that this will adjust the privilege count in the
|
|||
|
// structure for us.
|
|||
|
//
|
|||
|
|
|||
|
SepConcatenatePrivileges(
|
|||
|
NewPrivilegeSet,
|
|||
|
NewPrivilegeSetSize,
|
|||
|
Privileges
|
|||
|
);
|
|||
|
|
|||
|
if (AccessState->PrivilegesAllocated) {
|
|||
|
ExFreePool( AuxData->PrivilegesUsed );
|
|||
|
}
|
|||
|
|
|||
|
AuxData->PrivilegesUsed = NewPrivilegeSet;
|
|||
|
|
|||
|
//
|
|||
|
// Mark that we've allocated memory for the privilege set,
|
|||
|
// so we know to free it when we're cleaning up.
|
|||
|
//
|
|||
|
|
|||
|
AccessState->PrivilegesAllocated = TRUE;
|
|||
|
|
|||
|
} else {
|
|||
|
|
|||
|
//
|
|||
|
// Note that this will adjust the privilege count in the
|
|||
|
// structure for us.
|
|||
|
//
|
|||
|
|
|||
|
SepConcatenatePrivileges(
|
|||
|
AuxData->PrivilegesUsed,
|
|||
|
sizeof(INITIAL_PRIVILEGE_SET),
|
|||
|
Privileges
|
|||
|
);
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
return( STATUS_SUCCESS );
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
VOID
|
|||
|
SepConcatenatePrivileges(
|
|||
|
IN PPRIVILEGE_SET TargetPrivilegeSet,
|
|||
|
IN ULONG TargetBufferSize,
|
|||
|
IN PPRIVILEGE_SET SourcePrivilegeSet
|
|||
|
)
|
|||
|
|
|||
|
/*++
|
|||
|
|
|||
|
Routine Description:
|
|||
|
|
|||
|
Takes two privilege sets and appends the second to the end of the
|
|||
|
first.
|
|||
|
|
|||
|
There must be enough space left at the end of the first privilege
|
|||
|
set to contain the second.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
TargetPrivilegeSet - Supplies a buffer containing a privilege set.
|
|||
|
The buffer must be large enough to contain the second privilege
|
|||
|
set.
|
|||
|
|
|||
|
TargetBufferSize - Supplies the size of the target buffer.
|
|||
|
|
|||
|
SourcePrivilegeSet - Supplies the privilege set to be copied
|
|||
|
into the target buffer.
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
None
|
|||
|
|
|||
|
--*/
|
|||
|
|
|||
|
{
|
|||
|
PVOID Base;
|
|||
|
PVOID Source;
|
|||
|
ULONG Length;
|
|||
|
|
|||
|
PAGED_CODE();
|
|||
|
|
|||
|
ASSERT( ((ULONG)SepPrivilegeSetSize( TargetPrivilegeSet ) +
|
|||
|
(ULONG)SepPrivilegeSetSize( SourcePrivilegeSet ) -
|
|||
|
SEP_PRIVILEGE_SET_HEADER_SIZE ) <=
|
|||
|
TargetBufferSize
|
|||
|
);
|
|||
|
|
|||
|
Base = (PVOID)((ULONG_PTR)TargetPrivilegeSet + SepPrivilegeSetSize( TargetPrivilegeSet ));
|
|||
|
|
|||
|
Source = (PVOID) ((ULONG_PTR)SourcePrivilegeSet + SEP_PRIVILEGE_SET_HEADER_SIZE);
|
|||
|
|
|||
|
Length = SourcePrivilegeSet->PrivilegeCount * sizeof(LUID_AND_ATTRIBUTES);
|
|||
|
|
|||
|
RtlMoveMemory(
|
|||
|
Base,
|
|||
|
Source,
|
|||
|
Length
|
|||
|
);
|
|||
|
|
|||
|
TargetPrivilegeSet->PrivilegeCount += SourcePrivilegeSet->PrivilegeCount;
|
|||
|
|
|||
|
}
|