[笔记]Windows安全之《五》提权技术

797 阅读13分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天

前言

令牌(Token): Windows的每个进程都是对应的权限去限制其功能的范围,而进程的令牌Token就记录对应的权限

用户帐户控制(User Account Control,简写作UAC): 是微软公司在其Windows Vista及更高版本操作系统中采用的一种控制机制。其原理是通知用户是否对应用程序使用硬盘驱动器和系统文件授权,以达到帮助阻止恶意程序(有时也称为“恶意软件”)损坏系统的效果。

Bypass UAC: 自从VISTA系统开始引入了UAC(用户账户控制),涉及权限操作时都会有弹窗提示,只有用户点击确认后,方可继续操作。所以,VISTA之后的提权操作主要是针对UAC不弹窗静默提权,即Bypass UAC。

提权技术: 拥有高于进程当前的权限,或者绕过UAC弹窗提示静默提权的技术.

本文章主要介绍两种提权技术:

  • 进程访问令牌权限提升
  • Bypass UAC
    • Bypass UAC提权技术主要利用白名单机制
    • COM组件接口技术来实现。

进程访问令牌权限提升

病毒木马想要实现一些关键的系统操作时,往往要求执行操作的进程拥有足够的权限方可成功操作。比如,通过调用ExitWindows函数实现关机或重启操作的时候,它就要求进程要有SE_SHUTDOWN_NAME权限,否则,会忽视操作不执行。这时,程序能够做的便是按照要求提升进程权限,或者另寻他法。

函数介绍

OpenProcessToken函数

打开与进程关联的访问令牌。

WINADVAPI
BOOL
WINAPI
OpenProcessToken (
    __in        HANDLE ProcessHandle,
    __in        DWORD DesiredAccess,
    __deref_out PHANDLE TokenHandle
    );

ProcessHandle: 打开访问令牌的进程句柄。该进程必须具有PROCESS QUERY INFORMATION访问权限。

DesiredAccess: 指定一个访问掩码,并指定访问令牌的请求类型。这些请求的访问类型与令牌的自由访问控制列表(DACL)进行比较,从而确定哪些访问应同意或拒绝。

TokenHandle: 指向一个句柄的指针,用于标识当函数返回时新打开的访问令牌。

如果函数成功,则返回值不为零。 如果函数失败,则返回值为零

LookupPrivilegeValue函数

查看系统权限的特权值,返回信息到一个LUD结构体里。

WINADVAPI
BOOL
WINAPI
LookupPrivilegeValue(
    __in_opt LPCSTR lpSystemName,
    __in     LPCSTR lpName,
    __out    PLUID   lpLuid
    );

IpSystemName: 指向以NULL结尾的字符串指针,该字符串指向要获取特权值的系统名称。如果指定了NULL字符串,则该函数尝试在本地系统上查找特权名称。

IpName: 指向空终止字符串的指针,并指定特权的名称,它在Wint.h头文件中定义。例如,该参数可以指定常量SE_SECURITY_ NAME或其相应的字符串"SeSecurityPrivilege"

lpLuid: 指向LUID变量的指针,该变量接收由lpSystemName参数指定系统中已知权限的LUD。

如果函数成功,则函数将返回非零值。 如果函数失败,则返回值为零。

AdjustTokenPrivileges函数

启用或禁用指定访问令牌中的权限。在访问令牌中启用或禁用权限时需要TOKEN_ADJUST_PRIVILEGES访问。

WINADVAPI
BOOL
WINAPI
AdjustTokenPrivileges (
    __in      HANDLE TokenHandle,
    __in      BOOL DisableAllPrivileges,
    __in_opt  PTOKEN_PRIVILEGES NewState,
    __in      DWORD BufferLength,
    __out_bcount_part_opt(BufferLength, *ReturnLength) PTOKEN_PRIVILEGES PreviousState,
    __out_opt PDWORD ReturnLength
    );

