揭秘Apple Silicon中的新硬件特性:SPRR与GXF保护机制(下)
2021-05-31 10:13:44 Author: www.4hou.com(查看原文) 阅读量:110 收藏

最近,苹果公司推出了自家研发的芯片M1 SoC,其中含有许多有趣且未公开的硬件新特性,例如SPRR机制可用于重新定义页表权限位的含义,而GXF机制则引入了横向执行级别。在这篇文章中,我们将为读者详细介绍这些新特性,以及苹果公司是如何利用它们来保护macOS系统的。

(接上文)

通过Python探索未知的系统寄存器

实际上,m1n1最大的特点就是:允许直接通过python shell来操作硬件,而不是重新编译和重新加载shellcode,以及通过python来处理数据提取和所有这些恼人的琐事。marcan最近还合并了我的USB gadget代码,因此,大家要想复现这些实验的话,只要一台M1 Mac和一根普通的USB线就能搞定了。

让我们从运行proxyclient/shell.py开始。不幸的是,访问用户态的SPRR寄存器只是触发了一个异常。(但我注意到,m1n1很快从这个异常中恢复过来了,根本不需要重启!)

>>> u.mrs((3, 6, 15, 1, 5))
TTY> Exception: SYNC
TTY> Exception taken from EL2h
TTY> Running in EL2
TTY> MPIDR: 0x80000000
TTY> Registers: (@0x8046b3db0)
TTY>   x0-x3: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
TTY>   x4-x7: 0000000810cb8000 0000000000007a69 0000000804630004 0000000804630000
TTY>  x8-x11: 0000000000000000 00000000ffffffc8 00000008046b3eb0 000000000000002c
TTY> x12-x15: 0000000000000003 0000000000000001 0000000000000000 00000008046b3b20
TTY> x16-x19: 00000008045caa80 0000000000000000 0000000000000000 000000080462b000
TTY> x20-x23: 00000008046b3f78 00000008046b3fa0 0000000000000002 00000008046b3f98
TTY> x24-x27: 00000008046b3f70 0000000000000000 0000000000000001 0000000000000001
TTY> x28-x30: 00000008046b3fa0 00000008046b3eb0 00000008045bad90
TTY> PC:       0x810cb8000 (rel: 0xc70c000)
TTY> SP:       0x8046b3eb0
TTY> SPSR_EL1: 0x60000009
TTY> FAR_EL1:  0x0
TTY> ESR_EL1:  0x2000000 (unknown)
TTY> L2C_ERR_STS: 0x11000ffc00000000
TTY> L2C_ERR_ADR: 0x0
TTY> L2C_ERR_INF: 0x0
TTY> SYS_APL_E_LSU_ERR_STS: 0x0
TTY> SYS_APL_E_FED_ERR_STS: 0x0
TTY> SYS_APL_E_MMU_ERR_STS: 0x0
TTY> Recovering from exception (ELR=0x810cb8004)
Traceback (most recent call last):
  File "/opt/homebrew/Cellar/[email protected]/3.9.4/Frameworks/Python.framework/Versions/3.9/lib/python3.9/code.py", line 90, in runcode
    exec(code, self.locals)
  File "
  File "/Users/speter/asahi/git/m1n1/proxyclient/utils.py", line 80, in mrs
    raise ProxyError("Exception occurred")
proxy.ProxyError: Exception occurred
>>>

但是,内核必须能够在上下文切换期间修改这个寄存器。这就意味着可能存在一些使能位。幸运的是,m1n1存储库中已经提供了一个现成的python工具,可用于查找所有可用的系统寄存器。该工具的内部运作方式为:为所有寄存器生成相应的mrs指令,并从未定义寄存器引起的异常中恢复。下面,让我们运行它并寻找附近的寄存器: 

$ python3 proxyclient/find_all_regs.py | grep s3_6_c15_c1_
s3_6_c15_c1_0 (3, 6, 15, 1, 0) = 0x0
s3_6_c15_c1_2 (3, 6, 15, 1, 2) = 0x0
s3_6_c15_c1_4 (3, 6, 15, 1, 4) = 0x0

