找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 4538|回复: 10

【处理器】大学英语四六级:一种动态防止ROP攻击的新技术

[复制链接]
发表于 2021-2-9 19:30:30 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
本帖最后由 tangptr@126.com 于 2021-11-18 00:50 编辑

前言

ROP攻击(即Return-Oriented Programming)是一种通过攻击栈上的函数返回地址的方式来执行恶意代码。这种攻击方式在过去一般没法让系统软件以异常之类的方式主动拦截。以往防止ROP攻击是通过编译器往生成的代码里加特技实现静态防止ROP。(比如MSVC的编译器里加一个/GUARD:CF参数)
但在2020年中Intel的x86手册更新之后我发现Intel加入了CET(Control-flow Enforcement Technology)技术实现动态防止ROP攻击。很快啊,AMD也在手册里拟定了CET的草稿。
鉴于大学英语四六级考试的简写也是CET,因此该技术也可以戏称为大学英语四六级。

宏观框架

CET技术的核心是往处理器里引入了影栈(Shadow Stack Pointer,以下简称ssp)寄存器(或许也可以称之为暗栈,暗影之栈,命名逐渐中二。明修栈道,暗度陈仓,我™到底在说啥。。。),它的特点是与一般的栈只在调用与返回时同时变化。换言之,只有callintretiret指令以及异常和中断可以向影栈读写玩意。
每当出现call指令、int指令、中断、异常的时候,处理器除了向rsp压栈,同时向ssp压栈。而当ret指令或iret指令执行的时候,会同时从rspssp里弹出返回地址并进行比较,如果不相等就产生执行流异常(Control-flow Exception,简称#CP异常)。

页表变化

由于ROP攻击的重点就是在于能成功的改写控制流,因此攻击者在了解到存在控制流守护(Control-Flow Guard,简称CFG)时,就会试图在修改栈的同时也要去绕过CFG。而CET技术的核心就是在于新增的ssp寄存器,因此攻击者若是获取到了ssp寄存器的值,就会去修改影栈从而绕过CET的保护。
不过显然x86的CET工程师在设计影栈的时候就想到过这一点。对此x86的工程师就设计了一种降维打击的方式:在页表里标记该影栈的页面归属于影栈,于是使用一般的操作内存的指令写影栈就会产生异常。
当操作系统分配影栈的时候,需要将影栈页的最后一级的页表记录中将R/W复位并将Dirty位置位,以此标记该页属于影栈,可以说非常会节省空间了属于是。只有写入才能使得Dirty位被置位,对于不可写入的位那就不可能把Dirty位置位了,于是这反而成为了一种保留位组合,被x86工程师利用起来了。

影栈令牌

影栈令牌(Shadow Stack Token)是切换影栈时的校验机制。按照影栈令牌的使用规则,切换栈的步骤应当是:

  1. 生成影栈令牌并压入到影栈上

  2. 取出新影栈的令牌并校验,其中:

    • 若令牌校验成功,则修改成功,处理器修改ssp寄存器的值
    • 若令牌校验失败,则修改失败,处理器跑出#CP异常
  3. 完成切换,接下来爱干嘛干嘛

影栈令牌的结构其实很简单,如图所示:
ssp-token.PNG
最低位为Mode模式位:若复位则表示它是保护模式或兼容模式影栈,即32位影栈;若置位则表示它是长模式影栈,即64位影栈。
第1位为Busy繁忙位:若复位则表示处理器暂时没使用这个影栈;若置位则表示该影栈正在被处理器使用。
高62位为Token域,即令牌域:该域表示影栈线性地址的高62位。
而当处理器生成影栈令牌时,会计算ssp压入一个帧后的值的高62位作为令牌的高62位,第1位视繁忙与否进行置位或复位,最低位视运行模式进行置位或复位,最后将影栈令牌压到栈上。
也就是说,当处理器生成完一个影栈令牌后,理论上ssp == token & 0xfffffffffffffffc。这个比较式是用于验证影栈令牌的合法性的其中一步,其他步骤视校验影栈令牌的目的而定。

影栈指令

本章节简介所有影栈相关的指令。一切影栈指令在访问内存时,皆进入原子态,故其他处理器核心及系统外设的内存访问将会被临时挂起,直到离开原子态才能继续。

clrssbsy指令

clrssbsy指令名称来自Clear Shadow Stack Busy的缩写。其作用是停用影栈寄存器。该指令具有操作数:

clrssbsy mem64

其操作数是指向影栈的栈指针的线性地址。
该指令会校验影栈令牌的合法性,除了影栈地址之外,还要校验影栈令牌的Busy位是否置位。
若校验失败,则将RFlags.CF置位表示执行失败。
若校验成功,则将ssp寄存器清零,并将RFlags.CF复位表示执行成功。
该指令是特权指令,必须在Ring0执行。

setssbsy指令

setssbsy指令名称来自Set Shadow Stack Busy的缩写。其作用是启用影栈寄存器。该指令没有操作数:

setssbsy

它不从某个内存地址取得影栈的栈指针,而是从PL0_SSP这个MSR寄存器(Index=0x6A4)里获取。
该指令会校验影栈令牌的合法性,除了影栈地址之外,还要校验影栈令牌的Busy位是否复位。
若校验失败,则产生#CP异常。
若校验成功,则设置新的ssp寄存器的值为PL0_SSP这个MSR寄存器的值,并生成Busy置位的令牌写到影栈上。
该指令是特权指令,必须在Ring0执行。

rstorssp指令

rstorssp指令名称来自Restore (Saved) Shadow Stack Pointer的缩写。其作用是切换影栈。该指令具有操作数:

rstorssp mem64

其操作数是指向新影栈的栈指针的线性地址。
该指令会校验新影栈的影栈令牌合法性,除了影栈地址之外,还要校验影栈令牌的Busy是否复位,且Mode位必须与当前模式相同。
若校验失败,则产生#CP异常。
若校验成功,则切换ssp到新影栈上,并将新影栈上存有的新影栈令牌替换为旧影栈令牌。
若影栈切换成功,且处理器运行在32位上,则将RFlags.CF置位。
该指令不是特权指令,只要当前CPL启用了影栈,就可以使用该指令。

saveprevssp指令

saveprevssp指令名称来自Save Previous Shadow Stack Pointer的缩写。其作用是保存旧影栈以备恢复。该指令没有操作数:

saveprevssp

该指令直接使用当前ssp的值作为旧影栈的栈指针,值得注意的是获取值的行为是弹出栈,因此会增加ssp的值。
该指令会校验旧影栈的影栈令牌合法性,除了影栈地址之外,还要校验影栈令牌的Busy是否置位,且Mode位必须与当前模式相同。在此之上还要校验ssp的对齐性。
若校验失败,则产生#GP异常。注意这里不是#CP异常了。
若校验成功,则生成一个旧影栈令牌并写到旧影栈上。
该指令不是特权指令,只要当前CPL启用了影栈,就可以使用该指令。

incssp指令

incssp指令名称来自Increment Shadow Stack Pointer的缩写。其作用是增加ssp寄存器的值。该指令具有操作数:

incsspd reg32
incsspq reg64

该指令将寄存器的低八位作为增加的帧数量。值得注意的是处理器视低八位是无符号的,因此只能加不能减。用伪汇编来描述的话:
incsspd eax相当于add ssp,al*4
incsspq rax相当于add ssp,al*8
特殊情况是,若al=0则直接add ssp,4add ssp,8。具体要看incssp指令中REX.W是否置位。(什么?你不知道REX.W为何物?
该指令不是特权指令,只要当前CPL启用了影栈,就可以使用该指令。

rdssp指令

rdssp指令名称来自Read Shadow Stack Pointer的缩写。其作用是读取ssp寄存器的值。该指令具有操作数:

rdssp reg

用伪汇编表达其含义就是

mov reg,ssp

值得注意的是,如果处理器不支持CET技术,或者未启用CET技术,则该指令会被视为NOP指令。

wrss指令

wrss指令名称来自Write to Shadow Stack的缩写。其作用是向影栈中写东西。该指令具有操作数:

wrss mem,reg

由于影栈受到页表的保护,普通的写内存指令无法修改影栈里的内容,该指令就是用来专门修改影栈的内容的。它具有的特点是:

  • 只能写32位或64位,且地址必须对齐,否则产生#GP异常。
  • 必须启用影栈写入,否则产生#UD异常。
  • 所引用的页必须是被标记为影栈的页,否则产生#PF异常。

wruss指令

wruss指令名称来自Write to User Shadow Stack的缩写。其作用是向用户影栈中写东西。该指令具有操作数:

wruss mem,reg

相比于wrss指令,wruss指令的执行成功条件还要多:

  • 必须在Ring0里执行,否则产生#GP异常。
  • 所引用的页还必须得是用户模式页,否则产生#PF异常。

影栈相关MSR

S_CET(MSR Index=0x6A0) 与 U_CET(MSR Index=0x6A2) MSR

这两个MSR分别控制CET技术在Supervisor模式(CPL<3)与User模式(CPL=3)的行为。它们的位域定义是一样的:

  • 第0位表示是否为该模式启用CET技术。
  • 第1位表示是否为该模式启用影栈写入。
  • 高62位为保留位。

原则上不要给用户态启用影栈写入能力。

PLX_SSP(MSR Index=0x6A4-0x6A7) MSR与ISST_ADDR(MSR Index=0x6A8) MSR

这五个MSR控制权限跃迁时如何处理ssp寄存器的值,这要分类讨论。
原则上可以吧PLX_SSP MSR理解为它包含CPL=X用的ssp的值。

代码远转移(Far Transfer)行为

这里不讨论不涉及权限跃迁的代码远转移行为,否则就是在脱离章节主旨。本章节讨论:

  • 远调用(Far Call)、中断(Interrupt)与异常(Exception)产生的权限提升
  • 远返回(Far Return)与中断返回(Interrupt Return)产生的权限下降

此外,远跳(Far Jump)不产生栈操作,故远跳与影栈无关。

从Ring3代码远转移

如果Ring3代码远转移时产生权限提升,处理器会将当前ssp的值保存到PL3_SSP MSR中,然后切换ssp
当执行retfiret指令回到Ring3时,处理器从PL3_SSP MSR恢复ssp寄存器的值。
但此时处理器不会对影栈有压栈操作,故从Ring3产生的代码远转移到高权限再返回时处理器不会把返回地址与影栈相比对。

从Ring1或Ring2代码远转移

如果从Ring1或Ring2代码远转移产生权限提升,处理器会先切换ssp的值,然后再将转移源的信息(源cs,ssp,lip)压入到影栈。
相应的,当代码远转移使得权限降低到Ring1或Ring2时,处理器从影栈中弹出转移源的信息并做比对,若不相符则产生#CP异常。若成功则切换回原来的影栈。

影栈选择子

如果用远调用跃迁到Ring0,1,2,则将PL0,1,2_SSP MSR的值作为新ssp的值。
如果是中断或异常跃迁到Ring0,则要看IDT表上这个中断/异常项的IST(Interrupt Stack Table)域。

  • IST域为零,则将PL0_SSP MSR的值作为新ssp的值。
  • IST域非零,则ISST_ADDR MSR的值作为ISST(Interrupt Shadow Stack Table)数组地址,根据IST域选择数组中的一个作为新ssp的值。如图所示:
    isst.PNG

影栈令牌检查

和手动切换影栈一样,代码远转移造成权限跃迁时切换影栈也会检查影栈令牌。
当权限提升时:

  1. 检查新ssp是否在8字节边界上对齐,若不对齐则产生#GP异常。
  2. 从新ssp处获取影栈令牌,除了检查影栈指针之外,还要检查Busy是否复位。
  3. 若检查通过,将新影栈Busy位置位。否则产生#GP异常。

当权限下降时:

  1. 检查新ssp是否在4字节边界上对齐,若不对齐则产生#CP异常。
  2. 从当前ssp+24处获取影栈令牌,除了检查影栈指针之外,还要检查Busy是否置位。
  3. 若检查通过,将获取的影栈令牌的Busy位复位。否则什么都没发生,处理器继续执行。

为什么从当前ssp+24处取影栈令牌?答案:当远转移的高级别的时候,处理器有对新影栈会有压栈操作以保存cs,lip,ssp,压入的量正好是24字节。因此当返回的时候,在影栈的24字节之上就是当前影栈的令牌。(鼠标左键三击以显示答案)
除了对齐检查这一步外,处理器执行后面的步骤会进入原子态。

任务切换

任务切换也会产生栈切换,故任务切换时也必须切换掉影栈。于是x86工程师对32位TSS做出了修改,在TSS+0x68处增设了ssp成员来指定任务切换后目标ssp的值。
tss32.PNG
为啥不修改64位TSS结构体?因为AMD64直接在长模式里废掉了任务机制。
至于保存原ssp,其行为与一般的远转移保存ssp一致。返回时也会检查返回的目标,若不一致也一样会产生#CP异常。

64位系统调用

syscallsysret指令不会对栈产生任何操作,因此处理器不会切换影栈。虽然处理器不会切换影栈,但是会把当前ssp保存到PL3_SSP
由于syscall时处理器不会切换影栈,因此操作系统要在切换到内核栈的同时也要切换到内核影栈,可以用setssbsy指令切换到内核影栈,只需要事先把PL0_SSP MSR设置好即可。
sysret之前,操作系统也应当用clrssbsy指令停用内核影栈,然后再用rstorssp指令切换回用户影栈,最后再用sysret指令返回到用户模式。

对处理器流水线的影响

先说一下处理器分支预测的基本原理吧,处理器的分支预测器预测的东西其实不只是一条分支指令的对与错,它预测的是:

  1. 这个指令要不要跳
  2. 下一条指令的具体位置(当然也包括分支指令的对错)

由于分支预测器的执行一般是放在指令的第一阶段里的,即发射指令(Issue)阶段。故在此阶段无法计算出准确的跳转位置,分支预测器只能猜。猜对了,那流水线继续执行;猜错了,把走错路的流水线刷新掉重来。
对于返回性质的指令(即ret,retf,iret指令),如果处理器预测出返回地址与影栈上保存的不符,则流水线不会去取返回地址的指令。
虽然不论Intel还AMD,文档都没写流水线在预测出返回地址与影栈不符情况下的行为如何,但可以做一个简单的推测:

  1. 停滞流水线,直到比较完影栈与返回地址后,再决定是走到返回地址还是#CP异常,最后启动流水线继续执行。
  2. 不停滞流水线,直接走到#CP异常处理函数,如果猜错了就直接K&R(Kill and Restart)。

硬件虚拟化

在硬件虚拟化中,若产生状态切换(比如Host向Guest切换的VM-Entry)时,处理器需要将目标状态覆盖掉当前状态,其中就包括了ssp寄存器。
伴随着ssp一起被覆盖的,还有S_CETISST_ADDR这两个MSR寄存器。但不会覆盖掉U_CET,而是顺延。故如有必要,VMM需要自己覆写U_CET MSR。
此外,PLX_SSP MSR也不会被处理器自动覆盖,需要VMM自己覆写。

回复

使用道具 举报

发表于 2021-2-10 10:03:59 | 显示全部楼层
如果没理解错, 像mov [rsp+xx],yyy 或 add rsp, zzz之类的指令都不会影响影栈或ssp,那有些代码混淆似乎就不能用了。
比如ret换成
push rbx
add rsp,8
ret

本来是没啥问题的。然而开启CET的话,执行到ret似乎就会触发执行流异常
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2021-2-10 14:30:14 | 显示全部楼层
本帖最后由 tangptr@126.com 于 2021-5-1 12:00 编辑
dearfuture 发表于 2021-2-10 10:03
如果没理解错, 像mov [rsp+xx],yyy 或 add rsp, zzz之类的指令都不会影响影栈或ssp,那有些代码混淆似乎就 ...

你这个例子不会,因为ret指令的返回地址仍然是正确的地址,前两条指令都不会修改原来ssp的值,并且还在ret前回到了原rsp的值。如此一来rsp指向的值和ssp指向的值仍然是一致的。

回复 赞! 靠!

使用道具 举报

发表于 2021-2-10 15:07:13 | 显示全部楼层
本帖最后由 dearfuture 于 2021-2-10 15:08 编辑
tangptr@126.com 发表于 2021-2-10 14:30
[md]你这个例子不会,因为`ret`指令的返回地址仍然是正确的地址,前两条指令都不会修改原来`rsp`指向的值 ...


执行完push rbx,同时更新了普通栈和影栈,此时[rsp]和[ssp]还是一致的。但接下来add rsp, 8 只更新了rsp, 却没有更新ssp,那么此时[rsp]和[ssp]就不同了啊,这时执行ret不是会触发执行流异常么?
回复 赞! 靠!

使用道具 举报

发表于 2021-2-10 15:31:11 | 显示全部楼层
还是说我理解错了,rsp和ssp这两个寄存器的值是永远一致的?mov/add/sub rsp, XXX都会同步更新到ssp?
对影栈内容的影响必须通过call,ret,int,iret这几条指令还有异常中断(也就是通过其他方式修改普通栈的内存才会和影栈出现不一致)?
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2021-2-10 15:42:04 | 显示全部楼层
dearfuture 发表于 2021-2-10 15:07
执行完push rbx,同时更新了普通栈和影栈,此时[rsp]和[ssp]还是一致的。但接下来add rsp, 8 只更新了rsp ...

push指令不会操作影栈,只有call之类的能转移执行流还同时访问栈的指令才会访问影栈。
说白了那种push+ret性质的跳转代码会产生#CP异常。
如果push指令能修改影栈,那ROP攻击者直接插入push指令就可以绕过CET在ret时转移掉执行流了。
所以要强调的是:如果不会在返回时转移执行流到别处,就不会触发CET的保护机制。

回复 赞! 靠!

使用道具 举报

发表于 2021-2-10 15:56:45 | 显示全部楼层
tangptr@126.com 发表于 2021-2-10 15:42
[md]`push`指令不会操作影栈,只有call之类的能转移执行流还同时访问栈的指令才会访问影栈。
说白了那种` ...

哦哦,我傻了,把call和push搞混了。忘了push也不会影响的
回复 赞! 靠!

使用道具 举报

发表于 2021-2-10 16:17:30 | 显示全部楼层
不过vmp之类的就是用了一大堆push ret,hook用push ret也挺常见的,以后os对这种异常的处理应该不能像软件cfg那么激进
回复 赞! 靠!

使用道具 举报

发表于 2021-2-16 11:12:18 | 显示全部楼层
push ret操作,我感觉主要还是在实模式代码里常见。保护模式或者长模式除非手写汇编,否则编译器不会生成这样的指令。
回复 赞! 靠!

使用道具 举报

发表于 2021-5-11 02:17:09 | 显示全部楼层
push eax
ret
来代替jmp eax还是比较常见的混淆手段的,主要是可以对抗静态分析(让静态分析软件比如IDA无法分析控制流)
所以在开启这个机制的时候这种调用就是不可用的了么
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2021-5-11 18:39:41 | 显示全部楼层
大能猫 发表于 2021-5-11 02:17
push eax
ret
来代替jmp eax还是比较常见的混淆手段的,主要是可以对抗静态分析(让静态分析软件比如IDA无 ...

没错,这样的混淆就是不可用了。但操作系统是可以控制对特定进程不启用影栈的。
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-11-21 20:34 , Processed in 0.040195 second(s), 27 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表