SkillAgentSearch skills...

TaskPortHaxxApp

Attempt to manipulate platform process task port with CoreTrust bug alone

Install / Use

/learn @khanhduytran0/TaskPortHaxxApp
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

TaskPortHaxxApp

Attempt to manipulate platform process task port with CoreTrust bug alone. This is a PoC for iOS 17.0 semi-jailbreak without kernel exploit.

It used to be that having a CoreTrust (therefore TrollStore) bypass does not equal to a jailbreak. However, it it now possible to achieve a semi-jailbreak on these (at least until all supported versions for now, 16.7RC and 17.0) Read below for more info.

  • Update (2025-12-05): NathanLR 2.0 utilizing this PoC is out. You can get it here

There are many loopholes used in this PoC:

  • Launch Constraint bypass (for iOS 16.0+)
  • Spawn root process from launchd without being a platform binary
  • Arbitrary code execution in platform process by controlling a platform process's thread state via an exception port
  • Userspace PAC bypass made trivial using hardware breakpoint

Bypassing Launch Constraint

Since iOS 16.0, Apple introduced Launch Constraint which prevents many platform binaries from being executed under certain conditions. You can read more info about it here

For this chain to work, a Launch Constraint bypass is required to spawn the target platform binary. Although there’s a recent bypass (CVE-2025-43253), it didn’t seem to work on iOS at all.

For most platform binaries, one of the requirements are that they have to be spawned by parent PID 1, aka launchd. Is there any way to fool Launch Constraint? A few ideas came to my mind:

  • double-fork: never tried this before since stock fork() is broken since iOS 15.
  • double-posix_spawn() (similar to double-fork above): didn’t work
  • execve(): didn’t work
  • posix_spawn() with POSIX_SPAWN_SETEXEC: worked! Obviously, this is the same thing that xpcproxy used to spawn daemons, so Launch Constraint has to somehow allow it.

It is unknown whether it was patched at some point, Apple could probably patch this by checking it it was called from xpcproxy.

With a Launch Constraint bypass, we can now spawn platforms binaries. But, wait, we can’t just use posix_spawn since it is still required to have parent PID 1. Of course, we can use the main app itself which is spawned by launchd to spawn platform binary, but it is cumbersome since it would take down the entire app UI and we can only spawn one platform binary.

Another problem is that you will need root to get launchd task port. You can’t just mix posix_spawnattr_set_persona_uid_np and POSIX_SPAWN_SETEXEC to elevate to root directly, Apple knew it already. So we need to find a way to trick launchd into spawning a root process that we can control..

Spawning a binary from launchd with root

It is common knowledge that launchd would reject any launchctl requests, including those that allow you to submit a launch job, unless your binary is a platform binary with root privilege.

My first thought was to borrow an arm64, unsandboxed, platform binary with no Launch Constraint using the rest of the PoC chain to submit a launch job for us. However, I had many issues with this so I abandoned this method.

Next, I tried to look into how runningboardd submits launch job to spawn app processes. I extracted its exact payload and put it in an app installed via TrollStore. Surprisingly, it worked! The launch job routine that runningboardd used is not protected by a platform binary check. Here’s how its payload looks like:

<table> <tr><td> <a href="https://github.com/khanhduytran0/TaskPortHaxxApp/blob/713d60ab36d7e4fa5d53a0504d98e25846dce8f8/TaskPortHaxxApp/launch.m#L47-L61"><b>TaskPortHaxxApp/launch.m</b></a><br/> lines 47 to 61 in <a href="https://github.com/khanhduytran0/TaskPortHaxxApp/blob/713d60a"><code>713d60a</code></a> </td></tr> <tr><td>
NSDictionary *root = @{
    @"monitor": @NO,
    @"handle": @(0),
    @"type": @(7),
    @"plist": plist // plist dict is a typical launchd plist similar to those in /System/Library/LaunchDaemons
};

// Convert to xpc_object_t
xpc_object_t xpcDict = _CFXPCCreateXPCObjectFromCFObject(root);
// For some reason _CFXPCCreateXPCObjectFromCFObject doesn't produce correct uint64, so we set them again here
xpc_dictionary_set_uint64(xpcDict, "handle", 0);
xpc_dictionary_set_uint64(xpcDict, "type", 7);

