Reverse Engineering iOS Shortcuts Deeplinks

I'm a big fan of my phone being in grayscale, which reduces screentime and cuts into into how attention-grabbing the bright red app badges are. The problem is that I don't want all apps to be in grayscale: some because they're effectively broken (Photos, Camera) and others because I'm okay spending time on them (Books, Chess, Crosswords). I used to selectively do this with a Cydia tweak I wrote but nowadays 1 Jailbreaking has pretty much died, rest in peace. (Also for some interesting hard data check out the downloads of my tweaks over time!) I do it with Shortcuts. A friend asked me how to set it up on their phone, but while the actual shortcut bit is trivial, creating the automation to run it requires tapping a row for every app on your phone. So before I encouraged them to do just that (you should do just that) I wanted to see if Shortcuts had a better way to import or programmatically create automations—through deeplinking. 2 Alright, the short answer is you can't. Go click over to the manual setup writeup. Unless you're an Apple engineer, in which case please add a 'Select All' affordance to the Shortcuts app automation screen.

Deeplinking is a way that apps can handle custom URLs, with arbitrary parameters passed through the URL’s query. I've done some work around this in the past 3 God, has it really been 8 years? on a jailbroken device to extract the deeplink URL schemes for Facebook and Venmo. My primary device is no longer jailbroken though, but luckily we can do similar things for native apps using the Simulator. This also provides a great opportunity to dig into some more reverse engineering tools.

strings

We'll start with a similar approach to the previous blog posts by leveraging strings. strings is a command-line tool that takes in compiled application code and extracts any strings that are at least 4 characters long. To run it, we need the path to that application code for Shortcuts. We can see the binary paths for running processes, so if we boot up the Simulator and open the Shortcuts app 4 If it matters, I'm using Simulator v16.0 (bundled with v16.1 of Xcode) for iOS 17.4 on an iPhone 15 Pro. If you can't find Simulator in Spotlight it's at /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app. and run ps -axo pid,command | grep Shortcuts to filter to the relevant processes and their paths.

abeals@Alexs-MacBook-Pro ~ % ps -axo pid,command | grep Shortcuts 
95542 /Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/Applications/Shortcuts.app/Shortcuts
95483 /Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/System/Library/CoreServices/ShortcutsActions.app/Extensions/ShortcutsTopHitsExtension.appex/ShortcutsTopHitsExtension
95391 /Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/VoiceShortcuts.framework/Support/siriactionsd
94911 /System/Library/PrivateFrameworks/VoiceShortcuts.framework/Versions/A/Support/siriactionsd
96286 grep Shortcuts

The first one is what we want, for the raw Shortcuts.app/Shortcuts file. We can then run strings directly on this, which will extract the printable strings from the compiled binary. By filtering for :// we can see any deeplink shortcuts that the app explicitly references.

strings \
  "/Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/Applications/Shortcuts.app/Shortcuts" \
  | grep :// \
  | sort \
  | uniq

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
shortcuts://create-workflow?source=3d_touch
shortcuts://gallery

Xcode exposes simctl as a CLI for the Simulator tool, and we can test triggering these deeplinks with xcrun simctl openurl booted "shortcuts://create-workflow?source=3d_touch". 5 Note that for deeplinks with query parameters like source you need to wrap the string in quotes, or you'll get a "no matches found" error. The first one opens the "Create Shortcuts" UI, and the second one navigates to the Gallery tab in the app. The "3d_touch" source makes me think these are the Quick Link affordances, and by longpressing the app icon in Simulator we can confirm this is true.

Neither of these are that useful for us though—we need something related to automations. It's time to break out the big guns.

lldb

We know that the base URL scheme is shortcuts://, 6 If they're not showing up in strings you can find them in the "URL Schemes" section under "URL type" in the Info.plist file within Shortcuts.app (or check out this blog for non-native apps). but we don't know what's available to use with it. Ideally we could find the code that handles the deeplinks, and see what it supports. Here's where we get into the main reverse engineering. By connecting a debugger to the app we can tell it to stop running when it does some deeplink parsing code, allowing us to see what code is running and step through execution. We can connect lldb, Apple's debugger 7 This description is a little debatable, but I think it's accurate. Thanks Chris. , to our running Shortcuts app with lldb -p 95542. 8 This uses the PID from the ps call higher up. Heads up that the PIDs that show up in screenshots and videos may change, as I'm closing and reopening the app while doing this writeup.

NSURL