这里给出了三个候选寄存器。如果将0x1写入第一个候选寄存器,似乎会导致m1n1无法正常工作——很明显,这说明m1n1是从具有rwx权限的页面运行的。SPRR寄存器最初的值为0x0,这意味着没有任何访问权限。如果SPRR突然启动,使CPU认为rwx实际上是---,会发生什么?一切都完了,因为没有可以读取或执行的内存了。 

现在,让我们禁用MMU,并再次将0x1这些寄存器(并很快注意到第三个寄存器似乎只是引发了故障并忽略了该故障),然后,查找所有寄存器,最后确定新的寄存器。这一切都可以通过几行python代码来完成: 

with u.mmu_disabled():
    for reg in [(3, 6, 15, 1, 0), (3, 6, 15, 1, 2)]:
        old_regs = find_regs()
        u.msr(reg, 1)
        new_regs = find_regs()
 
        diff_regs = new_regs - old_regs
 
        print(reg)
        for r in sorted(diff_regs):
            print("  %s" % list(r))
 
    u.msr((3, 6, 15, 1, 2), 0)
    u.msr((3, 6, 15, 1, 0), 0)

哦,天哪,有很多新的寄存器被启用了!

启用的系统寄存器

接下来,让我们把S3_6_C15_C1_0改名为SPRR_CONFIG_EL1。这里的第1位启用了SPRR,设置所有的位似乎可以锁定所有的SPRR寄存器,以作进一步的修改。S3_6_C15_1_2和它所启用的寄存器对于第二部分内容来说是非常重要的。

而我们现在确实可以翻转S3_6_C15_C1_5的所有位了:

>>> p.mmu_shutdown()
TTY> MMU: shutting down...
TTY> MMU: shutdown successful, clearing cache
>>> u.msr((3, 6, 15, 1, 0), 1)
>>> u.mrs((3, 6, 15, 1, 5))
0x0
>>> u.msr((3, 6, 15, 1, 5), 0xffffffffffffffff)
>>> u.mrs((3, 6, 15, 1, 5))
0xffffffffffffffff
>>>

虽然这个寄存器可能适用于EL0,但我们在这里的运行级别为EL2。我们可以做一个有根据的猜测,假设新启用的寄存器S3_6_C15_C1_6可能是用于EL1级别的,S3_6_C15_C1_7用于EL2。M1总是与HCR_EL2.E2H一起运行,它(除其他外)将对EL1寄存器的访问重定向到其EL2对应的寄存器。我们可以用下面的实验来验证我们的猜测:

>>> u.msr((3, 6, 15, 1, 6), 0xdead0000)
>>> u.mrs((3, 6, 15, 1, 7))
0xdead0000
>>>

到目前为止,看起来还很顺利:现在,我们可以启用SPRR机制了,而且发现了一个可疑的寄存器——它很可能与EL2权限有关。下面,我们将重复在用户态下面的实验了,以了解这些寄存器中上面介绍的四位以外的内容了。

逆向分析SPRR寄存器

我们可以编写一些python代码,来建立一个简单的页表,然后重复我们在用户态下面所做的那些实验。首先,映射一个页面,使其具备S3_6_C15_C1_6对应的权限,然后,尝试对其中的内存进行读/写/执行操作。 

为了通过python代码完成上述操作,必须对m1n1本身进行一些侵入性的修改,使其从r-x内存页运行,并将其堆栈分配到具有rw-权限的内存页中。我们要力争通过Python完成尽可能多的设置工作,然后,编写一些shellcode并在其他CPU核上运行,这能够让事情容易得多:即使其中一个挂掉了,在重启之前,至少还有其他的可用。   