xpc_object_t result;
kern_return_t kr = _launch_job_routine(0x3e8, xpcDict, &result);
</td></tr> </table>

During testing this, I realized that I can respawn my UIKit app to run as root, such that a root helper would no longer be needed, unfortunately respawning didn’t work correctly on 17.0.

Executing arbitrary code in platform processes

Putting two first steps together, we can successfully spawn a platform binary as root. Now that we need to find a way to gain code execution in it.

I had the very same idea as psychicpaper’s method by Siguza. It’s worth checking it out since it provided the entire foundation to make this possible, but TL;DR: you can set an exception port to a platform binary via posix_spawn API. It’s done as the follows:

  • Spawn our binary from launchd with root, as described above.
  • From our binary, we use posix_spawn to spawn a platform binary with our controlled exception port and a fake bootstrap port. A fake bootstrap port is required to purposefully crash the victim process for our exception handler to be fired, as described in the psychicpaper’s blog post.

Once the exception port fires, we will get the victim’s task port and its thread state. However, we can do nothing with the task port we got since we are not a platform process. Fortunately, at this point we have total control over process’s registers. We can set anything unless it is PAC protected, which I will describe some ways to bypass it below.

I have made a ProcessContext class which is essentially a wrapper around a process's exception handler with convenient methods to read/write as well as calling arbitrary functions. Reading and writing memory call __atomic_load_X and __atomic_store_X instead of some random gadgets :D. Also, dyld shared cache ASLR slide is the same across all processes, so we can just take our process's pointer and perform arbitrary calls directly.

[!NOTE] This technique was patched in iOS 18.0, so in the event of another CoreTrust bypass drops, a new technique is required. As noted by @alfiecg24, this is likely due to thid_should_crash mitigation, and palera1n had to set thid_should_crash=0 boot arg to bypass this.

Bypassing userspace PAC

On arm64, everything above is enough to do anything with the victim process, including reading/writing memory and doing arbitrary function calls. However, for arm64e, we need a way to sign a br gadget that would allow us to completely bypass userspace PAC, since we can always set PC to reuse that signed br gadget. Moreover, a userspace PAC bypass is required even if you have a kernel r/w exploit on iOS 17.0 to overwrite launchd executable path, since SPTM now manages userland PAC.

Here are some approaches:

Use posix_spawnattr_set_ptrauth_task_port_np

This is the quickest approach. Used in opainject, this allows you to steal another process’s PAC IA signing key and use it to sign pointers on your own, or you can force a platform process to use your app’s PAC signing key.

Unfortunately, Apple nuked it in iOS 16.6:

  • xnu-8796.121.2: last version with known functional posix_spawnattr_set_ptrauth_task_port_np (16.5.1?)
  • xnu-8796.141.3: they nuked it completely: /* No one uses this, this is no longer supported, just a no-op */. So opainject’s ROP/PAC bypass method is dead at some point in 16.6 or 16.6.1
  • xnu-10063.101.15: Apple realized they still need it, but they put it behind #if (DEVELOPMENT || DEBUG) :/

Brute-forcing approach

Okay so initially I thought if we couldn’t bypass it, let brute force do the job. It of course works but has extremely poor reliability and may take a long time. Here’s why:

Before sending thread state to userland exception handler, the kernel would sign PC, LR, etc with its own discriminator. If we try to modify PC, we need to clear __DARWIN_ARM_THREAD_STATE64_FLAGS_KERNEL_SIGNED_PC flag otherwise the kernel would reject it. When receiving a modified thread state from userland, the kernel would validate PC using a userland-specfic discriminator, a combination of pc discriminator and a nonzero random 8-bit diversifier stored in __opaque_flags field in the thread state.

8-bit diversifier equals to 255 possible valid signatures of a PC. This made brute-force sucks as we cannot deterministically find a valid pointer, only with hopes and dreams that both PC signature and diversifier match. I tried to make it retry the same signature 255 times to crack the diversifier, but that never seems worked either.

~~TrollPAC:~~ Hardware breakpoint approach

You know, hardware breakpoint is pretty powerful. It can make a process stop at anywhere we want, so why not make it stop at a pacia instruction, overwrite registers storing pc and `disc

View on GitHub
GitHub Stars110
CategoryDevelopment
Updated14d ago
Forks13

Languages

C

Security Score

80/100

Audited on Mar 11, 2026

No findings