iOS 17: New Version, New Acronyms
August 8, 2023
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 introduces such changes. Two notable ones are SPTM and TXM, two binaries included in the beta IPSW. We expect these to have a serious impact on system security, perhaps the greatest since the introduction of the Page Protection Layer (PPL). The scope of this post is to provide an initial glance into their inner workings, providing a reproducible step-by-step flow which the interested reader is encouraged to follow with the disassembler of choice. For the impatient, you can just skip to the end.
First Glance
Unpacking the iOS 17 beta IPSW (we used iPhone15,3_17.0_21A5291h_Restore.ipsw for this post) reveals two new binary objects:
The "im4p" suffix identifies these as IMG4 containers, Apple's pet format for IPSW payloads. Internally, this is just a fancy DER (Data Encoding Representation), which can be easily extracted using tools like openssl
. Looking a bit deeper we see:
The "bvx2" magic indicates a payload compressed by LZFSE, another favorite of Apple, used in IPSW component compression. The payload can be extracted using a variety of tools, from the reference implementation tool through Jonathan Levin's imjtool. Another option is to analyze the file using disarm
, the successor to Jonathan Levin's jtool2
, which natively supports both IM4P and LZFSE.
By one way or another, we end up with both these binaries revealed as Mach-O executables:
Let the reversing begin!
Exhibit A: TXM
TXM (Trusted Execution Monitor) is the first Mach-O we consider. Looking at its load commands, we see:
A few points which stand out:
LC_SEGMENT
s show kernel-space addresses, which seem to imply this is a kernel component.LC_SOURCE_VERSION
is low, indicating a new project (but that's obvious)LC_UNIXTHREAD
: Specifying the entry point. This has been superseded in *OS byLC_MAIN
, but only fordyld
binaries.LC_UNIXTHREAD
is still used in firmware components.
The LC_SEGMENT
s show kernel-space addresses, which seem to imply this is a kernel component, looking further, however, we see two things which imply it isn't: The lack of any _ELx
register access, and the presence of SVC
s:
The SVC
instruction is a supervisor call, which serves as the kernel-mode gate for a system call. Thus, TXM runs at GL0. The choice of #37 and #38 for the call numbers, however, is unusual, and implies that its "system calls" aren't the traditional Mach traps or BSD-style system calls of XNU (since those take #128 as an argument). More on that in a bit.
Strings, aid our hand:
A good idea of what a binary does can often be gleaned from strings. Using disarm
, we can track the strings as they are used:
This seemingly chaotic list of strings reveals quite a bit of information:
func_0xfffffff01701a810
is used with boolean entitlements. Filtering the output accordingly, we get the list:
func_0xfffffff0170184e0
is used for boot arguments. Similarly to the method used for the entitlements, we can find the list:
From this, We see that TXM augments (and potentially takes over from) AppleMobileFileIntegrity (AMFI) in enforcing iOS's most important tenet of security - code signing.
func_0xfffffff01704a0780
parses the device tree.func_0xfffffff017020fc0
ispanic(…)
.
More on code signing
func_0xfffffff017028b18
quickly stands out as validating code signatures. This can be seen easily, since the code signature version (and other structure magics) stand out:
Tracing back, we see it is called from _func_0xfffffff017029420
, which is itself called from _func_0xfffffff01702ad68
, from _func_0xfffffff01701bda8
, from _func_0xfffffff017021c78
.
So we see TXM is the component in charge of doing the code signature validation. But that's only one piece of the puzzle.
Exhibit B: SPTM
Applying the same basic techniques we did on TXM to SPTM initially reveals a similar construct:
While there are similarities from the Mach-O perspective, there are also significant differences. Most notably, the abundnace of _ELx
register access - and not just _EL1
, but also EL2
. This implies SPTM is a component which runs at EL2 (else it could not have accessed those registers), which is where XNU runs on newer Apple silicon. There are also references to GL1 and GL2 registers (more on that later).
SPTM vows death before dishonor, and panics (using func_0xffff00708e570) left and right in case of any unexpected issues. The panics disclose (at least, till Apple reads this), a large number of function names (pointed to by the stack pointer in the panicking call). This also holds true for the logging function (_func_0xfffffff00708e590), and a few other functions in the ..570 area, which funnel to func_0xfffffff00708e408. An example of the function name on the stack can be seen here:
Next comes the tedious process of extracting the panicking names and matching them to their corresponding addresses. The list of functions is displayed here, but for the purposes of this post we will focus on a few:
acquire_root_pt
acquire_shared_root_pt
acquire_user_root_pt
assert_ctrr_amcc_region_unlocked
bootstrap_map_region
bootstrap_register_papt_range
bootstrap_retype_papt_range
0xfffffff007074fa4|bootstrap_stage_enforce_after
bootstrap_stage_enforce_before
bootstrap_unmap_region
copy_array_to_scratch
cpt_mapcnt_dec
cpt_mapcnt_inc
cpu_page_table_retype_out
cpu_root_table_retype_in
cpu_root_table_retype_out
crt_mapcnt_dec
crt_mapcnt_inc
ctrr_amcc_map_lock_group
ctrr_dt_get_lock_group
ctrr_dt_get_uint32
ctrr_lock_boot
current_iommu
dispatch_state_machine
dispatch_table_lookup
enforce_paddr_managed
env_violation
genter_dispatch_entry
get_ptep
helper_validate_aligned_vaddr_range
init_get_image_region
init_xnu_ro_data
invalidate_tcb_entry
io_range_get_papt
iommu_bootstrap_register_io_range
iommu_frame_acquire
iommu_frame_release
iommu_refcnt_add
iommu_refcnt_sub
iommu_validate_instance
issue_tlbi_by_asid
0xfffffff007075e10|nvme_bootstrap
papt_update_mapping
refcounts_update_page_op
sart_add_region
sart_bootstrap
sart_bootstrap_parse_edt
sart_bootstrap_register_bootloader_mappings
sart_get_registers
sart_instance_acquire
sart_set_registers
sart_validate_address_range
shared_region_configure
sk_types_retype_out
sptm_auth_user_pointer
sptm_bootstrap_early
sptm_bootstrap_late
sptm_broadcast_tlbi
sptm_compute_io_ranges
sptm_dispatch
sptm_init_txm_bootstrap_complete
sptm_iommu_bootstrap
0xfffffff00708a0a8|sptm_map_page
sptm_map_table
sptm_nest_region
sptm_nvme_init
sptm_nvme_map_pages
sptm_nvme_set_sq_entry
sptm_nvme_unmap_pages
sptm_register_cpu
sptm_register_dispatch_table
sptm_retype
sptm_sart_map_region
sptm_sign_user_pointer
sptm_slide_region
sptm_t8110dart_clamp_tlimits
sptm_t8110dart_clear_err
sptm_t8110dart_clear_perf_interrupts
sptm_t8110dart_disable_translation
sptm_t8110dart_enable_translation
sptm_t8110dart_init
sptm_t8110dart_map
sptm_t8110dart_map_table
sptm_t8110dart_powerdown
sptm_t8110dart_powerup
sptm_t8110dart_query_tlb
sptm_t8110dart_read_smmu_stt_index
sptm_t8110dart_set_smmu_window
sptm_t8110dart_sk_tlbi_barier
sptm_t8110dart_sk_tlbi_request
sptm_t8110dart_unmap
sptm_t8110dart_unmap_table
sptm_uat_destroy_state
sptm_uat_get_info
sptm_uat_init_state
sptm_uat_map_continue
sptm_uat_map_table
sptm_uat_prepare_fw_unmap_continue
sptm_uat_remove_ctx_id
sptm_uat_set_ctx_id
sptm_uat_unmap_continue
sptm_uat_unmap_table
sptm_unmap_table
sptm_unnest_region
sptm_update_disjoint
sptm_update_disjoint_multipage
sptm_update_region
sptm_validate_io_ranges
t8110dart_addr_to_page
t8110dart_addr_to_te_phy
t8110dart_assert_prop_size
t8110dart_bootstrap
t8110dart_bootstrap_allocate
t8110dart_invalidate_tlb_by_sid
t8110dart_powerup_instance
t8110dart_retype_in
t8110dart_tlb_flush_unlock
t8110dart_update_ttbr
t8110dart_walk_tables
table_acquire
uat_bootstrap_parse_dt
uat_copy_segments_locally
uat_get_dt_prop
uat_get_table_ttep
uat_map_segment
uat_retype_in
uat_retype_out
uat_state_object_acquire
uat_validate_map_segment
uat_validate_paddr
uat_validate_unmap_segment
uat_validate_vaddr
uat_walk_tables
unmap_preflight_op
update_preflight_op
uuc_segment_walker
uuc_unmap_pte_update
validate_aligned_vaddr
validate_asid
validate_attribute_index
validate_cid
validate_dispatch_id
validate_frame_type
validate_managed_addr
validate_nvme_call_allowed
validate_pte
validate_qid
validate_region_order
validate_root_config
xnu_default_retype_out
xnu_exec_retype_out
xnu_rozone_retype_out
Dispatch tables
Let’s take as an example one function from the above list, sptm_map_page (0xfffffff00708a0a8
). Looking through the code we see no call to this function. This could either imply dead code, or - that the function is called through some pointer. In that case, our next destination is __DATA_CONST.__const
.
The __DATA_CONST.__const
is full of pointers. These are all rebased by chained fixups (from the __TEXT.__chain_starts
section. We can take advantage of disarm
's ability to automatically fixup before dumping/disassembling, like so:
We next look around the address in __DATA_CONST.__const
, which reveals this is just one of several function pointers:
This implies 0xfffffff00705b5d8
is some type of dispatch table. Looking back in the disassembly, we see:
From the above, we see that fffffff0070899e0
is indeed an LDRSW... X16, LSL #2
statement. This means that the value of X16 is taken as a table index (shifted by two, so each table entry is four bytes). The table entries are easy to map using a bit of hex math, plus the BTI j
command, an ARMv8.6 feature indicating that the opcode is safe to be jumped (=branched) to. This enables us to construct the method to index mapping easily, since each block loads its method into X9 (using the ADR X9, ...
instruction). For example, in the above, sptm_map_page
is loaded from fffffff00705b5e8
, making it option 0. The other addresses also hold similar dispatch tables.
Subsystems
Looking at another example - _func_0xfffffff007075e10
(nvme_bootstrap
), we can look again through __DATA_CONST.__const
(fixed up), and see this:
If that is indeed correct, there should be other "subsystems" registered. And , indeed:
Interlude: GENTER
GENTER (0x0020142x) is proprietary instruction found only on Apple Silicon and discussed by Sven Peter. He, and the other fine folks at Asahi Linux also figured out all the registers discussed next. Along with its counterpart, GEXIT (0x00201400), these transition in and out of the Guarded eXecution Feature (GXF).
XNU calls GENTER in very few places, all wrapped by a key function:
Going back from _func_0xfffffff028448e70 to find its callers, we see one other function using it:
so we see that _func_0xfffffff0281b6684 is the TXM gate, calling GENTER via _func_0xfffffff028448e70
genter_dispatch_entry
The entry point into GXF is set by an MSR to a special register, GXF_ENTRY_EL1.
The genter_dispatch_entry function (func_0xfffff00708986c) is a good place to start. As the name implies, this is the other side of GENTER. sptm_register_dispatch (func_0xfffffff00708975c) is called with sptm_dispatch (0xffffff0070899a4) as an argument, which sets the dispatch table.
SPTM (presumably, the Secure Page Table Monitor) is responsible, therefore, for several main domains:
Signing user pointers
Controlling DART access
Maintaining Page tables for separate operational components
Transitioning between the different components
This is in line with the few mentions of "exclaves" spotted elsewhere in strings. It seems Apple's new design is to transition away from the PPL to this new, micro-kernel like architecture, in which XNU's security functionality is refactored and isolated into exclave domains. Those are kept physically separate from XNU proper, so that even a hypothetical kernel compromise would be unable to further jeopardize the integrity of the other exclave components.
Another hint, which only adds more mystery, is the mention of "CL4-.." components set up by SPTM. CL4 is the Apple modified L4 microkernel, which is the base of SEPOS. The current IPSW images do not have the CL4 component, for which we will likely have to wait for the iPhone16,x_17.0... images.
Back to SVC handling
Recall those SVCs in TXM? 37, 38 and 0? Well, there has to be a handler for them somewhere. Sifting through SPTM's disassembly, we encounter this interesting snippet (from the GXF setup, shown above, repeated here with focus):
(Reasonably) Assuming the VBAR_GL1 works the same way as VBAR_EL1 does, we can expect the synchronous handler to be at +0x400. So - we next check offset 0x400 of the VBAR_GL1(0xfffffff007064400), and find a single instruction - an unconditional branch to 0xfffffff007061b84, wherein we see..
Looking through the ARM documentation for ESR_EL1, we see that the ESR's EC bits are 31:26, and that an SVC (from AArch64) would be indicated by these bits set to 0b010101 (= 16 + 4 +1 = 21). EC fields are 31:26, and if the value is 0b010001 (#21), it is an SVC from AArch64, in which case the argument to the SVC is in the lower 16-bits. Indeed, this is what the code says.
Looking at the SVC handlers:
The handler reads the value of the Saved Program Status Register of GL1, then uses the BIC instruction, to perform a logical AND on the 2's complement of 0x1c0 (in X9). This has the effect of ANDing to zero just the bits of 0x1c0 (i.e. bits 6,7,8). Looking once more at the ARM documentation for SPSR_EL1 (and assuming that Apple follows the same definitions for GL), we find these are the AIF bits of the PSTATE (for SError, IRQ and FIQ, respectively). In other words - this is equivalent to re-enabling interrupts. Likewise, SVC 38 logical ORs the bits, which is equivalent to disabling interrupts.
TL;DR
Putting it all together, we can (carefully) draw the following:
PPL as we knew it is no longer.
TXM, the Trusted eXecution Monitor, runs in GL0, and handles code signing and entitlements, much as PPL used to.
SPTM, the Secure Page Table Monitor, runs in GL1/2
SPTM provides three "system calls":
SVC #0: TBD
SVC #37: enable all interrupts.
SVC #38: disable all interrupts.
The refactoring and relocation of this security critical code to GXF makes it far less likely that an attacker with arbitrary kernel r/w privileges could potentially compromise the security posture of the system.
At this point (given that some components, notably CL4, seem to be missing), we will wait for the iPhone16,x_17.0… images to drop (presumably in September 2023), when we will reconvene here for Part II.