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:
DFFenders@xxxx (~/Downloads) % unzip -l iPhone15,3_17.0_21A5291h_Restore.ipsw ... 96776 01-09-2007 09:41 Firmware/sptm.t8120.release.im4p 130197 01-09-2007 09:41 Firmware/txm.iphoneos.release.im4p ...
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:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % hexdump -C sptm.t8120.release.im4p| head -2 00000000 30 83 01 7a 03 16 04 49 4d 34 50 16 04 73 70 74 |0..z...IM4P..spt| 00000010 6d 16 01 31 04 83 01 79 04 62 76 78 32 f8 4e 05 |m..1...y.bvx2.N.| DFFenders@xxxx (~/Downloads/extracted/Firmware) % hexdump -C txm.iphoneos.release.im4p| head -2 00000000 30 83 01 fc 90 16 04 49 4d 34 50 16 04 74 72 78 |0......IM4P..trx| 00000010 6d 16 01 31 04 83 01 fb 69 62 76 78 32 d8 45 01 |m..1....ibvx2.E.|
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:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % disarm -I txm.iphoneos.release.im4p txm.iphoneos.release.im4p: This is an IM4P... with a BVX2 payload... Uncompressed 360608 bytes Mach-O header information: Magic: 64-bit Mach-O Type: executable CPU: ARM64e (ARMv8.3) Cmds: 11 Size: 1824 Flags: 0x200001 (NOUNDEFS PIE)
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:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % disarm -L txm.iphoneos.release.im4p txm.iphoneos.release.im4p: This is an IM4P... with a BVX2 payload... Uncompressed 360608 bytes LC 0: LC_SEGMENT_64 Mem: 0xfffffff017004000-0xfffffff017010000 __TEXT Mem: 0xfffffff0170070ac-0xfffffff01700ba8d __TEXT.__cstring (C-String Literals) Mem: 0xfffffff01700ba90-0xfffffff01700ff80 __TEXT.__const Mem: 0xfffffff01700ff80-0xfffffff01700ffc0 __TEXT.__binname Mem: 0xfffffff01700ffc0-0xfffffff01700fff8 __TEXT.__chain_starts LC 1: LC_SEGMENT_64 Mem: 0xfffffff017010000-0xfffffff017018000 __DATA_CONST Mem: 0xfffffff017010000-0xfffffff017010038 __DATA_CONST.__auth_ptr Mem: 0xfffffff017010038-0xfffffff0170165f8 __DATA_CONST.__const LC 2: LC_SEGMENT_64 Mem: 0xfffffff017018000-0xfffffff017050000 __TEXT_EXEC Mem: 0xfffffff017018000-0xfffffff01704f5e4 __TEXT_EXEC.__text (Normal) LC 3: LC_SEGMENT_64 Mem: 0xfffffff017050000-0xfffffff017058000 __TEXT_BOOT_EXEC Mem: 0xfffffff017050000-0xfffffff017054084 __TEXT_BOOT_EXEC.__text Mem: 0xfffffff017054084-0xfffffff0170540a4 __TEXT_BOOT_EXEC.__bootcode (Normal) LC 4: LC_SEGMENT_64 Mem: 0xfffffff017058000-0xfffffff01705c000 __DATA Mem: 0xfffffff017058000-0xfffffff017058038 __DATA.__data Mem: 0xfffffff017058040-0xfffffff017058aa0 __DATA.__common Mem: 0xfffffff017058aa0-0xfffffff017058ea0 __DATA.__bss LC 5: LC_SEGMENT_64 Mem: 0xfffffff01705c000-0xfffffff01705c0a0 __LINKEDIT LC 6: LC_SYMTAB Symtab: 1 entries @0x58000(360448), Strtab is 16 bytes @0x58010(360464) LC 7: LC_DYSYMTAB No local symbols 1 external symbols at index 0 No undefined symbols No TOC No modtab No Indirect symbols No External relocations LC 8: LC_UUID UUID: E31CB7A6-7B71-3116-B272-48702E390229 LC 9: LC_SOURCE_VERSION Source Version: 80.0.2.0.0 LC 10: LC_UNIXTHREAD Entry Point: 0x50000 (Mem: 0xfffffff017054000)
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:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % disarm txm.iphoneos.release.im4p | grep SVC txm.iphoneos.release.im4p: This is an IM4P... with a BVX2 payload... Uncompressed 360608 bytes Disassembling from 0x14000-0x4c000(0xfffffff017018000-0xfffffff017050000) fffffff0170237b4 d40004a1 SVC #37 ; fffffff01702383c d40004a1 SVC #37 ; fffffff017023984 d40004c1 SVC #38 ;
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:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % disarm txm.iphoneos.release.im4p | grep \" Disassembling from 0x14000-0x4c000(0xfffffff017018000-0xfffffff017050000) func_0xfffffff017020fc0("attempted to initialize boot-args again"); func_0xfffffff01701d0e8("page enforcement failed (%u | %u): (%p | %u) --> %u | 0x%016llX"); func_0xfffffff017020fc0("attempted to initialize ASID table again"); func_0xfffffff01701d304("shared region base range: %p --> %p"); func_0xfffffff0170250d0(???,"com.apple.oah.runtime_arm_internal"); func_0xfffffff0170250d0(???,"com.apple.runtime_arm_internal"); func_0xfffffff01701d0e8("%s: association spans outside of code limit"); func_0xfffffff01701a810(0,"dynamic-codesigning",0,ARG3,ARG4); func_0xfffffff01701a810(0,"research.com.apple.license-to-operate",0,ARG3,ARG4); func_0xfffffff01701a810(0,"get-task-allow",0); func_0xfffffff01701d0e8("build variant: %s"); func_0xfffffff01701d304("Exemption (allowModifiedCode): %u"); func_0xfffffff01701d304("Exemption (allowUnrestrictedDebugging): %u"); .. func_0xfffffff0170184d4("amfi",0x10); func_0xfffffff0170184e0("amfi_get_out_of_my_way"); func_0xfffffff0170184e0("cs_enforcement_disable"); ... func_0xfffffff0170211c0("Device tree pointer outside of device tree region: pointer %p, region [0x%lx, 0x%lx]"); func_0xfffffff01704ce44("Not a CoreEntitlements error!",0x58); func_0xfffffff017021060("Assertion failed: %s, function %s, file %s, line %d.\n",???);
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:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % disarm txm.iphoneos.release.im4p | grep func_0xfffffff01701a810 | grep \" |cut -d'"' -f2 | sort -u
com.apple.private.amfi.can-load-cdhash
com.apple.private.enable-swift-playgrounds-validation
com.apple.private.pmap.load-trust-cache
dynamic-codesigning
get-task-allow
research.com.apple.license-to-operate
func_0xfffffff0170184e0
is used for boot arguments. Similarly to the method used for the entitlements, we can find the list:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % disarm txm.iphoneos.release.im4p | grep 0xfffffff0170184e0 | grep \" | cut -d'"' -f2 | sort -u amfi_allow_any_signature amfi_get_out_of_my_way amfi_unrestrict_task_for_pid amfi_unrestricted_local_signing cs_enforcement_disable srd_fusing txm_allow_any_signature txm_cs_disable txm_cs_enforcement_disable txm_developer_mode txm_enforce_coretrust txm_platform_code_only txm_skip_trust_evaluation txm_unrestrict_task_for_pid txm_unrestricted_local_signing
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:
_func_0xfffffff017028b18: fffffff017028b18 d503237f PACIBSP ; fffffff017028b1c d10143ff SUBi X31, X31, #80 ; SP = SP - 0x50 fffffff017028b20 a9034ff4 STP X20, X19, [X31, #48] ; *[SP +48] = [X20, X19] fffffff017028b24 a9047bfd STP X29, X30, [X31, #64] ; *[SP +64] = [X29, X30] fffffff017028b28 910103fd ADD X29, X31, #64 ; FP = SP + 0x40 = 0x40! fffffff017028b2c f81e83bf STUR X31, [X29, #-24] ; fffffff017028b30 92407c48 AND X8, X2, #0x0 ; fffffff017028b34 f100b11f CMPi X8, #44 ; fffffff017028b38 54000082 B.CS 0xfffffff017028b48 ; fffffff017028b3c 52a00028 MOVZ W8, #1, LSL #16 ; X8 = 0x10000 fffffff017028b40 52846009 MOVZ W9, #8960 ; X9 = 0x2300 fffffff017028b44 1400006f B 0xfffffff017028d00 ; fffffff017028b48 b9400029 LDRi X9, [X1] ; fffffff017028b4c 529bdf4a MOVZ W10, #57082 ; X10 = 0xdefa fffffff017028b50 72a0418a MOVK W10, #524, LSL #16 ; X10 := 0x20cdefa fffffff017028b54 6b0a013f CMPsr W9, W10 ; kSecCodeMagicCodeDirectory(big endian) fffffff017028b58 54000281 B.NE 0xfffffff017028ba8 ; not_a_code_directory.. fffffff017028b5c 2940a42a LDP W10, W9, [X1, #4] ; [X10, X0] = *[X1] fffffff017028b60 5ac00929 REV W9, W9 ; convert sig ver from big endian fffffff017028b64 52805feb MOVZ W11, #767 ; X11 = 0x2ff fffffff017028b68 72a0004b MOVK W11, #2, LSL #16 ; X11 := 0x202ff fffffff017028b6c 6b0b013f CMPsr W9, W11 ; fffffff017028b70 5400022c B.GT 0xfffffff017028bb4 ; over compatibilityLimit fffffff017028b74 5140812b SUBi W11, W9, #32 ; X11 = X9 - 0x20 fffffff017028b78 7100097f CMPi W11, #2 ; fffffff017028b7c 54000563 B.CC 0xfffffff017028c28 ; fffffff017028b80 5280200b MOVZ W11, #256 ; X11 = 0x100 fffffff017028b84 72a0004b MOVK W11, #2, LSL #16 ; X11 := 0x20100 fffffff017028b88 6b0b013f CMPsr W9, W11 ; fffffff017028b8c 540005e0 B.EQ 0xfffffff017028c48 ; signature_ver_2.1 fffffff017028b90 5280400b MOVZ W11, #512 ; X11 = 0x200 fffffff017028b94 72a0004b MOVK W11, #2, LSL #16 ; X11 := 0x20200 fffffff017028b98 6b0b013f CMPsr W9, W11 ; fffffff017028b9c 54000381 B.NE 0xfffffff017028c0c ; signature_ver 2.2 fffffff017028ba0 5280068b MOVZ W11, #52 ; X11 = 0x34 fffffff017028ba4 1400002a B 0xfffffff017028c4c ; fffffff017028ba8 52a00048 MOVZ W8, #2, LSL #16 ; X8 = 0x20000 fffffff017028bac 5284c009 MOVZ W9, #9728 ; X9 = 0x2600 fffffff017028bb0 14000054 B 0xfffffff017028d00 ; fffffff017028bb4 52809feb MOVZ W11, #1279 ; X11 = 0x4ff fffffff017028bb8 72a0004b MOVK W11, #2, LSL #16 ; X11 := 0x204ff fffffff017028bbc 6b0b013f CMPsr W9, W11 ; fffffff017028bc0 5400016c B.GT 0xfffffff017028bec ; fffffff017028bc4 5280600b MOVZ W11, #768 ; X11 = 0x300 fffffff017028bc8 72a0004b MOVK W11, #2, LSL #16 ; X11 := 0x20300 fffffff017028bcc 6b0b013f CMPsr W9, W11 ; fffffff017028bd0 54000340 B.EQ 0xfffffff017028c38 ; signature ver 2.3.. fffffff017028bd4 5280800b MOVZ W11, #1024 ; X11 = 0x400 ..
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:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % disarm -L sptm.t8120.release.im4p sptm.t8120.release.im4p: This is an IM4P... with a BVX2 payload... Uncompressed 721064 bytes LC 0: LC_SEGMENT_64 Mem: 0xfffffff007004000-0xfffffff007010000 __TEXT Mem: 0xfffffff007006434-0xfffffff00700faf4 __TEXT.__cstring (C-String Literals) Mem: 0xfffffff00700faf4-0xfffffff00700fb34 __TEXT.__binname Mem: 0xfffffff00700fb34-0xfffffff00700fb50 __TEXT.__chain_starts Mem: 0xfffffff00700fb50-0xfffffff007010000 __TEXT.__const LC 1: LC_SEGMENT_64 Mem: 0xfffffff007010000-0xfffffff007060000 __DATA_CONST Mem: 0xfffffff007010000-0xfffffff00705c328 __DATA_CONST.__const LC 2: LC_SEGMENT_64 Mem: 0xfffffff007060000-0xfffffff007090000 __TEXT_EXEC Mem: 0xfffffff007060000-0xfffffff00708e738 __TEXT_EXEC.__text (Normal) LC 3: LC_SEGMENT_64 Mem: 0xfffffff007090000-0xfffffff007094000 __LAST Mem: 0xfffffff007090000-0xfffffff007090008 __LAST.__pinst LC 4: LC_SEGMENT_64 Mem: 0xfffffff007094000-0xfffffff0070a0000 __DATA Mem: 0xfffffff007094000-0xfffffff00709401a __DATA.__data Mem: 0xfffffff007094020-0xfffffff007098821 __DATA.__common Mem: 0xfffffff007098830-0xfffffff00709d528 __DATA.__bss LC 5: LC_SEGMENT_64 Mem: 0xfffffff0070a0000-0xfffffff0070b4000 __BOOTDATA Mem: 0xfffffff0070a0000-0xfffffff0070b4000 __BOOTDATA.__data LC 6: LC_SEGMENT_64 Mem: 0xfffffff0070b4000-0xfffffff0070b40a8 __LINKEDIT LC 7: LC_SYMTAB Symtab: 1 entries @0xb0000(720896), Strtab is 16 bytes @0xb0010(720912) LC 8: LC_DYSYMTAB No local symbols 1 external symbols at index 0 No undefined symbols No TOC No modtab No Indirect symbols No External relocations LC 9: LC_UUID UUID: 8EE47874-9E3F-3FC3-896B-A7BE2395C816 LC 10: LC_SOURCE_VERSION Source Version: 184.0.24.0.0 LC 11: LC_UNIXTHREAD Entry Point: 0x64388 (Mem: 0xfffffff007068388)
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:
.. fffffff00708a1ec ea0a039f TST X28, X10 ; fffffff00708a1f0 54002700 B.EQ 0xfffffff00708a6d0 ; ... fffffff00708a6d0 50c249a9 ADR X9, 0xfffffff00700f006 ... fffffff00708a728 d503201f NOP ; fffffff00708a72c f90003e8 STRi X8, [X31] ; fffffff00708a730 52800380 MOVZ W0, #28 ; X0 = 0x1c _func_0xfffffff00708e590(0x1c,%s(%s:%d) - %s(%#llx), %s(%#llx), " "%s(%#llx), %s(%#llx), %s(%#llx)\n", (stack)"validate_pte", (stack)"sptm.c");
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:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % disarm -r __DATA_CONST.__const \ sptm.t8120.release.im4p.decompressed | grep fffffff00708a0a8 Showing __DATA_CONST.__const (0xc000-0x58328) as data fffffff00705b5e8: 0xfffffff00708a0a8 _func_0xfffffff00708a0a8 # # For comparison, this is what the offset looks like without fixups: # fffffff00705b5e8: A8 60 08 00 00 00 10 80 |.`......|
We next look around the address in __DATA_CONST.__const
, which reveals this is just one of several function pointers:
fffffff00705b5c8: 0xfffffff0070069ee "VIOLATION_UAT_ILLEGAL_CONTINUE_FW_UNMAP" fffffff00705b5d0: 00 00 00 00 00 00 00 00 |........| fffffff00705b5d8: 0xfffffff007072afc _func_0xfffffff007072afc fffffff00705b5e0: 0xfffffff007089be8 _func_0xfffffff007089be8 fffffff00705b5e8: 0xfffffff00708a0a8 _func_0xfffffff00708a0a8 fffffff00705b5f0: 0xfffffff00708a9e8 _func_0xfffffff00708a9e8 fffffff00705b5f8: 0xfffffff00708b19c _func_0xfffffff00708b19c fffffff00705b600: 0xfffffff00708c3f4 _func_0xfffffff00708c3f4 fffffff00705b608: 0xfffffff00708ca68 _func_0xfffffff00708ca68 fffffff00705b610: 0xfffffff00708c038 _func_0xfffffff00708c038 fffffff00705b618: 0xfffffff00708cb08 _func_0xfffffff00708cb08 fffffff00705b620: 0xfffffff00708cf18 _func_0xfffffff00708cf18 fffffff00705b628: 0xfffffff00708d1e4 _func_0xfffffff00708d1e4 fffffff00705b630: 0xfffffff00708d898 _func_0xfffffff00708d898 fffffff00705b638: 0xfffffff00708dc98 _func_0xfffffff00708dc98 fffffff00705b640: 0xfffffff00708dd74 _func_0xfffffff00708dd74 fffffff00705b648: 0xfffffff00707300c _func_0xfffffff00707300c fffffff00705b650: 0xfffffff007074e5c _func_0xfffffff007074e5c fffffff00705b658: 0xfffffff00708e22c _func_0xfffffff00708e22c fffffff00705b660: 0xfffffff00708e2ec _func_0xfffffff00708e2ec fffffff00705b668: 0xfffffff00708991c _func_0xfffffff00708991c fffffff00705b670: 0xfffffff007073744 _func_0xfffffff007073744 fffffff00705b678: 0xfffffff007073460 _func_0xfffffff007073460 fffffff00705b680: 0xfffffff00708cb40 _func_0xfffffff00708cb40
This implies 0xfffffff00705b5d8
is some type of dispatch table. Looking back in the disassembly, we see:
fffffff0070899e0 b8b07a30 LDRSW X16, [X17, X16, LSL #2] ; br. table @..ff007089b94 fffffff0070899e4 10000011 ADR X17, 0xfffffff0070899e4 ; X17 = 0xfffffff0070899e4 fffffff0070899e8 8b100230 ADDsr X16, X17, X16 ; R16 = R17 + R16 fffffff0070899ec d61f0200 BR X16 ; table_entry_0: fffffff0070899f0 d503249f BTI j ; fffffff0070899f4 10e8df29 ADR X9, 0xfffffff00705b5d8 fffffff0070899f8 d503201f NOP ; fffffff0070899fc 14000026 B 0xfffffff007089a94 ; table_entry_2: fffffff007089a00 d503249f BTI j ; fffffff007089a04 10e8eea9 ADR X9, 0xfffffff00705b7d8 ; X9 = 0xfffffff00705b7d8 fffffff007089a08 d503201f NOP ; fffffff007089a0c 14000022 B 0xfffffff007089a94 ; table_entry_3: fffffff007089a10 d503249f BTI j ; fffffff007089a14 528000ca MOVZ W10, #6 ; X10 = 0x6 fffffff007089a18 f900052a STRi X10, [X9, #8] ; fffffff007089a1c 10e77be9 ADR X9, 0xfffffff007058998 ; X9 = 0xfffffff007058998 fffffff007089a20 d503201f NOP ; fffffff007089a24 1400001c B 0xfffffff007089a94 ; table_entry_4: fffffff007089a28 d503249f BTI j ; fffffff007089a2c 528000ca MOVZ W10, #6 ; X10 = 0x6 fffffff007089a30 f900052a STRi X10, [X9, #8] ; fffffff007089a34 10e78329 ADR X9, 0xfffffff007058a98 ; X9 = 0xfffffff007058a98 fffffff007089a38 d503201f NOP ; fffffff007089a3c 14000016 B 0xfffffff007089a94 ; table_entry_1: fffffff007089a40 d503249f BTI j ; fffffff007089a44 10e8e4a9 ADR X9, 0xfffffff00705b6d8 ; X9 = 0xfffffff00705b6d8 .. table_entry_5: fffffff007089a50 d503249f BTI j ; .. fffffff007089a5c 10e771a9 ADR X9, 0xfffffff007058890 ; X9 = 0xfffffff007058890 ... table_entry_6: fffffff007089a68 d503249f BTI j ; fffffff007089a74 10e76829 ADR X9, 0xfffffff007058778 ; X9 = 0xfffffff007058778 .. table_entry_7: fffffff007089a80 d503249f BTI j ; fffffff007089a84 5280008a MOVZ W10, #4 ; X10 = 0x4 fffffff007089a88 f900052a STRi X10, [X9, #8] ; fffffff007089a8c 10e72ba9 ADR X9, 0xfffffff007058000 ; X9 = 0xfffffff007058000 .. branch_table_from_fffffff0070899e0: fffffff007089b94 0000000c DCD 0xc ; = 0xfffffff0070899f0 fffffff007089b98 0000005c DCD 0x5c ; = 0xfffffff007089a40 fffffff007089b9c 0000001c DCD 0x1c ; = 0xfffffff007089a00 fffffff007089ba0 0000002c DCD 0x2c ; = 0xfffffff007089a10 fffffff007089ba4 00000044 DCD 0x44 ; = 0xfffffff007089a28 fffffff007089ba8 0000006c DCD 0x6c ; = 0xfffffff007089a50 fffffff007089bac 00000084 DCD 0x84 ; = 0xfffffff007089a68 fffffff007089bb0 0000009c DCD 0x9c ; = 0xfffffff007089a80
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:
fffffff00705b390: 0xfffffff007008a68 "SART" fffffff00705b398: 0xfffffff007077a2c _func_0xfffffff007077a2c fffffff00705b3a0: 00 00 00 00 00 00 00 00 |........| fffffff00705b3a8: 00 00 00 00 00 00 00 00 |........| fffffff00705b3b0: 05 00 00 00 00 00 00 00 |........| fffffff00705b3b8: 0xfffffff007058890 // dispatch table? fffffff00705b3c0: 00 00 00 00 00 00 00 00 |........| fffffff00705b3c8: 01 00 00 00 00 00 00 00 |........| fffffff00705b3d0: 38 01 00 00 00 00 00 00 |8.......| fffffff00705b3d8: 0xfffffff007008a6d "VIOLATION_SART_INVALID_PT" fffffff00705b3e0: 0xfffffff007008a87 "VIOLATION_SART_INVALID_PADDR" fffffff00705b3e8: 0xfffffff007008aa4 "VIOLATION_SART_INVALID_N_OPS" fffffff00705b3f0: 0xfffffff007008ac1 "VIOLATION_SART_INVALID_SIZE" fffffff00705b3f8: 0xfffffff007008add "VIOLATION_SART_INVALID_PERM" fffffff00705b400: 0xfffffff007008af9 "VIOLATION_SART_ILLEGAL_STATE" fffffff00705b408: 0xfffffff007008b16 "VIOLATION_SART_NO_SPACE" fffffff00705b410: 0xfffffff007008b2e "VIOLATION_SART_ILLEGAL_MAP" fffffff00705b418: 0xfffffff007008b49 "VIOLATION_SART_ILLEGAL_UNMAP" fffffff00705b420: 0xfffffff007008b66 "VIOLATION_SART_CPU_RACE" fffffff00705b428: 0xfffffff007008b7e "VIOLATION_SART_INVALID_CONFIG" fffffff00705b430: 0xfffffff007008001 "NVMe" fffffff00705b438: 0xfffffff007075e10 _func_0xfffffff007075e10 fffffff00705b440: 00 00 00 00 00 00 00 00 |........| fffffff00705b448: 00 00 00 00 00 00 00 00 |........| fffffff00705b450: 06 00 00 00 00 00 00 00 |........| fffffff00705b458: 0xfffffff007058778 // dispatch table? fffffff00705b460: 00 00 00 00 00 00 00 00 |........| fffffff00705b468: 01 00 00 00 00 00 00 00 |........| fffffff00705b470: 70 07 00 00 00 00 00 00 |p.......| fffffff00705b478: 0xfffffff007008006 "VIOLATION_NVME_INVALID_QID"
If that is indeed correct, there should be other "subsystems" registered. And , indeed:
DFFenders@xxxx (~/Downloads/extracted/Firmware) % disarm sptm.t8120.release.im4p.decompressed| grep fffffff007088df0 | grep -v ^f _func_0xfffffff007088df0(0x1,0xfffffff00705b390); // "SART" subsystem _func_0xfffffff007088df0(0x2,0xfffffff00705b430); // "NVMe" subssystem _func_0xfffffff007088df0(0x3,0xfffffff00705b4a8); // "uat" subsystem _func_0xfffffff007088df0(0x5,0xfffffff00705b240); // "t8110dart" subsystem
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:
_func_0xfffffff028448e70: fffffff028448e70 d503237f PACIBSP fffffff028448e74 2a0003f0 MOV W16, W0 ; X16 = X0 (ARG0) fffffff028448e78 f2e00050 MOVK X16, #2, LSL #48 ; X16 := 0x2???????????? fffffff028448e7c f2c00010 MOVK X16, #0, LSL #32 ; X16 := 0x20000???????? fffffff028448e80 aa0103ea MOV X10, X1 ; X10 = X1 (0x0) fffffff028448e84 a9400540 LDP X0, X1, [X10] ; [X0, X0] = *[X10] fffffff028448e88 a9410d42 LDP X2, X3, [X10, #16] ; [X2, X0] = *[X10] fffffff028448e8c a9421544 LDP X4, X5, [X10, #32] ; [X4, X0] = *[X10] fffffff028448e90 a9431d46 LDP X6, X7, [X10, #48] ; [X6, X0] = *[X10] fffffff028448e94 00201420 GENTER #0 ; fffffff028448e98 d65f0fff RETAB ; _func_0xfffffff028448e9c: fffffff028448e9c 00201421 GENTER #1 ; fffffff028448ea0 00201422 GENTER #2 ; fffffff028448ea4 14000000 HALT #0 ; _func_0xfffffff028448ea8: fffffff028448ea8 00201423 GENTER #3 ; fffffff028448eac 14000000 HALT #0 ;
Going back from _func_0xfffffff028448e70 to find its callers, we see one other function using it:
_func_0xfffffff0281b6684: . fffffff0281b691c 940a4955 BL 0xfffffff028448e70 ; .. fffffff0281b6b38 14000004 B 0xfffffff0281b6b48 ; fffffff0281b6b3c 528000c0 MOVZ W0, #6 ; X0 = 0x6 fffffff0281b6b40 39401a68 LDRB W8, [X19, #24] ; fffffff0281b6b44 34000228 CBZ W8, 0xfffffff0281b6b88 ; fffffff0281b6b48 b9400268 LDRi X8, [X19] ; fffffff0281b6b4c 52803189 MOVZ W9, #396 ; X9 = 0x18c fffffff0281b6b50 90ff768a ADRP X10, #-4400 ; X10 = 0xfffffff027086000 fffffff0281b6b54 9123e14a ADD X10, X10, #2296 ; X10 = X10 + 0x8f8 fffffff0281b6b58 a90127ea STP X10, X9, [X31, #16] ; *[SP +16] = [X10, X9] fffffff0281b6b5c a9002fe8 STP X8, X11, [X31] ; *(X31) = [X8, X11] fffffff0281b6b60 90ff7680 ADRP X0, #-4400 ; X0 = 0xfffffff027086000 fffffff0281b6b64 9123f800 ADD X0, X0, #2302 ; X0 = X0 + 0x8fe = 0xfffffff0270868fe! _panic("received fatal error for a selector from TXM: selector: %u | 0x%0llX @%s:%d",...);
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.
_func_0xfffffff00706c818:
fffffff00706c818 d2800021 MOVZ X1, #1 ; X1 = 0x1
fffffff00706c81c d51ef141 MSR GXF_CONFIG_EL1, X1 ;
fffffff00706c820 d0000021 ADRP X1, 0xfffffff007072000
fffffff00706c824 91239021 ADD X1, X1, #2276 ; X1 = 0xfffffff0070728e4
fffffff00706c828 d51ef841 MSR GXF_PABENTRY_EL1, X1 ;
fffffff00706c82c 90000001 ADRP X1, 0xfffffff00706c000
fffffff00706c830 911f3021 ADD X1, X1, #1996 ; X1 = 0xfffffff00706c7cc
fffffff00706c834 d51ef821 MSR GXF_ENTRY_EL1, X1 ;
fffffff00706c838 d5033fdf ISB ;
fffffff00706c83c 910003e1 ADD X1, X31, #0 ; X1 = SP + 0x0 = 0x0!
fffffff00706c840 00201420 GENTER #0 ;
fffffff00706c844 90ffffa2 ADRP X2, #-12 ; X2 = 0xfffffff007060000
fffffff00706c848 91149042 ADD X2, X2, #1316 ; X2 = 0xfffffff007060524
fffffff00706c84c d51ef822 MSR GXF_ENTRY_EL1, X2 ;
fffffff00706c850 90ffffc2 ADRP X2, 0xfffffff007064000
fffffff00706c854 91000042 ADD X2, X2, #0 ; X2 = 0xfffffff007064000
fffffff00706c858 d51efa42 MSR VBAR_GL1, X2 ;
fffffff00706c85c d5033fdf ISB ;
fffffff00706c860 f2e00000 MOVKKKK X0, 45 ; X0 := 0x2D
fffffff00706c870 d51ef140 MSR GXF_CONFIG_EL1, X0 ;
fffffff00706c874 d65f03c0 RET ;
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):
fffffff00706c850 90ffffc2 ADRP X2, #-8 ; X2 = 0xfffffff007064000 fffffff00706c854 91000042 ADD X2, X2, #0 ; X2 = 0xfffffff007064000! fffffff00706c858 d51efa42 MSR VBAR_GL1, X2
(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..
fffffff007061b84 d500409f MSR S0_0_C4_C0_4, XZR fffffff007061b88 a93f27e8 STP X8, X9, [SP, #-16] ; fffffff007061b8c d53efb28 MRS X8, TPIDR_GL2 ; fffffff007061b90 91000108 ADD X8, X8, #0 ; X8 = X8 + 0x0 = 0x0! fffffff007061b94 f9400d08 LDRi X8, [X8, #24] ; X8 = *(X8 + 0x18) = ??? fffffff007061b98 eb2863ff CMP SP, X8 fffffff007061b9c 54000001 B.NE 0xfffffff007061b9c ; = halt if not equal fffffff007061ba0 d53efaa8 MRS X8, ESR_GL1 ; fffffff007061ba4 d35a7d08 UBFX x8, x8, #26, #6 ; Take bits 26-31 of the ESR... fffffff007061ba8 f100551f CMPi X8, #21 ; ; Compare to SVC argument fffffff007061bac 54000080 B.EQ 0xfffffff007061bbc ; svc_call_handler fffffff007061bb0 f100591f CMPi X8, #22 ; fffffff007061bb4 54000740 B.EQ 0xfffffff007061c9c ; fffffff007061bb8 14000046 B 0xfffffff007061cd0 ; svc_call_handler: fffffff007061bbc d53efaa8 MRS X8, ESR_GL1 ; re-read exc. syndrome register fffffff007061bc0 92403d08 AND X8, X8, #0xFFFF ; Isolate SVC argument fffffff007061bc4 f100011f CMPi X8, #0 ; fffffff007061bc8 540002e0 B.EQ 0xfffffff007061c24 ; svc 0 handler fffffff007061bcc f100951f CMPi X8, #37 ; fffffff007061bd0 540000c0 B.EQ 0xfffffff007061be8 ; svc 37 handler fffffff007061bd4 f100991f CMPi X8, #38 ; fffffff007061bd8 54000180 B.EQ 0xfffffff007061c08 ; svc 38 handler ; otherwise fail.. fffffff007061bdc d29bd5a0 MOVZ X0, #57005 ; X0 = 0xdead fffffff007061be0 d503205f WFE ;
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:
svc_37_handler: fffffff007061be8 d53efa68 MRS X8, SPSR_GL1 ; not yet.. fffffff007061bec d2803809 MOVZ X9, #448 ; X9 = 0x1c0 fffffff007061bf0 8a290108 BIC X8, X8, X9 ; X8 = X8 & (~0x1c0) fffffff007061bf4 d51efa68 MSR SPSR_GL1, X8 ; fffffff007061bf8 d69f03e0 ERET ; svc_38_handler: fffffff007061c08 d53efa68 MRS X8, SPSR_GL1 ; not yet.. fffffff007061c0c b27a0908 ORR X8, X8, 0x1c0 ; X0 = X8 | 0x0 fffffff007061c10 d51efa68 MSR SPSR_GL1, X8 ; fffffff007061c14 d69f03e0 ERET ;
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.