Our goal at DFF is to reveal any threats on mobile devices, and that requires us to keep up to date with every single version of Android and iOS, including the beta and "Developer Preview" phases. Often, these are the under-the-hood, undocumented changes which have the real impact on operating system security.
iOS 17 indeed introduced such changes. Two notable ones were SPTM and TXM, two binaries included in the beta IPSW. Our previous article took a first look at them, but left much unanswered, and promised "more soon". We've been keeping busy, but haven't forgotten our readers' first for detail. As with its predecessor, this article provides a reproducible step-by-step flow which the interested reader is encouraged to follow with the disassembler of choice. And, as before, the impatient reader can just skip to the end.
.
iOS has moved a bit since our last post. We were hoping to wait for the mid-year release (to see if CL4 is put in), but Apple is moving slowly. So this analysis is on the SPTM and TXM binaries from 17.1.1 (21B91) - specifically, the iPhone 15 Pro Max. For SPTM, that's LC_SOURCE_VERSION 184.42.1.0.0. For TXM it's 80.40.3.0.0. This picks up where we left off, and corrects an inaccuracy (in the previous article's "SUBSYSTEMS" sections the numbers didn't align up, since the numbers registered appear to be for IOMMU, not actual dispatchers - more on this later here).
TXM runs in GL0, and handles code signing and entitlements, much as PPL used to.
SPTM runs in GL1/2
SPTM provides three "system calls":
SVC #0: TBD
SVC #37: enable all interrupts.
SVC #38: disable all interrupts.
And pose some questions to answer here:
What is the mysterious SVC #0 for?
What is the general choreography between XNU, TXM, and SPTM?
How does SPTM actually manage page tables?
What are the exact responsibilities assumed by TXM?
SPTM, revisited
Let's begin by re-examining the SPTM binary (this time, from the t8122, iPhone 15 Pro).
About that SVC #0...
Remember SVC #0? We saw the check for it in SPTM, but didn't really look where it was called. Well, looking at TXM's __TEXT_BOOT_EXEC.__bootcode we find:
Labeling this as _sptm_syscall, we can then disassemble the rest of TXM (that is, from __TEXT_EXEC, wherein we see the following:
The first three calls have X16 as a value of 0x100000002, 3 and 4 (1 is missing). We can expect this pattern to be found in other clients of SPTM.
Indeed, we find the following pattern in XNU. Using disarm's gadget finding (and focusing on X16) uncovers these sequences:
It looks like XNU is calling a host of functions, with "families" (which we'll refer to as "subsystem IDs") of 0, 6, 3, 5 and 7. But XNU can't issue an SVC (it's already at a level higher than EL0!). So looking at the common branch point, 0xfffffff02a614e20:
So XNU makes a "system call" to SPTM by means of GENTER #0, as expected. The two functions there (0xfffffff027e8cb44 and 0xfffffff027e8cb84) appear to do before/after processing, with the latter (0xfffffff027e8cb84) calling into XNU's AST - Asynchronous Software Traps, which are often called post interrupt handling and certain kernel events.
Figuring out the subsystems
So far, then, we see that there are thus several subsystems - 0, 3, 5 and 7, along with TXM's 1, and 253/254/255 (which seem to be individual codes). Recall (from the previous article) that we had already identified several subsystems in SPTM:
These registrations, however, appear to be IOMMU related, and weren't the actual numbers (as could also be observed from the previous article). There is a current_iommu() routine (0xfffffff0070891c8, which is sometimes inlined), which the TLS (TPIDR_GL2 at offset 0x8), to get the active IOMMU index, from the above list. Looking into three of these IOMMUs (which are contiguous in __DATA_CONST):
You might wonder how we deduced those …_dispatch_tables above - To get those we have to look into sptm_dispatch (q.v. first article, where table was @fffffff00705b5d8):
<<< @@TODO: Explain about dispatch state machine >>>
We now have a nearly complete picture of the subsystem dispatchers:
#
Subsystem
Descriptor
# of calls/services
0
XNU
0xfffffff00705b600
22
1
TXM
0xfffffff00705b700
5
2
?
0xfffffff00705b800
3
3
t8110dart
0xfffffff0070589a8
17
4
??
0xfffffff007058aa8
..
5
SART
0xfffffff0070588a0
3
6
NVMe
0xfffffff007058788
5
7
UAT
0xfffffff007058000
13
And note that the numbers for 0, 3, 5, 6 and 7 - those numbers we saw earlier are used by XNU - indeed line up, if counting from 0)!
Example: The SPTM XNU Subsystem
We can piece together the XNU subsystem of SPTM by looking through its dispatch table (we we uncovered as "case 0", above), and then matching some function call names (left in debug messages by our friends at Apple, but probably not for long), as well as correlating them from the places where they are called out in XNU. This would result in the following:
SPTM retyping
Looking through symbols, we see quite a few references to ...page_type_retype_[in/out]:
What's interesting is that these functions have no references from elsewhere inside the code. But.. they must be called from somewhere, right? So - symbolicating accordingly, and dumping __DATA_CONST, we see what appears to be a descriptor table:
The structure leads us to deduce the following:
Retyping a page changes its designation, and essentially which subsystem can use it.
There are some @@@TODO types of pages, each associated with an index in a global (read: in SPTM's __DATA_CONST) descriptor table. The Table is at fffffff00705b920, a global which is referenced several types. The table can be found from its "magic" (in reality, constants values) of 0x01010000000000 followed by 0xFFFFFFFFFFFFFFFF.
From the positions in the table, coupled with the names, we can reconstruct (some of) the constants to be:
Constant
Retype class
In handler
Out handler
0x8 or 0x11 or 0x12
cpu_root_table
_func_0xfffffff0070875ec
_func_0xfffffff007087374
0xb
xnu_default
0xfffffff00708735c
0xfffffff007087174
0xe or 0xf or 0x10
xnu_default
N/A
0xfffffff0070870e8
0x13, 0x14
cpu_page_table
....
0x15, 0x16
cpu_page_table
....
N/A
0x18
xnu_rozone
N/A
....
The retype operation has an "in" callback and an "out" callback. Neither or either or both can be implemented. Two null functions (BTI c/RET) are used if a given callback is left unimplemented. You can find those easily looking for the gadgets:
Though from the position in the descriptor table it seems like they're reversed (first one being the "out" callback, and second being the "in", not that it matters much).
TXM
XNU → XTM transitions
As described in the previous article, XNU is a client of TXM for all of its code signing/entitlement needs. This means that XNU needs a way to call out to TXM - which is technically a separate address space than itself (presumably, this is the Apple idea of an "exclave".
To get from XNU to TXM, therefore, will require involving SPTM. The sequence is as follows:
XNU makes an SPTM call - via GENTER. This was explained in the previous article (0xfffffff028448e70) and is in fffffff02850e01c in the 17.1.1 kernel:
Control is transferred to SPTM (via GENTER handler), which realizes (via X16 0x20000........) this is a call from XNU proper.
SPTM somehow transfers control to TXM. But how?
The TXM Side of things
Early in its initialization, TXM only signs two pointers:
Both pointers are provided as arguments to 0xfffffff01704b234. Where have we seen that before?
That is, in a call to SPTM! We can therefore deduce these two pointers are callbacks into TXM, and selector 0x100000002 indicates sptm_register_callback(...) or similar The first of them - 0x2 - is of particular interest. After finding a TXM Thread and ensuring stack alignment, it calls SVC #37 goes to fffffff017021e40. Wherein we shortly find ourselves in a giant switch table. The switch table is automatically reconstructed by disarm, and can be double checked thanks to ARMv8.5 Branch Target Indicators, marking the branch addresses as safe to BTI jump to.
At least one of the functions invoked, we can immediately tell - since it is the only argument to a format string of "build variant", we know it reports the TXM build variant (from previous listing). How do we find the rest? Let's head back to XNU.
Back to XNU
Apple released the XNU sources for iOS 17 (XNU-10002) surprisingly early. The 8792 sources already showed the PPL-client side (in bsd/kern/kern_codesigning.c, with #if PMAP_CS_PPL_MONITOR), but the 10002 sources remove that #if, instead providing many CSM_PREFIXed call-outs, and a brand new /bsd/sys/code_signing_internal.h. From the header we learn there are two possible prefixes - ppl_ (for PMAP_CS) and xnu_ (for older, non PPL devices). But what of the newer, GXF devices? Apple redacts this, because obviously if the #if for that is gone, nobody will consider disassembling the kernel, will they?
If they did, however, they'd quickly find the calls to TXM. We would start with one of the kern_codesigning.c panics:
Going back from it:
Then disassembling ...2827f16c:
The "selector" is stored in the arguments to the TXM kernel call (specifically, loaded into W8 (as in "#11", above), and shoved on the buffer argument to the TXM gate (as shown with selector #11, in fffffff02827f198 above). We leave it as a (lengthy) exercise to the reader to find all the selector calls. But, what of the names?
Looking at the header, we may not know the prefix, but the calls will still have the same:
As another example (from the TXM side), consider one of the above functions - image4_set_release_type. This happens to be reported in one of the TXM panic messages:
This is a "cold" path from the actual function, so going back we find its caller (and therefore, image4_set_release_type is _func_0xfffffff0170445ac. This is called from only one place - func_0xfffffff017023520, which is called from - you guessed it - that big switch table:
A little hex math shows that 0xfffffff017022238 - 0xfffffff017021f1c is 0x31c - which in the switch table was at entry #37 (counting from 1). Locating this in the kernel is trivial:
Another example can be seen by looking at entitlements validated in TXM-land. research.com.apple.license-to-operate and get-task-allow are both validated by 0xfffffff01701b080, so it makes sense they would be in TXM's implementation of allow_invalid_code. Likewise, com.apple.private.pmap.load-trust-cache is called from fffffff01701d65c, which is itself called from ffffff0170224b0 - which is at fffff017021ff0 in the dispatching branch table. Some hex math again shows us this is at 0xfffff017021ff0 - 0xfffffff017021f1c = 0xd4, or function #8.
Digging in __TEXT.__const - TXM certificates and OIDs
For your daily reminder it's not about __DATA_CONST alone, TXM's __TEXT.__const has a few certificates in it, which are easily identifiable from the DER header of "30 82..." and the textual strings therein:
0xfffffff01700ced8 is the Apple Root CA
0xfffffff01700d3d0 is the Extra Content Root CA
0xfffffff01700d958 is the Secure Boot Root CA
0xfffffff01700dec0 is the Basic Attestation User Root CA
0xfffffff01700e0e8 is the DDI Root CA (Developer Disk Image)
0xfffffff01700e668 is an X86 Root CA (odd?)
And func_0xfffffff01703c85c handles certificate chain walking.
- There are a few other DER constructs embedded in __TEXT.__const, which can be extracted using some brute forcing with your favorite (or least hated) DER parsing tool of choice. For example, we see at relative offset 0x14 an array of entitlements to process:
- There are also quite a few encryption/hashing algorithm OID encodings embedded. For example,
encodes the OID of the RSA algorithm (OID 1.2.840.113549.1.1.1 ) followed (at ..cb91) by SHA1-RSA (1.2.840.113549.1.1.5), and others. For more on OID encodings, this reference is handy.
TXM IMG4 support
Looking through TXM's __TEXT again, we come across this interesting bit of code:
Looking at the 3rd argument, we see it is a region in __DATA_CONST:
This structure encodes the well known IMG4 manifest identifiers, and can be seen in other places as well. Specifically:>
Address (in __DATA_CONST)
identifier
Full name
Datatype
fffffff017012728
nsph
preboot splat manifest hash
digest
fffffff017012810
DGST
payload digest
digest
0xfffffff0170128f0/0xfffffff017012960
ECID
unique chip identifier
u64
fffffff017012880
AMNM
allow mix-n-match
bool
0xfffffff017012a40
vnum
maximum restore version
cstring [version]
And the following functions get the data from the IMG4:
func_0xfffffff017045254 - gets a u64
func_0xfffffff0170431fc - gets a bool
func_0xfffffff017043834 - gets a cstring
Other notes:
func_0xfffffff00706c77c activates SPRR, by messing with S3_6_C15_C1_* registers, which we know are the SPRR_* registers.
func_0xfffffff00706c854 activates GXF, by GXF_CONFIG_EL1, GXF_[PAB]ENTRY_EL1
....
TL;DR
Putting it all together, we can (carefully) draw the following:
XNU and/or any other privileged components (EL1 or higher) enter SPTM through GENTER
TXM (EL0) enters SPTM through SVC #0
SPTM provides several dispatch tables, with 8 "subsystems" (#0-7) correponding to the calling components. We listed those back here
Of the 8, two remain unexplained/unknown at this time
SPTM manages page tables (for all components) by assigning "types" to pages. This marks the pages as belonging to one or another of the domains (XNU, SART, DART, XNU RO, etc.). Pages may also be "retyped" (=repurposed) according to built-in rules.
SPTM also handles XNU lockdown (in ffffff007072ba0, called from XNU at 0xfffffff0285bfcfc, in between machine_lockdown() and PE_lockdown_iokit()), and protects its exception handlers.
TXM (subsystem 1) is redirected to from SPTM using a callback it registers.
TXM has its own CSM_PREFIX, meaning it is the third (but redacted) provider of CS functionality, alongside XNU proper (for non-PPL devices) and PMAP_CS.
Though redacted, it is relatively straightforward to find the TXM implementation of the calls, by first mapping the selectors from XNU and the bsd/sys/codesigning_internal.h, then looking at the TXM side of things and its entry.
TXM also provides Img4 services (as can be verified from the CSM_PREFIX. Quite possibly it has a full libDER implementation.
TXM has some half dozen Apple root certificates in it, implying it verifies not only iOS binaries' code signatures, but also those of the DDI and firmware images.
Moving to different binaries in 17.1.1 set back our plans to provide symbol files. Those will still be provided, so stay tuned.
@@@ Some tacky slogan here @@@
Extra stuff
func_0xfffffff017020fc0 is panic from TXM
0xfffffff017010420 is apparently a global data structure, in which are stored (among other things) the ASID table (#16), boot-args (#520), logging (#24), and more