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!
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 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 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).
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!
-
Jailbreaking has pretty much died, rest in peace. (Also for some interesting hard data check out the downloads of my tweaks over time!) ↩︎
-
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. ↩︎
-
God, has it really been 8 years? ↩︎
-
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
. ↩︎ -
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. ↩︎ -
If they're not showing up in
strings
you can find them in the "URL Schemes" section under "URL type" in theInfo.plist
file withinShortcuts.app
(or check out this blog for non-native apps). ↩︎ -
This description is a little debatable, but I think it's accurate. Thanks Chris. ↩︎
-
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. ↩︎ -
Ironically enough this section is probably illegible in grayscale, sorry about that. Toggle it off! Revel in the colors! ↩︎
-
breakpoint set
can be shortened tobr s
or justb
inlldb
. ↩︎ -
Or
con
orc
. ↩︎ -
Or
bt
. ↩︎ -
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. ↩︎
-
FAT Archive, and AArch64, with the Mach-O AArch64 loader. ↩︎
-
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. ↩︎
-
You can evaluate this directly in
lldb
withp/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. ↩︎ -
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 ofShortcuts
, just using the simulator file is easier. ↩︎ -
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. ↩︎
-
The address from frame #4 from the previous stack trace. ↩︎
-
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! ↩︎
-
Explaining this would be a lot easier if Hopper's decompiled pseudocode window had line numbers. ↩︎
-
You can see some other schemes for
en-team146
(Evernote) andpocketapp36486
(Pocket) as holdovers from the Workflow acquisition back in 2017. The full list of supported schemes also includes other clear and legible options such asdb-i0cvbg4s5rzbeys://
. ↩︎ -
Clearly the good money's on create-automation, but I went for completeness. ↩︎
-
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. ↩︎
-
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. ↩︎
-
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. ↩︎
-
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. ↩︎ -
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". ↩︎