Chapters

Hide chapters

Advanced Apple Debugging & Reverse Engineering

Third Edition · iOS 12 · Swift 4.2 · Xcode 10

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Low Level

Section 3: 7 chapters
Show chapters Hide chapters

Section IV: Custom LLDB Commands

Section 4: 8 chapters
Show chapters Hide chapters

15. Dynamic Frameworks
Written by Derek Selander

If you’ve developed any type of Apple GUI software, you’ve definitely used dynamic frameworks in your day-to-day development.

A dynamic framework is a bundle of code loaded into an executable at runtime, instead of at compile time. Examples in iOS include UIKit and the Foundation frameworks. Frameworks such as these contain a dynamic library and optionally assets, such as images.

There are numerous advantages in electing to use dynamic frameworks instead of static frameworks. The most obvious advantage is you can make updates to the framework without having to recompile the executable that depends on the framework.

Imagine if, for every major or minor release of iOS, Apple said, “Hey y’all, we need to update UIKit so if you could go ahead and update your app as well, that would be grrrreat.” There would be blood in the streets and the only competition would be Android vs. Windows Phone!

Why dynamic frameworks?

In addition to the positives of using dynamic frameworks, the kernel can map the dynamic framework to multiple processes that depend on the framework. Take CFNetwork, for example: it would be stupid and a waste of disk space if each running iOS app kept a unique copy of CFNetwork resident in memory. Furthermore, there could be different versions of CFNetwork compiled into each app, making it incredibly difficult to track down bugs.

As of iOS 8, Apple decided to lift the dynamic library restriction and allow third-party dynamic libraries to be included in your app. The most obvious advantage was that developers could share frameworks across different iOS extensions, such as the Today Extension and Action Extensions.

Today, all Apple platforms allow third party dynamic frameworks to be included without rejection in the ever-so-lovely Apple Review process.

With dynamic frameworks comes a very interesting aspect of learning, debugging, and reverse engineering. Since you’ve the ability to load the framework at runtime, you can use LLDB to explore and execute code at runtime, which is great for spelunking in both public and private frameworks.

Statically inspecting an executable’s frameworks

Compiled into each executable is a list of dynamic libraries (most often, frameworks), expected to be loaded at runtime. This can be further broken down into a list of required frameworks and a list of optional frameworks. The loading of these dynamic libraries into memory is done using a special framework called the dynamic loader, or dyld.

If a required framework fails to load, the dynamic library loader will kill the program. If an optional framework fails to load, everything continues as usual, but code from that library will obviously not be able to run!

You may have used the optional framework feature in the past, perhaps when your iOS or Mac app needed to use code from a library added in a newer OS version than the version targeted by your app. In such cases, you’d perform a runtime check around calls to code in the optional library to check if the library was loaded.

I spout tons of this theory stuff, but it’ll make more sense if you see it for yourself.

Open Xcode and create a new iOS project, Single View Application named DeleteMe. Yep, this project won’t hang around for long, so feel free to remove it once you’re done with this chapter.

You’ll not write a line of code within the app (but within the load commands is a different story). Make sure you choose Objective-C then click Next.

Note: You’re using Objective-C because there’s more going on under the hood in a Swift app. At the time of writing, the Swift ABI is not finalized, so every method Swift uses to bridge Objective-C uses a dynamic framework packaged into your app to “jump the gap” to Objective-C. This means within the Swift bridging frameworks are the corresponding dependencies to the proper Objective-C Frameworks. For example, libswiftUIKit.dylib will have a required dependency on the UIKit framework.

Click on the Xcode project at the top of the project navigator. Then click on the DeleteMe target. Next, click on the Build Phases and open up the Link Binary With Libraries.

Add the CoreBluetooth and CallKit framework. To the right of the CallKit framework, select Optional from the drop-down. Ensure that the CoreBluetooth framework has the Required value set as shown below.

Build the project on the simulator using Cmd + B. Do not run just yet. Once the project has been successfully built for the simulator, open the products directory in the Xcode project navigator.