TokenHandle: 指向访问令牌的句柄,其中包含要修改的权限。句柄必须有TOKEN ADJUST PRIVILE GES访问令牌。如果PreviousState参数不为NULL,则该句柄还必须具有TOKEN Q UERY访问权限。

DisableAllPrivileges: 指定该功能是否禁用所有令牌的权限。如果此值为T℉UE,则该函数将禁用所有权限,并忽略NewState参数。如果此值为FALSE,则该函数将根据NewState参数指向的信息来修改权限。

NewState: 指向TOKEN PRIVILEGES结构的指针,该结构指定特权数组及其属性。如果DisableAllPrivileges参数为FALSE,则将启用AdjustTokenPrivileges函数,禁用或删除令牌的这些权限。

下表描述了基于特权属性的AdjustTokenPrivileges函数的执行操作。

截图_20220623110750.png BufferLength: 指定由PreviousState参数指向的缓冲区大小(以字节为单位)。如果PreviousState参数为NULL,则此参数可以为零。

PreviousState 指向缓冲区的指针,该函数使用包含函数修改的任何特权先前状态的TOKEN PRIVILEGES结构填充。也就是说,如果已使用功能修改特权,则该特权及其先前状态将包含在由PreviousState引用的TOKEN PRIVILEGES结构中。如果TOKEN PRIVILEGES的PrivilegeCount成员为零,则此功能不会更改任何权限。此参数可以为NULL。

ReturnLength 指向一个变量的指针,该变量接收由PreviousState参数指向的缓冲区所需的大小(以字节为单位)。如果PreviousState为NULL,则此参数可以为NULL.

截图_20220623111002.png

原理

进程访问令牌权限提升的实现步骤较为固定。要想提升访问令牌权限,首先就要获取进程的访问令牌,然后将访问令牌的权限修改为指定权限。

1.首先,程序需要调用OpenProcessToken函数打开指定的进程令牌,并获取TOKEN ADJUST PRIVILEGES权限的令牌句柄。之所以要指定进程令牌权限为TOKEN ADJUST_PRIVILEGES,是因为AdjustTokenPrivileges函数要求有此权限,方可修改进程令牌的访问权限。

2.再接着调用LookupPrivilege Value函数,获取本地系统指定特权名称的LUID值,这个LUID值相当于该特权的身份标号。

3.接着,程序就开始对进程令牌特权结构体TOKEN PRIVILEGES进行赋值,设置新特权的数量、特权对应的LUID值以及特权的属性状态。

4.最后,程序调用AdjustTokenPrivileges函数对进程令牌的特权进行修改,将上面设置好的新特权设置到进程令牌中,这样就完成了进程访问令牌的修改工作

代码实现

BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName)
{
	HANDLE hToken = NULL;
	LUID luidValue = {0};
	TOKEN_PRIVILEGES tokenPrivileges = {0};
	BOOL bRet = FALSE;
	DWORD dwRet = 0;


	// 打开进程令牌并获取具有 TOKEN_ADJUST_PRIVILEGES 权限的进程令牌句柄
	bRet = ::OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken);
	if (FALSE == bRet)
	{
		ShowError("OpenProcessToken");
		return FALSE;
	}
	// 获取本地系统的 pszPrivilegesName 特权的LUID值
	bRet = ::LookupPrivilegeValue(NULL, pszPrivilegesName, &luidValue);
	if (FALSE == bRet)
	{
		ShowError("LookupPrivilegeValue");
		return FALSE;
	}
	// 设置提升权限信息
	tokenPrivileges.PrivilegeCount = 1;
	tokenPrivileges.Privileges[0].Luid = luidValue;
	tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	// 提升进程令牌访问权限
	bRet = ::AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, 0, NULL, NULL);
	if (FALSE == bRet)
	{
		ShowError("AdjustTokenPrivileges");
		return FALSE;
	}
	else
	{
		// 根据错误码判断是否特权都设置成功
		dwRet = ::GetLastError();
		if (ERROR_SUCCESS == dwRet)
		{
			return TRUE;
		}
		else if (ERROR_NOT_ALL_ASSIGNED == dwRet)
		{
			ShowError("ERROR_NOT_ALL_ASSIGNED");
			return FALSE;
		}
	}

	return FALSE;
}

