Apple Silicon硬件秘密:SPRR和守卫异常水平(GXF)

2021-05-07 03:26:14

多年前,Siguza发布了一个关于Apple'Saprr的文章 - 一个自定义ARM扩展,重新定义可偏执的权限并保护内核的某些部分。从那以后,苹果已发布他们的M1芯片不仅有一个更新的APR版本,而且很容易允许在Boot之后很快运行裸金属代码。有一些RumorSabout新版本,但没有任何特定于(公开)尚未记录。

本篇文章的第一部分是对内存管理,分页和用户/内核模式OnaArch64的非常简要介绍。它也将总结APRR,这是以前的Apple SoC上的等同功能。如果你已经意识到了这些,那么你就会感到无聊,可能只是跳过开始。

然后我们最终可以到达主要部分:逆向工程如何SPRR和GXF工作以及他们的所作所为。如果您想了解我如何接近这一挑战,这部分会对您交响。如果你另一方面只想要Tounderstand,Sprr和GXF都会自由地直接前往Asahi Linux Wiki!

在ARM上,CPU在所谓的异常级别运行。如果您熟悉x86,则这些称为rings .el0是应用程序运行的用户空间,EL1是(通常)内核本身运行,EL2是运行的el2。 (还有固件或信任区域的EL3,但M1没有那个级别)

在ARM64 CPU上,具有虚拟化主机extensionstere,也是一种让EL2看起来像EL1,使得内核也可以轻松运行。

其中一个内核的任务是欺骗在Usillland中运行的每个应用程序,并告诉他们它们是地址空间中唯一的应用程序。它确实通过使用内存管理单元。 MMU允许从虚拟地址创建别名以在例如,在例如,以区域物理地址。内存。此映射的最小粒度称为一个页面,通常为4kib solul.each具有虚拟地址和物理地址。当应用程序或内核本身的指令现在尝试在位置X处访问存储器时,MMU在其pagetableand中查找页面,而是从另一个地址y返回内存。这就是内核可以为每个userland应用程序提供的,它是单独的地址空间:它只需为每个进程创建不同的匹配集。

除此映射外,每个页面还包含四个编码某些访问标志的位。这些标志确定了,如果可以从页面读取,写入页面或从页面执行代码,以获取userland应用程序或kernelitself。可以在ARMv8-A CPU上的每个可匹配项中找到以下四个位:

AP0和AP1:这些只是一种略微混淆的方式来编码四种不同的写入和读取内核和Userland RW / - ,RW / RW,R - / - 和R- / R-的读取访问设置。

还有一些额外的复杂性与确定最终访问标志(PAN,分层控制)忽略此博客文章。这里要注意的一件事是userland和内核权限是紧密耦合的。它不可能创建一个页面在userland中的rw-但是r-为内核。

如上所述,每个页面都有四个标志,用于控制EL0 / 1(用户/内核模式)的访问权限(读/写/执行)。 APR完全更改此行为:而不是将四个标志存储为可解码条目中的位,而是将四个位作为索引重新播放为单独的表(即,直接编码访问权限,则位于4位索引作为[AP1 ] [ap0] [pxn] [uxn])。然后,此单独的表编码页面的实际验证。此外,某些寄存器允许进一步限制这些权限的预防者空间。这些寄存器也分为内核和userland,并且在创建页面Persmissions时允许更大的灵活性。

APR引入了一层间接到可分离的权限,这种方式允许通过单个寄存器写入非常有效地翻转许多页面的访问受试者。通常,这需要一个相当昂贵的页面WalkTo修改所有单独的条目。

通常,应用程序由更高语言编译到计算机代码,然后分发。代码可以轻松地映射为R-X,因为它已修复,并且通常在运行时通常不会在运行时进行修改。

另一方面的JIT编译器动态生成机器代码。传统上,这需要映射存储器区域,使得可以首先写入新代码然后执行。

