1. 前言 |
在使用 Sysinternals 出品的 Process Explorer 过程中,对 “Run as Limited User” 功能的实现方式颇感兴趣,一番搜寻之下发现Mark大神在《Running as Limited User – the Easy Way》中对该功能的实现做了相关的阐述:
use the CreateRestrictedToken API to create a security context, called a token, that’s a stripped-down version of its own, removing administrative privileges and group membership. After generating a token that looks like one that Windows assigns to standard users Process Explorer calls CreateProcessAsUser to launch the target process with the new token.
使用 CreateRestrictedToken API来创建安全上下文,降低令牌(Token)的管理员权限和组成员资格,使其创建的令牌看起来像Windows赋予普通用户时一样,然后使用此令牌作为传入参数调用CreateProcessAsUser来创建新的子进程。
Process Explorer queries the privileges assigned to the Users group and strips out all other privileges, including powerful ones like SeDebugPrivilege, SeLoadDriverPrivilege and SeRestorePrivilege.
查询赋予用户组的特权并从当前进程权限中剔除这些权限比如SeDebugPrivilege、SeLoadDriverPrivilege和SeRestorePrivilege。
刚好最近有个项目需要实现降低进程权限的功能,在一翻折腾下就将其实现了,下面将谈谈实现的历程,如果纰漏之处,不吝指出。
2. 知识背书 |
在列出代码前需要了解一下一些实现原理,下面是一些相关的知识点,如果无耐心往下看,可以直接点击这里跳到代码实现处。
安全对象
有资格拥有安全描述符的对象如文件、管道、进程、进程间同步对象等。所有已命名的Windows对象都是安全的,那些未被命名的对象比如线程或进程对象也可以拥有安全描述符。
对于大多数的安全对象,当创建该对象时可以指定它的安全描述符。当一个安全对象被创建时,系统会对其赋予一个安全描述符,安全描述符包含由其创建者指定的安全信息,或者缺省的安全信息(如果没有特意进行指定的话)。
- 应用程序可以使特定的函数对已有的对象进行操作以来获取和设置安全信息。
- 每种类型的安全对象定义了它自身的访问权限和自身映射的通用访问权限。
更详细内容见:https://msdn.microsoft.com/en-us/library/windows/desktop/aa379557(v=vs.85).aspx
安全描述符(security descriptor)
包含用于保护安全对象的安全信息。
安全描述符描述 对象的所有者(SIDs) 和 以下的访问控制列表:
- *访问控制列表(DACL):指明特定用户或组对该对象的访问是允许或拒绝;
- 安全访问控制列表(SACL):控制系统审计如何访问对象。
安全标识(Security Identifiers)
一定长度用来表示托管的唯一值。
安全标识主要运用于如下几个方面:
- 在安全描述符中定义对象的所有者和基本组;
- 在访问控制项中定义托管的权限是允许、拒绝或是审计;
- 在访问令牌中定义用户和用户所在的组。
访问令牌
包含登录用户的信息。用来描述一个进程或线程的安全上下文的对象,令牌的信息包含关联到进程或线程的账号的标识和特权。
当一个用户登录时,系统对用户的账号和密码进行认证,如果登录成功,系统则创建一个访问令牌,每个进程运行时都有一个访问令牌代表当前的用户,访问令牌中的安全描述符指明当前用户所属的账号和所属的组账号,令牌也包含一系列由用户或用户所在组进行维护的权限,在一个进程试图进行访问安全对象或执行系统管理员任务过程中需要权限时,系统通过这个令牌来确认关联的用户。
访问控制列表及其访问控制项
*访问控制列表(DACL)包含若干个访问控制项(ACEs)。
约定的执行规则如下:
- 如果对象没有*访问控制列表(DACL),则任何用户对其均有完全的访问权限;
- 如果对象拥有DACL,那么系统仅允许那些在访问控制项(ACE)显式指明的访问权限;
- 如果在访问控制列表(DACL)中不存在访问控制项(ACE),那么系统不允许任何用户对其进行访问;
- 如果访问控制列表中的访问控制项对准许访问的用户或组数目有限,那么系统会隐式拒绝那些不在访问控制项中的其他托管的访问
需要注意的是访问控制项的排序很重要。因为系统按照队列的方式读取访问控制项,直到访问被拒绝或允许。用户的访问拒绝ACE必须放在访问允许ACE的前头,否则当系统读到对组的访问允许ACE时,它会给当前限制的用户赋予访问的权限。系统在检测到请求访问被允许或拒绝后就不再往下检查。
你可以通过标识允许访问的ACE来控制对对象的访问,你无需显式地拒绝一个对象的访问。
线程和安全对象间的交互
当一个线程想要使用一个安全对象时,系统在线程执行前会进行访问审核,在访问审核中,系统将线程访问令牌中的安全信息与对象安全描述符中的安全信息进行比对。
访问令牌中包含的安全标识(SIDs)可以指明与线程关联的用户,系统查看线程访问令牌中用户或组的SID,同时检查对象的*访问控制列表(DACL),*访问控制列表(DACL)中包含存储有指明对指定的用户或组的访问权限是允许或拒绝信息的访问控制项(ACE),系统检查每个访问控制项(ACE)直至出现指明针对此线程(的用户或组的SID)的访问权限是允许还是拒绝的ACE,或者到最终都没有对应的ACEs可以检查。
(图片出处:https://msdn.microsoft.com/en-us/library/windows/desktop/aa378890(v=vs.85).aspx)
系统按照序列检查每个ACE,查询ACE中的托管与定义在线程中的托管(根据托管的SID)一致的ACE,直到如下的情况出现:
- 表明访问拒绝的ACE显式拒绝在线程的访问令牌中的一个托管的任何访问权限;
- 一个或多个表明访问允许的ACEs显式地为线程访问令牌中的托管提供所有访问权限;
- 所有ACEs已经比对审核完但至少有一个请求访问权限没有显式允许,这种情况下该访问权限则被隐式拒绝。
一个访问控制列表(ACL)可以有多个的访问控制项(ACE)针对令牌的(同一个)SIDs,这种情况下每个ACE授予的访问权限是可以进行累积叠加,比如,如果一个ACE对一个组允许读的访问权限,而另一个ACE对该组内的一个用户允许写的访问权限,那么该用户对于当前对象就拥有了读和写的访问权限。
(图片出处:https://msdn.microsoft.com/en-us/library/windows/desktop/aa446597(v=vs.85).aspx)
如上图所示,对于线程A,尽管在ACE@3中允许写权限,但因为在ACE@1中已经显式拒绝“Andrew”用户的访问权限,所以该安全对象对于线程A是不可访问的;对于线程B,在ACE@2中显式指明A组用户可以有写的权限,并且在ACE@3中允许任何用户读和执行的权限,所以线程B对这个安全对象拥有读、写以及执行的权限。
完整性级别
Windows完整性机制是对Windows安全架构的扩展,该完整性机制通过添加完整性等级到安全访问令牌和添加强制性标记访问控制项到安全描述符中的系统访问控制列表(SACL)
进程在安全访问令牌中定义完整性等级,IE在保护模式下的完整性等级为低,从开始菜单运行的应用程序的等级为中等,需要管理员权限并以管理员权限运行的应用程序的等级为高。
保护模式能够有效地减少IE进程附带的攻击行为如篡改和摧毁数据、安装恶意程序;相比其他程序,连接网络的程序更容易遭受网络的攻击因为它们更可能从未知源地址下载未受信任的内容,通过降低完整性等级或限制对其的允许权限,可以减少篡改系统或污染用户数据文件的风险。
在系统访问控制列表(SACL)中有一个称为强制标识的访问控制项(ACE),该控制项的安全描述符定义完整性等级或允许访问当前对象需要达到的等级,安全对象如果没有该控制项则默认拥有中等的完整性等级;
即使用户在*访问控制列表(DACL)中已经明确授予相应的写权限,低等级的进程也不能获取比其高等级的安全对象的写权限,完整性等级检验在用户访问权限审查之前完成。
所有的文件和注册表键在缺省下的完整性等级为中,而由低等完整性进程创建的安全对象,系统会自动地赋予其低等完整性强制标志,同样,由低等完整性进程创建的子进程也是在低完整性等级下运行。
完整性访问等级(IL) |
系统权限 |
安全标识 |
|
System |
System |
S-1-16-16384 |
|
High |
Administrative |
S-1-16-12288 |
可安装文件到Program Files文件夹、往敏感的注册表中如HKEY_LOCAL_MACHINE写数据 |
Medium |
User |
S-1-16-8192 |
创建和修改用户文档中的文件、往特定用户的注册表如HKEY_CURRENT_USER中写数据 |
Low |
Untrusted |
S-1-16-4096 |
仅能往低等级位置写数据如临时网络文件夹和注册表 HKEY_CURRENT_USER\Software\LowRegistry |
低完整性进程可以往用户存档文件下写文件,通常为%USER PROFILE%\AppData\LocalLow,可以通过调用SHGetKnownFolderPath 函数并传入FOLDERID_LocalAppDataLow参数来获取完整的路径名称
SHGetKnownFolderPath(FOLDERID_LocalAppDataLow, 0, NULL, szPath, ARRAYSIZE(szPath));
同样低完整性进程可以往指定的注册表下创建和修改子键,该注册表路径通常为HKEY_CURRENT_USER\Software\AppDataLow
3. 代码实现 |
实现思路
- 创建新的普通用户组和系统管理员的安全描述符标识;
- 获取当前进程的令牌,并根据令牌句柄获取当前进程所拥有的特权;
- 通过已创建的普通用户组的安全描述符标识获取普通用户组所拥有的特权;
- 给当前进程令牌中的管理员安全描述符添加Deny-only属性,以此达到避免新创建的进程以管理员作为其所有者;
- 从当前进程拥有的特权中剔除普通用户组所没有的特权;
- 从新的受限令牌中复制为模拟令牌;
- 将模拟令牌的完整性特权设为低级,以限制新创建的进程对普通文档、可执行程序的写、执行等访问权限;
代码实现
void CreateRestrictedProcess()
{
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL; TCHAR szCmdLine[CMDLINE_SIZE] = {};
HANDLE hToken = NULL;
HANDLE hNewToken = NULL;
HANDLE hNewExToken = NULL; CHAR szIntegritySid[] = "S-1-16-4096";
PSID pIntegritySid = NULL;
PSID pUserGroupSID = NULL;
PSID pAdminSID = NULL; TOKEN_MANDATORY_LABEL tml = {}; PROCESS_INFORMATION pi;
STARTUPINFO si; BOOL bSuc = FALSE;
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
GetStartupInfo(&si);
DWORD fdwCreate = ; __try
{ if (!OpenProcessToken(GetCurrentProcess(),
//MAXIMUM_ALLOWED,
TOKEN_DUPLICATE |
TOKEN_ADJUST_DEFAULT |
TOKEN_QUERY |
TOKEN_ASSIGN_PRIMARY,
&hToken))
{
char szMsg[DEFAULT_MSG_SIZE] = {};
Dbg("OpenProcessToken failed, GLE = %u.", GetLastError());
__leave;
} Dbg("Using RestrictedTokens way !!!");
DWORD dwSize = ;
DWORD dwTokenInfoLength = ;
SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;
SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY;
if(!AllocateAndInitializeSid(
&SIDAuthNT,
0x2,
SECURITY_BUILTIN_DOMAIN_RID/*0×20*/,
DOMAIN_ALIAS_RID_USERS,
, , , , , ,
&pUserGroupSID))
{
Dbg("AllocateAndInitializeSid for UserGroup Error %u", GetLastError());
__leave;
} // Create a SID for the BUILTIN\Administrators group.
if(! AllocateAndInitializeSid( &SIDAuth, ,
SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS,
, , , , , ,
&pAdminSID) )
{
Dbg("AllocateAndInitializeSid for AdminGroup Error %u", GetLastError());
__leave;
} SID_AND_ATTRIBUTES SidToDisable[] = {};
SidToDisable[].Sid = pAdminSID;
SidToDisable[].Attributes = ; PTOKEN_PRIVILEGES pTokenPrivileges = NULL;
PTOKEN_PRIVILEGES pTokenPrivilegesToDel = NULL;
if(!GetTokenInformation(hToken, TokenPrivileges, NULL, , &dwSize))
{
if(GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
pTokenPrivileges = (PTOKEN_PRIVILEGES)LocalAlloc(, dwSize);
pTokenPrivilegesToDel = (PTOKEN_PRIVILEGES)LocalAlloc(, dwSize);
if(pTokenPrivileges != NULL && pTokenPrivilegesToDel != NULL)
{
if(!GetTokenInformation(hToken, TokenPrivileges, pTokenPrivileges, dwSize, &dwSize))
{
Dbg("GetTokenInformation about TokenPrivileges failed GTE = %u.", GetLastError());
__leave;
}
}
else
{
char szMsg[DEFAULT_MSG_SIZE] = {};
Dbg("LocalAlloc for pTokenPrivileges failed GTE = %u.", GetLastError());
__leave;
}
}
} LUID_AND_ATTRIBUTES *pTokenLUID = pTokenPrivileges->Privileges;
Dbg("CurrentToken's TokenPrivileges Count: %u", pTokenPrivileges->PrivilegeCount);
DWORD dwLuidCount = ;
PLUID pPrivilegeLuid = NULL;
if(!CTWProcHelper::GetPrivilegeLUIDWithSID(pUserGroupSID, &pPrivilegeLuid, &dwLuidCount))
{
Dbg("GetPrivilegeLUIDWithSID failed GTE = %u.", GetLastError());
if(pPrivilegeLuid)
{
//HeapFree(GetProcessHeap(), 0, pPrivilegeLuid);
LocalFree(pPrivilegeLuid);
pPrivilegeLuid = NULL;
}
__leave;
}
Dbg("UserGroup's TokenPrivileges Count: %u", dwLuidCount); DWORD dwDelPrivilegeCount = ;
for(DWORD dwIdx=; dwIdx<(pTokenPrivileges->PrivilegeCount); dwIdx++)
{
BOOL bFound = FALSE;
DWORD dwJdx = ;
for(; dwJdx<dwLuidCount; dwJdx++)
{
//if(memcmp(&(pTokenLUID[dwIdx].Luid), &(pPrivilegeLuid[dwJdx]), sizeof(LUID)) == 0)
if((pTokenLUID[dwIdx].Luid.HighPart == pPrivilegeLuid[dwJdx].HighPart)
&&
(pTokenLUID[dwIdx].Luid.LowPart == pPrivilegeLuid[dwJdx].LowPart))
{
bFound = TRUE;
break;
}
}
if(!bFound)
{
char szPrivilegeName[MAX_PATH] = {};
DWORD dwNameSize = MAX_PATH;
if(!LookupPrivilegeName(NULL, &(pTokenLUID[dwIdx].Luid), szPrivilegeName, &dwNameSize))
{
Dbg("LookupPrivilegeName failed GTE = %u.", GetLastError());
//Dbg("NoFound[%u]: i=%u, j=%u", dwDelPrivilegeCount, dwIdx, dwJdx);
}
//else
//{
// Dbg("NoFound[%u]: i=%u, j=%u -> %s", dwDelPrivilegeCount, dwIdx, dwJdx, szPrivilegeName);
//}
pTokenPrivilegesToDel->Privileges[dwDelPrivilegeCount++].Luid = pTokenLUID[dwIdx].Luid;
}
}
pTokenPrivilegesToDel->PrivilegeCount = dwDelPrivilegeCount;
Dbg("TokenPrivileges to delete Count: %u", dwDelPrivilegeCount);
if(pPrivilegeLuid)
{
//HeapFree(GetProcessHeap(), 0, pPrivilegeLuid);
LocalFree(pPrivilegeLuid);
pPrivilegeLuid = NULL;
} if(!CreateRestrictedToken(hToken,
,
, SidToDisable,
//0, NULL,
dwDelPrivilegeCount, pTokenPrivilegesToDel->Privileges,
, NULL,
&hNewToken
))
{
char szMsg[DEFAULT_MSG_SIZE] = {};
Dbg("CreateRestrictedToken failed GTE = %u.", GetLastError());
__leave;
} // Duplicate the primary token of the current process.
if (!DuplicateTokenEx(hNewToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation,
TokenPrimary, &hNewExToken))
{
Dbg("DuplicateTokenEx failed GTE = %u.", GetLastError());
hNewExToken = NULL;
//__leave;
}
else
{
if (ConvertStringSidToSid(szIntegritySid, &pIntegritySid))
{
tml.Label.Attributes = SE_GROUP_INTEGRITY;
tml.Label.Sid = pIntegritySid; // Set the process integrity level
if (!SetTokenInformation(hNewExToken, TokenIntegrityLevel, &tml,
sizeof(TOKEN_MANDATORY_LABEL) + GetLengthSid(pIntegritySid)))
{
Dbg("SetTokenInformation failed GTE = %u.", GetLastError());
//__leave;
}
else
{
CloseHandle(hNewToken);
hNewToken = hNewExToken;
hNewExToken = NULL;
Dbg("Assign Low Mandatory Level to New Token which used to CreateProcessAsUser.");
}
} } if(!(bSuc = CreateProcessAsUser(hNewToken, NULL,
szCmdLine, // command line
NULL, // TODO: process security attributes
NULL, // TODO: primary thread security attributes
TRUE, // handles are inherited ??
fdwCreate, // creation flags
NULL, // use parent's environment
NULL, // use parent's current directory
&si, // STARTUPINFO pointer
&pi))) // receives PROCESS_INFORMATION
{
Dbg("CreateProcessAsUser failed GTE = %u.", GetLastError());
__leave;
} if(pTokenPrivileges)
{
LocalFree(pTokenPrivileges);
}
if(pTokenPrivilegesToDel)
{
LocalFree(pTokenPrivilegesToDel);
}
}
__finally
{
if(pIntegritySid)
{
LocalFree(pIntegritySid);
}
if(pUserGroupSID)
{
LocalFree(pUserGroupSID);
}
if(pAdminSID)
{
LocalFree(pAdminSID);
}
//
// Close the access token.
//
if (hToken)
{
CloseHandle(hToken);
}
if(hNewToken)
{
CloseHandle(hNewToken);
}
if(hNewExToken)
{
CloseHandle(hNewExToken);
}
if(!bSuc)
{
Dbg("Retry to Create process in normal way.");
//Create process.
bSuc = CreateProcess(NULL,
szCmdLine, // command line
NULL, // TODO: process security attributes
NULL, // TODO: primary thread security attributes
TRUE, // handles are inherited ??
fdwCreate, // creation flags
NULL, // use parent's environment
NULL, // use parent's current directory
&si, // STARTUPINFO pointer
&pi); // receives PROCESS_INFORMATION
}
}
}
其中 GetPrivilegeLUIDWithSID 函数的实现如下:
BOOL GetPrivilegeLUIDWithSID(PSID pSID, PLUID *pLUID, PDWORD pDwCount)
{
LSA_OBJECT_ATTRIBUTES ObjectAttributes;
NTSTATUS ntsResult;
LSA_HANDLE lsahPolicyHandle; // Object attributes are reserved, so initialize to zeros.
ZeroMemory(&ObjectAttributes, sizeof(ObjectAttributes)); // Get a handle to the Policy object.
ntsResult = LsaOpenPolicy(
NULL, //Name of the target system.
&ObjectAttributes, //Object attributes.
POLICY_ALL_ACCESS, //Desired access permissions.
&lsahPolicyHandle //Receives the policy handle.
); if (ntsResult != STATUS_SUCCESS)
{
printf("OpenPolicy failed returned %lu", LsaNtStatusToWinError(ntsResult));
return FALSE;
} PLSA_UNICODE_STRING UserRights = NULL;
ULONG uRightCount;
ntsResult = LsaEnumerateAccountRights(lsahPolicyHandle, pSID, &UserRights, &uRightCount);
if (ntsResult != STATUS_SUCCESS)
{
printf("LsaEnumerateAccountRights failed returned %lu", LsaNtStatusToWinError(ntsResult));
LsaClose(lsahPolicyHandle);
return FALSE;
} printf("LsaEnumerateAccountRights returned Right count: %lu", uRightCount); (*pDwCount) = ;
//pLUID = (PLUID)HeapAlloc(GetProcessHeap(), 0, uRightCount*sizeof(LUID));
(*pLUID) = (PLUID)LocalAlloc(LPTR, uRightCount*sizeof(LUID));
if((*pLUID) == NULL)
{
printf("HeapAlloc for PLUID failed returned %u", GetLastError());
LsaClose(lsahPolicyHandle);
return FALSE;
} for(ULONG uIdx=; UserRights != NULL && uIdx<uRightCount; uIdx++)
{
int nLenOfMultiChars = WideCharToMultiByte(CP_ACP, , UserRights[uIdx].Buffer, UserRights[uIdx].Length,
NULL, , NULL, NULL);
PTSTR pMultiCharStr = (PTSTR)HeapAlloc(GetProcessHeap(), , nLenOfMultiChars*sizeof(char));
if(pMultiCharStr != NULL)
{
WideCharToMultiByte(CP_ACP, , UserRights[uIdx].Buffer, UserRights[uIdx].Length,
pMultiCharStr, nLenOfMultiChars, NULL, NULL);
LUID luid;
if(!LookupPrivilegeValue(NULL, pMultiCharStr, &luid))
{
printf("LookupPrivilegeValue about %s failed, GLE=%u.", pMultiCharStr, GetLastError());
HeapFree(GetProcessHeap(), , pMultiCharStr);
continue;
}
(*pLUID)[(*pDwCount)++] = luid;
HeapFree(GetProcessHeap(), , pMultiCharStr);
}
}
if((ntsResult = LsaFreeMemory(UserRights)) != STATUS_SUCCESS)
{
printf("LsaFreeMemory failed returned %lu", LsaNtStatusToWinError(ntsResult));
}
LsaClose(lsahPolicyHandle);
return TRUE;
}
下图是普通创建子进程效果(使用 Process Explorer获取的进程信息,下同),可以看到:
- 该进程的所有者为 Administrators;
- 强制性标识等级为:高
- 部分特权为 Enabled
通过执行上方的代码对新进程的创建令牌进行一系列的限制后,可以看到:
- 进程的所有者:添加了Deny属性;
- 强制性标识等级:低;
- 部分特权被删除。
4. 扩展延伸 |
托管
一个托管可以是用户账号、组账号或者登陆会话。是由访问控制项(ACE)赋予的,每个访问控制项(ACE)中都有一个安全标识(SID)用来表明特定的托管。
特权(Privilege)
特权用于对一个对象或服务的访问控制,比*访问控制更为严格,一个系统管理员通过使用特权控制那些用户可以操纵系统资源,一个应用程序在修改系统层级的资源需要使用到特权,比如修改系统时间和关闭系统。
更多内容请查看如下链接:
Windows Integrity Mechanism Design
Designing Applications to Run at a Low Integrity Level
Understanding and Working in Protected Mode Internet Explorer
Browsing the Web and Reading E-mail Safely as an Administrator(DropMyRight.exe的实现)