Now that that's connected we'd like to add a breakpoint for URL parsing. We need to know what functions to set a breakpoint on though, so it's worth a quick divagation into how URLs are constructed, and specifically NSURL, Apple's implementation class. Here are the main sections for a typical URL, like something you would see for this blog. 9 Ironically enough this section is probably illegible in grayscale, sorry about that. Toggle it off! Revel in the colors!

Annotated structure of a URL

Each of these are exposed as properties on NSURL with the same name, so [url host] would give blog.alexbeals.com. In this case, we're looking at deeplink URLs, so the key bit will be scheme matching "shortcuts".

Breakpoints

I'm reasonably confident that at some point in the URL handling flow we will need to check the scheme. We can add a breakpoint anytime this function is hit on an NSURL by going to the lldb window and running breakpoint set -n "-[NSURL scheme]". 10 breakpoint set can be shortened to br s or just b in lldb. The debugger will pause execution by default when starting, so we can type continue 11 Or con or c. to have the app start running again. In another Terminal window I can trigger the deeplink processing code with xcrun simctl openurl booted shortcuts://, and voila—we hit the breakpoint!

We can use thread backtrace 12 Or bt. to see the full stack up to this point. We can see that it goes through Shortcuts in frame #26, but most of the code seems like core-iOS about whether the app should handle the URL, not specifically what to do with it. Let's continue on (c) and see if the later traces are more helpful.

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
  * frame #0: 0x0000000180e13dd8 Foundation`-[NSURL(NSURL) scheme]
    frame #1: 0x00000001856558a8 UIKitCore`-[UIApplication(UIApplicationTesting) _shouldHandleTestURL:] + 24
    frame #2: 0x000000018573e500 UIKitCore`_UISceneOpenURLContextsFromActionsSessionAndTransitionContext + 1040
    frame #3: 0x000000018573e054 UIKitCore`-[_UISceneOpenURLBSActionsHandler _respondToActions:forFBSScene:inUIScene:fromTransitionContext:] + 124
    frame #4: 0x00000001848f1684 UIKitCore`-[UIScene scene:didReceiveActions:fromTransitionContext:] + 228
    frame #5: 0x00000001848f0e84 UIKitCore`__64-[UIScene scene:didUpdateWithDiff:transitionContext:completion:]_block_invoke.199 + 324
    frame #6: 0x00000001848efd50 UIKitCore`-[UIScene _emitSceneSettingsUpdateResponseForCompletion:afterSceneUpdateWork:] + 208
    frame #7: 0x00000001848f0c24 UIKitCore`-[UIScene scene:didUpdateWithDiff:transitionContext:completion:] + 220
    frame #8: 0x0000000184ed3338 UIKitCore`-[UIApplicationSceneClientAgent scene:handleEvent:withCompletion:] + 308
    frame #9: 0x0000000186f306ac FrontBoardServices`-[FBSScene updater:didUpdateSettings:withDiff:transitionContext:completion:] + 600
    frame #10: 0x0000000186f5ad48 FrontBoardServices`__94-[FBSWorkspaceScenesClient _queue_updateScene:withSettings:diff:transitionContext:completion:]_block_invoke_2 + 124
    frame #11: 0x0000000186f3be54 FrontBoardServices`-[FBSWorkspace _calloutQueue_executeCalloutFromSource:withBlock:] + 160
    frame #12: 0x0000000186f5ac94 FrontBoardServices`__94-[FBSWorkspaceScenesClient _queue_updateScene:withSettings:diff:transitionContext:completion:]_block_invoke + 312
    frame #13: 0x0000000180171978 libdispatch.dylib`_dispatch_client_callout + 16
    frame #14: 0x00000001801758d8 libdispatch.dylib`_dispatch_block_invoke_direct + 380
    frame #15: 0x0000000186f7cb20 FrontBoardServices`__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 44
    frame #16: 0x0000000186f7c9fc FrontBoardServices`-[FBSMainRunLoopSerialQueue _targetQueue_performNextIfPossible] + 196
    frame #17: 0x0000000186f7cb54 FrontBoardServices`-[FBSMainRunLoopSerialQueue _performNextFromRunLoopSource] + 24
    frame #18: 0x000000018040ee88 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #19: 0x000000018040edd0 CoreFoundation`__CFRunLoopDoSource0 + 172
    frame #20: 0x000000018040e540 CoreFoundation`__CFRunLoopDoSources0 + 232
    frame #21: 0x0000000180408c28 CoreFoundation`__CFRunLoopRun + 768
    frame #22: 0x0000000180408514 CoreFoundation`CFRunLoopRunSpecific + 572
    frame #23: 0x000000018ef06ae4 GraphicsServices`GSEventRunModal + 160
    frame #24: 0x00000001853e8040 UIKitCore`-[UIApplication _run] + 868
    frame #25: 0x00000001853ebcc8 UIKitCore`UIApplicationMain + 124
    frame #26: 0x0000000102b64c24 Shortcuts`___lldb_unnamed_symbol4135 + 76
    frame #27: 0x0000000102d09544 dyld_sim`start_sim + 20
    frame #28: 0x0000000102db6274 dyld`start + 2840