Right click on the produced executable, DeleteMe, and select Show in Finder.

Next, open up the DeleteMe IPA by right clicking the IPA and selecting Show Package Contents.

Next, open a new Terminal window and type the following but don’t press Enter:

otool -L 

Be sure to add a space at the end of the command. Next, drag the DeleteMe executable from the Finder window into the Terminal window. When finished, you should have a command that looks similar to the following:

otool -L /Users/derekselander/Library/Developer/Xcode/DerivedData/DeleteMe-fqycokvgjilklcejwonxhuyxqlej/Build/Products/Debug-iphonesimulator/DeleteMe.app/DeleteMe

Press Enter and observe the output. You’ll see something similar to the following:

/System/Library/Frameworks/CallKit.framework/CallKit (compatibility version 1.0.0, current version 1.0.0)

/System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth (compatibility version 1.0.0, current version 1.0.0)

/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1556.0.0)

/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.200.5)

/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 61000.0.0)

You found the compiled binary DeleteMe and dumped out the list of dynamic frameworks it links to using the ever-so-awesome otool. Take note of the instructions to CallKit and the CoreBluetooth framework you manually added earlier. By default, the compiler automatically adds the “essential” frameworks to the iOS app, like UIKit and Foundation.

Take note of the directory path responsible for loading these frameworks:

/System/Library/Frameworks/
/usr/lib/

Remember these directories; you’ll revisit them for a “eureka” moment later on.

Let’s go a tad bit deeper. Remember how you optionally required the CallKit framework, and required the CoreBluetooth framework? You can view the results of these decisions by using otool.

In Terminal, press the up arrow to recall the previous Terminal command. Next, change the capital L to a lowercase l and press Enter. You’ll get a longer list of output that shows all the load commands for the DeleteMe executable.

otool -l /Users/derekselander/Library/Developer/Xcode/DerivedData/DeleteMe-fqycokvgjilklcejwonxhuyxqlej/Build/Products/Debug-iphonesimulator/DeleteMe.app/DeleteMe

Search for load commands pertaining to CallKit by pressing Cmd + F and typing CallKit. You’ll stumble across a load command similar to the following:

Load command 12
          cmd LC_LOAD_WEAK_DYLIB
      cmdsize 80
         name /System/Library/Frameworks/CallKit.framework/CallKit (offset 24)
   time stamp 2 Wed Dec 31 17:00:02 1969
      current version 1.0.0
compatibility version 1.0.0

Next, search for the CoreBluetooth framework as well:

Load command 13
          cmd LC_LOAD_DYLIB
      cmdsize 96
         name /System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth (offset 24)
   time stamp 2 Wed Dec 31 17:00:02 1969
      current version 1.0.0
compatibility version 1.0.0

Compare the cmd in the load commands output. In CallKit, the load command is LC_LOAD_WEAK_DYLIB, which represents an optional framework, while the LC_LOAD_DYLIB of the CoreBluetooth load command indicates a required framework.

This is ideal for an application that supports multiple iOS versions. For example, if you supported iOS 9 and up, you would strongly link the CoreBluetooth framework and weak link the CallKit framework since it’s only available in iOS 10 and up.

Modifying the load commands

There’s a nice little command that lets you augment and add the framework load commands named install_name_tool.

Open Xcode and build and run the application so the simulator is running DeleteMe. Once running, pause execution and in the LLDB Terminal, verify the CallKit framework is loaded into the DeleteMe address space. Pause the debugger, then type the following into LLDB:

(lldb) image list CallKit

If the CallKit module is correctly loaded into the process space, you’ll get output similar to the following:

[  0] 0484D8BA-5CB8-3DD3-8136-D8A96FB7E15B 0x0000000102d10000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/CallKit.framework/CallKit 

Time to hunt down where the DeleteMe application is running from. Open a new Terminal window and type the following:

pgrep -fl DeleteMe

Provided DeleteMe is running, this will give you the full path of DeleteMe under the simulator app. You’ll get output similar to the following:

