Blog

Research and Musings

Extracting Sandbox Profiles on iOS with SandBlaster

Or: SandBlaster: The Missing README

In August of 2023, Cellebrite pushed a fork of a tool called SandBlaster, which decompiles iOS sandbox profiles from a proprietary binary format to a human-readable format. While running the tool is straightforward, the documentation doesn't explain where or how to obtain the binary blobs that contain all of the sandbox rules, so we hope to fix that now.

The original sandblaster, from malus-security only supports iOS 4-12, and Cellebrite's fork adds support for 16.5 and 17.0 beta.

We'll start with some quick background on sandbox profiles, go over some historical tooling for extracting the blobs, and finally walk through extracting from the iOS 16.5 kernelcache and decompiling with SandBlaster.

Sandbox Profiles 101

For the iOS sandbox to appropriately restrict what each process can do, a set of profiles is built-in to the sandbox kernel extension in a binary format. Each profile defines a set of rules for various operations such as file I/O, IPC, networking, system calls, etc. For example, here's a simple profile, nointernet.sb:

(version 1)
(allow default)
(deny file-write-setugid)
(deny network*
	(local ip "*:*"))
(deny storage-class-map)

The profiles are implemented in a variant of Scheme known as SandBox Profile Language (SBPL), where each rule starts with either allow or deny, followed by an operation and any arguments (such as modifiers and filters). A default rule is required (in this case allow), which determines whether the profile starts restrictive and defines exceptions, or starts permissive and defines restrictions.

On iOS 16.5, there are 274 sandbox profiles, ranging from very simple (nointernet.sb, 113 bytes) to extremely complex (platform.sb, 1.3MB). platform.sb is essentially the default or baseline profile for all processes, and then an additional/more specific profile is applied on top.

In order to examine the profiles to find weaknesses or interesting attack surface, it's first necessary to extract the binary profile data and decompile to something readable.

How it used to be...

There are a couple of existing tools for extracting the data automatically, primarily iExtractor from Malus Security and sandbox_toolkit from Stefan Esser. Jonathan Levin mentions in his book macOS and iOS Internals, Volume III: Security and Insecurity that his tool joker is capable of dumping and decompiling profiles, but then recommends SandBlaster anyway, so we didn't test this. It looks like joker has been replaced with jtool2, but that doesn't appear to have this support unless it's hidden away somewhere.

The necessary information used to be split between userspace (/usr/libexec/sandboxd) and the kernel (com.apple.security.sandbox kext), so Stefan Esser's extract_sbprofiles parses the sandboxd binary. Entrusting a userspace process with such responsibility had some drawbacks, of course, so in modern times it's all in the kernel extension (and protected with KTRR to prevent shenanigans).

Since these tools don't work on current versions, we'll have to do a little reverse engineering and figure out how to extract the blobs ourselves.

Sandboxectomy on iOS 16.5

Fortunately, we have a treasure map from argp's great slide deck on the sandbox, although the island has been terraformed a little bit. This slide contains the general idea:

The kext that it's referring to is com.apple.security.sandbox, so we'll need to get a copy of that first. For this example, we'll be looking at iOS 16.5 for the iPhone XS, and we'll need the kernelcache. The ipsw utility by Blacktop is the go-to for this, since it can smartly download only the kernel instead of the entire iOS firmware image:

$ ipsw download ipsw -d "iPhone11,2" -b "20F66" --kernel
   • Parsing remote IPSW       build=20F66 device=iPhone11,2 signed=false version=16.5
   • Extracting remote kernelcache
      • Created 20F66__iPhone11,2/kernelcache.release.iPhone11,2_4_6
$

It can also then extract the sandbox kext:

$ ipsw kernel extract 20F66__iPhone11,2/kernelcache.release.iPhone11,2_4_6 com.apple.security.sandbox
   • Created 20F66__iPhone11,2/com.apple.security.sandbox
$ file 20F66__iPhone11,2/com.apple.security.sandbox
20F66__iPhone11,2/com.apple.security.sandbox: Mach-O 64-bit kext bundle arm64e
$

It's important to note that this extracted kext won't have any of its imports/dependencies resolved, so reverse-engineering will be somewhat complicated. Fortunately, for this process, the single kext is sufficient, but for deeper analysis it's probably better to pop the entire kernelcache into your disassembler of choice.

With the extracted com.apple.security.sandbox kext loaded into Binary Ninja (or equivalent tool), search the strings for "builtin collection" and going to the first entry:

The string has two cross-references:

The function being called is most like _profile_create (although the arguments may have changed a bit), and the enclosing function is most likely the _hook_policy_init function named on the slide. Here's the function call in full, in Binary Ninja's pseudo-C:

x0_34 = _profile_create(&data_fffffff007cfb768, "builtin collection", &data_fffffff0076d6be0, 0xcf8c3, &data_fffffff009f93fa8);