Apple真的不想允许这样的映射,因为它们理想地想要签署每个单个指令,CPU在其iPhone上执行。如果任何应用程序只请求RWX映射,那么整个练习都会毫无意义:应用程序可以运行它想要的任何指令。即使只有某些应用程序有权此类映射,那么它将成为漏洞的目标(定位苏格索和获取任意写入和跳转小工具当然仍然具有挑战性)。

苹果想要有一个JIT编译器。或者,好吧,他们真的没有选择。他们需要一个JIT编译器,因为javascript存在。

怎么可以解决?通过使用APR当然。某些使用者(iOS上的Safari,MacOS上的每个应用程序)都可以通过可以在RW-和Rx.behind在场景之间快速切换的特殊内存区域(使用MAP_JIT和PTHread_JIT_WRITE_PROTECT_NP 1),此开关翻转两位内部APRR注册到X代替JIT页面的X代替W,请立即将所有这些页面从RW到Rx更改,反之亦然。

如前所述,Apple希望在可能的情况下强制执行所有可执行文件的代码。在iOS上,Chesesignatures必须来自Apple本身,而在可以在本地创建的麦斯科座上的签名上是足够的。码签名通常由内核强制执行。内核也有很多不相关的代码,虽然像devicedrivers一样,为一个巨大的攻击表面制作。任何驱动程序中的任何错误都足以绕过代码签名(这是不确定的,因为您可能需要一个Infoleak,然后才能rop写入旁观物品)。很久以前,这个问题已经通过视频游戏控制台解决了:微软的Xbox 360虚拟机管理程序是一个小型的代码,即仅强制强制执行代码签名和同样重要的任务。所有内核代码中没有确保在任何内核代码中存在任何剥削性比值,而是确保虚拟机程序本身中没有任何严重的错误。在该虚拟机管理程序中发现了一个严重的错误。

同样,Apple使用APRR在内核本身内有效地创建一个非常低的高度管理程序。首先,将分页(以及具有重要数据结构的其他方法)被重新映射为kernel本身的只读。此外,还映射了一个名为PPL的专业代码的小部分也被映射为只读。然后,小蹦床函数使用APRR将Pagetables作为RW-和PPL码作为R-X重新映射为R-X。由于这个小蹦床是PPL代码ITBehaves就像超级手册的唯一入口点,而PPL本身就像一个非常低于开销的虚拟机管理程序。

有关此信息的更多详细信息,请在乔纳森的Casa de P(a)p(e)lwrite上找到。

如前所述,Apple Silicon上的JITS可以分配一个特殊区域,其权限可以快速切换,RW-和R-X。在以前的SoC中,这是使用APR强制执行的,并且应该提供一个良好的起点,使其成为SPRR。

Apple对_pthread_jit_write_protect_np函数的正常编译器Leads的官方文档仍然在M1.let的首次使用Otool -XV /usr/lib/system/libsystem_pthread.dylib上执行此开关,以弄清楚幕后的内容。相关说明从这个功能

_pthread_jit_write_protect_np:[...] 00000000000000000000007fe0 movk x0,#0xffff,lsl#0xf,lsl#32000000000000007fe8 movk x0,#0x0,lsl#48000000000000007fec ldr x0,[x0];延迟:4000000000000007FF0 MSR S3_6_C15_C1_5,X0000000000000007FF4 ISB [...]

从常量地址0xFFFFFC118加载64位整数,然后将其写入将其注册为S3_6_C15_C1_5的系统。有类似的代码,而是从0xFFFFFC110加载新的系统寄存器值。这些地址属于称为常规的区域。此页面映射到每个userlandProcess中,并包含内核暴露的各种变量到Userland。

不出所料,从开源XNucode缺少在句子中设置这些变量的代码。但是,在XNUCODE中留下的前一代APRR码使用的CP_APRR_SHADOW_JIT_RW有引用。