61175 /Users/derekselander/Library/Developer/CoreSimulator/Devices/D0576CB9-42E1-494B-B626-B4DB75411700/data/Containers/Bundle/Application/474C8786-CC4F-4615-8BB0-8447DC9F82CA/DeleteMe.app/DeleteMe

You’ll now modify this executable’s load commands to point to a different framework.

Grab the fullpath to the DeleteMe executable and assign it to a Terminal variable called app, like so:

app=/Users/derekselander/Library/Developer/CoreSimulator/Devices/D0576CB9-42E1-494B-B626-B4DB75411700/data/Containers/Bundle/Application/474C8786-CC4F-4615-8BB0-8447DC9F82CA/DeleteMe.app/DeleteMe

While you’re at it, assign the CK and NC Terminal variables to the respective framework paths as well, like so:

CK=/System/Library/Frameworks/CallKit.framework/CallKit
NC=/System/Library/Frameworks/NotificationCenter.framework/NotificationCenter

Stop the execution of the DeleteMe executable and temporarily close Xcode. If you were to accidentally build and run the DeleteMe application through Xcode at a later time, it would undo any tweaks you’re about to make.

In the same Terminal window, use the install_name_tool command, along with the three newly-created Terminal variables, to change around the CallKit load command to call the NotificationCenter framework.

install_name_tool -change "$CK" "$NC" "$app"

If this were an app on a real iOS device, this would actually fail to run since this is invalidating the app’s code signature. You have made changes to the application without resigning it, which breaks the cryptographic seal. Fortunately, this is an iOS Simulator app, so the rules are not as strict. You’ll explore code signing further in the last chapter of this section.

Verify if your changes were actually applied:

otool -L "$app"

If everything went smoothly, you’ll notice something different about the linked frameworks now:

/System/Library/Frameworks/NotificationCenter.framework/NotificationCenter (compatibility version 1.0.0, current version 1.0.0)

/System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth (compatibility version 1.0.0, current version 1.0.0)

/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1556.0.0)

/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.200.5)

/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 61000.0.0)

Verify these changes exist at runtime.

You’re in a bit of a predicament here. If you were to build and run a new version of DeleteMe using Xcode, it would erase these changes. Instead, launch the DeleteMe application through the simulator and then attach to it in a new LLDB Terminal window. To do this, launch DeleteMe in the simulator. Next, type the following into Terminal:

lldb -n DeleteMe

In LLDB, check if the CallKit framework is still loaded.

(lldb) image list CallKit

You’ll get an error as output:

error: no modules found that match 'CallKit'

Can you guess what you’ll do next? Yep! Verify the NotificationCenter framework is now loaded.

(lldb) image list NotificationCenter

Boom! You’ll get output similar to the following:

[  0] 0FCE1DF5-7BAC-3195-94CB-C6100116FF99 0x000000010b8c7000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/NotificationCenter.framework/NotificationCenter

Changing around frameworks (or adding them!) to an already compiled binary is cool, but that took a little bit of work to set up. Fortunately, LLDB is wonderful for loading frameworks into a process at runtime, which is what you’ll do next. Keep that LLDB Terminal session alive, because you’ll learn about a much easier way to load in frameworks.

Loading frameworks at runtime

Before you get into the fun of learning how to load and explore commands at runtime, let me give you a command to help explore directories using LLDB. Start by adding the following to your ~/.lldbinit file:

command regex ls 's/(.+)/po @import Foundation; [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"%1" error:nil]/'

This creates a command named ls, which will take the directory path you give it and dump out the contents. This command will work on the directory of the device that’s being debugged. For example, since you’re running on the simulator on your computer’s local drive it will dump that directory. If you were to run this on an attached iOS, tvOS or other appleOS device, it would dump the directory you give it on that device, with one minor caveat which you’ll learn about shortly.

Since LLDB is already running and attached to DeleteMe, you’ll need to load this command into LLDB manually as well since LLDB has already read the ~/.lldbinit file. Type the following into your LLDB session:

