目前基于侧信道的检测越来越多了, 可以说侧信道算是为数不多能够对抗 kernel 的检测手法
由于其检测基于寄存器,并且检测点极多(所有用户 app 有权利执行的 syscall),因此需要找一个对抗点
介绍
首先我们要知道侧信道检测的原理
以目前比较流行的 KernelSU 的侧信道检测为例,实际上是计算了正常 faccessat 和特殊参数的 faccessat 调用经过的 CPU Cycle 的比例,而获取这个 Cycle 数的方法,是通过一个辅助寄存器 CNTVCT_EL0
这个寄存器是一个 Virtual Counter,计算了 CPU 执行的指令数。
寄存器的命名也是有规律的。CNT 代表了 counter,VCT 代表了 Virtual Timer Counter,EL0 代表了 Exception Level
ARMv8 将特权级别分为 4 个 level,分别是 EL0,EL1,EL2,EL3。而每个 level 的特权不一样的,特权大小 EL0<EL1<EL2<EL3。因为 ARMv8 中 ARM TrustZOne 的广泛使用,则将整个系统分为两部分一个是 Normal World,一个是 Secure World.
Normal World 就代表的是正常的世界,比如 android 手机中 linux 操作系统就在 normal world,
Secure World 就是安全世界,比如 android 手机中的高通的 QSEE 就是运行在 Secure World
而每个 Exception Level 都代表啥意思呢:EL0:就是用户空间,在 Noraml world 中比如运行的应用程序,在 Secure world 就是 TA,Trust Application
EL1:运行操作系统,Noramal World 比如 Linux 操作系统,或者 WInce 操作系统。在 Secure World 则就是 Trusted OS,比如高通的 QSEE, 开源的 OP-TEE,豌豆荚的 TEE 等
EL2:ARM 为了支持虚拟化,设计的 Hypervisor 层,只有在 Noraml world 使用。EL3:Secure Moniter 的作用是用于 Noraml world 和 Secure world 切换使用。当 noraml world 想要访问 Secure world 需要发送 SMC 指令进入 Secure Moniter 层,然后进入到 Secure world
也就是说,CNTVCT_EL0 设计之初就是为了让用户层读取的,用于性能监视之类的目的
基本思路
从内核层攻防的视角来看,我们的目的就是让用户层访问这些计数器的操作被内核拦截,然后我们可以给他返回一个预定的值,这样就和正常的内核没有什么区别了
arm 在设计的时候,其实就有考虑过侧信道的问题,因此还提供了两个控制寄存器,CNTV_CTL_EL0
以及CNTKCTL_EL1
通过刚才的介绍,实际上我们可以从名字上发现这两个寄存器分别对应了用户层和内核层的 counter controller
CNTKCTL_EL1
首先是内核层的 controller,这个寄存器控制的东西比较广泛,不过我们主要关注两个 bit,以下引用一下 arm 官方 doc
EL0VCTEN, bit [1]
Traps EL0 accesses to the frequency register and virtual counter registers to EL1, or to EL2 when it is implemented and enabled for the current Security state and HCR_EL2.TGE is 1, as follows:
In AArch64 state, accesses to the following registers are trapped and reported using EC syndrome value 0x18:
CNTVCT_EL0.
CNTVCTSS_EL0.
If CNTKCTL_EL1.EL0PCTEN is 0, CNTFRQ_EL0.
这个 bit 如果被设置,那么就能够将 CNTVCT_EL0 的 access 直接 trap 掉,交给内核来处理,那么我们内核设置的异常处理程序就能拦截用户层的访问,之后要想修改,只要直接给 pt_regs🧔就可以了
第二个关键 bit 是
EL0VTEN, bit [8]
Traps EL0 accesses to the virtual timer registers to EL1, or to EL2 when it is implemented and enabled for the current Security state and HCR_EL2.TGE is 1, as follows:
In AArch64 state, accesses to the following registers are trapped and reported using EC syndrome value 0x18:
CNTV_CTL_EL0, CNTV_CVAL_EL0, and CNTV_TVAL_EL0.
文档里告诉我们,这个 bit 控制了用户层的对于 CNTV_CTL_EL0
的 access。通过手动写个内核模块加载然后在内核中读取这个寄存器可以发现,我手上几个手机这个 bit 都是没有设置的,因此用户层对于 CNTV_CTL_EL0
的访问会被 trap 掉。至于这个的作用暂且不表,后面还会提及
CNTV_CTL_EL0
作为用户层的 controller,它能做的就比内核的 controller 少多了
需要关注的只有 ENABLE
ENABLE, bit [0]
Setting this bit to 0 disables the timer output signal, but the timer value accessible from CNTV_TVAL_EL0 continues to count down.
Disabling the output signal might be a power-saving option.
非常易懂,就是控制计数器的开关
尝试修改
有了这些知识之后就可以开始了,直接写一个内核模块修改 CNTKCTL_EL1
的EL0VCTEN
就行了?
然而,经过尝试之后,发现就算这个 bit 变成了 0,用户层还是可以访问计数器?
原因
事实上,就像我们刚才介绍的 Exception Level, 作为能够修改内核的攻击者,我们事实上只获得了 EL1 权限,而 EL2 是比我们权限更高的。而 EL2 的 Hypervisor 实际上向用户开放了对 counter 的访问,拦截了我们在 EL1 上做的限制
而 EL2 的 Hypervisor 代码实际上一般是我们无法控制的。国内大部分厂商都是不开源 TrustZone 部分代码的,而只有 Google 使用的是开源的 TEE 代码。而即使这样对其进行修改编译测试仍然是十分难以实现的,因此需要转换思路
另一个 Controller
刚才我们提到了还有一个用户层的 controller,那么是否可以通过这个 controller 来控制访问呢
首先,在内核中启用对于用户层控制器的访问
不过经过尝试,直接在内核中修改这个 controller, 并没有作用,会被直接覆盖
那么写一个用户程序进行修改,然后再在内核中关闭即可
效果
不过这么修改了之后,虽然发现那些检测器确实检测不到了,然而 demo 中可以发现,实际上是获取的 CNTVCT_EL0
变成了 0
这是十分意想不到的。虽然看似解决了问题,实际上引入了新的问题,会造成设备离群
那么,就没有真正能解决的办法了吗?
实际上还是有的,但是未完待续。。。