#include< stdio.h> #include< stdint.h> int main(int argc,char * argv []){uint64_t * sprr =(uint64_t *)0xfffffc110; printf("%llx%llx \ n&#34 ;, sprr [0],sprr [1]);}

产生值0x2010000030300000和0x2010000030100000,其在r-x和rw-权限之间切换了JIT页面。到现在为止还挺好。这类似于APRR如何工作,但这些是不同的寄存器,并且它们包含我们必须脱斯蒂格的奇怪数字。

有了关于SPRR的这一粗略的想法,我们现在可以拆卸内核并查找使用这些ORNEarby寄存器的功能。我不太喜欢尽可能地盯着拆卸。 (但是,我再次享受级别的硬件逆向工程,所以也许你不应该相互信任我)。 TheKernel也不太可能导致我们的个人位的含义:寄存器可能只是用魔法常量初始化一次,从来没有再次触摸。

幸运的是还有另一种选择:未经教育的猜测!尝试在我们发现的登记册中翻转位,看看它是如何表现的。我们甚至可以从M1上的常规用户节目中开始。

我们可以做的第一件事是尝试并将每一位设置为0和1:

#include< stdbool.h> #include< stdint.h> #include< stdio.h> #include< string.h> void write_sppr(uint64_t v){__asm__ __volatile __(" msr s3_6_c15_c1_5,%0 \ n"" ISB sy \ n" ::" R"(v) :);} uint64_t read_sppr(void){uint64_t v; __asm__ __volatile __(" ISB sy \ n"" Mrs%0,s3_6_c15_c1_5 \ n":" = R"(v)::"记忆和#34;);返回v; in int main(int argc,char * argv []){for(int i = 0; i< 64; ++ i){write_sppr(1ull< i); printf("位%02d:%016llx \ n",i,read_sppr()); }}

我们很快观察到几乎所有位都被锁定到它们的初始值,除了我们在句柄中找到的thetwo值中的两个不同。我们还知道这些与JIT页面权限有关。我们可以使用MMAP映射SuchPages。从读取或写入读取或写作保护的页面生成一个sigbus。跳转到anon-executable页面导致sigsev。我们可以通过设置信号处理程序来捕获Usilland应用程序中的信号。这些工具AREALL我们需要了解这些位如何映射到页面权限!

要从访问受保护的页面恢复,我们设置了以下信号处理程序,该命令将X0设置为MagicConstant,然后在返回之前将程序计数器递增:

void bus_handler(int signo,siginfo_t * info,void * cx _){ucontext_t * cx = cx_; cx - > uc_mcontext - > __ ss .__ x [0] = 0xdeadbeef; cx - > uc_mcontext - > __ ss .__ pc + = 4;}

从执行非可执行页面恢复类似:将程序计数器设置为链接寄存器以返回到Callee并存储X0中的魔法值:

void sev_handler(int signo,siginfo_t * info,void * cx _){ucontext_t * cx = cx_; cx - > uc_mcontext - > __ ss .__ x [0] = 0xdeadbeef; cx - > uc_mcontext - > __ ss .__ pc = cx - > uc_mcontext - > __ ss .__ lr;}

剩余的所有这些都是用map_jit映射一个页面,并尝试阅读,写入或执行系统寄存器中所有四种遗传值的内存。

