Skip to content

Instantly share code, notes, and snippets.

@WHW0x455
Last active December 23, 2025 07:13
Show Gist options
  • Select an option

  • Save WHW0x455/2b720322f1dacfcb5c649205a521c4ac to your computer and use it in GitHub Desktop.

Select an option

Save WHW0x455/2b720322f1dacfcb5c649205a521c4ac to your computer and use it in GitHub Desktop.

2023 predator sample

Thanks to Google for sharing the sample.

VersionDispatcher

The int uname(struct utsname *); function retrieves the current device information, containing info such as iPhone13,2\x00 to identify the device model.

VersionDispatcher::OffsetsVersionByDeviceInit

Return value saved to VersionDispatcher::m_deviceClass

deviceClass uname.machine device
5 N/A N/A
4 iPhone15,3\x00 iPhone 14 Pro Max
iPhone15,2\x00 iPhone 14 Pro
iPhone14,8\x00 iPhone 14 Plus
iPhone14,7\x00 iPhone 14
iPhone14,6\x00 iPhone SE (3rd generation)
iPhone14,5\x00 iPhone 13
iPhone14,4\x00 iPhone 13 mini
iPhone14,3\x00 iPhone 13 Pro Max
iPhone14,2\x00 iPhone 13 Pro
3 iPhone13,4\x00 iPhone 12 Pro Max
iPhone13,3\x00 iPhone 12 Pro
iPhone13,2\x00 iPhone 12
iPhone13,1\x00 iPhone 12 mini
1 iPhone12,8\x00 iPhone SE (2nd generation)
iPhone12,5\x00 iPhone 11 Pro Max
iPhone12,3\x00 iPhone 11 Pro
iPhone12,1\x00 iPhone 11
0 iPhone11,8\x00 iPhone XR
iPhone11,6\x00 iPhone XS Max (Global)
iPhone11,4\x00 iPhone XS Max (China)
iPhone11,2\x00 iPhone XS

VersionDispatcher::m_currentVersion

It looks like to be determined by the ipsw BuildID. 0 or 1 is valid, and 2 is invalid.

KernelRW technique

What is 'Neon' ?

There are Smack::NeonRW and FDGuardNeonRW. It's not just a fancy name, it's a pointer. The machine_thread.uNeon is dPAC-ed in xnu-11215.1.10

struct machine_thread {
    ...
    arm_context_t *           contextData;             /* allocated user context */
	arm_saved_state_t *       XNU_PTRAUTH_SIGNED_PTR("machine_thread.upcb") upcb;   /* pointer to user GPR state */
	arm_neon_saved_state_t *  uNeon;                   /* pointer to user VFP state */
    ...

If this pointer can be specified, an attacker can perform kernel read and write using thread_get/set_state with ARM_NEON_STATE64/ARM_NEON_STATE/ARM_VFP_STATE.

Smack::NeonRW

The code for Smack::NeonRW partially exists in the sample, it is not actually used. There is no constructor for this technique. Based on the Smack::NeonRW::readLength and Smack::NeonRW::writeLength functions, we can speculate on the actual structure.

Smack::NeonRW has two thread ports and their exception ports. Let's call these two threads the control_thread and the victim_thread.

  • control_thread->machine.uNeon = &victim_thread->machine.uNeon - 0x28.
  • Attacker sets or gets control_thread state through exception messages (mach_exception_raise_state). The flavor is ARM_VFP_STATE.
  • In control_thread state, offset 0x18 holds victim_thread->machine.uNeon. Set state+0x18 to kaddr - 0x10, since arm_neon_saved_state has a 8-bytes hdr and 8-bytes padding. Set control_thread state through exception message. victim_thread->machine.uNeon should be kaddr - 0x10 now.
  • Set or get victim_thread state = write or read in kernel memory with the given kaddr.

FDGuardNeonRW

The kernel read/write primitive used in the sample should be FDGuardNeonRW. RWTransfer helps Watcher to transfer kernelRW to Helper.

The basic technique is the same one in kwrite_dup of KFD, targetting the fileproc.fp_guard. fp_guard is dPAC-ed in xnu-10063.101.15

Technique mitigated with dPAC in iOS 17.4 - Keynote by Ivan Krstić

struct fileproc {
    os_refcnt_t      fp_iocount;
    _Atomic fileproc_vflags_t fp_vflags;
    fileproc_flags_t fp_flags;
    uint16_t         fp_guard_attrs;
    struct fileglob *XNU_PTRAUTH_SIGNED_PTR("fileproc.fp_glob") fp_glob;
    union {
        struct select_set     *fp_wset;   /* fp_guard_attrs == 0 */
        struct fileproc_guard *fp_guard;  /* fp_guard_attrs != 0 */
    };
};

FDGuardNeonRW holds two fd. Let's call them control_fd and victim_fd. So there are two fileprocs in kernel, the control_fileproc and the victim_fileproc.

With control_fileproc->fp_guard is set to &victim_fileproc->fp_guard - 8, control_fileproc->fp_guard->fpg_guard holds victim_fileproc->fp_guard. Call change_fdguard_np on control_fd helps attacker set the fp_guard of victim_fileproc.

FDGuardNeonRW::readLength