需要注意的是,即使AdjustTokenPrivileges返回TRUE,并不代表特权设置成功,还需要使用GetLastError来判断错误码返回值。若错误码返回值为ERROR SUCCESS,则表示所有特权设置成功;若为ERROR NOT ALL ASSIGNED,则表示并不是所有特权都设置成功。换句话说,如果在程序中只提升了一个访问令牌特权,且错误码为ERROR NOT ALL ASSIGNED,则提升失败。如果程序运行在Windows7或者以上版本的操作系统,可以尝试以管理员身份运行程序然后再测试。

小结

token提权技术 主要思路就是修改进程的token为指定权限.

Bypass UAC

UAC(User Account Control)是微软在Windows VISTA以后版本中引入的一种安全机制,通过UAC,应用程序和任务可始终在非管理员账户的安全上下文中运行,除非特别授予管理员级别的系统访问权限。UAC可以阻止未经授权的应用程序自动进行安装,并防止无意地更改系统设置。

UAC需要授权的动作包括:配置Windows Update、增加或删除用户账户、改变用户账户的类型、改变UAC设置、安装ActiveX、安装或移除程序、安装设备驱动程序、设置家长控制、将文件移动或复制到Program Files或Windows目录、查看其他用户文件夹等。

触发UAC时,系统会创建一个consent.exe进程,该进程通过白名单程序和用户选择来判断是否创建管理员权限进程。请求进程将要请求的进程cmdline和进程路径通过LPC接口传递给appinfo的RAiLuanchAdminProcess函数。该函数首先验证路径是否在白名单中,并将结果传递给consent.exe进程,该进程验证请求进程的签名以及发起者的权限是否符合要求后,决定是否弹出UAC窗口让用户确认。这个UAC窗口会创建新的安全桌面,屏蔽之前的界面。同时这个UAC窗口进程是系统权限进程,其他普通进程无法和其进行通信交互。用户确认之后,会调用CreateProcessAsUser函数以管理员身份启动请求的进程。

病毒木马如果想要实现更多的权限操作,那么就不得不绕过UAC弹窗,在没有通知用户的情况下,静默地将程序的普通权限提升为管理员权限,从而使程序可以实现一些需要权限的操作。

目前实现Bypass UAC主要有两种方法:

  • 一种是利用白名单提权机制,
  • 另一种是利用COM组件接口技术。

接下来,分别介绍这两种Bypass UAC的实现方法。

基于白名单程序Bypass UAC

有些系统程序是直接获取管理员权限,而不触发UAC弹框的,这类程序称为白名单程序。例如,slui.exe、wusa.exe、taskmgr.exe、msra.exe、eudcedit.exe、eventvwr.exe、CompMgmtLauncher.exe等。

这些白名单程序可以通过DLL劫持注入或是修改注册表执行命令的方式启动目标程序,实现Bypass UAC提权操作。

选取白名单程序CompMgmtLauncher.exe进行详细分析,利用它实现Bypass UAC提权。

下述的分析过程是在64位Windows10操作系统上完成的,使用到的关键工具软件是进程监控器Procmon.exeo

函数介绍

原理

利用白名单程序CompMgmtLauncher.exe,Bypass UAC提权的原理: 程序自己创建并添加注册表HKCU\Software\Classes\mscfile\shell\open\command(Default),并写入自定义的程序路径。接着,运行CompMgmtLauncher.exe程序,完成Bypass UAC提权操作。其中, HKEY_CURRENT_USER注册表是用户注册表,程序使用普通权限即可进行修改。

代码实现

修改注册表 白名单路径 添加白名单程序