#define _xopen_source #include< signal.h> #include< stdbool.h> #include< stdint.h> #include< stdio.h> #include< string.h> #include< sys / mman.h> #include< sys / utsname.h> #include< ucontext.h>静态void sev_handler(int signo,siginfo_t * info,void * cx _){(void)signo; (void)信息; ucontext_t * cx = cx_; cx - > uc_mcontext - > __ ss .__ x [0] = 0xdeadbeef; cx - > uc_mcontext - > __ ss .__ pc = cx - > uc_mcontext - > __ ss .__ lr;静态void bus_handler(int signo,siginfo_t * info,void * cx _){(void)signo; (void)信息; ucontext_t * cx = cx_; cx - > uc_mcontext - > __ ss .__ x [0] = 0xdeadbeef; cx - > uc_mcontext - > __ ss .__ pc + = 4;}静态void write_sprr_perm(uint64_t v)(uint64_t v){__asm__ __volatile __(" msr s3_6_c15_c1_5,%0 \ n"" isb sy \ n " ::" R"(v):);静态UINT64_T READ_SPRR_PERM(void){UINT64_T v; __asm__ __volatile __(" ISB sy \ n"" Mrs%0,s3_6_c15_c1_5 \ n":" = R"(v)::"记忆和#34;);返回v;静态BOOL CAN_READ(void * PTR){UINT64_T v = 0; __asm__ __volatile __(" ldr x0,[%0] \ n"" mov%0,x0 \ n":" = r"(v):& #34; R"(PTR):"记忆&#34 ;," x0"); if(v == 0xdeadbeef)返回false;返回true;静态bool can_write(void * ptr){uint64_t v = 0; __asm__ __volatile __(" str x0,[%0] \ n"" mov%0,x0 \ n":" = r"(v):& #34; R"(PTR + 8):"记忆&#34 ;," x0"); if(v == 0xdeadbeef)返回false;返回true;静态bool can_exec(void * ptr){uint64_t(* fun_ptr)(uint64_t)= ptr; UINT64_T RES = FUN_PTR(0); if(res == 0xdeadbeef)返回false;返回true;静态void sprr_test(void * ptr,uint64_t v){uint64_t a,b; a = read_sprr_perm(); write_sprr_perm(v); b = read_sprr_perm(); printf("%llx:%c%c%c \ n",b,can_read(ptr)?' r':' - ',can_write( ptr)?' w':' - ',can_exec(ptr)?' x':' - ');静态uint64_t make_sprr_val(uint8_t nibble){uint64_tres = 0; for(int i = 0; i< 16; ++ i)res | =((Uint64_t)啃)< (4 * i);返回res;} int main(int argc,char * argv []){(void)argc; (空虚)argv; struct sigaction sa; sigfillet(& sa.sa_mask); sa.sa_sigaction = bus_handler; sa.sa_flags = sa_restart | sa_siginfo; sigaction(sigbus,& sa,0); sa.sa_sigaction = sev_handler; sigaction(sigsegv,& sa,0); UINT32_T * PTR = MMAP(null,0x4000,prot_read | prot_write | prot_exec,map_private | map_anonymous | map_jit, - 1,0); write_sprr_perm(0x3333333333333333); PTR [0] = 0xD65F03C0; // ret for(int i = 0; i< 4; ++ i)sprr_test(ptr,make_sprr_val(i));}

这比APR工作的工作要简单得多:而不是使用两个寄存器来第一次设置权限,然后屏蔽其他寄存器,我们现在只能将它们更改为这四个值中的一个。 APR还允许一个聪明的黑客在用户空间中创建RWX映射,这些映射不再可能没有办法编码这一点。据推测,系统寄存器中的不同字节对应于页面表条目中编码的16个不同的可能权限。这留下了一半的系统寄存器符合人物未知的含义!

我们现在可能弄清楚我们现在可以从MacOS用户悬浮镜的所有东西,并且是时候带出一些重大的工具,了解这个新的硬件功能如何工作。

我希望我能刚刚使用Apple的HyperVisor.framework来在El1中运行我的代码,并调查如何从那里开始Sprbehaves。但是,遗憾的是,每次对寄存器的访问可能与SPRR都有相关。那好吧。幸运的是,我们可以使用更强大的工具,以便在EL2上运行“裸金属”代替。

以前iPhone黑客必须静态逆向工程师XNU或利用他们的方式到EL1然后运行他们的实验以了解新硬件。这使他们所有的成就都更加令人印象深刻。然而,这几天,我的水我们更加简单:Apple已发布M1,其中许多新的硬件添加,并且还在引导过程中非常早期运行无符号代码。

