Rethinking & Repackaging iOS Apps: Part 1
In October 2014, Jonathan Zdziarksi (“JZ”) wrote a blog post about a little-known feature of the iOS app ecosystem: it’s possible to patch App Store apps and redeploy them on to non-jailbroken devices. (You should probably read his post before reading this one.)
This is the first installment of a two-part series in which we will build on JZ’s work to present a more flexible, powerful means of modifying App Store apps on jailed iOS devices. To play along, you will need an Apple iOS account.
iOS Tools on Jailbroken Devices
We’re used to using our favorite tools like CydiaSubstrate and Theos that allow us to write runtime patches in Objective-C and apply them to apps on jailbroken devices. Another stalwart tool is Cycript, which allows us to explore and modify running applications on iOS.
Typically, iOS penetration testers will use jailbroken devices to install and run these tools against App Store apps. However, jailbroken devices aren’t actually necessary. This series will show you how to do the same work on jailed devices, and will provide full source code for a patched version of Theos that does the heavy lifting for you.
Part 1 – Dynamic Libraries
When reading JZ’s blog post, you’ll see that to patch an iOS app, it was necessary to crank up a disassembler, reverse-engineer the binary, and patch it in ARM assembly language before re-signing and re-installing the app. While effective, that technique is slow, cumbersome, and complex.
We wanted something more user-friendly, something where we could write Objective-C code and automatically load it into our App Store apps without disassembling anything. We wanted Theos for jailed devices — but let’s learn to walk before we run.
Without going into too much detail, every iOS app is a Mach-O binary, which contains a header containing “load commands.” These commands tell the dynamic linker (the software responsible for loading an app and the shared libraries which the app depends upon) what shared/dynamic libraries must be loaded before the app can run.
You can see the libraries required by an app by running the otool command:
[email protected] ~> otool -L FooApp FooApp: /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 235.1.0) /usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0) /System/Library/Frameworks/CFNetwork.framework/CFNetwork (compatibility version 1.0.0, current version 711.0.6) /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.5) /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 3318.0.0) /System/Library/Frameworks/Security.framework/Security (compatibility version 1.0.0, current version 0.0.0) /System/Library/Frameworks/QuartzCore.framework/QuartzCore (compatibility version 1.2.0, current version 1.10.0) /System/Library/Frameworks/OpenGLES.framework/OpenGLES (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1140.11.0) /System/Library/Frameworks/CoreVideo.framework/CoreVideo (compatibility version 1.2.0, current version 1.8.0) /System/Library/Frameworks/CoreMedia.framework/CoreMedia (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/CoreGraphics.framework/CoreGraphics (compatibility version 64.0.0, current version 600.0.0) /System/Library/Frameworks/AVFoundation.framework/AVFoundation (compatibility version 1.0.0, current version 2.0.0) /System/Library/Frameworks/AudioToolbox.framework/AudioToolbox (compatibility version 1.0.0, current version 492.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 1213.0.0) /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1140.1.0)
You can see that the app loads the CFNetwork framework; here’s the corresponding load command shown in the Hopper disassembler:
Given this knowledge, all we need to do is:
- Create a custom shared library (.dylib)
- Patch the target app’s Mach-O header to insert a new load command for our .dylib
- Sign the app
- Redeploy the app to a jailed device
Creating a Custom Shared Library (.dylib)
This is easy. Take the following Objective-C code and save it as test.m:
Then compile it as an ARM .dylib:
clang -arch armv7 -isysroot $(xcodebuild -sdk iphoneos -version Path) -shared test.m -framework Foundation -o test.dylib
Voila! You now have test.dylib that will automatically call injected_function() whenever it is loaded by an iOS app.
Inserting a New Load Command into the Target App
Fortunately, we don’t have to modify the target app’s Mach-O load commands by hand. There’s a tool for that: optool.
Assuming you’ve already decrypted and copied the target app off a jailbroken device (you did read JZ’s blog post, right?), you can run optool to insert the new load command:
optool install -c load -p "@executable_path/test.dylib" -t /path/to/your/Application.app/binary
And it runs:
Found thin header... Inserting a LC_LOAD_DYLIB command for architecture: arm Successfully inserted a LC_LOAD_DYLIB command for arm Writing executable to /path/to/your/Application.app/binary...
That’s it! The load command has been inserted into the app. Note the use of the magic value “@executable_path,” which tells the dynamic linker to look for the .dylib in the same directory as the app binary. You can now add your new .dylib to the app’s main directory:
cp test.dylib /path/to/your/Application.app/
Next, re-sign everything with your Apple developer certificate:
codesign -fs "iPhone Developer" /path/to/your/Application.app/test.dylib codesign -fs "iPhone Developer" /path/to/your/Application.app
Finally, re-ZIP the .app folder, save the ZIP as an IPA, and publish it to your device using Xcode (see JZ’s blog for full instructions). The next time you run the app, it will automatically load test.dylib, which will run injected_function() from test.dylib. Text.dylib writes “Testing!!” to the system log before continuing to normally run the app.
In Part 2…
Now that we have discussed the essentials of adding dynamic libraries to App Store apps, we can refine the technique to make our favorite tools – CydiaSubstrate, Theos, and Cycript – work with jailed devices. We’ll cover:
- Patching CydiaSubstrate to load and work on jailed devices
- Patching Theos to generate .dylibs compatible with jailed devices
- Adding functionality to enable function hooking on jailed devices
- Embedding Cycript into apps on jailed devices
- MSFunctionHook() doesn’t work on jailed devices because the kernel prohibits memory pages with PROT_EXEC|PROT_WRITE flags
- We work around this by abusing Mach-O lazy binding to replace function pointers
NOTE: The source code for the final product is available here; you can also see the second installment of this series for further details.
Please note that it’s a work-in-progress. We welcome any bug reports and patches.
Subscribe to Bishop Fox's Security Blog
Be first to learn about latest tools, advisories, and findings.
Thank You! You have been subscribed.