// 修改注册表 白名单路径 添加白名单程序
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)
	{
		ShowError("RegCreateKeyEx");
		return FALSE;
	}
	// 设置键值
	::RegSetValueEx(hKey, NULL, 0, REG_SZ, (BYTE *)lpszExePath, (1 + ::lstrlen(lpszExePath)));
	// 关闭注册表
	::RegCloseKey(hKey);
	return TRUE;
}

截图_20220623112859.png

小结

基于COM组件接口的Bypass UAC

COM提升名称(COM Elevation Moniker)技术允许运行在用户账户控制下的应用程序用提升权限的方法来激活COM类,以提升COM接口权限。其中,ICMLuaUtil接口提供了ShellExec方法来执行命令,创建指定进程。所以,本节介绍基于ICMLuaUtil接口的Bypass UAC的实现原理是利用COM提升名称来对ICMLuaUtil接口提权,提权后通过调用ShellExec方法来创建指定进程,实现Bypass UAC操作。

使用权限提升COM类的程序必须通过调用CoCreateInstanceAsAdmin函数来创建COM类, CoCreateInstanceAsAdmin函数的代码可以在MSDN官网上找到,下面给出的是CoCreateInstance-AsAdmin函数的改进代码,它增加了初始化COM环境的代码。那么,COM提升名称具体的实现代码如下所示。

函数介绍

原理

利用COM提升名称技术提升代码的权限.

如果执行COM提升名称代码的程序身份是不可信的,则会触发UAC弹窗:若可信,则不会触发UAC弹窗。所以,要想Bypass UAC,则需要想办法让这段代码在Windows的可信程序中运行。 其中,可信程序有计算器、记事本、资源管理器、undl32.exe等。所以可以通过DLL注入或是劫持等技术,将这段代码注入到这些可信程序的进程空间中。其中,最简单的莫过于直接通过rundll32.exe来加载DLL,执行COM提升名称的代码。 其中,利用rundll32.exe调用自定义DLL中的导出函数,导出函数的参数和返回值是有特殊规定的,必须是如下形式

代码实现

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);
	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;
}

小结

对于上述基于白名单程序实现Bypass UAC的程序编译的是32位程序,而测试环境运行在64位Windows10系统上。

当32位程序访问64位的System32文件目录的时候,会出现文件重定向,调用Wow64 DisableWow64 FsRedirection和Wow64 RevertWow64 FsRedirection函数来关闭和恢复文件重定向。而且,32位程序在操作64位系统注册表的时候,也会出现注册表重定向的情况,可以在调用RegCreateKeyEx函数打开注册表的时候,设置KEY WOW6464KEY的注册表访问权限,以确保能正确访问64位下的注册表,不被注册表重定向。

对于上述基于COM组件接口技术实现Bypass UAC的程序编译的是DLL项目工程,加载调用undl32.exe等可信程序方可不弹窗Bypass UAC。调用COM函数之前,一定要先调用CoInitialize函数来初始化CoM环境,否则会出现调用CoM接口函数失败。

实现Bypass UAC的方法很多,并不局限于白名单程序和COM接口技术,对于不同的Bypass UAC方法,其具体的实现过程大都不一样。随着操作系统的升级更新,现在对于Bypass UAC成功的方法,可能在以后不再适用,但也会有新的Bypass UAC方法出现,攻与防是相互博弈的过程。

对这方面技术感兴趣的读者,可以到GITHUB开源平台上搜索UACME开源项目,里面收集了很多Bypass UAC的方法。

可以采取下述两种方法来防止Bypass UAC。

  • 不要给普通用户设置管理员权限。
  • 在“更改用户账户控制设置”中,将用户账户控制(UAC)设置为“始终通知”。

总结

Windows主要有两种提权技术:

  • 进程访问令牌权限提升
  • Bypass UAC
    • Bypass UAC提权技术主要利用白名单机制
    • COM组件接口技术来实现。