The third time it trips is more interesting. We can see that it's still downstream of the _UISceneSendOpenURLActionCallbackForScene function, but now we're hitting Shortcuts code again, and WorkflowKit which is doing some URL handling work (-[ICManager handleIncomingRequest] and -[_ICURLRequest parseActions] seem like great places to look).

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
  * frame #0: 0x0000000180e13dd8 Foundation`-[NSURL(NSURL) scheme]
    frame #1: 0x00000001a583e334 WorkflowKit`-[_ICURLRequest parseActions] + 116
    frame #2: 0x00000001a583e280 WorkflowKit`-[_ICURLRequest action] + 32
    frame #3: 0x00000001a58ffeb0 WorkflowKit`-[ICManager handleIncomingRequest:] + 200
    frame #4: 0x00000001a58ffc6c WorkflowKit`-[ICManager handleOpenURL:fromSourceApplication:errorHandler:postNotification:] + 536
    frame #5: 0x0000000102b627c8 Shortcuts`___lldb_unnamed_symbol4075 + 392
    frame #6: 0x0000000102b625d4 Shortcuts`___lldb_unnamed_symbol4074 + 184
    frame #7: 0x000000018573e7a0 UIKitCore`_UISceneSendOpenURLActionCallbackForScene + 108
    frame #8: 0x000000018573e080 UIKitCore`-[_UISceneOpenURLBSActionsHandler _respondToActions:forFBSScene:inUIScene:fromTransitionContext:] + 168
...

We're also seeing more of the ___lldb_unnamed_symbol callsites within Shortcuts. Let's try and figure out what frame #5 is doing, which is a great opportunity to introduce our other big gun.

Hopper

Hopper is a disassembler, like IDA and Ghidra, allowing us to turn compiled code from binaries into something closer to source code. The benefit of using Hopper over an alternative is mostly because it handles iOS's idiosyncrasies well. 13 And also it has a free demo, compared to IDA Pro's whopping $1,099+ per year. The annoying downside of this is that it only works for 30 minutes at a time, which means I got very quick at resetting up my window state. We can drag and drop the Shortcuts.app/Shortcuts file that we ran strings on higher up, and after accepting the defaults 14 FAT Archive, and AArch64, with the Mach-O AArch64 loader. a window opens up into a somewhat intimidating screen of raw assembly. It's okay if you're not familiar with ASM, we'll be using very little of it today.

ASLR

The first thing we're going to need to do is handle the ASLR offset. ASLR or Address Space Layout Randomization randomly arranges the address space when loading programs so that attackers can't reliably jump to a fixed portion of memory and know what's there. 15 This was first introduced circa 2003 and so notably doesn't happen for many GBA games, which is what makes arbitrary code execution in Pokemon Silver easier than in late Gen III and beyond. In practice what this means for us is that when we want to see what's at address 0x102b627c8 from frame #5: 0x0000000102b627c8 Shortcuts`___lldb_unnamed_symbol4075 + 392 it won't line up with the address jumps in Hopper.

Luckily we can fix this. By running image list -o -f | grep Shortcuts in lldb we can get the offset that it's currently placed in memory. In this case that's 0x2af4000.

image list -o -f | grep Shortcuts
[  0] 0x0000000002af4000 /Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/Applications/Shortcuts.app/Shortcuts

In Hopper go to Modify > Change File Base Address..., where it will say that the offset is 0x100000000. Adding 0x2af4000 to that gives us 0x102af4000. 16 You can evaluate this directly in lldb with p/x 0x100000000 + 0x2af4000, or just use an online Hex calculator. I'd say that just searching it in Google would do it, but it routes more and more math to Gemini, and Gemini is...unreliable at best. If we change the offset to that, then we should be good to jump directly there with Navigate > Go To Address or Symbol... (or just G) and type 0x102b627c8. If we change the mode from "ASM mode" to "Pseudo-code mode" in the top bar then it's almost legible. We're within the class WFMainSceneDelegate in the -(int)handleOpenURL:(int)arg2 options:(int)arg3 function. The first if case handles file URLs, and the else block uses [ICManager sharedManager] to handle deeplinks like the ones we're using.