arg4 looks like it could be a size, and arg3 points into the __const section to a blob that starts like this:

Comparing with this slide we can see that the first couple of bytes match the "iOS version magic" of 0x8000 (assuming little-endian uint16_t):

So now we can extract this blob into a binary file by selecting the first byte, right-clicking, and selecting "Selection->Extend by bytes", entering the size (0xcf8c3 in this case), then right-clicking again to select "Selection->Save To...". Save it as collection.bin.

Going back to the _hook_policy_init and scrolling up a bit, we can see another call to _profile_create with another string of "protobox collection" and size of 0x4fe81. Repeat to process to export this data into protobox.bin.

One more blob to grab: the platform profile, which is loaded slightly differently, at least in this version of iOS. Going back to the original call to _profile_create and scrolling down a little bit, there's this sequence:

data_fffffff007cfb8f0 = &data_fffffff0076bd1f0;
data_fffffff007cfb8f8 = 0x19389;
data_fffffff007cfb934 = 0;
v0_3 = 0x2c;
*(uint64_t*)((char*)v0_3)[8] = 0x2c;
data_fffffff007cfb920 = v0_3;
data_fffffff007cfb938 = 0;
data_fffffff007cfb93b = 0;
__builtin_memcpy(&data_fffffff007cfb908, "\x2c\x00\x00\x00\x00\x00\x00\x00\xa0\x01\x00\x00\x00\x00\x00\x00\x58\xb9\x00\x00\x00\x00\x00\x00", 0x18);
int64_t x0_35 = sub_fffffff009f85ab8(&data_fffffff007cfb8f0, 0x20, 6);

This jumped out because of the constant 0x19389, which looks similar to the sizes of the other profile collections. Following data_fffffff0076bd1f0 shows a blob that still looks like a compiled sandbox profile:

The first couple of bytes don't have the 0x8000 header, but the fifth byte is 0xBA, which matches the slide for the packed profile structure. Another hint that this is the platform sandbox comes from the error handling:

data_fffffff007cfb8f0 = &data_fffffff0076bd1f0;
data_fffffff007cfb8f8 = 0x19389;
data_fffffff007cfb934 = 0;
v0_3 = 0x2c;
*(uint64_t*)((char*)v0_3)[8] = 0x2c;
data_fffffff007cfb920 = v0_3;
data_fffffff007cfb938 = 0;
data_fffffff007cfb93b = 0;
__builtin_memcpy(&data_fffffff007cfb908, "\x2c\x00\x00\x00\x00\x00\x00\x00\xa0\x01\x00\x00\x00\x00\x00\x00\x58\xb9\x00\x00\x00\x00\x00\x00", 0x18);
int64_t x0_35 = sub_fffffff009f85ab8(&data_fffffff007cfb8f0, 0x20, 6);
      