pagetable = ARMPageTable(heap.memalign, heap.free)
pagetable.map(0x800000000, 0x800000000, 0xc00000000, 0)   # normal memory, we run from here
pagetable.map(0xf800000000, 0x800000000, 0xc00000000, 1)  # probe memory, we'll try to read/write/execute this
# ...
code_page = build_and_write_code(heap, """
    // [...]
                // prepare and enable MMU
                ldr x0, =0x0400ff
                msr MAIR_EL1, x0
                ldr x0, =0x27510b510 // borrowed from m1n1's MMU code
                msr TCR_EL1, x0
                ldr x0, =0x{ttbr:x}
                msr TTBR0_EL1, x0
                mrs x0, SCTLR_EL1
                orr x1, x0, #5
                msr SCTLR_EL1, x1
                isb
    // [...]
""".format(ttbr=pagetable.l0)
# ...
ret = p.smp_call_sync(1, code_page, sprr_val)
# ...

这样一来,过去的信号处理程序现在变成了一个小型的异常向量。我们在这里所做的只是修改一个寄存器来指示故障,然后在返回之前让程序计数器再跳过两条指令即可。第一条指令是发生故障的那条,我们不想再运行它了。第二条指令是mov x10, 0x80,表示访问成功,如果我们遇到了异常,访问肯定就不成功了。

_fault_handler:
# store that we failed
mov x10, 0xf1
 
mrs x12, ELR_GL2  # get the PC that faulted
add x12, x12, 8   # skip two instructions
msr ELR_GL2, x12  # store the updated PC
 
isb
# eret restores the state from before the exception was taken
eret
 
 
_sprr_test:
# ...
 
# test read access, x1 contains an address to a page for which we modify the SPRR register values
mov x10, 0    # x10 is our success/failure indicator
ldr x1, [x1]  # this instruction will fault if we can't read from [x1]
mov x10, 0x80 # this instruction will be skipped if the previous one faulted

通过上面的工作,我们最终得到了所有16种可能配置的含义:

 2.png 

很明显,这里有些地方很奇怪。在大多数情况下,较低的两个比特指定了权限。但有两个例外情况,较高的位也会以某种方式改变权限。0111似乎不允许访问一个本应是rw-权限的页面,而1001通常应该是可读和可执行的,但这里只有可执行权限。

按理说,根本就没有必要再浪费两个比特来编码这个权限。乍一看,这可能是用于处理用户与内核的write-or-execute权限。但我们知道,EL0使用的是一个完全不同的寄存器。那么,这还能是什么作用呢?

受保护的异常级别/GXF

从上一节我们知道,在PPR寄存器中编码了一些奇怪的东西。此外,之前我们也曾提到了受保护的异常级别,它与常规的异常级别是不同的。显然,这些是由0x00201420和0x00201400这两条被称为genter和gexit的定制指令触发的。 

让我们把XNU附加到反汇编程序中,看看能否通过otool -xv /System/Library/Kernels/kernel.release.t8101找到一些可疑的东西。在查找这些指令时,发现了如下所示的可疑指令,它也恰好在初始化早期被调用: 

fffffe00071f80f0        mov     x0, #0x1
fffffe00071f80f4        msr     S3_6_C15_C1_2, x0
fffffe00071f80f8        adrp    x0, 2025 ; 0xfffffe00079e1000
fffffe00071f80fc        add     x0, x0, #0x9d8
fffffe00071f8100        msr     S3_6_C15_C8_2, x0
fffffe00071f8104        adrp    x0, 2025 ; 0xfffffe00079e1000
fffffe00071f8108        add     x0, x0, #0x9dc
fffffe00071f810c        msr     S3_6_C15_C8_1, x0
fffffe00071f8110        isb
fffffe00071f8114        mov     x0, #0x0
fffffe00071f8118        msr     ELR_EL1, x0
fffffe00071f811c        isb
fffffe00071f8120        .long   0x00201420
fffffe00071f8124        ret

还记得S3_6_C15_C1_2吗?(我当时印象深刻,因为对我来说,所有这些数字看起来都一样。)这是我们前面找到的第二个使能寄存器,也是这里使用的第一个寄存器。然后,将两个指针写入未知系统寄存器,最后执行未定义的指令0x00201420。第一个指针只是指向一个无限循环,但第二个指针指向一个函数,它似乎也使用了我们前面找到的SPRR寄存器。

因此,S3_6_C15_C8_1中可能是一个指针,一旦0x00201420被执行,处理器就会跳转到这个指针。第二条未知指令0x00201420似乎在那时恢复执行。这一切听起来与管理程序调用的工作方式非常相似。0x00201420对应于smc,以捕获到EL3权限,0x00201400是eret,将我们带回EL2级别。不同的是,对于这种新的执行模式,没有找到不同的页表。还记得SPRR寄存器中未知的两个位吗?如果这些对应于GL2的页面权限呢? 

我们可以通过使用与之前相同的方法,再次用m1n1快速验证这一点。我们在保护性执行模式下设置异常向量,并重复同样的实验。 

但是,我们如何在这种新模式下设置异常向量呢?通常有一个叫VBAR的寄存器来实现这个目的。让我们简单看看S3_6_C15_C10_2所指向的代码,它是XNU在genter之后首先设置的寄存器之一。   

fffffe00079e0000        b       0xfffffe00079e15d0
fffffe00079e0004        nop
fffffe00079e0008        nop
fffffe00079e000c        nop
[...]
fffffe00079e007c        nop
fffffe00079e0080        b       0xfffffe00079e1000
fffffe00079e0084        nop
[...]
fffffe00079e00fc        nop
fffffe00079e0100        b       0xfffffe00079e11f0
fffffe00079e0104        nop
[...]

唷,这看起来像是一个异常向量表,这意味着S3_6_C15_C10_2是VBAR_GL1。 

这样的话,我们就得到了一个完整的权限表,并破解了SPRR寄存器的所有位的秘密。

这个完整的权限表看起来正是我们要找的:SPRR寄存器的值为0100、0110或1111时,如果从EL2跳转到代码时,内核似乎就会崩溃。所有这些值都对应于这样一个内存页:该页显然不能从EL2执行,但能从GL2执行。如果这些故障由于某种原因转移到不同的地址,结果会怎样?不要再拐弯抹角了,这正是现在所发生的情况。这三个特殊的故障使用了XNU指向无限循环的系统寄存器,即 

·    当EL2试图跳转到只能在GL2中执行的代码时,引发的异常终止将转到S3_6_C15_C8_2(我称之为GXF_ABORT_EL2);

·    任何其他来自EL2的异常终止都转到VBAR_EL2;

·    任何其他来自GL2的异常终止都转到VBAR_GL2。 

这样的话,我们再次就得到了一个完整的权限表,从而破解了SPRR寄存器的所有位的秘密。

1.png 

下面,让我们详细了解一下通过GL权限位修改EL权限位含义的两种特殊情况:

·  第一种情况(0111)确保无法创建在GL中可执行、在EL中可写入的内存页。这为防止软件漏洞提供了额外的硬件层保护。如果能够从EL中改变在GL中运行的代码的话,将使整个横向保护变得毫无意义。

·  第二种情况(1001)将r-x EL权限替换为--x权限,如果该内存页只能从GL中读取的话。我不知道为什么要强制执行这个保护。也许是为了能够对EL隐藏一些秘密代码,或者作为对我不熟悉的安全漏洞的一些额外缓解措施?我很想知道为什么这样的映射会有帮助,如果有人能够给出一个很好的解释的话。

利用Python探测GL2

掌握了这些知识,我们现在旧可以很轻松地在GL2和m1n1中添加对运行自定义payload的支持了。我们所要做的,就是利用已经存在的框架将运行级别降到EL1/EL0。为此,我们只需要禁用MMU(因为m1n1假设它是从具有rwx权限的内存页运行的,而我们在启用SPRR的情况下无法做到这一点),然后跳转到payload,最后,在返回之前再次启用MMU。

这使得我们可以很轻松地探测GL2,例如,看看S3_6_C15_C10_3是否就是SPSR_GL2: 

>>> u.mrs((3, 6, 15, 10, 3), call=p.gl_call)
0x60000009
>>> u.mrs(SPSR_EL2)
0x60000009

或者,我们可以直接重新运行MSR查找器,但这次是在GL2中: 

gxf_regs = find_regs(call=p.gl_call)
 
print("GXF")
for r in sorted(gxf_regs - all_regs):
    print("  %s" % list(r))

这样,我们就发现一大堆神秘的新系统寄存器,它们只有在该上下文中才能找到:

GXF
  [3, 6, 15, 0, 1]
  [3, 6, 15, 0, 2]
  [3, 6, 15, 1, 1]
  [3, 6, 15, 2, 6]
  [3, 6, 15, 8, 5]
  [3, 6, 15, 8, 7]
  [3, 6, 15, 10, 2]
  [3, 6, 15, 10, 3]
  [3, 6, 15, 10, 4]
  [3, 6, 15, 10, 5]
  [3, 6, 15, 10, 6]
  [3, 6, 15, 10, 7]
  [3, 6, 15, 11, 1]
  [3, 6, 15, 11, 2]
  [3, 6, 15, 11, 3]
  [3, 6, 15, 11, 4]
  [3, 6, 15, 11, 5]
  [3, 6, 15, 11, 6]
  [3, 6, 15, 11, 7]

也许以3、6、15、10开头的是GL1,以3、6、15、11开头的是GL2,或者反过来?这很容易搞清楚:只要在EL2中启用SPRR和GXF后,将运行级别下降到EL1,并重新进行同样的实验即可。这一次我们只得到如下所示的新寄存器: 

  [3, 6, 15, 0, 1]
  [3, 6, 15, 8, 7]
  [3, 6, 15, 10, 1]
  [3, 6, 15, 10, 2]
  [3, 6, 15, 10, 3]
  [3, 6, 15, 10, 4]
  [3, 6, 15, 10, 5]
  [3, 6, 15, 10, 6]
  [3, 6, 15, 10, 7]

这意味着3、6、15、10组确实代表EL1寄存器。这一点并不重要,因为M1总是以HCR_EL2.E2H运行,这意味着_EL1寄存器在EL2级别运行时被重定向到_EL2。同样的情况似乎也适用于GL1和GL2寄存器。 

我们能不能也搞清楚它们的确切含义呢?幸运的是,一个早期的开源XNU版本含有如下所示的一些名称:

#define KERNEL_MODE_ELR      ELR_GL11
#define KERNEL_MODE_FAR      FAR_GL11
#define KERNEL_MODE_ESR      ESR_GL11
#define KERNEL_MODE_SPSR     SPSR_GL11
#define KERNEL_MODE_ASPSR    ASPSR_GL11
#define KERNEL_MODE_VBAR     VBAR_GL11
#define KERNEL_MODE_TPIDR    TPIDR_GL11

我不清楚为什么这些寄存器带有GL11的后缀,但除此之外,它们可以很容易地与上面发现的未知寄存器匹配起来。ASPSR至少包含一个位,它决定了gexit是否应该返回到保护性执行或正常执行。

即使只是这两个扩展,也还有很多未知的寄存器和谜团。如果你想一起玩,就拿起最新的m1n1,看看到底能搞清楚什么:-)