(lldb) command source ~/.lldbinit

This simply reloads your lldbinit file.

Next, find the full path to the frameworks directory in the simulator by typing the following:

(lldb) image list -d UIKit

This will dump out the directory holding UIKit.

[  0] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/Library/Frameworks/UIKit.framework

You actually want to go one level higher to the Frameworks directory. Copy that full directory path and use the new command ls that you just created, like so:

(lldb) ls /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/Library/Frameworks/

This will dump all the public frameworks available to the simulator. There are many more frameworks to be found in different directories, but you’ll start here first.

From the list of frameworks, load the Speech framework into the DeleteMe process space like so:

(lldb) process load /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/Library/Frameworks/Speech.framework/Speech

LLDB will give you some happy output saying the Speech framework was loaded into your process space. Yay!

Here’s something even cooler. By default, dyld will search a set of directories if it can’t find the location of the framework. You don’t need to specify the full path to the framework, just the framework library along with the framework’s name.

Try this out by loading the MessagesUI framework.

(lldb) process load MessageUI.framework/MessageUI

You’ll get the following output:

Loading "MessageUI.framework/MessageUI"...ok
Image 1 loaded.

Sweet.

Exploring frameworks

One of the foundations of reverse engineering is exploring dynamic frameworks. Since a dynamic framework requires the code to compile the binary into a position independent executable, you can still query a significant amount of information in the dynamic framework — even when the compiler strips the framework of debugging symbols. The binary needs to use position-independent code because the compiler doesn’t know exactly where the code will reside in memory once dyld has done its business.

Having solid knowledge of how an application interacts with a framework can also give you insight into how the application works itself. For example, if a stripped application is using a UITableView, I’ll set breakpoint queries in certain methods in UIKit to determine what code is responsible for the UITableViewDataSource.