if (x0_35 != 0)
{
label_fffffff009f7c5b8:
	char const* const var_298_1 = "kext.c";
	int64_t var_290_1 = 0x171f;
	int64_t var_2a0_4 = x0_35;
	&data_fffffff00842f504("failed to initialize platform sandbox: %d\" @%s:%d");

So, go ahead and extract 0x19389 bytes from data_fffffff0076bd1f0 and save as platform.bin. We're almost ready to decompile these!

Sandbox Operations

The final piece we need is the list of sandbox operations, which is simply an array of strings. In the compiled profiles, each operation is stored as an integer. For example, the network* operation in the nointernet.sb profile maps to integer 0x6D (109 in decimal). These operations may shift around between iOS versions, so we need to grab the correct list for this version.

The simplest path is probably to check the Strings view for "default":

Which goes to a lengthy list of strings:

Select the entire list (in this case ending with xpc-message-send and don't forget the null byte at the end) and save it to a file operations.bin. SandBlaster needs this in a one-per-line format, so in the terminal run this to convert those null-terminators to newlines:

$ tr '\0' '\n' < operations.bin > operations.txt
$ head operations.txt
default
appleevent-send
authorization-right-obtain
boot-arg-set
device*
device-camera
device-microphone
darwin-notification-post
distributed-notification-post
dynamic-code-generation

Now we're ready to decompile!

Running SandBlaster

First, clone Cellebrite's fork and create a Python virtual environment for it:

$ git clone https://github.com/cellebrite-labs/sandblaster.git
$ cd sandblaster
$ python3 -m venv ENV
$ source ENV/bin/activate

SandBlaster requires the tqdm Python library, so install that in the virtualenv:

$ pip install tqdm
Collecting tqdm
  Using cached tqdm-4.66.4-py3-none-any.whl.metadata (57 kB)
Using cached tqdm-4.66.4-py3-none-any.whl (78 kB)
Installing collected packages: tqdm
Successfully installed tqdm-4.66.4
$ cd reverse-sandbox
$ python reverse_sandbox.py --help
usage: reverse_sandbox.py [-h] -r RELEASE -o OPERATIONS_FILE [-p PROFILE [PROFILE ...]] [-n OPERATION [OPERATION ...]] [-d DIRECTORY] [-psb]
                          [-kbf] [-c] [-m]
                          filename

positional arguments:
  filename              path to the binary sandbox profile

options:
  -h, --help            show this help message and exit
  -r RELEASE, --release RELEASE
                        iOS release version for sandbox profile
  -o OPERATIONS_FILE, --operations_file OPERATIONS_FILE
                        file with list of operations
  -p PROFILE [PROFILE ...], --profile PROFILE [PROFILE ...]
                        profile to reverse (for bundles) (default is to reverse all operations)
  -n OPERATION [OPERATION ...], --operation OPERATION [OPERATION ...]
                        particular operation(s) to reverse (default is to reverse all operations)
  -d DIRECTORY, --directory DIRECTORY
                        directory where to write reversed profiles (default is current directory)
  -psb, --print_sandbox_profiles
                        print sandbox profiles of a given bundle (only for iOS versions 9+)
  -kbf, --keep_builtin_filters
                        keep builtin filters in output
  -c, --c_output        output a C file rather than Scheme
  -m, --macho           generate a reversible Mach-O file (implies --c_output)

Now we're good to go, let's start with the platform profile. One important note: the tool expects that the output directory already exists, so make sure to create it first, then provide the iOS version (just "16" in this case), path to the operations file, output directory, and path to the profile blob:

$ mkdir platform
$ python3 reverse_sandbox.py -r 16 -o ../../20F66__iPhone11,2/operations.txt -d platform ../../20F66__iPhone11,2/platform.bin

                struct_size: 0xe
                header: 0x0
                op_nodes_count: 0x16f7
                sb_ops_count: 0xba
                vars_count: 0x6
                states_count: 0x0
                num_profiles: 0x0
                re_table_count: 0x9
                entitlements_count: 0x0

                regex_table_offset: 0xe
                pattern_vars_offset: 0x20
                states_offset: 0x2c
                entitlements_offset: 0x2c
                profiles_offset: 0x2c
                profiles_end_offset: 0x2c
                operation_nodes_offset: 0x1a0
                operation_nodes_size: 0xb7b8
                base_adrr: 0xb958

2024-08-03 10:52:17,526 - __main__ - INFO - num_sb_ops: 192
2024-08-03 10:52:17,539 - __main__ - INFO - [['^/private/var/mobile/Library/Safari/Bookmark', '^/private/var/mobile/Library/Safari/com[.]apple[.]Bookmark', '^/private/var/euser[0-9]+/Library/Safari/(com[.]apple[.])?Bookmark', '^/private/var/[-0-9A-F]+/Library/Safari/(com[.]apple[.])?Bookmark', '^/private/var/Users/[^/]+/Library/Safari/(com[.]apple[.])?Bookmark'], ['^/private/var/mobile/Containers/Data/[^/]+/[^/]+/Library/Application Support/iCloudDrive/[^/]+/session/r/', '^/private/var/mobile/Containers/Data/[^/]+/[^/]+/Library/Application Support/iCloudDrive/[^/]+/session/r$', '^/private/var/euser[0-9]+/Containers/Data/[^/]+/[^/]+/Library/Application Support/iCloudDrive/[^/]+/session/r/', '^/private/var/euser[0-9]+/Containers/Data/[^/]+/[^/]+/Library/Application Support/iCloudDrive/[^/]+/session/r$', '^/private/var/[-0-9A-F]+/Containers/Data/[^/]+/[^/]+/Library/Application Support/iCloudDrive/[^/]+/session/r/', '^/private/var/[-0-9A-F]+/Containers/Data/[^/]+/[^/]+/Library/Application Support/iCloudDrive/[^/]+/session/r$', '^/private/var/Users/[^/]+/Containers/Data/[^/]+/[^/]+/Library/Application Support/iCloudDrive/[^/]+/session/r/', '^/private/var/Users/[^/]+/Containers/Data/[^/]+/[^/]+/Library/Application Support/iCloudDrive/[^/]+/session/r$'], ['^/private/var/mobile/Containers/Shared/AppGroup/[^/]+/File Provider Storage/', '^/private/var/mobile/Containers/Shared/AppGroup/[^/]+/File Provider Storage$', '^/private/var/euser[0-9]+/Containers/Shared/AppGroup/[^/]+/File Provider Storage/', '^/private/var/euser[0-9]+/Containers/Shared/AppGroup/[^/]+/File Provider Storage$', '^/private/var/[-0-9A-F]+/Containers/Shared/AppGroup/[^/]+/File Provider Storage/', '^/private/var/[-0-9A-F]+/Containers/Shared/AppGroup/[^/]+/File Provider Storage$', '^/private/var/Users/[^/]+/Containers/Shared/AppGroup/[^/]+/File Provider Storage/', '^/private/var/Users/[^/]+/Containers/Shared/AppGroup/[^/]+/File Provider Storage$'], ['^/private/var/PersonaVolumes/[^/]+/', '^/private/var/PersonaVolumes/[^/]+$'], ['^/private/var/mobile/Containers/Data/', '^/private/var/mobile/Containers/Shared/AppGroup/', '^/private/var/euser[0-9]+/Containers/(Data|Shared/AppGroup)/', '^/private/var/[-0-9A-F]+/Containers/(Data|Shared/AppGroup)/', '^/private/var/Users/[^/]+/Containers/(Data|Shared/AppGroup)/'], ['^/private/var/Users/[-0-9A-F]+$', '^/private/var/Users/[-0-9A-F]+/$'], ['^/dev/ttys[0-9]', '^/dev/ttys[0-9]+'], ['^/private/var/mobile/Media/iTunes_Control/iTunes/', '^/private/var/mobile/Media/iTunes_Control/iTunes$', '^/private/var/mobile/Media/[^/]+/iTunes_Control/iTunes/', '^/private/var/mobile/Media/[^/]+/iTunes_Control/iTunes$', '^/private/var/euser[0-9]+/Media/([^/]+/)?iTunes_Control/iTunes/', '^/private/var/euser[0-9]+/Media/([^/]+/)?iTunes_Control/iTunes$', '^/private/var/[-0-9A-F]+/Media/([^/]+/)?iTunes_Control/iTunes/', '^/private/var/[-0-9A-F]+/Media/([^/]+/)?iTunes_Control/iTunes$', '^/private/var/Users/[^/]+/Media/([^/]+/)?iTunes_Control/iTunes/', '^/private/var/Users/[^/]+/Media/([^/]+/)?iTunes_Control/iTunes$'], ['^/private/var/mobile/Library/Preferences/com[.]apple[.]dt[.]xctest[.]automation-mode[.]plist$', '^/private/var/euser[0-9]+/Library/Preferences/com[.]apple[.]dt[.]xctest[.]automation-mode[.]plist$', '^/private/var/[-0-9A-F]+/Library/Preferences/com[.]apple[.]dt[.]xctest[.]automation-mode[.]plist$', '^/private/var/Users/[^/]+/Library/Preferences/com[.]apple[.]dt[.]xctest[.]automation-mode[.]plist$']]
2024-08-03 10:52:17,539 - __main__ - INFO - 6 global vars at offset 32
2024-08-03 10:52:17,539 - __main__ - INFO - global variables are FRONT_USER_HOME, HOME, ANY_UUID, ENTITLEMENT:com.apple.trial.client, ENTITLEMENT:com.apple.security.exception.iokit-user-client-class, ENTITLEMENT:com.apple.security.iokit-user-client-class
2024-08-03 10:52:17,539 - __main__ - INFO - number of operation nodes: 5879
2024-08-03 10:52:17,546 - __main__ - INFO - operation nodes
2024-08-03 10:52:17,598 - __main__ - INFO - operation nodes after filter conversion
2024-08-03 10:52:17,598 - __main__ - INFO - number of operation nodes: 5879
$ ls -lh platform/
total 4112
-rw-r--r--  1 chris  staff   1.3M Aug  3 10:52 platform.sb
$ head platform/platform.sb
(version 1)
(allow default)
(deny dynamic-code-generation
	(require-not (entitlement-is-bool-true dynamic-codesigning))
	(require-not (process-attribute is-sandboxed))
	(process-attribute is-protoboxed))
(deny file-chroot
	(require-not (require-ancestor-with-entitlement com.apple.private.security.storage-exempt.heritable))
	(require-all
		(require-not (storage-class-extension 0))
$

And now repeat for the protobox and collection blobs:

$ mkdir 
$ python3 reverse_sandbox.py -r 16 -o ../../20F66__iPhone11,2/operations.txt ../../20F66__iPhone11,2/protobox.bin -d protobox
[snip]
$ python3 reverse_sandbox.py -r 16 -o ../../20F66__iPhone11,2/operations.txt ../../20F66__iPhone11,2/collection.bin -d collection
[snip]
$ ls -l collection | wc -l
274
$ ls -l protobox | wc -l
389

Now you have a complete set of the sandbox profiles for iOS 16.5, ready for further analysis. For additional fun (and/or if you just don't like reading Scheme), try the --macho option to generate a MachO file out of the profiles, which can then be loaded into your disassembler of choice.

Want to learn more about iOS vulnerability research and exploit development? We're preparing to launch our transition course for experienced Windows/Linux exploit developers!

Additional Reading