作为Asahi Linux项目的一部分,旨在为M1引入上游Linux支持,Marcan LED的LED开发了一个名为M1N1.M1N1的小引导加载程序/硬件实验平台,同时XNU通常与剩余的所有硬件相同原始状态。虽然所有义的工作也可以通过手动编写Shellcode在El2 M1N1中运行的虽然实际上使得这种乐趣(如果yourust我的乐趣定义)。

M1N1的最佳方面是,我们可以直接从Python shell中操作硬件,而不是重新编译和重新加载shellcode和处理数据提取以及所有这些令人讨厌的小细节。 Marcan已激起我的USB小工具代码,以便您需要重复这些实验的所有实验是M1 Mac和正常的载物。

让我们首先运行proxyclient / shell.py.unnityInce访问userland SPRR寄存器只是触发异常。 (但请注意,M1N1如何在EL2中快速恢复此异常。之后无需重新启动!)

>>> u .mrs((3,6,15,15,5))tty>例外:Synctty> el2htty&gt的例外。在el2tty&gt跑步; MPIDR:0x80000000tty>寄存器:( @ 0x8046b3db0)tty>宝x4-x7:000000000000007a69 0000000 804630000000 804630004 0000000404630004 0000000404630000000 804630000> X8 -X11:0000000000FFFFFFC8 0000000 8046B3EB0 00000000000000002CTTY> X12 -x15:0000000000000003 0000000000000001 0000000000000000 0000000 8046b3b20TTY> X16 -x19:0000000 0000000000000000 8045caa80 0000000000000000 0000000 80462b000TTY> X20 -x23:0000000 0000000 8046b3f78 8046b3fa0 0000000000000002 0000000 8046b3f98TTY> X24 -X27:00000000000000000001 000000000000000000000000000000000000000000000000000000000000000000000000/30100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 X28 -X30:0000000 8046B3FA0 0000000 8046B3EB0 0000000 8045BAD90tty> PC:0x810CB8000(rel:0xc70c000)tty> SP:0x8046b3eb0tty> spsr_el1:0x60000009tty> far_el1:0x0tty> ESR_EL1:0x2000000(未知)TTY> l2c_err_sts:0x11000ffcc00000000tty> l2c_err_adr:0x0tty> l2c_err_inf:0x0tty> sys_apl_e_lsu_err_sts:0x0tty> sys_apl_e_fed_err_sts:0x0tty> sys_apl_e_mmu_err_sts:0x0tty>从异常(ELR = 0x810CB8004)恢复)回溯(最近呼叫最后):文件" /opt/homebrew/cellar/[email protected]/3.9.4/frameworks/python.framework/versions/3.9/lib/python3。 9 / code.py",第90行,在runco​​de exec(代码,自我.locals)文件"<控制台>",第1行,在<模块> file" /users/speter/asahi/git/m1n1/proxyclient/utils.py" ;,行80,在MRS中升级ProxyError("异常发生)")代理.proxyError:异常发生异常&gt ;>>

内核必须能够在上下文交换机期间修改此寄存器。这可能意味着有一些启用位.Luckily已经存在M1N1存储库中的一个Python工具,其中批准了所有可用的SystemRegisters。在内部它只会为所有寄存器引起的异常生成所有的MRS指令。我们刚刚运行它并寻找附近的任何寄存器:

$ python3 proxyclient / find_all_regs.py | grep s3_6_c15_c1__s3_6_c15_c1_0(3,6,15,1,0)= 0x0s3_6_c15_c1_2(3,6,15,1,2)= 0x0s3_6_c15_c1_4(3,6,15,1,4)= 0x0

这给了我们三个候选人。将0x1写入第一个似乎停止M1N1工作。现在这应该对我有愚蠢的:M1N1运行

......