Dump iOS apps in Javascript (Part I)

The very first step of iOS app penetration is to dump decrypted binary from app store, with a jailbroken device and some debugging tools. If you have already done this before, you must be familiar with the following tools:

The decryption is automatically done by the operating system, so all we need is to locate the right memory range and reconstruct the MachO.

If the app is from AppStore, it probably will have the LC_ENCRYPTION_INFO(_64)? load command, marking the encryption state and the actual protected offset and size. The decryption tools search for this command, copy plain code from running process and patch this flag to mark the binary as plain.

hello:~ root# ps aux | grep WordPress
mobile 1151 0.0 3.6 1837760 111664 ?? Ts 10:47PM 0:49.28 /var/containers/Bundle/Application/7C8E9F26-BF79-48FD-BFB0-2069035D5836/WordPress.app/WordPress
root 1258 0.0 0.0 1050496 272 s000 R+ 1:03AM 0:00.01 grep WordPress
hello:~ root# jtool -l /var/containers/Bundle/Application/7C8E9F26-BF79-48FD-BFB0-2069035D5836/WordPress.app/WordPress
LC 11: LC_MAIN Entry Point: 0x86d8 (Mem: 0x1000086d8)
LC 12: LC_ENCRYPTION_INFO_64 Encryption: 1 from offset 16384 spanning 8781824 bytes
LC 13: LC_LOAD_DYLIB /usr/lib/libxml2.2.dylib

However, here are some points I really want to improve:

  1. dumpdecrypted may be the earliest one. Inspiring and original. But it only dump the executable itself, without the frameworks and not to mention xpc services.
  2. Clutch claims it supports decrypting App Extensions but it’s implemented on posix_spawn(https://github.com/KJCracks/Clutch/blob/abdde3/Clutch/Framework64Dumper.m#L159). I doubt that this may not work on iOS 11.
  3. They both require SSH. It’s acceptable for handy work, but I prefer solutions without a SSH client when developing my own tools.

Actually the idea was stolen here: https://codeshare.frida.re/@lichao890427/dump-ios/

This guy ported some hand written MachO parsing to frida! The snippet looks a bit crazy. Does it remind you of browser exploits? Arbitrary memory access in JavaScript…

But since there’s already a node module for MachO parsing, I don’t have to start everything from that scratch.

It is designed for NodeJS and based on buffer module, which means it’s not compatible with frida’s Memory access api. Really?

type mapping

Buffer.readInt (nodejs) and Memory.readInt (frida) acts the same. So I wrote an adapter to handle the byte order and match more types. It works!


So now we have the solution for dumping any framework or main binary from current process.

What about the App Extensions? iOS App Extension has been introduced since iOS 8. Currently lots of apps uses it for sharing, TodayWidget, iMessage, etc.

Various App Extensions on iOS

Starting from iOS 11 there’s an extra entitlement platform-application requirement, without whom the application can’t be execute from command line. The main executable can be launched by SpringBoard, but App Extensions seem to have their own way.

Although these PlugIns are lying on the same directory, they actually have their own process, and even isolated sandbox from main process!

hello:/var/containers/Bundle/Application/7C8E9F26-BF79-48FD-BFB0-2069035D5836/WordPress.app root# ls PlugIns/
WordPressDraftActionExtension.appex WordPressNotificationServiceExtension.appex WordPressTodayWidget.appex
WordPressNotificationContentExtension.appex WordPressShareExtension.appex

Bazad has exploited this mechanism to achieve multi-process on iOS: https://github.com/bazad/presentations/blob/master/beVX-2018-Crashing-to-root.pdf

So we have a clue from this article, that the @interface NSExtension api have the ability to launch external extension process via IPC.

NSError *error;
NSExtension *extension = [NSExtension extensionWithIdentifier:@"com.example.App.AppExtension" error:&error];

// This block will be called if the extension calls [context completeRequestReturningItems:completionHandler:]
[extension setRequestCompletionBlock:^(NSUUID *uuid, NSArray *extensionItems) {
NSLog(@"Request %@ completed. Extension items: %@", uuid, extensionItems);

I ported the snippet to frida REPL, played several apps, and found that org.wordpress.WordPressShare can be launched inside WordPress app this way, but org.wordpress.WordPressTodayWidget failed. That’s weird.

After hitting some breakpoints on some XPC related functions, I finally found the daemon responsible for launching extension processes:pkd.

Every App Extensions have some attributes to define where they are used for, and which client is allowed. For example, NSExtensionPointName = “com.apple.intents-ui-service”; means it’s for Siri Intents, access to the point requires com.apple.intents.uiextension.discovery entitlement. The following rule is copied from WordPress app, showing there’s even a query DSL to filter client (well, where there’s a interpreter, there may be bugs…😁):

"NSExtensionAttributes" : {
"NSExtensionActivationRule" : "\n SUBQUERY(\n extensionItems,\n $extensionItem,\n (SUBQUERY(\n $extensionItem.attachments,\n $attachment,\n ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"public.image\"\n || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"public.png\"\n || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"public.jpeg\"\n || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"public.jpeg-2000\"\n || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"public.tiff\"\n || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"com.compuserve.gif\"\n || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"com.microsoft.bmp\"\n ).@count < 3\n OR\n SUBQUERY(\n $extensionItem.attachments,\n $attachment,\n ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"public.plain-text\"\n || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"public.text\"\n ).@count == 1\n OR\n SUBQUERY(\n $extensionItem.attachments,\n $attachment,\n ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"public.url\"\n || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"public.file-url\"\n ).@count == 1)\n AND\n SUBQUERY(\n $extensionItem.attachments,\n $attachment,\n ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"org.appextension.find-login-action\"\n || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO \"org.appextension.save-login-action\"\n ).@count == 0\n ).@count >= 1\n ",
"NSExtensionJavaScriptPreprocessingFile" : "WordPressShare"

Function pkd`-[PKDPlugIn allowForClient:] is responsible for the check. It checks com.apple.developer.pluginkit.allowedplugins entitlement first. Then the method -[PKDPlugIn match:] checks for app developer defined rules. Only the matching client can successfully invoke [NSExtension extensionWithIdentifier:] to get a communication endpoint.

Finally pkd will start the XPC service via launch_add_external_service.

On jailbroken devices, I can inject to pkd and patch this method to return YES. Now all the App Extensions are running and injectable:

plugin org.wordpress.WordPressNotificationContentExtension, pid=1280
plugin org.wordpress.WordPressShare, pid=1281
plugin org.wordpress.WordPressNotificationServiceExtension, pid=1282
plugin org.wordpress.WordPressTodayWidget, pid=1283
plugin org.wordpress.WordPressDraftAction, pid=1284

The part two will be about frida script development, like to leverage iOS private API and write a plain JavaScript streaming file transfer without SSH dependency.

Anyways, the code tells everything. Happy hacking!