So let's start digging into ICManager!

ICManager and WorkflowKit

We'll start off in a similar way. image lookup -n "+[ICManager sharedManager]" will let us know the path of the framework that we're working with. We can drag that into Hopper. 17 The video that encouraged me to do some of this digging actually pulls it from the dyld_shared_cache in an extracted IPSW file using Blacktop. In the case of Shortcuts, just using the simulator file is easier.

1 match found in /Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/WorkflowKit.framework/WorkflowKit:
        Address: WorkflowKit[0x0000000000246bd4] (WorkflowKit.__TEXT.__text + 2373260)
        Summary: WorkflowKit`+[ICManager sharedManager]

We also need to handle the ASLR offset again. image list -o | grep WorkflowKit gives 0x1a56bb000, which we can use directly in the Change File Base Address affordance again. 18 The offsets for the libraries don't seem to change for me. Not sure if this will be constant for other simulators or Macs, but if you calculate this once you can likely keep using it. From there, we can jump directly to 0x1a58ffc6c where that ICManager handleOpenURL: call was happening. 19 The address from frame #4 from the previous stack trace.

Inside that function you can see that we're first calling [*0x1a5b38188 requestWithURL:r22 fromSourceApplication:r19]; to build an object (jumping to 0x1a5b38188 reveals that's the _ICURLRequest class), adding some callback handlers, and then passing it to handleIncomingRequest. If it's not nil then the app does some additional processing. handleIncomingRequest is probably what we want.

One of the first things handleIncomingRequest does is call [request action], which extracts it from [request parseAction]. Let's use this as an example of how to reverse engineer back to more legible code. On the left is the raw output from Hopper's generated pseudocode for parseAction. On the right is what I manually reverse-engineered that code into. You can see that firstly, it's much shorter (71 lines down to 29) and secondly, it's much easier to follow what's going on. 20 Hopefully that's true without me voicing over everything that I did. This is a situation where the written blog medium really suffers compared to video, which is why all of Bryce's stuff is so great. Go check him out!

The types of requests it will parse are https://whatever.com/action/subaction, shortcuts://action/subaction and shortcuts://x-callback-url/action/subaction.

We can take a similar approach to reverse-engineering handleIncomingRequest, which boils down to this code.

NSString *action = [request action];
NSString *action = [[request URL] scheme];
ICURLRequestRegistry *registry = [ICURLRequestRegistry sharedRegistry];
handler = [registry handlerForIncomingRequestWithAction:action scheme:scheme];
if (handler) {
  handler(request);
}

That points us to handlerForIncomingRequestWithAction:

This starts by getting a dictionary with [self requestHandlers] and then indexes into it in a number of ways, 21 Explaining this would be a lot easier if Hopper's decompiled pseudocode window had line numbers. going from the hyper-specific requestHandlers[scheme][action], through requestHandlers['*'][action], requestHandlers[scheme]['*'] and finally ending at requestHandlers['*']['*']. We can take a look at requestHandlers in lldb with po [[ICURLRequestRegistry sharedRegistry] requestHandlers], and tada, we have the full list! 22 You can see some other schemes for en-team146 (Evernote) and pocketapp36486 (Pocket) as holdovers from the Workflow acquisition back in 2017. The full list of supported schemes also includes other clear and legible options such as db-i0cvbg4s5rzbeys://.

{
    "*" =     {
        app = "<__NSMallocBlock__: 0x600000c40a80>";
        "app-shortcut-add-home-screen" = "<__NSMallocBlock__: 0x600000c40930>";
        "app-shortcut-create-workflow" = "<__NSMallocBlock__: 0x600000c40510>";
        automations = "<__NSMallocBlock__: 0x600000c402a0>";
        "continue-user-activity" = "<__NSMallocBlock__: 0x600000c078d0>";
        "create-automation" = "<__NSMallocBlock__: 0x600000c40270>";
        "create-shortcut" = "<__NSMallocBlock__: 0x600000c406f0>";
        "create-workflow" = "<__NSMallocBlock__: 0x600000c406f0>";
        gallery = "<__NSMallocBlock__: 0x600000c40180>";
        home = "<__NSMallocBlock__: 0x600000c40210>";
        "ic-cancel" = "<__NSGlobalBlock__: 0x1f12dc4c0>";
        "ic-error" = "<__NSGlobalBlock__: 0x1f12dc4e0>";
        "ic-success" = "<__NSGlobalBlock__: 0x1f12dc4a0>";
        "import-shortcut" = "<__NSMallocBlock__: 0x600000c409f0>";
        "import-workflow" = "<__NSMallocBlock__: 0x600000c409f0>";
        "open-shortcut" = "<__NSMallocBlock__: 0x600000c40720>";
        "open-workflow" = "<__NSMallocBlock__: 0x600000c40870>";
        "run-shortcut" = "<__NSMallocBlock__: 0x600000c40810>";
        "run-workflow" = "<__NSMallocBlock__: 0x600000c40900>";
        shortcuts = "<__NSMallocBlock__: 0x600000c40240>";
        workflows = "<__NSMallocBlock__: 0x600000c40240>";
    };
    "en-team146" =     {
        "*" = "<__NSMallocBlock__: 0x600000c17300>";
    };
    pocketapp36486 =     {
        "*" = "<__NSMallocBlock__: 0x600000dbe1c0>";
    };
}

This gives us our final list of actions, and prompts the million-dollar question. 🥁 Drumroll, please...

Are any of these useful?

We now know which actions are handled, but not what they do or what options they support. We have a couple ways to try and dive into them to see if any of them can help us with our goal of programmatically creating an automation. 23 Clearly the good money's on create-automation, but I went for completeness.

Checking block info

We can print out the information around one of the blocks. I'll start out of order, with 0x1f12dc4c0 for ic-cancel. 24 Mainly because it works with this one, but not with the first few. There's always a bit of history rewriting when turning two days of investigation into a single writeup, but hindsight is 20/20 (or 6/6!) and it'd be a waste to not use that to structure stuff better.

po 0x1f12dc4c0
<__NSGlobalBlock__: 0x1f12dc4c0>
 signature: "v16@?0@"_ICURLRequest"8"
 invoke   : 0x1a59019fc (/Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/WorkflowKit.framework/WorkflowKit`__17-[ICManager init]_block_invoke_2)

