最近想开个新坑学习下windows下的二进制知识
判断aslr, dep(nx), cfg
aslr是地址随机化. dep和nx一个意思, 都是将执行段和数据段区分的缓解措施. 只不过在windows上更习惯叫做dep. cfg(Control Flow Guard).
windows下没有checksec, 所以我们用msvc的dumpbin来检查这几个选项是否开启.dumpbin /headers .\target.exe
找到其中OPTIONAL HEADER VALUES的段, 再其中有DLL characteristics. 可以观察到几个选项是否开启Aslr(Dynamic base), dep(NX compatible), cfg(Control Flow Guard)
1 | OPTIONAL HEADER VALUES |
对于cfg, 需要再确认一步. dumpbin /loadconfig .\target.exe. 确认CF instrumented和FID table present存在才可以确认开启了cfg
1 | 00017500 Guard Flags |
cfg是什么以及如何工作
网上详细的资料很多, 推荐阅读Analysis of the Windows Control Flow Guard. 由于是英文, 让ai翻译了一下. Analysis of the Windows Control Flow Guard-ZH
这里先只简单介绍下, 哪天研究更深入了再补充.
cfg基本原理
cfg的影响范围只有间接调用和跳转, 比如函数指针调用(call rax jmp rax), 但不包括栈上返回地址被覆盖的情况. 另外cfg对于一个程序可以部分开启, 不是开启cfg之后所有间接调用和跳转都会受到影响
下面是未使用 CFG 的间接调用, 看到cfg检查入口是__guard_check_icall_fptr这个函数. 该函数在检查调用地址是否合法后会返回并继续正常执行流. 另一个函数是通过__guard_dispatch_icall_fptr函数, 差别在于该函数不会返回, 而是验证后直接跳转到调用的地址.
有趣的是这两个函数自身也通过函数指针被调用, 但他们处于ro段, 无法被直接修改.
1 | jscript9!Js::JavascriptOperators::HasItem+0x15: |
下面是启用 CFG 后的间接调用。
1 | jscript9!Js::JavascriptOperators::HasItem+0x1b: |
cfg fun facts
早期动态申请rwx内存时, 新内存会默认被cfg图标记为有效. 为了解决这个问题, 微软引入了
VirtualAlloc的Page_TARGET_INVALID, 使得新内存的地址为无效目标.VirtualProtect支持PAGE_TARGET_NO_UPDATE, 通常将地址改为可执行会自动将其地址设为有效目标, 设置该flag后会阻止更新cfg信息.
合法地址默认是在编译期间决定的, 所以对于jit这种运行时申请rwx内存并执行的情况会比较特殊. 由于上一条说到的原因, 现在的jit分配内存通常让新内存为无效目标, 需要使用
SetProcessValidCallTargets函数来进行标记. 在此基础上SetProcessValidCallTargets本身受到调用抑制的限制, 也就是其不能被间接调用或跳转.
cfg储存哪些地址合法所使用的数据结构为bitmap, 现在的cfg采取0x10字节2bit的形式, 其中
{0, 0}无效,{1, 0}代表目标0x10字节对齐的情况下有效,{1, 1}代表非16字节对齐时有效,{0, 1}代表该目标因被抑制而无效
这里解释下0x10字节对其的问题, 由于受bitmap尺寸限制, 不能为每个地址在bitmap中储存一个元素. 于是采取了每0x10字节共享2bit. 以检验0x1000-0x100f地址为例, 0x1000是0x10字节对齐, 按照一种情况考虑; 而0x1001-0x100f由于信息被压缩, 会被一起按照不对齐的情况考虑. 也就是说, 如果0x1002为有效地址, 0x1001-0x100f都会被bitmap认为有效
Windows 为降低 ASLR 成本,通常只在系统启动时随机化一次库的基址,重启后才确保产生新基址。因此同一库在多个进程中的 CFG 位图相同,可作为以页文件为后备、可共享的内存映射;物理页在 RAM 中只需一份。这样对已加载模块,主要只须计算进程私有内存的位状态,加载开销接近于零。来源1 来源2
加载未启用cfg的dll时, 该dll的每个地址都可以被间接调用或跳转. 所以只有每个模块都开启cfg时, cfg才可以保证完全有效.
绕过思路和已知弱点
- 打rop, cfg只控制间接调用和跳转, 所以ret导致的控制流并不被影响
- 利用cfg的0x10字节精度问题
- 利用未启用cfg的映像
- 更改只读段内存权限, 修改bitmap或
__guard_check_icall_fptr指针为其他函数