用户、认证和对象安全
Windows系统具有很完善的安全和认证机制,称作访问控制机制。程序的执行主体(线程)在访问对象(文件、事件等)时,系统会根据线程的“权限”和线程需要访问的对象所具有的访问控制列表(ACL)中的“安全描述符”是否匹配来进行认证,决定一个线程是否可以操作一个对象。
1. 关于权限、访问控制列表、安全描述符等在安全认证中所依赖的数据结构,并重点讲解安全认证的过程
A需要访问(Access)B,A就是访问的主体,B就是访问的客体。A的“访问令牌”和B的安全描述符共同决定了A是否可以访问B.
访问的主体是进程。在系统中,线程才是程序执行的流程,因此只有线程才能操作对象。每个线程都是属于一个进程的,线程并没有属于自己的权限,而是来源于线程所属于的进程。一个进程中的所有线程都具有同样的权限,可以把进程看作访问的主体。一个线程能访问哪些对象,能进行哪此操作,是由线程权限决定的。访问的客体是安全对象,所有被访问的对象都具有安全描述符,包括了文件、注册表、事件(Event)、互斥(Mutex)、管道等。
1.1 访问令牌、权限和用户标识
进程的权限继承自创建进程用户和用户所属的用户组。用户有专用数据结构来表示权限—访问令牌(Access Token)。访问令牌包括两个部分:一个是令牌所表示的用户,包括用户标识符(SID),用户所属的用户组等;另一部分是“权限”(Privilege)。
在进程访问安全对象时,会用到SID。每个安全对象都有访问控制列表(ACL),ACL说明了哪些用户( SID)能访问本对象,哪些不能,以及能进行哪种访问等。而“权限”在访问某个具体的安全对象时并没有作用,“权限”是表示进程是否能够进行特定的系统操作,如关闭系统、修改系统时间、加载设备驱动等。
创建进程的API函数是CreateProcess,CreateProcess函数所创建的进程使用的访问令牌是当前登录用户的访问令牌。 此外还可以指定进程的用户。使用CreateProcessAsUser和CreateProcessWithTokenW等API函数,在创建前需要先得到用户的令牌,可以使用LogonUser登录用户(是否可以同时登录多个用户受操作系统版本限制),LogonUser函数用返回用户的令牌。 如果需要得到进程和线程的访问令牌,可以使用OpenProcessToken、 OpenThreadToken等函数。获取令牌中的信息可以使用API函数GetTokenInformation。如果需要修改权限,可以使用AdjustTokenPrivileges等函数。
1.2 进程的系统操作权限
进程的权限特指进程是否能够进行各种系统操作,例如是否可以关闭系统,是否能够修改系统时间,是否能够加载设备驱动等。权限是一个列表,每种权限是列表中的一项。权限列表存在于进程的访问令牌中。
权限有很多种,每一种表示了一个特定的操作是否能够进行,如果进程的访问令牌中的权限列表中有这个权限,则表示进程可以进行这种操作,比如SE_LOAD_DRIVER_ NAME表示进程可以加载驱动。
#define SE_CREATE_TOKEN_NAME “SeCreateTokenPrivilege”
#define SE_ASSIGNPRIMARYTOKEN_NAME “SeAssignPrimaryTokenPrivilege”
#define SE_LOCK_MEMORY_NAME “SeLockMemoryPrivilege”
#define SE_INCREASE_QUOTA_NAME “SeIncreaseQuotaPrivilege”
#define SE_UNSOLICITED_INPUT_NAME “SeUnsolictedInputPrivilege”
1.3 安全对象
Windows系统几乎所有的对象都有安全属性,包括文件、文件夹、注册表、线程同步对象、进程间通信对象、网络共享等,进程和线程也可以是其他进程的操作对象,所以进程和线程也是安全对象。在创建对象时都可以指定对象的安全属性,比如CreateFile、CreatePipe、CreateProcess、RegCreateKeyEx和RegSaveKeyEx等,SECURITY_ATTRIBUTES结构用于指定对象的安全属性。GetNamedSecurityInfo、GetSecurityInfo、 SetSecurityInfo、 SetKernelObjectSecurity、SetNamedSecurityInfo等API函数可以获取和设置对象的安全属性。对象的安全属性是以安全描述符(Security Descriptor)的形式存在的,安全描述符中包括了访问控制列表。
1.4 访问控制列表(ACL)
每个安全对象都有访问控制列表。
访问控制列表有两种,一种是选择访问控制列表(discretionary access control list,DACL),另一种是系统访问控制列表(system access controllist,SACL)。DACL决定了用户或用户组是否能访问这个对象,SACL控制了尝试访问安全对象的检测信息的继承关系。
DACL是访问控制的关键,DACL中包括一个访问控制入口(AccessControl Entries,ACE)列表。ACE表明了用户(通过用户SID或用户组SID)是否能进行操作以及能进行哪种操作。在进行访问控制检测时,会依次检测DACL中的ACE,直到被允许或被拒绝
ACE
2 安全机制程序示例
本节通过实例说明访问令牌和安全描述符访问控制列表等内容和程序设计的方法
◇列举进程访问令牌内容和权限;
◇修改进程的权限;
◇显示安全描述符的内容,列举DACL;
◇修改对象的安全描述符。
2.1 列举进程访问令牌内容和权限
(1) 显示进程的访问令牌内容
BOOL DisplayCallerAccessTokenInfomation()
{
HNADLE hToken = NULL;
BOOL bResult = FALSE;
//使用OpenThreadToken()函数判断线程运行的状态
bResult = OpenThreadToken(GetCurrentThread(),
TOKEN_QUERY|TOKEN_QUERY_SOURCE,
TRUE,
&hToken);
if(bResult == FALSE && GetLastError() == ERROR_NO_TOKEN)
{
//否则使用进程入口标志
bResult = OpenThreadToken(GetCurrentProcess(),
TOKEN_QUERY|TOKEN_QUERY_SOURCE,
TRUE,
&hToken);
}
if(bResult)
{
bResult = DisplayTokeninfomation(hToken);
CloseHandle(hToken);
}
else
{
MyPrintf(“OpenThread/ProcessToken failed with %d\n”, GetLastError);
}
}
DisplayTokenInformation函数分别调用了DisplayUserInfo、DisplayOwnerInfo、DisplayPrimaryGroupInfo、 DisplayStatistics、DisplaySource、DisplayGroupsInfo和DisplayPrivileges等函数分别显示了用户名、用户组和权限等内容。各函数的实现原理类似,都是调用了Retrieve TokenInformationClass函数从令牌中获得信息,然后根据信息的类型,进行转换并显示,如果获取的是SID则调用ConvprtBinarySidToName等API函数获取SID对应的用户名并显示。
其中DisplayUserInfo、DisplayStatistics、DisplayGroupsInfo代码如下:
BOOL DisplayUserInfo(HNADLE hToken)
{
TOKEN_USER *pUserInfo = NULL;
DWORD dwSize = 0;
LPTSTR pName = NULL;
//从令牌获取用户信息
pUserInfo = (TOKEN_USER *)RetrieveTokenInfomationClass(hToken, TokenUser, &dwSize);
if(pUserInfo)
return FALSE;
pName = ConvertBinarySidName(pUserInfo -> User.Sid);
if(pName)
return FALSE;
MyPritf(“User: %s\n”, pName);
CheckAndLocalFree(pUserInfo);
CheckAndLocalFree(pName);
return TRUE;
}
BOOL DisplayStatistice(HNADLE hToken)
{
TOKEN_STATISTICS *pStatiscs = NULL;
DWORD dwSize = 0;
//获取Token的统计信息
pStatiscs = (TOKEN_STATISTICS*)RetrieveTokenInfomationClass(hToken,
pStatiscs,
&dwSize);
if(pStatiscs == NULL)
return FALSE;
//列出统计信息
MyPritf(“LUID : %s\n”, pStatiscs ->TokenId);
…..
}
ConvertBinarySidToName是API函数,通过令牌中的用户或用户组SID得到了用户名。
RetrieveTokenInformationClass函数的功能是从Token中获取信息
RetrieveTokenInformationClass函数的InfoClass参数是TOKEN_INFORMATION_CLASS枚举类型,指定不同的值会获取不同的信息。InfoClass最后传递给了GetTokenInformation API函数
LPVOID RetrieveTokenInformationClass(HANDLE hToken,
TOKEN_INFOMATION_CLASS InfoClass,
LPDWORD lpdwSize)
{
LPVOID pInfo = 0;
//确定缓冲区的大小
GetTokenInfomation(hToken, infoClass, NULL, *lpdwSize, lpdwSize);
// 为token信息分配缓冲区
pInfo = Localloc(LPTR, *lpdwSize);
…….
GetTokenInfomation(hToken, infoClass, pInfo, *lpdwSize, lpdwSize);
return pInfo;
}
(2). 显示SID
获取SID较为简单,LookupAccountName API函数就可以实现
实例的关键函数是GetTextualSid,函数代码如下:
BOOL GetTextualSid(PSID pSid, //二进制SID
LPTSTR TextualSid,//为文本表述的SID设置缓冲区
LPDWORD cchSidSize)//需要提供的文本的大小
{
PSID_IDENTIFIER_AUTHORITY psia;
DWORD dwSubAuthorities;
DWORD dwCounter;
DWORD cchSidCopy;
DWORD cchMaxLen;
//检查参数是否顺利通过,ValidSid不可以接受参数为空
if(!pSid || !IsValidSid(pSid) || !TextualSid || !cchSidSize)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
//获取SidIndentifierAutority
pSia = GetSidIdentifierAuthority(pSid);
…..
}
2.2修改进程的权限
在获得进程的权限之后,还可以对进程的权限进行修改,以使进程可以进行特定的系统操作。
使用AdjustTokenPrivileges函数可以修改令牌的权限。
BOOL EnablePrivilege(LPSTR name)
{
HANDLE hToken;
BOOL rv;
TOKEN_PRIVILEGES priv = {1, {0, 0, SE_PRIVILEGE_ENABLED}};
LookupPrivilegeValue(0, name, &priv.Privileges[0].Luid);
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken);
AdjustTokenPrivileges(hToken, FLASE, &priv, sizeof priv, 0, 0);
return rv
}
2.3列举安全对象的安全描述符
2.4修改安全描述符
对不同对象的安全描述符的修改使用的API函数不同。如果是文件则使用SetFileSecurity,函数原型如下:
InitiateSystemShutdown(
NULL, // 要关的计算机用户名
"由于系统不稳定,WINDOWS将在上面的时间内关机,请做好保存工作!", // 显示消息
0, // 关机所需的时间
FALSE, // 是否提示用户
FALSE); //设为TRUE为重起,设为FALSE为关机
BOOL WINAPI OpenProcessToken(
__in HANDLE ProcessHandle, //进程句柄。通过GetCurrentProcess函数取得当前进程句柄
__in DWORD DesiredAccess, //要对令牌进行何种操作。如TOKEN_ADJUST_PRIVILEGES用于调整权限
__out PHANDLE TokenHandle //进程令牌句柄
);
HANDLE hToken;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES,
&hToken))
{
}
对令牌可以进行的操作
Value | Meaning |
TOKEN_ADJUST_DEFAULT | 改变令牌所有者、主组或访问控制列表DACL |
TOKEN_ADJUST_GROUPS | 改变令牌的组属性 |
TOKEN_ADJUST_PRIVILEGES | enable or disable 令牌的特权 |
TOKEN_ADJUST_SESSIONID | 调整令牌的Session ID。进程需要 SE_TCB_NAME 特权. |
TOKEN_ASSIGN_PRIMARY | 为进程分配主令牌。需要 SE_ASSIGNPRIMARYTOKEN_NAME 特权 |
TOKEN_DUPLICATE | 复制令牌 |
TOKEN_EXECUTE | 合并 STANDARD_RIGHTS_EXECUTE 和 TOKEN_IMPERSONATE. |
TOKEN_IMPERSONATE | 附加一个模拟令牌到进程 |
TOKEN_QUERY | 查询令牌 |
TOKEN_QUERY_SOURCE | 查询令牌源 |
TOKEN_READ | 合并 STANDARD_RIGHTS_READ 和TOKEN_QUERY. |
TOKEN_WRITE | 合并 STANDARD_RIGHTS_WRITE, TOKEN_ADJUST_PRIVILEGES, TOKEN_ADJUST_GROUPS, 和 TOKEN_ADJUST_DEFAULT. |
TOKEN_ALL_ACCESS | 合并所以可能的操作 |
与访问令牌关联的特权
TOKEN_PRIVILEGES令牌特权结构体,该结构体定义了访问令牌所拥有的一系列特权。其原型定义如下:
typedef struct _TOKEN_PRIVILEGES {
DWORD PrivilegeCount; //特权数量(数组的长度)
LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY]; //特权数组
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
其中,Privileges数组类型为 LUID_AND_ATTRIBUTES结构体,其原型定义如下:
typedef struct _LUID_AND_ATTRIBUTES {
LUID Luid; //局部唯一标识符,代表某种特权的Value
DWORD Attributes; //Luid的属性,代表特权的属性(Enabled or Disabled)
} LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES;
其特权属性Attributes可以是如下常量:
值 | 含义 |
SE_PRIVILEGE_ENABLED | 使特权有效 |
SE_PRIVILEGE_ENABLED_BY_DEFAULT | 使特权默认有效 |
SE_PRIVILEGE_REMOVED | 移除该特权 |
SE_PRIVILEGE_USED_FOR_ACCESS | 取得对象或服务的访问权 |
TOKEN_PRIVILEGES tkpPriv = { 0 };
for( int nIndex = 0; lpctszPrivileges[nIndex]; ++nIndex )
{
ZeroMemory( &tkpPriv, sizeof(tkpPriv));
LookupPrivilegeValue( NULL, lpctszPrivileges[nIndex], &tkpPriv.Privileges[0].Luid );
tkpPriv.PrivilegeCount = 1;
tkpPriv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges( hToken, FALSE, &tkpPriv, 0, 0, 0 );
}