The first part of the layout is the signature, which is the type-encoded method string, and translates to -(void)handler:(_ICURLRequest *)request (the first arg is just the pointer for the block itself).

Block signature

The second is the invoke address, which points to a block invoke in [ICManager init] in WorkflowKit. We can jump to address 0x1a59019fc and directly look at the pseudocode:

This simplifies down to the following.

uuid = [NSUUID initWithUUIDString:[request subAction]]
successHandler = [[ICURLRequestRegistry sharedRegistry] popRequestWithUUID:uuid] successHandler];
successHandler([request properties], 1);

If we go back to the handleOpenURL call from higher up, we can see that you can set the success/cancel/failure handlers on the _ICURLRequest in the deeplink using x-success, x-cancel, and x-error respectively. Here, we can see that this handles the handler for the request identified by the UUID in the subaction field, or requests like shortcuts://ic-cancel/uu1duu1d-c0d3-h3r3-f04e-ca5c311ngr3q.

This approach also works for ic-success, which is the same code but invokes successHandler([request properties], 0) and ic-error which has some more involved code creating an NSError from the domain (handling a request like shortcuts://ic-error/uu1duu1d-c0d3-h3r3-f04e-ca5c311ngr3q?errorDomain=error&errorCode=12345&errorMessage=whoops):

uuid = [NSUUID initWithUUIDString:[request subAction]];
poppedRequest = [[ICURLRequestRegistry] popRequestWithUUID:uuid];
failureHandler = [poppedRequest failureHandler];
if (failureHandler) {
  failureHandler([NSError errorWithDomain:[[request properties] @"errorDomain"] ?? @"InterchangeCallbackErrorDomain" code:[[[request properties] @"errorCode"] integerValue] userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@[request properties] @"errorMessage"]]);
} else if ([poppedRequest successHandler]) {
  [poppedRequest successHandler](nil, nil);
}

Checking the block info also works for some other actions.

open-shortcut
This is handled by 0x1a59054b4 in +[WFRunWorkflowURLHandler registerOpenWorkflowHandler:] which can be hit by something like shortcuts://open-shortcut?name=Shortcut%20Name. It will open the shortcut with the matching name (or id if you use the raw identifier). Once it's been loaded it gets forwarded through WorkflowUI (sub_60d48) and Shortcuts (sub_10000f094) to Swift code in WorkflowUI.OpenWorkflowOptions, finally hitting WFMainViewController runCoordinator which does the actual UI presentation. Unfortunately there's no other option that I could see in here to do something with the opened shortcut. 25 I'll admit that this starts getting into decompiled Swift code, which is hellish. Not sure if there are tricks to make reverse engineering Swift easier, but if there are, I'm not well versed enough in them. Definitely could have missed something here.

workflow = [WFRunWorkflowURLHandler workflowWithName:[[request parameters] @"name"] identifier:[[request parameters] @"id"]]
[[WFDatabase defaultDatabase] referenceForWorkflowID:identifier] 
[[WFDatabase defaultDatabase] uniqueVisibleReferenceForWorkflowName:name]
if (!workflow) {
  [request failureHandler](error);
  // do some additional processing using "actionIndex" and "actionErrorMessage" from parameters
}

open-workflow
This points to the exact same function as open-shortcut, and is downstream of the legacyAction invocation for setting up handlers (likely to support old Workflow code):

This same thing applies for create-workflow mirroring create-shortcut, import-workflow mirroring import-shortcut, run-workflow mirroring run-shortcut and workflows mirroring shortcuts.

We can use the block inspection trick 26 None of these pointers are useful for us, but including here as a searchable reference to some future sap if they want to pull on these threads. for the universal handlers for en-team146:// (WFEvernoteAccessResource in ActionKit) and pocketapp36486:// (WFPocketAccessResource in ActionKit), continue-user-activity (does some file loading using userInfoURL and passes off to _WFHandoffContinueWorkflowActivityType in WFMainSceneDelegate) and run-shortcut (does the same workflow loading that open-shortcut does, but can pass in data like shortcuts://run-shortcut/UUID?input=text&text=Hello, where text can be swapped in both locations for clipboard or pasteboard).

But unfortunately, for the remaining ones (including the potentially helpful create-automation action) the invoke functions all point to the same address, this vague Swift __swift_project_boxed_opaque_existential_0 wrapper.