XNU中的SPRR和GXF

最后,我们来考察一下XNU是如何使用这些新功能的。实际上,这里并没有什么可考察的,因为Jonathan已经在一篇文章中介绍了SPR和GXF的使用情况。SPRR只是取代了过去APRR的功能:禁止内核对页面执行写操作,以及禁止它执行PPL代码。

最大的不同在于GXF:不需要精心设计一个修改APRR寄存器的小蹦床函数,只需要设置GXF入口向量:页表权限就会自动翻转,并且,genter就可以直接指向PPL了。 

让我们通过查看XNU如何初始化SPR来确认这一点:启动函数通过SPRR将EL1 SPR权限寄存器初始化为0x2020A505F020F0F0。这个代码序列与所有的CPU chicken位纠缠在一起。marcan甚至正确地猜到了写的是什么,并将它们从实际的chicken位序列中剥离出来。 

稍后,初始的GL引导代码会将EL1的权限更新为0x2020A506F020F0E0,然后锁定所有内容,以防止进一步的修改。 

然后,受保护的执行模式入口点被设置为一个来自正常内核的text区段的函数,并且该函数必须迅速跳转到PPLTEXT的开头位置。PPL入口函数首先会验证SPRR权限的设置是否正确,之后的行为,具体如Jonathan的文章所述。