  1. set control_fileproc->->fp_guard->fpg_guard to &thread->machine.uNeon - 8
  2. then victim_fileproc->fp_guard is &thread->machine.uNeon - 8 now.
  3. set victim_fileproc->->fp_guard->fpg_guard to kaddr
  4. thread->machine.uNeon is kaddr now.
  5. thread_get_state = kread

FDGuardNeonRW::writeLength

  1. set control_fileproc->->fp_guard->fpg_guard to kaddr - 8
  2. then victim_fileproc->fp_guard is kaddr - 8 now.
  3. call change_fdguard_np on victim_fd = kwrite

As we all know, this method cannot overwrite a value of 0, nor overwrite any value to 0.

Again, as we all know, in-the-wild chains will not be satisfied with this.

If kmem_qword == 0:

  1. get thread special reply port, and find out the port kaddr with kread.
  2. allcoate new recv port and find out the port kaddr.
  3. send a msg to the new recv port with thread special reply port as msgh_local_port.
    • MACH_MSG_TYPE_MAKE_SEND for msgh_remote_port
    • MACH_MSG_TYPE_MAKE_SEND_ONCE for msgh_local_port
  4. corrupt ip_impcount and waitq_flags of special reply port
    1. kread 0x90 bytes from special_reply_port_kaddr
    2. check if it has an ip_imp_task
    3. set ip_imp_task to kaddr
    4. save current ip_impcount, and set to 0
    5. unset ip_specialreply and set ip_tempowner in ip_waitq_waitq_flags
  5. call mach_port_set_attributes on special reply port, flavor = MACH_PORT_TEMPOWNER
    1. if port->ip_specialreply = 1, it is unset.
    2. if (port->ip_tempowner != 0) , this is true
    3. get ip_imp_task, which is kaddr
    4. assertcnt = port->ip_impcount; this is 0
    5. ipc_importance_task_release(release_imp_task), release_imp_task = kaddr
    6. ipc_importance_release_locked(kaddr)
    7. ipc_importance_release_internal(kaddr)
    8. (os_atomic_dec(&(kaddr)->iie_bits, relaxed) & IIE_REFS_MASK), here is an underflow
    9. kmem_qword is not zero now. It's 0xffffffff
  6. recover ip_impcount and waitq_flags
    1. set the saved ip_impcount for special reply port
    2. set ip_specialreply and unset ip_tempowner in waitq_flags
  7. delete new recv port
  8. kmem_qword is now not 0, kwrite will be fine.

xnu-10063.101.15 added iit_over_release_panic for this.

If umem_qword == 0, we can find a primitive that sets arbitrary kernel memory to 0 to handle this situation:

  1. FDGuardNeonRW holds a recv right, and does some kevent thing on that.
  2. kwrite port->ip_messages.imq_klist.slh_first (a knote) to kaddr
  3. read the knote 0x20~0x28 from kernel
  4. (knote_0x20_qword & 0xfffffffffffff000) | 0x402),set kn_status to KN_VANISHED | KN_QUEUED
  5. write back to the knote 0x20~0x28 in kernel
  6. set the knote+8 (kn_tqe.tqe_prev) to kaddr with kwrite.
  7. kwrite knote_kaddr to kaddr, *(kaddr) is a knote ptr.(again ?)
  8. call kevent_qos, with kqueue and kevent_qos data in FDGuardNeonRW struct
    • knote_dequeue if (kn->kn_status & KN_QUEUED) {
    • TAILQ_REMOVE(queue, kn, kn_tqe);
    • queue->tqh_last = (kn)->kn_tqe.tqe_prev = kaddr
    • *(kn)->kn_tqe.tqe_prev which is *(kaddr) = TAILQ_NEXT((kn), kn_tqe) which is null;
  9. the qword in kaddr is set to 0 now.

Other primitives

port right insert

Attacker needs KernelRW and the kaddr of target port.

  1. create a new recv port with send right.
  2. kread to find out the ipc_port kaddr for the new recv port
  3. unset IO_BITS_KOLABEL for target port in kernel with kread and kwrite
  4. increase io_reference and ip_soright of target port in kernel with kread and kwrite
  5. set ip_nsrequest of the new recv port to target port with kwrite.
  6. request MACH_NOTIFY_NO_SENDERS notification on the new recv port to get a send-once right of the target port
  7. kread to find out the ipc_entry kaddr of the target port send-once right
  8. set MACH_PORT_TYPE_SEND in ipc_entry->ie_bits
  9. send a mach msg to the new recv port. The msg has a mach_msg_port_descriptor to copy a send right of the target port.
  10. receive the msg.
  11. cleanup

ip_nsrequest is PAC-ed in xnu-12377.1.9.

TODO

  • UMHooker
  • DMHooker
  • RWTranfer
    • Watcher and Helper communicate via mach msg and KernelRW.
    • Giver already has KernelRW.
    • Getter opens really a lot of fd.
    • An RB-tree in a malware. Cool.

Ideas

Code reuse? LogManager checks if current program name is "com.apple.WebKit.GPU" or "com.apple.WebKit.WebContent". Perhaps the same code will run in WebContent and WebGPU.

According to the keynote by Ivan Krstić, technique mitigated with dPAC in iOS 17.4. More than fp_guard are dPAC-ed. Are other dPAC-ed pointer related to CVE-2023-41992 ?

All the techniques I found out till now are patched. Apple issued new threat warning on Dec. 2, and I know quite a few cases of infection. It seems new technique and strategy has been developed.

I haven't seen related code of CVE-2023-41992 here. The bug is stage 2 only ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment