Tracing Back to the Source | SPTM Round 3
February 12, 2025
First a Quick Recap:
If you haven't read our previous blog post and the one that started our exploration into the SPTM images, now's a good time to do so. Let's summarize what we've ascertained thus far:
XNU is being refactored into a micro-kernel inspired architecture, aiming to reduce its code base, and move security sensitive operations out of it.
The memory space isolation is performed with the help of a Secure Page Table Monitor - SPTM.
The code signing, entitlement verification, Developer Mode, Restricted Execution Mode, and other security sensitive operations are handled by the Trusted eXectuion Monitor - TXM
Apple's XNU 10063 sources (from iOS 17.4) and the newly released 11215.1.10 (of iOS 18.0) are more revealing than their predecessors. Whether intentionally or by mistake, previously redacted sources are now (almost) fully visible, and reveal many more details about TXM, SPTM, and the new Exclaves infrastructure introduced in 17.5 on the iPad M4.
When we wrote our first blog post, XNU included bsd/kern/code_signing/txm.c at a measly 1,605 bytes - all #include
directives and no source. Later sources reveal that the body of the file - another 40K(!) was enclosed in an #if CONFIG_SPTM
directive, which Apple appeared to have purposely redacted. The XNU cross reference shows the number of mentions of SPTM went up from around 0 in 10002 to over a thousand. Definitely much to dig into.
We'll detail our findings on ExclaveCore in a separate post (soon!), but wanted to share our understanding of the new source findings, especially as they complement our previous two posts, which remain (at present) the only public discussion of either SPTM or TXM.
SPTM, revisited
A key header, <sptm/sptm_xnu.h> is missing. This contains the SPTM library functions, and the file that we did find - <sptm/sptm.h> is just for wrappers over it.
The SPTM entry point
We can easily find the entry point using disarm(j)
's gadget finder - the use of GENTER
ensures there's no chance of a false positive here:
$ disarm -g PACIBSP,STP,MOVr,BL,MOVr,LDP,GENTER kernel.18.0-iPhone17,1
fffffff00a924c98(0x28d4c98): PACIBSP ;
fffffff00a924c9c(0x28d4c9c): STP X29, X30, [X31, #-16]! ; SP -= 16; *[SP] = [X29, X30]
fffffff00a924ca0(0x28d4ca0): MOVr FP, SP ; FP = SP
fffffff00a924ca4(0x28d4ca4): BL 0xfffffff00805539c ; auditing_or_other_hook
fffffff00a924ca8(0x28d4ca8): MOVr SP, FP ; SP = FP
fffffff00a924cac(0x28d4cac): LDP X29, X30, [X31], #16 ; [X29, X0] = *[X31]; X31 += 2
fffffff00a924cb0(0x28d4cb0): GENTER #0 ;
With that located, another application of disarm(j)
with grep(1)
reveals all the call outs:
$ disarm kernel.18.0-iPhone17,1| grep -B5 fffffff00a924c98
Opened companion file kernel.18.0-iPhone17,1.ARM64.2C09F61F-8E85-33FA-AFF2-01FEC3E3144E
_func_0xfffffff008814eb4:
fffffff008814eb4 f2e00010 MOVK X16, #0, LSL #48 ; X16 := 0x0
fffffff008814eb8 f2c00010 MOVK X16, #0, LSL #32 ; X16 := 0x0
fffffff008814ebc f2a00010 MOVK X16, #0, LSL #16 ; X16 := 0x0
fffffff008814ec0 f2800010 MOVK X16, #0 ; X16 := 0x0
fffffff008814ec4 14843f75 B 0xfffffff00a924c98 ;
_func_0xfffffff00a924c98(0);
_func_0xfffffff008814ec8:
fffffff008814ec8 f2e00010 MOVK X16, #0, LSL #48 ; X16 := 0x0
fffffff008814ecc f2c00010 MOVK X16, #0, LSL #32 ; X16 := 0x0
fffffff008814ed0 f2a00010 MOVK X16, #0, LSL #16 ; X16 := 0x0
fffffff008814ed4 f2800030 MOVK X16, #1 ; X16 := 0x1
fffffff008814ed8 14843f70 B 0xfffffff00a924c98
_func_0xfffffff00a924c98(ARG0,ARG1,ARG2,ARG3,ARG4);
...
...
_func_0xfffffff00881542c:
fffffff00881542c f2e00010 MOVK X16, #0, LSL #48 ; X16 := 0x0
fffffff008815430 f2c00130 MOVK X16, #9, LSL #32 ; X16 := 0x900000000
fffffff008815434 f2a00010 MOVK X16, #0, LSL #16 ; X16 := 0x900000000
fffffff008815438 f2800090 MOVK X16, #4 ; X16 := 0x900000004
fffffff00881543c 14843e17 B 0xfffffff00a924c98
_func_0xfffffff00a924c98(ARG0,ARG1,ARG2,ARG3,ARG4);
The callouts are (in this order) 0-21,31
, 0x600000000-0x600000007
, 0x300000000-0x300000010
0x500000000-0x500000002
, 0x700000000-0x70000000c
, and 0x900000001-0x900000004
.
Looking through the __LINKINFO.__symbolsets
section reveals a couple of additional clues:
$ disarm -e __LINKINFO.__symbolsets kernel.18.0-iPhone17,1 Extracted __LINKINFO.__symbolsets (0x3e28000-0x3e6d532) to kernel.18.0-iPhone17,1.__LINKINFO.__symbolsets $ jlutil kernel.18.0-iPhone17,1.__LINKINFO.__symbolsets | grep sptm SymbolName: _sptm_cputrace_get_carveout_addr SymbolName: _sptm_cputrace_get_carveout_size SymbolName: _sptm_cputrace_set_base SymbolName: _sptm_cputrace_start SymbolName: _sptm_cputrace_stop
and these are presumably from some other subsystem, likely subsystem 9.
0xfffffff0088147b0
is_sptm_get_paddr_type
.
The startup code - from start_cold
- can now be easily found in the assembly, since it's in a .s file to begin with:
_start_cold: fffffff00a920038 f0feb989 ADRP X9, #-10445 ; X9 = 0xfffffff008053000- fffffff00a92003c 91000129 ADD X9, X9, #0 ; (0xfffffff008053000) 'ExceptionVectorsBase' fffffff00a920040 d518c009 MSR VBAR_EL1, X9 ; fffffff00a920044 d5033fdf ISB ; /* Set up exception stack */ fffffff00a920048 d50041bf MSR SPSel, #1 ; fffffff00a92004c 90001c8a ADRP X10, #912 ; X10 = 0xfffffff00acb0000- fffffff00a920050 9100014a ADD X10, X10, #0 ; (0xfffffff00acb0000) -- X10 = X10 + 0x0 = 0xfffffff00acb0000 -- ! fffffff00a920054 9100015f MOVr SP, X10 ; SP = X10 (excepstack_top) /* Set up IRQ stack */ fffffff00a920058 d50040bf MSR SPSel, #0 ; fffffff00a92005c 90001c4a ADRP X10, #904 ; X10 = 0xfffffff00aca8000- fffffff00a920060 9100014a ADD X10, X10, #0 ; (0xfffffff00aca8000) -- X10 = X10 + 0x0 = 0xfffffff00aca8000 -- ! fffffff00a920064 9100015f MOVr SP, X10 ; SP = X10 (intstack_top) /* Save off boot arguments */ fffffff00a920068 aa0103fa MOVr X26, X1 ; X26 = X1 (0x0) fffffff00a92006c aa0203fb MOVr X27, X2 ; X27 = X2 (0x0) arm_slide_rebase_and_sign_image(ARG0,ARG1,ARG2); fffffff00a920070 94000fe8 BL 0xfffffff00a924010 ; /** * Call into the SPTM for the first time. This function traps to GL2 to * signal the SPTM that the fixups phase has been completed. */ SPTM_LOAD_DISPATCH_ID SPTM_DOMAIN, SPTM_DISPATCH_TABLE_XNU_BOOTSTRAP, SPTM_FUNCTIONID_FIXUPS_COMPLETE SPTM_DOMAIN_ENTER x16 fffffff00a920074 f2e00010 MOVK X16, #0, LSL #48 ; X16 := 0x0 fffffff00a920078 f2c00010 MOVK X16, #0, LSL #32 ; X16 := 0x0 fffffff00a92007c f2a00010 MOVK X16, #0, LSL #16 ; X16 := 0x0 fffffff00a920080 f28001f0 MOVK X16, #15 ; X16 := 0xf fffffff00a920084 00201420 GENTER #0 ; /** * At this point, the SPTM has retyped the RX region to SPTM_XNU_CODE. */ /* Jump to handler */ fffffff00a920088 aa1a03e0 MOVr X0, X26 ; X0 = X26 (0x0) fffffff00a92008c aa1b03e1 MOVr X1, X27 ; X1 = X27 (0x0) fffffff00a920090 17634c41 B 0xfffffff0081f3194 ; _arm_init _arm_init(ARG1,ARG2); _start_warm: fffffff00a920094 aa0303f4 MOVr X20, X3 ; X20 = X3 (0x0) fffffff00a920098 d53800af MRS X15, MPIDR_EL1 ; >not yet..< fffffff00a92009c 92403de0 AND X0, X15, #0xffff ; fffffff00a9200a0 90001621 ADRP X1, #708 ; X1 = 0xfffffff00abe4000- fffffff00a9200a4 912cc021 ADD X1, X1, #2864 ; (0xfffffff00abe4000) -- X1 = X1 + 0xb30 = 0xfffffff00abe4b30 -- ! ..
About Those SPTM callouts..
The sources give a lot more information about the XNU ←→ interaction, in disclosing some interesting sptm_…
function names, but the implementations remain redacted. Nonetheless, it's easy to piece together now a more comprehensive list of callouts (building on what we provided in the previous blogpost:
_func_0xfffffff007ff3210
- a.k.a.machine_lockdown
calls_func_0xfffffff0085fb1d4
which callssptm_enter
with code 0 - that's_sptm_lockdown_xnu
.The above also calls
arm_vm_prot_finalize
(0xfffffff007fe9238
), which then callssptm_slide_region(…)
multiple times. We therefore deduce0xfffffff0085fb300
- which callssptm_enter
with code #20 - issptm_slide_region
.Right after all these calls are three calls to
_func_0xfffffff007ff55bc
, which is_ml_static_mfree
. The first of these calls gives usSPTMArgs->phys_slide_[papt/size]
, which - even though we don't have the args - fall in two adjacent fields at0xfffffff007a0cd60 + 32
Similary we can figure out
sptm_retype
is code #1 (called through0xfffffff0085fb1e8
). This associates one of several predefined types with physical pages, so as to restrict access to the page on type mismatch.
SPTM args
SPTM uses an argument structure which is passed along with the boot_args
structure from early initialization.
Xn👀p> slide 0xfffffff007a0cd60 0xfffffff007a0cd60 -> 0xfffffff039450d60 Xn👀p> dump 0xfffffff039450d60,8 Dumping 8 bytes from 0xfffffff039450d60 KERNEL 0xfffffff039450d60 0xfffffff0393bdcc0 Xn👀p> sym _sptm_args 0xfffffff0393bdcc0 Xn👀p> dump _sptm_args,256 KERNEL device tree 0xfffffff0393bdcc0 0xfffffff03e930000 0xfffffff018a04000 Unknown 0xfffffff0393bdcd0 0xfffffff131f44000 00 40 d8 09 08 00 00 00 .@.1.....@...... Unknown 0xfffffff0393bdce0 0xfffffff12a2b8000 00 c0 39 03 00 00 00 00 ..+*......9..... Unknown 0xfffffff0393bdcf0 0xfffffff018abcb58 08 00 00 00 00 00 00 00 X............... KERNEL KERNEL 0xfffffff0393bdd00 0xfffffff03eb3c000 0xfffffff03ec0c000 Unknown KERNEL 0xfffffff0393bdd10 0xfffffff038a48000 0xfffffff03c158000 Unknown 0xfffffff0393bdd20 0xfffffff018ac9000 00 08 00 00 72 61 6e 64 ............rand 0xfffffff0393bdd30 73 65 65 64 00 00 00 00 00 00 00 00 00 00 00 00 seed............ 0xfffffff0393bdd40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffffff0393bdd50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffffff0393bdd60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffffff0393bdd70 00 00 00 00 00 00 00 00 48 00 00 00 00 00 00 00 ........H....... Unknown 0xfffffff0393bdd80 00 00 00 00 00 00 00 00 0xfffffff018b0c020 Unknown libsptm_state starts here 0xfffffff0393bdd90 07 00 00 00 00 00 00 00 0xfffffff018b14878 Unknown gPhysBase 0xfffffff0393bdda0 0xfffffff018b14880 00 00 39 02 08 00 00 00 .H........9..... Unknown 0xfffffff0393bddb0 00 80 34 f5 08 00 00 00 0xfffffff018a04000
We can find const_sptm_args.libsptm_state
(and libsptm_init
) from arm_init
:
... fffffff007fe6e44 3d808120 STRi Q0, [X9, #512] ; *0xfffffff007979200 = 0x0 fffffff007fe6e48 52800000 MOVZ W0, #0 ; X0 = 0x0 fffffff007fe6e4c aa1303e1 MOVr X1, X19 ; X1 = X19 (0xfffffff007979840) ; _args _PE_init_platform(0,_args); fffffff007fe6e50 9417b327 BL 0xfffffff0085d3aec ; fffffff007fe6e54 91034280 ADD X0, X20, #208 ; X0 = 0xfffffff007979d90 &const_sptm_args.libsptm_state libsptm_init(0xfffffff007979d90); fffffff007fe6e58 94184de3 BL 0xfffffff0085fa5e4 ; libsptm_init _wfe_timeout_configure(0); fffffff007fe6e5c 94003b13 BL 0xfffffff007ff5aa8 ; fffffff007fe6e60 f0ffcda8 ADRP X8, #-1609 ; X8 = 0xfffffff00799d000- fffffff007fe6e64 b941b908 LDRi W8, [X8, #440] ; ; X8 = *(0xfffffff00799d1b8) (8) = ??? fffffff007fe6e68 7101011f CMPi W8, #64 ; fffffff007fe6e6c 540032e2 B.CS 0xfffffff007fe74c8 ; fffffff007fe6e70 d538e109 MRS X9, CNTKCTL_EL1 ; fffffff007fe6e74 aa081128 ORR X8, X9, X8, LSL #4 ; X0 = X9 | 0x4 fffffff007fe6e78 b2400d08 _ORR X8, X8, #0xf ; X0 = X8 | 0xf fffffff007fe6e7c d518e108 MSR CNTKCTL_EL1, X8 ; fffffff007fe6e80 d0ffd134 ADRP X20, #-1498 ; X20 = 0xfffffff007a0c000- fffffff007fe6e84 f9470e88 LDRi X8, [X20, #3608] ; ; X8 = *(0xfffffff007a0ce18) (20) = ??? fffffff007fe6e88 9101b100 ADD X0, X8, #108 ; (0xfffffff00799d00f) -- X0 = X8 + 0x6c = 0xfffffff00799d06c -- ! fffffff007fe6e8c d0ff8361 ADRP X1, #-3986 ; X1 = 0xfffffff007054000- fffffff007fe6e90 9108f821 ADD X1, X1, #574 ; (0xfffffff007054000) -- X1 = X1 + 0x23e ='mdsb' fffffff007fe6e94 f0ffcf73 ADRP X19, #-1553 ; X19 = 0xfffffff0079d5000- fffffff007fe6e98 91146273 ADD X19, X19, #1304 ; (0xfffffff0079d5000) -- X19 = X19 + 0x518 = 0xfffffff0079d5518 -- ! fffffff007fe6e9c aa1303e2 MOVr X2, X19 ; X2 = X19 (0xfffffff0079d5518) fffffff007fe6ea0 52800083 MOVZ W3, #4 ; X3 = 0x4 fffffff007fe6ea4 52800004 MOVZ W4, #0 ; X4 = 0x0 _PE_parse_boot_argn_internal(0xfffffff00799d06c,"mdsb",0xfffffff0079d5518,0x4,0);
And, now that we have the state:
Xn👀p> slide 0xfffffff007979d90 0xfffffff007979d90 -> 0xfffffff0393bdd90 Xn👀p> dump 0xfffffff0393bdd90,256 Dumping 256 bytes from 0xfffffff0393bdd90 n_papt_ranges 0xfffffff0393bdd90 07 00 00 00 00 00 00 00 0xfffffff018b14878 papt_ranges sptm_phys_begin 0xfffffff0393bdda0 0xfffffff018b14880 00 00 39 02 08 00 00 00 .H........9..... sptm_phys_end __papt_of_phys_begin 0xfffffff0393bddb0 00 80 34 f5 08 00 00 00 0xfffffff018a04000 __papt_of_phys_end root_table_paddr 0xfffffff0393bddc0 0xfffffff12e020000 00 40 a9 08 08 00 00 00 .........@...... KERNEL Unknown 0xfffffff0393bddd0 0xfffffff03e564000 0xfffffff018a5d9a0 0xfffffff0393bdde0 00 00 00 00 00 00 00 00 47 00 00 00 00 00 00 00 ........G....... Unknown 0xfffffff0393bddf0 0xfffffff018a80000 00 00 00 00 00 00 00 00 ................ Unknown 0xfffffff0393bde00 00 00 00 00 00 00 00 00 0xfffffff018b0c01c Unknown 0xfffffff0393bde10 00 00 00 00 00 00 00 00 0xfffffff018abcad0 Unknown 0xfffffff0393bde20 08 00 00 00 00 00 00 00 0xfffffff018abcb10 0xfffffff0393bde30 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffffff0393bde40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ Xn👀p> dump 0xfffffff018b14878,8 Dumping 8 bytes from 0xfffffff018b14878 0xfffffff018b14878 0f 00 00 00 00 00 00 00 -- libsptm_n_papt_ranges Dumping 384 bytes from 0xfffffff018b14880 papt_table[0].paddr_start KERNEL 0xfffffff018b14880 00 00 09 09 08 00 00 00 0xfffffff03e000000 papt_table[1].paddr_start 0xfffffff018b14890 ae b0 03 00 00 00 00 00 00 00 39 02 08 00 00 00 ..........9..... Unknown 0xfffffff018b148a0 0xfffffff12a2b8000 e7 0c 00 00 00 00 00 00 ..+*............ KERNEL 0xfffffff018b148b0 00 c0 78 05 08 00 00 00 0xfffffff0398ac000 0xfffffff018b148c0 05 09 00 00 00 00 00 00 00 00 ba 07 08 00 00 00 ................ Unknown 0xfffffff018b148d0 0xfffffff038a48000 99 03 00 00 00 00 00 00 ...8............ Unknown 0xfffffff018b148e0 00 00 b5 08 08 00 00 00 0xfffffff12e150000 0xfffffff018b148f0 7d 00 00 00 00 00 00 00 00 40 a0 08 08 00 00 00 }........@...... Unknown 0xfffffff018b14900 0xfffffff018a04000 51 00 00 00 00 00 00 00 .@......Q....... KERNEL 0xfffffff018b14910 00 80 eb 08 08 00 00 00 0xfffffff03c028000 0xfffffff018b14920 4c 00 00 00 00 00 00 00 00 40 d8 08 08 00 00 00 L........@...... KERNEL 0xfffffff018b14930 0xfffffff03bef4000 2c 00 00 00 00 00 00 00 .@.;....,....... Unknown 0xfffffff018b14940 00 80 fe 08 08 00 00 00 0xfffffff12e0a8000 0xfffffff018b14950 2a 00 00 00 00 00 00 00 00 40 e3 08 08 00 00 00 *........@...... Unknown 0xfffffff018b14960 0xfffffff12e024000 21 00 00 00 00 00 00 00 .@......!....... Unknown 0xfffffff018b14970 00 c0 72 05 08 00 00 00 0xfffffff028a48000 0xfffffff018b14980 17 00 00 00 00 00 00 00 00 40 d4 08 08 00 00 00 .........@...... KERNEL 0xfffffff018b14990 0xfffffff03beb4000 0f 00 00 00 00 00 00 00 .@.;............ Unknown 0xfffffff018b149a0 00 80 b4 08 08 00 00 00 0xfffffff028aa4000 0xfffffff018b149b0 02 00 00 00 00 00 00 00 00 00 d8 08 08 00 00 00 ................ Unknown 0xfffffff018b149c0 0xfffffff12e020000 01 00 00 00 00 00 00 00 ................ KERNEL 0xfffffff018b149d0 00 80 78 05 08 00 00 00 0xfffffff03c200000 0xfffffff018b149e0 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffffff018b149f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
TXM
We've already established that TXM, like SPTM, can be entered from kernel. The newly visible <bsd/sys/trusted_execution_monitor.h> shows us:
typedef struct _txm_call {
/* Input arguments */
TXMKernelSelector_t selector; // 0x0 (SP+32)
TXMReturnCode_t failure_code_silent; // 0x4
bool failure_fatal; // 0x8
bool failure_silent; // 0x9
bool skip_logs; // 0xa
/* implcit padding of one byte to align the uint32_ts */
uint32_t num_input_args; // 0x0c
uint32_t num_output_args; // 0x10 (SP +48)
/* Output arguments */
TXMReturn_t txm_ret;
uint64_t num_return_words;
uint64_t return_words[kTXMStackReturnWords];
} txm_call_t;
/**
* The main function to use for calling into the TrustedExecutionMonitor. This
* function handles all the bits required, including allocation/deallocation of
* the thread stack pages, the CPU instructions required to reach TXM, and also
* going through the TXM buffer and capturing any logs left by the monitor.
*/
kern_return_t
txm_kernel_call(
txm_call_t *parameters, ...);
The fields of the txm_call_t
can be further explained as follows:
selector
: is a number indicating the call type. This is very similar to the selector notion in IOKit. Since multiple calls are multiplexed over a single entry point.failure_fatal
: Indicating a failure should result in a call topanic()
.failure_silent
: Indicating a failure doesn't merit anos_log
operation.num_[input/output]_args
: Similar toIOConnectCall
s, how many arguments the call either expects or returnstxm_ret
: The standard return value from a call.return_words
: Indicating the number and addresses of extra out parameters from this call.
TXM also uses special thread stacks. Every thread_t
can now be associated with a TXM thread stack (thread_[dis]associate_txm_thread_stack()
in osfmk/kern/thread.c). This is done by [acquire/release]_thread_stack()
, which are inlined and called during txm_kernel_call()
.
TXM_kernel_CALL
It's easy to match txm_kernel_call
with two following disarm(j)
rules:
0|received fatal error for a selector from TXM|_txm_kernel_call|_panic|bsd/kern/code_signing/txm.c
0|received excessive return words from TXM|_txm_kernel_call_internal|_panic|bsd/kern/code_signing/txm.c
Which in our kernel yielded 0xfffffff0083577b0
. Symbolication ends up leading to numerous functions in the disassembly of the general form:
_func_0xfffffff00835646c: fffffff00835646c d503237f PACIBSP ; fffffff008356470 d10203ff SUBi SP, SP, #128 ; SP = SP - 0x80 fffffff008356474 a9077bfd STP X29, X30, [X31, #112] ; *[SP +112] = [X29, X30] fffffff008356478 9101c3fd MOVr X29, X31 ; FP = XZR (0x0).. fffffff00835647c 6f00e400 MOVI.2D V0, #0000000000000000 ; fffffff008356480 f90033ff STRi XZR, [SP, #96] ; *0x60 = 0x0 fffffff008356484 ad0203e0 STP Q0, Q0, [SP, #64] ; *[SP +32] = [X0, X0] fffffff008356488 ad0103e0 STP Q0, Q0, [SP, #32] ; *[SP +16] = [X0, X0] fffffff00835648c 3d8007e0 STRi Q0, [SP, #16] ; *0x10 = 0x0 .... .... initialization of the structure fields which are being used: .... fffffff008356490 528003a8 MOVZ W8, #29 ; X8 = 0x1d fffffff008356494 b90013e8 STRi X8, [SP, #16] ; *0x10 = 0x1d fffffff008356498 52800028 MOVZ W8, #1 ; X8 = 0x1 fffffff00835649c 39005be8 STRBi W8, [SP, #88] ; fffffff0083564a0 b9001fe8 STRi X8, [SP, #28] ; *0x1c = 0x1 fffffff0083564a4 f90003e8 STRi X8, [SP] ; *0x0 = 0x1 fffffff0083564a8 910043e0 MOVr X0, SP ; X0 = XZR (0x0).. ... ... The txm_kernel_call - as a vararg, all arguments are on the stack ... _txm_kernel_call(SP); fffffff0083564ac 940004c1 BL 0xfffffff0083577b0 ; ... fffffff0083564b0 a9477bfd LDP X29, X30, [X31, #112] ; [X29, X0] = *[X31] fffffff0083564b4 910203ff MOVr X31, X31 ; SP = XZR (0x0).. fffffff0083564b8 d65f0fff RETAB ;
With only the two highlighted rows - MOVZ
into X8
, followed by stores - being different in between them. These align nicely with the txm_kernel_call
wrappers in the sources, which use kTXMKernelSelector*
constants as "selectors" for whichever TXM function is to be invoked.
(Unless we're missing something) the kTXMKernelSelector*
constants are also in some redacted .h file. Thanks to this pattern, however, it's simple to reconstruct these - Starting with kTXMKernelSelectorGetSecureChannelAddr = 7
(from txm_secure_channel_shared_page()
). Likewise, kTXMKernelSelectorImage4Dispatch
is 42. Doing this for all constants we found in the sources, we end up with a largely reconstructed enumeration:
enum { kTXMKernelSelectorGetLogInfo = 2, kTXMKernelSelectorEnterLockdownMode = 5, kTXMKernelSelectorEnableRestrictedMode = 6, kTXMKernelSelectorGetSecureChannelAddr = 7, kTXMSelectorUpdateDeviceState = 8, kTXMSelectorCompleteSecurityBootMode = 9, kTXMKernelSelectorRegisterProvisioningProfile = 15, kTXMKernelSelectorTrustProvisioningProfile = 16, kTXMKernelSelectorUnregisterProvisioningProfile = 17, kTXMKernelSelectorAssociateProvisioningProfile = 18, kTXMKernelSelectorDisassociateProvisioningProfile = 19, kTXMKernelSelectorRegisterCodeSignature = 20, kTXMKernelSelectorUnregisterCodeSignature = 21, kTXMKernelSelectorValidateCodeSignature = 22, kTXMKernelSelectorReconstituteCodeSignature = 23, kTXMKernelSelectorSetLocalSigningPublicKey = 24, kTXMKernelSelectorGetLocalSigningPublicKey = 25, kTXMKernelSelectorAuthorizeLocalSigningCDHash = 26, kTXMKernelSelectorAuthorizeCompilationServiceCDHash = 27, kTXMKernelSelectorMatchCompilationServiceCDHash = 28, kTXMKernelSelectorDeveloperModeToggle = 29, kTXMKernelSelectorAcquireSigningIdentifier = 30, kTXMKernelSelectorAssociateKernelEntitlements = 31, kTXMKernelSelectorAccelerateEntitlements= 32, kTXMKernelSelectorRegisterAddressSpace =34, kTXMKernelSelectorUnregisterAddressSpace = 35, kTXMKernelSelectorAssociateCodeSignature = 36, kTXMKernelSelectorAllowJITRegion = 37, kTXMKernelSelectorAllowInvalidCode = 39, kTXMKernelSelectorResolveKernelEntitlementsAddressSpace = 41, kTXMKernelSelectorImage4Dispatch = 42, kTXMKernelSelectorImage4SetNonce = 46, kTXMKernelSelectorImage4RollNonce = 47, kTXMKernelSelectorImage4GetNonce = 48, };
From the TXM side, the entry point is 0xfffffff017023eac, which is registered under subsystem 2 early in TXM's initialization:
fffffff01701ba64 d503237f PACIBSP ; fffffff01701ba68 d100c3ff SUBi SP, SP, #48 ; SP = SP - 0x30 fffffff01701ba6c a9014ff4 STP X20, X19, [X31, #16] ; *[SP +16] = [X20, X19] fffffff01701ba70 a9027bfd STP X29, X30, [X31, #32] ; *[SP +32] = [X29, X30] fffffff01701ba74 910083fd MOVr X29, X31 ; FP = SP (0x0).. fffffff01701ba78 10fa5293 ADR X19, 0xfffffff0170104c8 fffffff01701ba7c d503201f NOP ; fffffff01701ba80 52800028 MOVZ W8, #1 ; X8 = 0x1 fffffff01701ba84 39000268 STRBi ; func_0xfffffff0170212f0(0); fffffff01701ba88 9400161a BL 0xfffffff0170212f0 ; fffffff01701ba8c 10042110 ADR X16, 0xfffffff017023eac ; fffffff01701ba90 d503201f NOP ; fffffff01701ba94 dac123f0 PACIZA X16 ; fffffff01701ba98 aa1003e1 MOVr X1, X16 ; X1 = X16 (0xfffffff017023eac) fffffff01701ba9c 52800000 MOVZ W0, #0 ; X0 = 0x0 fffffff01701baa0 52800042 MOVZ W2, #2 ; X2 = 0x2 registerSubsystem(0,0xfffffff017023eac,0x2); fffffff01701baa4 9400a8b0 BL 0xfffffff017045d64 ; fffffff01701baa8 100414d0 ADR X16, 0xfffffff017023d40 ; fffffff01701baac d503201f NOP ; fffffff01701bab0 dac123f0 PACIZA X16 ; fffffff01701bab4 aa1003e1 MOVr X1, X16 ; X1 = X16 (0xfffffff017023d40) fffffff01701bab8 52800020 MOVZ W0, #1 ; X0 = 0x1 fffffff01701babc 52800022 MOVZ W2, #1 ; X2 = 0x1 _registerSubSystem(0x1,0xfffffff017023d40,0x1); fffffff01701bac0 9400a8a9 BL 0xfffffff017045d64 ; func_0xfffffff01702153c(0); // getsBuildVariant fffffff01701bac4 9400169e BL 0xfffffff01702153c ; fffffff01701bac8 f90003e0 STRi X0, [X31] ; *0x0 = 0x0 fffffff01701bacc 10f56f00 ADR X0, 0xfffffff0170068ac ; X0 = 0xfffffff0170068ac- fffffff01701bad0 d503201f NOP ; func_0xfffffff01701d77c("build variant: %s");
This transfers control to 0xfffffff017022798
which switch
es on the kTXMKernelSelector...
(at fffffff01702285c
) with a branch table @0xfffffff017023204
. The table has 48 entries, exactly up to kTXMKernelSelectorImage4GetNonce
(which is also 48). From this, we can work out and symbolicate the TXM handler names.
fffffff017022798 d503237f PACIBSP ; fffffff01702279c d10303ff SUBi SP, SP, #192 ; SP = SP - 0xc0 fffffff0170227a0 a90767fa STP X26, X25, [X31, #112] ; *[SP +112] = [X26, X25] .. fffffff017022808 b140113f CMN X9, #16384 (@TODO). ; fffffff01702280c 54000083 B.CC ok ; 0xfffffff01702281c .. some_panic(0x42,...); fffffff017022818 97fffbe2 BL 0xfffffff0170217a0 ; ok: fffffff01702281c aa0003f3 MOVr X19, X0 ; X19 = X0 (0x0) fffffff017022820 f9001be8 STRi X8, [X31, #48] ; *0x30 = 0x0 fffffff017022824 52880008 MOVZ W8, #16384 ; X8 = 0x4000 fffffff017022828 4e080d00 DUP.2D V0, X8 fffffff01702282c 3c8383e0 STUR Q0, [X31, #56] ; fffffff017022830 9100c3e0 MOVr X0, X31 ; X0 = XZR (0x0).. fffffff017022834 528004e1 MOVZ W1, #39 ; X1 = 0x27 page_type_maybe(0,0x27); fffffff017022838 9400045f BL 0xfffffff0170239b4 ; fffffff01702283c f9000e7f STRi XZR, [X19, #24] ; *0x18 = 0x0 fffffff017022840 51000730 SUBi W16, W25, #1 ; X16 = X25 - 0x1 fffffff017022844 7100be1f CMPi W16, #47 ; fffffff017022848 540011a8 B.HI 0xfffffff017022a7c ; fffffff01702284c f100be1f CMPi X16, #47 ; fffffff017022850 9a9f9210 CSEL X16, X16, X31, LS ; X16 = ( ) ? 0x0 : 0x0 fffffff017022854 10004d91 ADR X17, 0xfffffff017023204 ; X17 = 0xfffffff017023204- fffffff017022858 d503201f NOP ; fffffff01702285c b8b07a30 LDRSW(r) X16, [X17, X16, LSL #2] ; branch table @0xfffffff017023204 fffffff017022860 10000011 ADR X17, 0xfffffff017022860 ; X17 = 0xfffffff017022860- fffffff017022864 8b100230 ADDsr X16, X17, X16 ; R16 = R17 + R16 = 0xfffffff017022870 fffffff017022868 d61f0200 BR X16 ; (no symbol) code_: fffffff01702286c d503249f BTI j ; ... fffffff017023204 00000224 DCD 0x224 ; fffffff017023208 00000254 DCD 0x254 ; fffffff01702320c 0000029c DCD 0x29c ; fffffff017023210 0000001c DCD 0x1c ; fffffff017023214 000002d4 DCD 0x2d4 ; ... fffffff0170232a8 000007cc DCD 0x7cc ; fffffff0170232ac 0000000c DCD 0xc ; fffffff0170232b0 0000000c DCD 0xc ; fffffff0170232b4 0000000c DCD 0xc ; fffffff0170232b8 0000000c DCD 0xc ; fffffff0170232bc 0000000c DCD 0xc ; fffffff0170232c0 0000000c DCD 0xc ;
Exiting TXM
TXM returns to whomever called it (presumably, XNU) loading a code into X16. There are several possible codes here:
_sptm_retype_from_txm: fffffff017045d50 f2e00010 MOVKKKK X16, #0, 0x100000003 fffffff017045d60 140058be B do_svc ; 0xfffffff01705c058 _registerSubSystem: fffffff017045d64 f2e00010 MOVKKKK X16, #0, 0x100000002 fffffff017045d74 140058b9 B do_svc ; 0xfffffff01705c058 svc_0_100000004_unknown: fffffff017045d78 f2e00010 MOVKKKK X16, 0x100000004 fffffff017045d88 140058b4 B do_svc ; 0xfffffff01705c058 svc_0_fe00000000: # triggers a panic fffffff017045d8c f2e00010 MOVKKKK X16, #0, 0xfe00000000 fffffff017045d9c 140058af B do_svc ; 0xfffffff01705c058 svc_0_fd00000000: # returns to caller fffffff017045da0 f2e00010 MOVKKKK X16, 0xfd00000000 fffffff017045db0 140058aa B do_svc ; 0xfffffff01705c058 svc_ff_fe00000000: fffffff017045db4 f2e00010 MOVKKKK X16, 0xff00000000 fffffff017045dc4 140058a5 B do_svc ; 0xfffffff01705c058
which all go to a common funnel, to issue an SVC. Since we are in GL0 at this point, this would go to GL1, which is where SPTM awaits:
$ disarm -a 0xfffffff01705c058 txm.iphoneos.release.im4p
fffffff01705c058 d503237f PACIBSP ;
fffffff01705c05c d4000001 SVC #0 ;
fffffff01705c060 d65f0fff RETAB ;
Back in SPTM this gets to the SVC #0 handler, which uses UBFX X9, X16, #32, #8 (unsigned bit field extract, to take out bits 32-39) and then operate on the code, e.g.:
fffffff027061928 d3609e09 UBFX X9, X16, #32, #8 ; fffffff02706192c d2801faa MOVZ X10, #253 ; X10 = 0xfd fffffff027061930 eb0a013f CMPsr X9, X10 ; fffffff027061934 54000041 B.NE 0xfffffff02706193c ; fffffff027061938 140000f6 B 0xfffffff027061d10 ; (no symbol)
From what we see:
0x10000000. codes are system calls. We've discussed those in a previous post
0xfd returns to the caller.
0xfe triggers an SPTM panic path (evident by so many [TXM] Panic strings in calls to 0xfffffff017045d8c).
0xff is unknown (at least, to us)
Example: TXM Logging
TXM logs its output to memory pages it shares with the kernel, with the TXM logging function at fffffff01701d77c
. The get_logging_info()
routine initializes this, by caching the address of the txm_log_page
, the txm_log_head
, and txm_log_sync
:
static void get_logging_info(void) { txm_call_t txm_call = { .selector = kTXMKernelSelectorGetLogInfo, .failure_fatal = true, .num_output_args = 3 }; txm_kernel_call(&txm_call); txm_log_page = (const char*)txm_call.return_words[0]; txm_log_head = (const uint32_t*)txm_call.return_words[1]; txm_log_sync = (const uint32_t*)txm_call.return_words[2]; }
We can find get_logging_info()
inlined in its caller code_signing_init()
(0xfffffff00835823c
). From the disassembly we find that the kTXMKernelSelectorGetLogInfo
is 2, and the address of txm_log_page
is 0xfffffff0079bc478
. From there, it's a simple matter of xn00ping around, getting to that page (whose address is in the variable), and seeing the 128-byte log records output by TXM:
Xn👀p> dump 0xfffffff050cc4000,4096
Dumping 4096 bytes from 0xfffffff050cc4000
0xfffffff050cc4000 54 58 4d 20 5b 4c 6f 67 5d 3a 20 73 65 74 75 70 TXM [Log]: setup
0xfffffff050cc4010 20 6c 6f 67 67 69 6e 67 3a 20 33 32 37 36 38 20 logging: 32768
0xfffffff050cc4020 62 79 74 65 73 20 28 32 35 36 20 7c 20 31 32 38 bytes (256 | 128
0xfffffff050cc4030 29 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 )...............
0xfffffff050cc4040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0xfffffff050cc4050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0xfffffff050cc4060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0xfffffff050cc4070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0xfffffff050cc4080 54 58 4d 20 5b 4c 6f 67 5d 3a 20 73 79 73 74 65 TXM [Log]: syste
0xfffffff050cc4090 6d 20 73 75 70 70 6f 72 74 73 20 44 49 54 20 66 m supports DIT f
0xfffffff050cc40a0 65 61 74 75 72 65 00 00 00 00 00 00 00 00 00 00 eature..........
0xfffffff050cc40b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
..
From the TXM side, we see 0xfffffff01701d77c is the log function:
$ disarm txm.iphoneos.release.im4p| grep fffffff01701d77c | grep -v ^f
txm.iphoneos.release.im4p: This is an IM4P... with a BVX2 payload... Uncompressed 409760 bytes
func_0xfffffff01701d77c("profile does not have a standard length UUID",ARG1,ARG2,ARG3,ARG4);
func_0xfffffff01701d77c("page enforcement failed (%u | %u): (%p | %u) --> %u | 0x%016llX");
...
func_0xfffffff01701d77c("setup logging: %u bytes (%u | %u)");
func_0xfffffff01701d77c("loaded external trust cache modules: %u");
func_0xfffffff01701d77c("failed to load external trust cache module: %u");
func_0xfffffff01701d77c("system supports DIT feature");
...
func_0xfffffff01701d77c("%s");
func_0xfffffff01701d77c("%s: %s");
The function is a small wrapper of a bigger function to actually perform the logging:
fffffff01701d77c d503237f PACIBSP ; fffffff01701d780 d10083ff SUBi SP, SP, #32 ; SP = SP - 0x20 fffffff01701d784 a9017bfd STP X29, X30, [X31, #16] ; *[SP +16] = [X29, X30] fffffff01701d788 910043fd MOVr X29, SP ; FP = SP (0x0).. fffffff01701d78c 910043a8 ADD X8, X29, #16 ; (0x100) -- X8 = FP + 0x10 = 0x10 -- ! fffffff01701d790 f90007e8 STRi X8, [SP, #8] ; *0x58 = 0x10 fffffff01701d794 910043a1 ADD X1, X29, #16 ; (0x0) -- X1 = FP + 0x10 = 0x10 -- ! func_0xfffffff01701d7a8(0,0x10); fffffff01701d798 94000004 BL 0xfffffff01701d7a8 ; fffffff01701d79c a9417bfd LDP X29, X30, [SP, #16] ; [X29, X0] = *[X31] fffffff01701d7a0 910083ff ADD SP, X31, #32 ; (0x50) -- SP = SP + 0x20 = 0x20 -- ! fffffff01701d7a4 d65f0fff RETAB ;
Since get_logging_info() used selector 2, we look at the branch table (@fffffff017023204)
fffffff017023204 00000224 DCD 0x224 ; case 0
fffffff017023208 00000254 DCD 0x254 ; case 1
fffffff01702320c 0000029c DCD 0x29c ; case 2
fffffff017023210 0000001c DCD 0x1c ; case 3
..
..
Adding 0x29c to 0xfffffff017022860
gives us 0xfffffff017022afc
, where we find:
fffffff017022afc d503249f BTI j ;
func_0xfffffff017024084(ARG0,ARG1,ARG2,ARG3,ARG4);
fffffff017022b00 94000561 BL 0xfffffff017024084 ;
fffffff017022b04 52800088 MOVZ W8, #4 ; X8 = 0x4
fffffff017022b08 10f6ce09 ADR X9, 0xfffffff0170104c8 ; X9 = 0xfffffff0170104c8-
fffffff017022b0c d503201f NOP ;
fffffff017022b10 9102c12a ADD X10, X9, #176 ; (0x0) -- X10 = X9 + 0xb0 = 0xfffffff017010578 -- !
fffffff017022b14 a901a808 STP X8, X10, [X0, #24] ; *[? +24] = [X8, X10]
fffffff017022b18 b940d928 LDRi W8, [X9, #216] ; ; X8 = *(0xfffffff0170105a0) (9) = ???
fffffff017022b1c f9001408 STRi X8, [X0, #40] ; *0x28 = 0x4
fffffff017022b20 f9409928 LDRi X8, [X9, #304] ; ; X8 = *(0xfffffff0170105f8) (9) = ???
fffffff017022b24 f9001808 STRi X8, [X0, #48] ; *0x30 = 0x4
fffffff017022b28 f9409d28 LDRi X8, [X9, #312] ; ; X8 = *(0xfffffff017010600) (9) = ???
fffffff017022b2c f9001c08 STRi X8, [X0, #56] ; *0x38 = 0x4
fffffff017022b30 14000003 B 0xfffffff017022b3c ; (no symbol)
fffffff017022b3c d503249f BTI j ;
fffffff017022b40 f9000bff STRi XZR, [SP, #16] ; *[SP + 16] = 0x0
fffffff017022b44 1400012f B common_return ; 0xfffffff017023000
From the BTI j
we know (thanks to ARMv8.5 Branch Target Indicators) that our hex math was correct (since the instruction indicates this is a safe address to jump (BR
) to. We then see the setting of the output_words
[0] through [2] - (i.e. [X0, #40]
htrough [X0, #56]
. The txm_log_page
value written is from X9 + 216 - 0xfffffff0170105a0
And, to sum up the example with the return to caller:
common_return:
fffffff017023000 f9400be0 LDRi X0, [SP, #16] ; X0 = SP + 16
fffffff017023004 f9000660 STRi X0, [X19, #8] ; *[X19 + 8] = X0 = SP + 16
do_return(0x8080000000c);
fffffff017023008 97fffd3b BL do_return ; 0xfffffff0170224f4
...
do_return:
fffffff0170224f4 d503237f PACIBSP ;
fffffff0170224f8 a9be4ff4 STP X20, X19, [X31, #-32]! ; SP -= 32; *[SP] = [X20, X19]
fffffff0170224fc a9017bfd STP X29, X30, [X31, #16] ; *[SP +16] = [X29, X30]
fffffff017022500 910043fd MOVr X29, X31 ; FP = XZR (0x0)..
fffffff017022504 aa0003f3 MOVr X19, X0 ; X19 = X0 (0x0)
some_context();
fffffff017022508 940006df BL some_context ; 0xfffffff017024084 ;
fffffff01702250c 39400008 LDRB W8, X0]
fffffff017022510 34000088 CBZ W8, 0xfffffff017022520 ;
fffffff017022514 aa0003f4 MOVr X20, X0 ; X20 = X0 (0x0)
svc_0x38
fffffff017022518 940006d6 BL svc_0x38 ; 0xfffffff017024070
;; return to caller here
fffffff01702251c 3901629f STRBi WZR, [X20, #88]
fffffff017022520 aa1303e0 MOVr X0, X19 ; X0 = X19 (0x0)
svc_0_fd00000000 (X0)
fffffff017022524 94008e1f BL 0xfffffff017045da0 ;
PMAP/TXM interaction
XNU's pmap
layer has been modified to accommodate for TXM Address Spaces.
struct pmap {
/* Pointer to the root translation table. */
tt_entry_t *tte;
/* Physical page of the root translation table. */
pmap_paddr_t ttep;
...
bool reserved6;
#define PMAP_TYPE_USER 0 /* ordinary pmap */
#define PMAP_TYPE_KERNEL 1 /* kernel pmap */
#define PMAP_TYPE_COMMPAGE 2 /* commpage pmap */
#define PMAP_TYPE_NESTED 3 /* pmap nested within another pmap */
uint8_t type;
/*
* TrustedExecutionMonitor manages its own address space data structure and
* the PMAP is used as the owning structure for keeping this structure.
*/
uint32_t reserved7[4];
void *reserved8;
uint8_t reserved9;
};
Interestingly enough, pmap.h
is full of pmap_txm_*
functions, bridging betwen XNU and the TXM, and those refer to fields that are not declared in the pmap.h file. Specifically, txm_addr_space
and txm_trust_level
. It's likely that the dereferecing of these fields happens through macros, which are unavailable in the open sources. From the assembly, however, we can safely reckon reserved7
is a type of lock, reserved8
is the txm_addr_space
field, and the reserved9
is txm_trust_level
.
Other Tidbits:
TXM and SPTM register two pseudo-kexts:
g_sptm_kmod_info
andg_txm_kmod_info
(from /libkern/c++/OSKext.cpp). Unlike the other pseudokexts, however, these do record the (slid) addresses and mapped/wired sizes of both SPTM and TXM, along with their Mach-O UUID. In the output ofkextstat
, this looks like this:
sh-3.2# kextstat | head
Index Refs Address Size Wired Name (Version) UUID
1 0 0xfffffff027004000 0xec000 0xec000 com.apple.sptm (24.0.0) 2BB4E63C-24CC-3A4D-86F8-92A13349656E
2 0 0xfffffff017004000 0x68000 0x68000 com.apple.txm (24.0.0) 0F3F33B1-DA4D-3D23-A051-C08BA710ADA7
3 177 0 0 0 com.apple.kpi.bsd (24.0.0) 359DB623-5C72-3BF3-B5D2-785FB581FFF7
...