po 0x600000c40a80
<__NSMallocBlock__: 0x600000c40a80>
 signature: "v16@?0@"_ICURLRequest"8"
 invoke   : 0x1b5fc26e0 (/Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/WorkflowUI.framework/WorkflowUI`__swift_project_boxed_opaque_existential_0)
 copy     : 0x1b5fcc210 (/Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/WorkflowUI.framework/WorkflowUI`block_copy_helper.113)
 dispose  : 0x1b5fcc254 (/Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/WorkflowUI.framework/WorkflowUI`block_destroy_helper.114)

It's time to dive back into our lldb toolbox. First let's see where these are created by setting breakpoints when the handler is added with -‍[ICManager registerHandler:​forIncomingRequestsWithAction:​scheme:] and dig into the code there.

Setting initialization breakpoints with --waitfor

Up to this point our breakpoint approach has been getting the PID and attaching after the app has started up. That will be too late for these handlers being registered, so we'll need to take another approach. We can close the app, open lldb, and attach using --waitfor and a process name. This automatically attaches when the app is started, and will immediately break (before any libraries are loaded, or methods invoked) so we can set our breakpoint. 27 Because the code hasn't initialized you'll get a "WARNING: Unable to resolve breakpoint to any actual locations." when running b -n. You can ignore this, as the locations will be loaded as the app finishes loading. We can add a command to print out the action and scheme each time the breakpoint is hit, allowing us to use bt to find the code responsible.

lldb
process attach --name Shortcuts --waitfor
# Now that it's waiting, open the app. It will automatically stop, then add a breakpoint.
b -n "-[ICManager registerHandler:forIncomingRequestsWithAction:scheme:]"
breakpoint command add
po $x3
po $x4
DONE
c

This flags that the stacktrace that instantiates home, automations, gallery, create-shortcut, create-automation, app, app-shortcut-create-workflow, and app-shortcut-add-home-screen is the following, going through WorkflowUI:

frame #0: 0x00000001a5900074 WorkflowKit`-[ICManager registerHandler:forIncomingRequestsWithAction:scheme:]
    frame #1: 0x00000001b5fc1e60 WorkflowUI`___lldb_unnamed_symbol19880 + 284
    frame #2: 0x000000010429b768 Shortcuts`___lldb_unnamed_symbol2248 + 516
    frame #3: 0x000000010428e0e8 Shortcuts`___lldb_unnamed_symbol2054 + 128
    frame #4: 0x00000001042f1aac Shortcuts`___lldb_unnamed_symbol4064 + 564

With the image list offset of 0x1b5f62000 we can jump to 0x1b5fc1e60 in Hopper for the WorkflowUI binary to reveal that it's the WorkflowUI.RootView.setup function. This is a little helpful, but we're in Swift code, and so reading the actual handlers is pretty rough. Instead, let's swap to our final approach: stepping through execution in the boxed opaque stub.

Following blr and b commands

All of the code goes through __swift_project_boxed_opaque_existential_0 at 0x1b5fc26e0 before it goes elsewhere. Jumping to that address in Hopper for WorkflowKit shows that that we're jumping to is stored in register 21. So let's set a breakpoint here, and figure out where the real code that will be executed lives.

In ASM mode this is invoked at 0x1b5fc270c with blr, which branches to a function call. We can set a breakpoint here with b 0x1b5fc270c and then trigger this code with xcrun simctl openurl booted "shortcuts://create-automation".

When the breakpoint fires we can call p/x $x21 to see the register of the function we're about to call in hex, in this case 0x00000001b5fcbe74. We can find with binary this is in with image lookup --address 0x00000001b5fcbe74 (pointing us to WorkflowUI). This takes us to sub_69e74 which is another jump function, and following a similar breakpoint strategy at the blr call on 0x1b5fc3d20 points us towards 0x102736c7c in Shortcuts. This maps to an invocation of WorkflowUI.RootView.navigate(to:with:). This shows the screen to create an automation, but unfortunately only handles presenting a specific view without taking in any additional arguments (like pre-selecting specific triggers).

This was by far our best chance, so we're grasping at straws 28 Unrelated aside, but in my head this was literal plastic straws (though I'll admit I've never given it much thought). A lot of people cite this as dating back to Thomas Moore in 1534 (over 4 centuries before plastic straws came on the scene), but his original reference uses a stick, only changing to 'straw' in 1583 in John Prime's succinctly titled treatise "A fruitefull and briefe discourse in two bookes: the one of nature, the other of grace with conuenient aunswer to the enemies of grace, vpon incident occasions offered by the late Rhemish notes in their new translation of the new Testament, & others". for the remaining actions. Just to make sure, we can take a cursory look at the others. home and automations do the same thing as create-automation, deeplinking to a specific tab. shortcuts and gallery do very similar things, but allow for subaction customization (which shortcut, and which subsection of the gallery). I didn't dig into app, app-shortcut-create-workflow or app-shortcut-add-home-screen because they didn't help me solve my base problem, though we can now run a smarter version of strings from the beginning across all of the relevant frameworks to see some examples using bundleID for the latter two.

STRINGS=$(xcrun -f strings)
find "/Library/Developer/CoreSimulator/Volumes/iOS_21E213/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.4.simruntime/Contents/Resources/RuntimeRoot/" \
  -type f \( -path '*Shortcuts*' -o -path '*Workflow*' \) -print0 | \
  xargs -0 file --mime | \
  grep 'application/x-mach-binary' | \
  cut -d: -f1 | \
  while IFS= read -r path; do
    "$STRINGS" "$path"
  done | grep '://' | sort | uniq

shortcuts://app-shortcut-add-home-screen?bundleID=
shortcuts://app-shortcut-create-workflow?bundleID=
shortcuts://automations
shortcuts://create-workflow?source=3d_touch
shortcuts://gallery
shortcuts://open-shortcut?id=%@
shortcuts://open-shortcut?name=
shortcuts://run-app-shortcut
shortcuts://x-callback-url/run-shortcut?name=%@&id=%@%@

None of these were relevant, so we're out of luck.

Conclusion

Can you import or programmatically create automations through deeplinks? It seems like no 😢. There are some potential affordances for shortcut importing (though you can also do this directly through iCloud file sharing) but automations remain device-specific. Even though I didn't find anything, it was still a fun excuse to build practice with some reverse engineering tools and get more familiar with Obj-C internals for the first time in years (and another thanks to Bryce Bostwick here, whose videos reignited this flame again).

There's one final lead. As part of this investigation I found the SQLite database that backs this ([[WFDatabase defaultDatabase] fileURL] points to /data/Library/Shortcuts/Shortcuts.sqlite) where the ZTRIGGER table stores the automations. With a jailbroken device you could modify this directly, but for stock iPhones I think there's no better option than manually create them. Check out the next post for a quick overview on how to do that!


  1. Jailbreaking has pretty much died, rest in peace. (Also for some interesting hard data check out the downloads of my tweaks over time!) ↩︎

  2. Alright, the short answer is you can't. Go click over to the manual setup writeup. Unless you're an Apple engineer, in which case please add a 'Select All' affordance to the Shortcuts app automation screen. ↩︎

  3. God, has it really been 8 years? ↩︎

  4. If it matters, I'm using Simulator v16.0 (bundled with v16.1 of Xcode) for iOS 17.4 on an iPhone 15 Pro. If you can't find Simulator in Spotlight it's at /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app↩︎

  5. Note that for deeplinks with query parameters like source you need to wrap the string in quotes, or you'll get a "no matches found" error. ↩︎

  6. This description is a little debatable, but I think it's accurate. Thanks Chris. ↩︎

  7. This uses the PID from the ps call higher up. Heads up that the PIDs that show up in screenshots and videos may change, as I'm closing and reopening the app while doing this writeup. ↩︎

  8. Ironically enough this section is probably illegible in grayscale, sorry about that. Toggle it off! Revel in the colors! ↩︎

  9. breakpoint set can be shortened to br s or just b in lldb↩︎

  10. Or con or c↩︎

  11. Or bt↩︎

  12. And also it has a free demo, compared to IDA Pro's whopping $1,099+ per year. The annoying downside of this is that it only works for 30 minutes at a time, which means I got very quick at resetting up my window state. ↩︎

  13. FAT Archive, and AArch64, with the Mach-O AArch64 loader. ↩︎

  14. This was first introduced circa 2003 and so notably doesn't happen for many GBA games, which is what makes arbitrary code execution in Pokemon Silver easier than in late Gen III and beyond↩︎

  15. You can evaluate this directly in lldb with p/x 0x100000000 + 0x2af4000, or just use an online Hex calculator. I'd say that just searching it in Google would do it, but it routes more and more math to Gemini, and Gemini is...unreliable at best↩︎

  16. The video that encouraged me to do some of this digging actually pulls it from the dyld_shared_cache in an extracted IPSW file using Blacktop. In the case of Shortcuts, just using the simulator file is easier. ↩︎

  17. The offsets for the libraries don't seem to change for me. Not sure if this will be constant for other simulators or Macs, but if you calculate this once you can likely keep using it. ↩︎

  18. The address from frame #4 from the previous stack trace↩︎

  19. Hopefully that's true without me voicing over everything that I did. This is a situation where the written blog medium really suffers compared to video, which is why all of Bryce's stuff is so great. Go check him out! ↩︎

  20. Explaining this would be a lot easier if Hopper's decompiled pseudocode window had line numbers. ↩︎

  21. You can see some other schemes for en-team146 (Evernote) and pocketapp36486 (Pocket) as holdovers from the Workflow acquisition back in 2017. The full list of supported schemes also includes other clear and legible options such as db-i0cvbg4s5rzbeys://↩︎

  22. Clearly the good money's on create-automation, but I went for completeness. ↩︎

  23. Mainly because it works with this one, but not with the first few. There's always a bit of history rewriting when turning two days of investigation into a single writeup, but hindsight is 20/20 (or 6/6!) and it'd be a waste to not use that to structure stuff better. ↩︎

  24. I'll admit that this starts getting into decompiled Swift code, which is hellish. Not sure if there are tricks to make reverse engineering Swift easier, but if there are, I'm not well versed enough in them. Definitely could have missed something here. ↩︎

  25. None of these pointers are useful for us, but including here as a searchable reference to some future sap if they want to pull on these threads. ↩︎

  26. Because the code hasn't initialized you'll get a "WARNING: Unable to resolve breakpoint to any actual locations." when running b -n. You can ignore this, as the locations will be loaded as the app finishes loading. ↩︎

  27. Unrelated aside, but in my head this was literal plastic straws (though I'll admit I've never given it much thought). A lot of people cite this as dating back to Thomas Moore in 1534 (over 4 centuries before plastic straws came on the scene), but his original reference uses a stick, only changing to 'straw' in 1583 in John Prime's succinctly titled treatise "A fruitefull and briefe discourse in two bookes: the one of nature, the other of grace with conuenient aunswer to the enemies of grace, vpon incident occasions offered by the late Rhemish notes in their new translation of the new Testament, & others".  ↩︎