Often when I’m exploring a dynamic framework, I’ll simply load it into the processes address space and start running various image lookup queries (or my custom LLDB lookup command available at https://github.com/DerekSelander/lldb) to see what the module holds.

From there, I’ll execute various interesting methods that look like they’d be fun to play around with.

Here’s a nice little LLDB command regex you might want to stick into your ~/.lldbinit file. It dumps Objective-C easily accessible class methods (i.e. Singletons) for exploration.

Add the following to your ~/.lldbinit file.

command regex dump_stuff "s/(.+)/image lookup -rn '\+\[\w+(\(\w+\))?\ \w+\]$' %1 /"

This command, dump_stuff, expects a framework or frameworks as input and will dump Objective-C class methods that have zero arguments. This definitely isn’t a catch-all for all Objective-C naming conventions, but is a nice, simple command to use for a quick first pass when exploring a framework.

Load this command into the active LLDB session and then give it a go with the framework.

(lldb) command source ~/.lldbinit
(lldb) dump_stuff CoreBluetooth

You might find some amusing methods to play around with in the output…

If you jumped chapters and have that clueless face going on for the image lookup command, check out Chapter 7, “Image”. You will add some helper LLDB command regex’s from the private introspection methods found in that chapter.

Add the following commands to your ~/.lldbinit file as well:

command regex ivars 's/(.+)/expression -lobjc -O -- [%1 _ivarDescription]/'

This will dump all the ivars of a inherited NSObject instance.

command regex methods 's/(.+)/expression -lobjc -O -- [%1 _shortMethodDescription]/'

This will dump all the methods implemented by the inherited NSObject instance, or the class of the NSObject.

command regex lmethods 's/(.+)/expression -lobjc -O -- [%1 _methodDescription]/'

This will recursively dump all the methods implemented by the inherited NSObject and recursively continue on to its superclass.

Using these commands it’s quite easy to load, scan, and inspect interesting classes from different frameworks.

For example, you might choose to inspect classes found in the UIPhotos Framework. You can do the following:

(lldb) process load PhotosUI.framework/PhotosUI

From there, dump the class methods with no arguments:

(lldb) dump_stuff PhotosUI

Explore the methods and ivars found in the PUScrubberSettings class:

(lldb) ivars [PUScrubberSettings sharedInstance] 
<PUScrubberSettings: 0x7ffb12818fb0>:
in PUScrubberSettings:
  _usePreviewScrubberMargins (BOOL): NO
  _useTrianglePositionIndicator (BOOL): NO
  _useSmoothingAnimation (BOOL): NO
  _dynamicSeekTolerance (BOOL): YES
  _previewInteractiveLoupeBehavior (unsigned long): 2
  _interactiveLoupeBehavior (unsigned long): 0
  _tapAnimationDuration (double): 0.5
...

Or perhaps you’re just curious about what dynamic methods this class implements:

(lldb) methods PUScrubberSettings
<PUScrubberSettings: 0x11f80fc48>:
in PUScrubberSettings:
  Class Methods:
    + (id) sharedInstance; (0x11f57092b)
    + (id) settingsControllerModule; (0x11f570be8)
  Properties:
    @property (nonatomic) unsigned long previewInteractiveLoupeBehavior;  (@synthesize previewInteractiveLoupeBehavior = _previewInteractiveLoupeBehavior;)
    @property (nonatomic) BOOL usePreviewScrubberMargins;  (@synthesize usePreviewScrubberMargins = _usePreviewScrubberMargins;)

Or get all the methods available through this class and superclasses:

(lldb) lmethods PUScrubberSettings

Note: You only explored the frameworks in the public frameworks directory System/Library/Frameworks. There are many other fun frameworks to explore in other subdirectories starting in System/Library. For example, you’ll find some entertainment in System/Library/PrivateFrameworks

Loading frameworks on an actual iOS device

If you have a valid iOS developer account, an application you’ve written, and a device, you can do the same thing you did on the simulator but on the device. The only difference is the location of the System/Library path. If you’re running an app on the simulator, the public frameworks directory will be located at the following location:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/

But some super-observant readers might say, “Wait a second, using otool -L on the simulator gave us /System/Library/Frameworks as the absolute path, not that big long path above. What gives?”

Remember how I said dyld searches a specific set of directories for these frameworks? Well, there’s a special simulator-specific version named dyld_sim, which looks up the proper simulator location. This is the correct path where these frameworks reside on an actual iOS device. So if you’re running on an actual iOS device, the frameworks path will be located at:

/System/Library/Frameworks/

But wait, I hear some others say, “What about sandboxing?” The iOS kernel has different restrictions for different directory locations. In iOS 12 and earlier, the /System/Library/ directory is readable by your process!

This makes sense because your process needs to call the appropriate public and private frameworks from within the processes address space. If the Sandbox restricted reading of these directories, then the app wouldn’t be able to load them in and then the app would fail to launch.

You can try this out by getting Xcode up and running and attached to any one of your iOS applications. While LLDB is attached to an iOS device, try running ls on the root directory:

(lldb) ls /

Now try the /System/Library/ directory:

(lldb) ls /System/Library/

Some directories will fail to load. This is the kernel saying “Nope!” However, some directories can be dumped.

You have the power to look at live frameworks and dynamically load them inside your app so you can play with and explore them. There are some interesting and powerful frameworks hidden in the /System/Library subdirectories for you to explore on your iOS, tvOS or watchOS device.

Where to go from here?

That /System/Library directory is really something. You can spend a lot of time exploring the different contents in that subdirectory. If you have an iOS device, go explore it!

In this chapter, you learned how to load and execute frameworks through LLDB. However, you’ve been left somewhat high and dry for figuring out how to develop with dynamically loaded private frameworks in code. In the next two chapters, you’ll explore loading frameworks at runtime through code using Objective-C’s method swizzling, as well as function interposition, which is a more Swifty-style strategy for changing around methods at runtime.

This is especially useful if you were to pull in a private framework. I think it’s one of the most exciting things about reverse engineering Apple software.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.