Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.claude
dist/
.grepai/
.DS_Store
2 changes: 2 additions & 0 deletions cmd/ipadecrypt/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ func bootstrapHandler(cmd *cobra.Command, args []string) {
tui.Bullet("AppSync Unified bypasses installd's signature check")
tui.Bullet(" add repo: https://lukezgd.github.io/repo")
tui.Bullet("appinst installs modified IPAs on the device")
tui.Bullet("dpkg installs the trusted helper package on rootless jailbreaks")
tui.Bullet("zip packages the decrypted IPA on-device")
tui.Info("A reboot may be needed after installing; that's fine, we'll reconnect.")

Expand Down Expand Up @@ -285,6 +286,7 @@ func bootstrapHandler(cmd *cobra.Command, args []string) {
}{
{"AppSync Unified", pdev.LocateAppSync},
{"appinst", pdev.LocateAppinst},
{"dpkg", func() (string, error) { return pdev.LocateBinary("dpkg") }},
{"zip", func() (string, error) { return pdev.LocateBinary("zip") }},
}
for _, c := range checks {
Expand Down
33 changes: 32 additions & 1 deletion cmd/ipadecrypt/decrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ func runDecryptOnBundle(dev *device.Client, helperPath, bundleID, bundlePath, ve
helperStderr = stderrNoter
}

_, _, code, err := dev.RunHelper(helperPath, bundleID, bundlePath, outRemote, onEvent, helperStderr)
_, _, code, err := dev.RunHelper(helperPath, bundleID, bundlePath, outRemote, decryptSkipAppex, onEvent, helperStderr)

if stderrNoter != nil {
stderrNoter.Flush()
Expand Down Expand Up @@ -617,6 +617,19 @@ func runDecryptOnBundle(dev *device.Client, helperPath, bundleID, bundlePath, ve
return
}

ignoredEncrypted := 0
if decryptSkipAppex && len(res.Encrypted) > 0 {
encrypted := res.Encrypted[:0]
for _, n := range res.Encrypted {
if isAppexPath(n) {
ignoredEncrypted++
continue
}
encrypted = append(encrypted, n)
}
res.Encrypted = encrypted
}

if len(res.Encrypted) > 0 {
live.Fail("%d binary(ies) still have cryptid != 0", len(res.Encrypted))

Expand All @@ -631,6 +644,9 @@ func runDecryptOnBundle(dev *device.Client, helperPath, bundleID, bundlePath, ve
if len(res.Skipped) > 0 {
suffix = fmt.Sprintf(" (%d skipped)", len(res.Skipped))
}
if ignoredEncrypted > 0 {
suffix += fmt.Sprintf(" (%d encrypted appex ignored)", ignoredEncrypted)
}

live.OK("%d Mach-O(s) verified cryptid=0%s", res.Scanned, suffix)
}
Expand Down Expand Up @@ -670,6 +686,16 @@ func runDecryptOnBundle(dev *device.Client, helperPath, bundleID, bundlePath, ve
cleanupDecrypt(dev, decryptNoCleanup, stagingRemote, outRemote)
}

func isAppexPath(name string) bool {
for _, part := range strings.Split(name, "/") {
if strings.HasSuffix(part, ".appex") {
return true
}
}

return false
}

func lookupTargetApp(as *appstore.Client, acc *appstore.Account, target decryptTarget) (appstore.App, error) {
if target.appId != "" {
return as.LookupByAppID(acc, target.appId)
Expand Down Expand Up @@ -1106,6 +1132,11 @@ func (p *helperProgress) HandleEvent(ev device.Event) helperUpdate {
case "spawn_failed":
return helperUpdate{note: fmt.Sprintf("could not spawn %s (skipped)", path.Base(ev.Attr("src")))}

case "appex":
if ev.Attr("phase") == "skipped" {
return helperUpdate{note: "skipped app extensions"}
}

case "dyld":
switch ev.Attr("phase") {
case "resuming":
Expand Down
2 changes: 2 additions & 0 deletions cmd/ipadecrypt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var (
decryptUseInstalled bool
decryptPatchDevType bool
decryptVerbose bool
decryptSkipAppex bool

versionsLogResponses bool
)
Expand Down Expand Up @@ -61,6 +62,7 @@ func main() {
decrypt.Flags().BoolVarP(&decryptFromAppStore, "from-appstore", "f", false, "fetch from App Store and reinstall, ignoring what's installed on the device")
decrypt.Flags().BoolVar(&decryptUseInstalled, "use-installed", false, "decrypt the installed build directly; skip the App Store path even if a newer version exists")
decrypt.Flags().BoolVar(&decryptPatchDevType, "patch-device-type", false, "if the IPA's UIDeviceFamily excludes this device, append the device's family (iPadOS apps then run on iOS)")
decrypt.Flags().BoolVar(&decryptSkipAppex, "skip-appex", false, "skip app extension (.appex) launch/decrypt attempts")
decrypt.Flags().BoolVarP(&decryptVerbose, "verbose", "v", false, "stream the on-device helper's LOG/ERR lines to stderr (useful for debugging decryption failures)")

versions := &cobra.Command{
Expand Down
11 changes: 11 additions & 0 deletions helper/entitlements.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>get-task-allow</key>
<true/>

<!-- task_for_pid on the target -->
<key>task_for_pid-allow</key>
<true/>

<!-- cross-task thread_set_state -->
<key>com.apple.private.cs.debugger</key>
<true/>
<key>com.apple.private.thread-set-state</key>
<true/>
<key>com.apple.system-task-ports</key>
<true/>
<key>com.apple.system-task-ports.control</key>
<true/>

<!-- platform/sandbox/container slack. platform-application is required:
without it AMFI silently terminates the helper before it can spawn
the target. The two no-* keys remove container scoping so root can
read installed bundles + write to /tmp. -->
<key>platform-application</key>
<true/>
<key>com.apple.private.security.container-required</key>
<false/>
<key>com.apple.private.security.no-sandbox</key>
<true/>
<key>com.apple.private.security.no-container</key>
Expand Down
13 changes: 9 additions & 4 deletions helper/helper.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// hijack. Patches cryptid=0, packs IPA. Walks PlugIns/*.appex +
// Extensions/*.appex separately.
//
// CLI: ipadecrypt-helper-arm64 [-v] <bundle-id> <bundle-src> <out-ipa>
// CLI: ipadecrypt-helper-arm64 [-v] [--skip-appex] <bundle-id> <bundle-src> <out-ipa>
// bundle-id - CFBundleIdentifier; "" skips the main-app pass (appex only).
// Used for SpringBoard SBS launch - launchd-lineage spawn that
// bypasses Sandbox kext's hook_execve gate on Dopamine.
Expand Down Expand Up @@ -122,6 +122,7 @@ extern char **environ;
// ----- logging ---------------------------------------------------------

static int g_verbose = 0;
static int g_skip_appex = 0;

#define LOG(...) do { if (g_verbose) fprintf(stderr, __VA_ARGS__); } while (0)
#define ERR(fmt, ...) fprintf(stderr, "[helper] ERROR: " fmt "\n", ##__VA_ARGS__)
Expand Down Expand Up @@ -1975,7 +1976,6 @@ static int decrypt_bundle(const char *bundle_src, const char *bundle_dst,
EVT("event=spawn_failed src=\"%s\"", bundle_src);
return 0; // non-fatal; continue with rest of the IPA
}

// 1) Dump the main exec. Fingerprint runtime arch from the loaded
// mach_header so slice selection matches what dyld actually mapped.
// FairPlay decrypts on page fault during mach_vm_read_overwrite,
Expand Down Expand Up @@ -2231,11 +2231,12 @@ int main(int argc, char **argv) {
int pi = 1;
while (pi < argc && argv[pi][0] == '-') {
if (strcmp(argv[pi], "-v") == 0) { g_verbose = 1; pi++; continue; }
if (strcmp(argv[pi], "--skip-appex") == 0) { g_skip_appex = 1; pi++; continue; }
break;
}
if (argc - pi != 3) {
fprintf(stderr,
"usage: %s [-v] <bundle-id> <bundle-src> <out-ipa>\n"
"usage: %s [-v] [--skip-appex] <bundle-id> <bundle-src> <out-ipa>\n"
" bundle-id CFBundleIdentifier (for SBS), or \"\" to skip main app\n"
" bundle-src absolute path to the installed .app on disk\n"
" out-ipa where to write the decrypted IPA\n",
Expand Down Expand Up @@ -2273,7 +2274,11 @@ int main(int argc, char **argv) {
if (bundle_id && bundle_id[0]) {
decrypt_bundle(bundle_src, bundle_dst, bundle_id);
}
decrypt_appexes(bundle_src, bundle_dst);
if (g_skip_appex) {
EVT("event=appex phase=skipped");
} else {
decrypt_appexes(bundle_src, bundle_dst);
}

LOG("[helper] zipping → %s\n", out_ipa);
EVT("event=pack phase=start ipa=\"%s\"", out_ipa);
Expand Down
Binary file modified internal/device/ipadecrypt-helper-arm64
Binary file not shown.
Loading