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
- SandBlaster: Reversing the Apple Sandbox - The original paper on SandBlaster
- vs com.apple.security.sandbox - Slides from argp's presentation on the sandbox (2019)
- macOS and iOS Internals, Volume 3, Chapter 8: The Sandbox, by Jonathan Levin
- Hack in the (sand)Box - Slides from a Jonathan Levin's presentation (2016)
- The Sandbox Toolkit - Materials from Jonathan Levin's presentation (2016)
- Tweet about the "protobox" profiles by argp
- Apple's Sandbox Guide v1.0 by fG! (2011)