最后,我们来看看XNU使用的各种SPRR页权限(这里没有显示的条目对所有级别都没有访问权限。在chicken位序列中设置的原始值也将GL权限设为EL级别):

2.png

这一切看起来都很合理。GL的权限可能会被进一步锁定,例如,不允许GL执行常规的内核代码(参见第10项),不允许它访问任何用户数据(参见第7项)。

除此以外,感觉这就像以前的APRR硬件的一个增强版。这些变化不仅使整个系统不容易出错(寄存器可以被锁定,内核->PPL的转换完全通过硬件实现,内核和PPL的异常向量现在被明确的分隔开来),而且更加灵活。APRR过去只能剥离权限,但SPRR现在允许任意重新映射权限——只要不需要rwx页的话。遗憾的是,在Linux中还没有将它用起来。

小结

Apple Silicon通过了两个“秘密”功能,两者可以通过密切协作,来提供额外的防御机制。GXF引入了横向异常级别,称为GL1和GL2,它们使用的页表与相应的EL使用的相同,但具有不同的页面权限。SPRR允许重新定义EL和GL的页表项中的权限位。苹果公司利用这一点在GL中隐藏所有的页表操作代码,并且禁止EL修改任何页表。这有效地引入了一个具有较小攻击面的低开销管理程序,即使在内核模式下运行的代码也可以很好地保护页表。对于本文来说,其中大部分任务可以借助Python和m1n1进行逆向分析。

