Thanks to Google for sharing the sample.
The int uname(struct utsname *); function retrieves the current device information, containing info such as iPhone13,2\x00 to identify the device model.
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 |
It looks like to be determined by the ipsw BuildID. 0 or 1 is valid, and 2 is invalid.
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.
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.
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.
- set
control_fileproc->->fp_guard->fpg_guardto&thread->machine.uNeon - 8 - then
victim_fileproc->fp_guardis&thread->machine.uNeon - 8now. - set
victim_fileproc->->fp_guard->fpg_guardto kaddr thread->machine.uNeonis kaddr now.- thread_get_state = kread
- set
control_fileproc->->fp_guard->fpg_guardtokaddr - 8 - then
victim_fileproc->fp_guardiskaddr - 8now. - call
change_fdguard_nponvictim_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:
- get thread special reply port, and find out the port kaddr with kread.
- allcoate new recv port and find out the port kaddr.
- 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
- corrupt ip_impcount and waitq_flags of special reply port
- kread 0x90 bytes from special_reply_port_kaddr
- check if it has an ip_imp_task
- set ip_imp_task to kaddr
- save current ip_impcount, and set to 0
- unset ip_specialreply and set ip_tempowner in ip_waitq_waitq_flags
- call mach_port_set_attributes on special reply port, flavor = MACH_PORT_TEMPOWNER
- if port->ip_specialreply = 1, it is unset.
- if (port->ip_tempowner != 0) , this is true
- get ip_imp_task, which is kaddr
- assertcnt = port->ip_impcount; this is 0
- ipc_importance_task_release(release_imp_task), release_imp_task = kaddr
- ipc_importance_release_locked(kaddr)
- ipc_importance_release_internal(kaddr)
- (os_atomic_dec(&(kaddr)->iie_bits, relaxed) & IIE_REFS_MASK), here is an underflow
- kmem_qword is not zero now. It's 0xffffffff
- recover ip_impcount and waitq_flags
- set the saved ip_impcount for special reply port
- set ip_specialreply and unset ip_tempowner in waitq_flags
- delete new recv port
- 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:
FDGuardNeonRWholds a recv right, and does some kevent thing on that.- kwrite port->ip_messages.imq_klist.slh_first (a knote) to kaddr
- read the knote 0x20~0x28 from kernel
- (knote_0x20_qword & 0xfffffffffffff000) | 0x402),set kn_status to
KN_VANISHED | KN_QUEUED - write back to the knote 0x20~0x28 in kernel
- set the knote+8 (kn_tqe.tqe_prev) to kaddr with kwrite.
- kwrite knote_kaddr to kaddr, *(kaddr) is a knote ptr.(again ?)
- call kevent_qos, with kqueue and kevent_qos data in
FDGuardNeonRWstruct- 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;
- knote_dequeue
- the qword in kaddr is set to 0 now.
Attacker needs KernelRW and the kaddr of target port.
- create a new recv port with send right.
- kread to find out the ipc_port kaddr for the new recv port
- unset IO_BITS_KOLABEL for target port in kernel with kread and kwrite
- increase io_reference and ip_soright of target port in kernel with kread and kwrite
- set ip_nsrequest of the new recv port to target port with kwrite.
- request MACH_NOTIFY_NO_SENDERS notification on the new recv port to get a send-once right of the target port
- kread to find out the ipc_entry kaddr of the target port send-once right
- set MACH_PORT_TYPE_SEND in ipc_entry->ie_bits
- 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.
- receive the msg.
- cleanup
ip_nsrequest is PAC-ed in xnu-12377.1.9.
- UMHooker
- DMHooker
- RWTranfer
WatcherandHelpercommunicate via mach msg and KernelRW.- Giver already has KernelRW.
- Getter opens really a lot of fd.
- An RB-tree in a malware. Cool.
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 ?