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_SEGMENTs 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 by LC_MAIN, but only for dyld binaries. LC_UNIXTHREAD is still used in firmware components.


The LC_SEGMENTs 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 SVCs:

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 is panic(…).

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. 
Andy Bartlomain

I design and develop custom, professional Squarespace websites for businesses, restaurants, churches, and entrepreneurs. I code beyond Squarespace's limitations to create unique layouts that best fit my clients' content and needs. The custom Squarespace websites I develop are optimized for any browser window size and is coded to keep Squarespace's built-in ease-of-use intact. My custom Squarespace designs will not make ongoing updates and maintenance complicated!

https://www.connectionmadedesign.com
Previous
Previous

iOS 17: New Version, New Acronyms | Round 2