这对于将Linux移植到M1上并没有什么用处,但是一旦我们将XNU虚拟化,以便追踪其MMIO访问,我们可能就会遇到这种情况。

开放性问题

在EL1中使用Hypervisor.framework时,是否可以启用SPRR和GXF?

是的! Longhorn指出,存在一个com.apple.private.hypervisor.vmapple权限,允许macOS下的EL1虚拟机也使用这些系统寄存器。

SPRR_CONFIG和GXF_CONFIG中的其他位是做什么用的?

启用SPR和GXF有什么其他影响?至少HCR_EL2不再可以从EL2写入,而是需要GL2权限了,我很肯定这绝对不是唯一的区别。

当我们处于保护性执行模式时,中断会被送到哪里?我认为它们会被送到VBAR_GLx,但我还没有确认这一点。

当在EL2中运行时,应该有一个不同的寄存器用于管理EL0的权限,例如HCR_EL2.TGE = 0和HCR_EL2.TGE = 1。也应该有一种方法可以从EL2访问EL1寄存器(类似于常见的._EL12的寄存器)。哪些寄存器可以做到这一点?

marcan已经在一则视频中把它们讲清楚了;现在它们已经被记录在m1n1中。

siguza发现了涉及PAN和WXN的一些晦涩的硬件bugundefined行为——这个问题仍然存在,还是SPRR也有类似的问题?

SPR和GXF代表什么?可能是“shadow permission remap register(影子权限重映射寄存器)”和“guarded execution feature(受保护的执行功能)”。

本文翻译自:https://blog.svenpeter.dev/posts/m1_sprr_gxf/如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/E1g0
如有侵权请联系:admin#unsafe.sh