Sandbox.kext, entitlements & MACF
Sandbox’s MACF label slot is a struct “sandbox”
its slot is 1, and it’s not always present
#define EXT_TABLE_SIZE 9
struct sandbox {
/* 0x00 */ void* profile;
/* 0x08 */ extension_hdr_t ext_table[EXT_TABLE_SIZE]; // zeroed on create
/* 0x58 */ lck_rw_t ext_lck;
/* 0x68 */ uint32_t unk2; // unk2 = (unk2&0xe0)|0x10 on create
/* 0x6c */ uint32_t pad0; // or not pad :)
/* 0x70 */ uint64_t unk3; // zeroed on create
/* 0x78 */ uint64_t unk4; // zeroed on create
/* 0x80 */ lck_rw_t lck1;
/* 0x90 */ uint64_t unk5; // zeroed on create
/* 0x98 */ uint64_t unk6; // 0x128 + some profile name thing
/* 0xa0 */ uint64_t unk7;
/* 0xa8 */ uint32_t refcount; // = 1 on create
/* 0xac */ uint32_t pad1;
/* 0xb0 */ uint64_t unk8; // zeroed on create
/* 0xb8 */ lck_rw_t lck2;
/* 0xc8 */
}
What we’re most interested in is ext_table
There is a normal “extension”, and what I call “extenstion_hdr”
This is a three level structure
-
Array of EXT_TABLE_SIZE headers hashing on “key” is performed to get index in that array
-
extension_hdr’s form a linked list extension_hdr.desc is pointer to c-string long story short – that string is just entitlement which was source of extensions ext_lst points to next level
-
extensions form a linked list too for whatever reason extension and extension_hdr have same structure, and extension struct has redurant members:
- it’s desc is always 0xffffffffffffffff
- it’s ext_lst is zero
- there are likely reserved feilds, or I just haven’t found usage of them – I’ve named them “something” data points to c string which is actual value data_len is its length
struct extension_hdr {
/* 0x00 */ extension_hdr_t next;
/* 0x08 */ const char* desc;
/* 0x10 */ extension_t ext_lst;
/* 0x18 */
};
typedef struct extension_hdr* extension_hdr_t;
struct extension {
/* 0x00 */ extension_t next;
/* 0x08 */ uint64_t desc; // always 0xffffffffffffffff
/* 0x10 */ uint64_t ext_lst; // zero, since it's extension and not a header
/* 0x18 */ uint8_t something[32]; // zeroed from what I've seen
/* 0x38 */ uint32_t type; // see ext_type enum
/* 0x3c */ uint32_t subtype; // either 0 or 4 (or whatever unhex gave?..)
/* 0x40 */ void *data; // a c string, meaning depends on type and hdr which had this extension
/* 0x48 */ uint64_t data_len; // strlen(data)
/* 0x50 */ uint64_t unk0; // always 0
/* 0x58 */ uint64_t unk1; // always 0xdeadbeefdeadbeef
/* 0x60 */
};
typedef struct extension* extension_t;
enum ext_type {
ET_FILE = 0,
ET_MACH = 1,
ET_IOKIT_REG_ENT = 2,
ET_POSIX_IPC = 4,
ET_PREF_DOMAIN = 5, // inlined in issue_extension_for_preference_domain
ET_SYSCTL = 6, // inlined in issue_extension_for_sysctl
};
3 is assigned only in one place – in _hook_policy_syscall
3 is special, because it’s handled differently, see extension_destroy
There are some “tracking” memory management functions which are used everywhere:
void* smalloc_track(sandbox*, size_t size);
void* sstrdup_track(sandbox*, const char* str);
void sfree(void* ptr);
Now, when we’re somewhat familiar with “extension”s, let’s see how are they created
typedef int (*)(sandbox *, sb_creation_context *, char const*, void *) ent_exc_handler_t;
struct handler_map_entry {
const char* entitlement_cstr;
ent_exc_handler_t func;
const OSSymbol* entitlement_sym;
}
There’s an array of handler_map_entry’s (Obviously it’s in const region).
const handler_map_entry handler_map[] = {
// ...
{
"com.apple.security.exception.iokit-user-client-class",
issue_extension_for_iokit_registry_entry,
NULL
},
// ...
}
On sandbox’s kmod_start it initializes entitlement_sym’s:
struct handler_map_entry* entry = handler_map;
while (entry->func != 0) {
entry->entitlement_sym = OSSymbol::withCStringNoCopy(entry->entitlement_cstr);
++entry;
}
and another place where it’s used is cred_label_update_execve hook:
OSDictionary* entitlements = AppleMobileFileIntegrity::copyEntitlements(ucred);
struct handler_map_entry* entry = handler_map;
while (entry->func != 0) {
OSObject* val = entitlements->getObject(entry->entitlement_sym);
if (val != 0) {
iterate_entitlement_value_strings(
sbx, ctx, entry->entitlement_cstr,
val, entry->entitlement_cstr,
entry->func
);
}
++entry;
}
entitlements->free();
So, the flow looks like this:
- hook_cred_label_update_execve
- iterate_entitlement_value_strings
- issue_extension_for_X
-
extension_create_X && extension_add extension_destroy
See more manually rewritten from disasm code below with some comments :)
// most functions return 0xC on error :
#define RET_ERR 0xC
#define RET_OK 0x0
int extension_create(extension_t* saveto, sandbox* sb) {
extension_t ext = (extension_t) smalloc_track(sb, sizeof(extension_t));
*saveto = ext;
if (ext == NULL) {
return RET_ERR;
}
ext->desc = 0xFFFFFFFFFFFFFFFF;
bzero(&ext->something, sizeof(ext->something));
return RET_OK;
}
void extension_destroy(extension_t ext) {
switch(ext->type) {
case ET_FILE:
case ET_MACH:
case ET_IOKIT_REG_ENT:
case ET_POSIX_IPC:
case ET_PREF_DOMAIN:
case ET_SYSCTL:
sfree(ext->data);
// fallthrough
default:
sfree(ext);
}
}
// subtype is 0 for "com.apple.security.application-groups"
int extension_create_file(extension_t* saveto, sandbox* sb, const char* path, size_t path_len, uint32_t subtype) {
int ret;
ret = extension_create(saveto, sb);
if (ret) {
return ret;
}
extension_t ext = *saveto;
ext->type = ET_FILE;
ext->subtype = subtype;
ext->data = smalloc_track(sb, path_len);
if (ext->data == NULL) {
sfree(ext);
*saveto = NULL;
return RET_ERR;
}
memcpy(ext->data, path, path_len);
// ; x22 is saveto
// ; x19 is path_len
// ; [X22,#0x40] is (*saveto)->data
// ; i dont understand this
// LDR X8, [X22,#0x40]
// STRB WZR, [X8,X19]
ext->data_len = path_len;
ext->unk0 = 0;
return RET_OK;
}
// for entitlement
// "com.apple.security.exception.mach-(lookup|register).(local|global)-name"
// subtype is 0
// --- or ---
// for entitlement
// "com.apple.security.application-groups"
// subtype is 4
int extension_create_mach(extension_t* saveto, sandbox* sb, const char* name, uint32_t subtype) {
int ret;
ret = extension_create(saveto, sb);
if (ret) {
return ret;
}
extension_t ext = *saveto;
ext->type = ET_MACH;
ext->subtype = subtype;
ext->data = sstrdup_track(sb, path_len);
if (ext->data == NULL) {
sfree(ext);
*saveto = NULL;
return RET_ERR;
}
ext->data_len = strlen(ext->data);
return RET_OK;
}
// for entitlement
// "com.apple.security.application-groups"
// subtype is 4
int extension_create_posix_ipc(extension_t* saveto, sandbox* sb, const char* name, uint32_t subtype) {
int ret;
ret = extension_create(saveto, sb);
if (ret) {
return ret;
}
extension_t ext = *saveto;
ext->type = ET_POSIX_IPC;
ext->subtype = subtype;
ext->data = sstrdup_track(sb, path_len);
if (ext->data == NULL) {
sfree(ext);
*saveto = NULL;
return RET_ERR;
}
ext->data_len = strlen(ext->data);
return RET_OK;
}
// for entitlement
// "com.apple.security.exception.iokit-user-client-class"
// subtype is 0
int extension_create_iokit_registry_entry_class(extension_t* saveto, sandbox* sb, const char* name, uint32_t subtype) {
int ret;
ret = extension_create(saveto, sb);
if (ret) {
return ret;
}
extension_t ext = *saveto;
ext->type = ET_IOKIT_REG_ENT;
ext->subtype = subtype;
ext->data = sstrdup_track(sb, path_len);
if (ext->data == NULL) {
sfree(ext);
*saveto = NULL;
return RET_ERR;
}
ext->data_len = strlen(ext->data);
return RET_OK;
}
// get 64 higher bits of 64bit int multiplication
// https://stackoverflow.com/a/28904636
// ofc in asm it's done with 1 instruction huh
uint64_t mulhi(uint64_t a, uint64_t b) {
uint64_t a_lo = (uint32_t)a;
uint64_t a_hi = a >> 32;
uint64_t b_lo = (uint32_t)b;
uint64_t b_hi = b >> 32;
uint64_t a_x_b_hi = a_hi * b_hi;
uint64_t a_x_b_mid = a_hi * b_lo;
uint64_t b_x_a_mid = b_hi * a_lo;
uint64_t a_x_b_lo = a_lo * b_lo;
uint64_t carry_bit = ((uint64_t)(uint32_t)a_x_b_mid +
(uint64_t)(uint32_t)b_x_a_mid +
(a_x_b_lo >> 32) ) >> 32;
uint64_t multhi = a_x_b_hi +
(a_x_b_mid >> 32) + (b_x_a_mid >> 32) +
carry_bit;
return multhi;
}
int hashing_magic(const char *desc) {
// inlined into exception_add
uint64_t hashed = 0x1505;
// if desc == NULL, then returned value would be 8
// APPL optimizes it for some reason
// but meh, desc should never be NULL or you get
// null dereference in exception_add
// if (desc == NULL) return 8;
if (desc != NULL) {
for (const char* dp = desc; *dp != '\0'; ++dp) {
hashed += hashed << 5;
hashed += (int64_t) *dp;
}
}
uint64_t magic = 0xe38e38e38e38e38f;
uint64_t hi = mulhi(hashed, magic);
hi >>= 3;
hi = (hi<<3) + hi;
hashed -= hi;
return hashed;
}
int extension_add(extension_t ext, sandbox* sb, const char* desc) {
int ret = RET_OK;
int idx = hashing_magic(desc);
lck_rw_lock_exclusive(sb->ext_lck);
extension_hdr_t insert_at = sb->ext_table[idx];
if (insert_at == NULL) {
insert_at = smalloc_track(sb, sizeof(extension_hdr));
if (insert_at == NULL) {
ret = RET_ERR;
goto end;
}
insert_at->next = NULL;
insert_at->desc = sstrdup_track(sb, desc);
if (insert_at->desc == NULL) {
sfree(insert_at);
ret = RET_ERR;
goto end;
}
insert_at->ext_lst = NULL;
sb->ext_table[idx] = insert_at;
}
while (insert_at->next != NULL) {
// APPL doesn't check for (insert_at->desc != NULL)
// but extension_add is never called with desc=NULL so meh
if (strcmp(insert_at->desc, desc) == 0) {
break;
}
insert_at = insert_at->next;
}
ext->next = insert_at->ext_lst;
insert_at->ext_lst = ext;
ret = 0;
end:;
lck_rw_unlock_exclusive(sb->ext_lck);
return ret;
}
int iterate_entitlement_value_strings(sandbox *sb, sb_creation_context *ctx, const char* entitlement_cstr, const OSObject* val, void *data, ent_exc_handler_t f) {
{
const char *c_val = NULL;
OSString* as_str = OSDynamicCast(OSString, val);
if (as_str != NULL) {
c_val = as_str->getCStringNoCopy();
} else {
OSBoolean* as_bool = OSDynamicCast(OSBoolean, val);
if (as_bool != NULL && as_bool->isTrue()) {
c_val = "true";
}
// false is considered invalid...
}
if (c_val != NULL) {
return (*f)(sb, ctx, c_val, data);
}
}
{
OSArray *as_arr = OSDynamicCast(OSArray, val);
if (as_arr == NULL) {
return 8;
}
unsigned int cnt = as_arr->getCount();
for (unsigned int i = 0; i != cnt; ++i) {
OSObject* obj = as_arr->getObject(i);
OSString* str = OSDynamicCast(OSString, obj);
if (str == NULL) {
// value #%u for entitlement %s is type %s, expected string
return 8;
}
c_val = str->getCStringNoCopy();
int ret = (*f)(sb, ctx, c_val, data);
if (ret) {
return ret;
}
}
return 0;
}
}
// sample handler
int issue_extension_for_iokit_registry_entry(sandbox *sb, sb_creation_context *ctx, const char* entry_name, void *desc) {
extension_t ext;
int ret = extension_create_iokit_registry_entry_class(&ext, sb, entry_name, 0);
if (ret == 0) {
ret = extension_add(ext, sb, desc);
if (ret) {
extension_destroy(ext);
}
}
return ret;
}
Update 2020 05 12
SandBox kext was refactored by Apple in next iOS version.
I’ve never got to finish reversing newer versions, and while looking through
old projects I stumbled into file named sandbox.c
, which is likely my WIP RE
effort for new Sandbox.
I don’t think I’ll get to it anytime soon, so here’s that file.
struct sandbox {
/* 0x00 */ void *idc;
/* 0x08 */ extension_set_t ext_set;
/* moar */
}
typedef struct sandbox* sandbox_t;
#define EXT_TABLE_SIZE 9
struct extension_set_t {
/* 0x00 */ extension_t ext_table[EXT_TABLE_SIZE];
/* 0x48 */ void* another_table[EXT_TABLE_SIZE];
/* 0x90 */ uint64_t refcnt;
/* 0x98 */ lck_rw_t ext_lck; // lock_group // attr_540
/* 0x.. */
/* ---- */
/* 0xa8 */
}
typedef struct extension_set_t* extension_set_t;
struct extension_hdr {
/* 0x00 */ extension_hdr_t next;
/* 0x08 */ extension_t ext_lst;
/* 0x10 */ char class[];
/* 0x10 + strlen(class) */
};
typedef struct extension_hdr* extension_hdr_t;
struct extension {
/* 0x00 */ extension_t next;
/* 0x08 */ uint64_t desc; // always -1, but changes when added to set
/* 0x10 */ uint8_t something0[20]; // zeroed in create, used in syscall_extension_consume
/* 0x24 */ uint16_t refcnt; // set to 1 in create
/* 0x26 */ uint8_t type; // see ext_type enum
/* 0x27 */ uint8_t unk0; // zeroed in create
/* 0x28 */ uint32_t subtype; // either 0 or 8 (or whatever unhex gave in syscall_extension_consume)
/* 0x2A */ uint32_t num4; // another number
/* 0x30 */ char* data; // meaning depends on type and hdr which had this extension
/* 0x38 */ uint64_t data_len; // strlen(data)
/* 0x40 */ uint32_t unk1; // zeroed in create_file, changed in
/* 0x44 */ uint32_t unk2; // used in syscall_extension_consume
/* 0x48 */ uint32_t unk3; // same
/* 0x4c */ uint32_t unk4; // filler
/* 0x50 */ uint64_t unk5; // zeroed in create_file
/* 0x58 */ uint64_t unk6; // pad
/* 0x60 */
};
typedef struct extension* extension_t;
enum ext_type {
ET_FILE = 0,
ET_MACH = 1,
ET_IOKIT_REG_ENT = 2,
ET_POSIX_IPC = 4,
ET_PREF_DOMAIN = 5, // inlined in issue_extension_for_preference_domain
ET_SYSCTL = 6, // inlined in issue_extension_for_sysctl
};
// most functions return 0xC on error
#define RET_ERR 0xC
#define RET_OK 0x0
static uint32_t extension_count;
static uint32_t extension_orphan_count;
unsigned int hashing_magic(const char* cls) {
unsigned int hash = 0x1505;
for (const char* c = cls; *c != '\0'; c++) {
hash = 33 * hash + (unsigned int)(*c);
}
return hash;
}
int extension_create(extension_t* saveto, sandbox_t sb) {
extension_t ext = (extension_t) smalloc_track(sb, sizeof(*extension_t));
if (ext == NULL) {
return RET_ERR;
}
ext->next = NULL;
ext->desc = (uint64_t) -1;
ext->refcnt = 1;
ext->unk0 = 0;
bzero(&ext->something0, sizeof(ext->something0));
OSAddAtomic(1, extension_count);
OSAddAtomic(1, extension_orphan_count);
*saveto = ext;
return RET_OK;
}
// replaces extension_destroy
void extension_release(extension_t* freeto) {
ext = *freeto;
if (ext == NULL) {
return;
}
*freeto = NULL;
if (OSAddAtomic16(-1, ext->refcnt) != 1) {
return;
}
switch(ext->type) {
// in [0, 6] but not 3
case ET_FILE:
case ET_MACH:
case ET_IOKIT_REG_ENT:
case ET_POSIX_IPC:
case ET_PREF_DOMAIN:
case ET_SYSCTL:
sfree(ext->data);
}
sfree(ext);
OSAddAtomic(-1, extension_count);
OSAddAtomic(-1, extension_orphan_count);
}
int extension_create_file(extension_t* saveto, sandbox_t sb, const char* path, size_t path_len, uint32_t subtype) {
int ret;
extension_t ext = NULL;
*saveto = NULL;
ret = extension_create(&ext, sb);
if (ret) {
return ret;
}
ext->type = ET_FILE;
ext->subtype = subtype;
ext->data = smalloc_track(sb, path_len + 1);
if (ext->data == NULL) {
extension_release(ext);
return RET_ERR;
}
memmove(ext->data, path, path_len);
ext->data[path_len] = '\0';
ext->data_len = path_len;
ext->unk1 = 0; // actually lower 16 bits
ext->unk5 = 0;
*saveto = ext;
return RET_OK;
}
int extension_create_with_string(extension_t* saveto, sandbox_t sb, uint8_t type, const char* str, uint32_t subtype) {
int ret;
extension_t ext = NULL;
*saveto = NULL;
ret = extension_create(&ext, sb);
if (ret) {
return ret;
}
ext->type = type;
ext->subtype = subtype;
ext->data = sstrdup_track(sb, str)
if (ext->data == NULL) {
extension_release(ext);
return RET_ERR;
}
ext->data_len = strlen(ext->data);
*saveto = ext;
return RET_OK;
}
int extension_add(extension_t ext, sandbox_t sb, const char* desc) {
int ret = RET_OK;
int idx = hashing_magic(desc) % EXT_TABLE_SIZE;
lck_rw_lock_exclusive(sb->ext_lck);
// extension_hdr_t insert_at = sb->ext_table[idx];
// if (insert_at == NULL) {
// insert_at = smalloc_track(sb, sizeof(extension_hdr));
// if (insert_at == NULL) {
// ret = RET_ERR;
// goto end;
// }
// insert_at->next = NULL;
// insert_at->desc = sstrdup_track(sb, desc);
// if (insert_at->desc == NULL) {
// sfree(insert_at);
// ret = RET_ERR;
// goto end;
// }
// insert_at->ext_lst = NULL;
// sb->ext_table[idx] = insert_at;
// }
// while (insert_at->next != NULL) {
// // APPL doesn't check for (insert_at->desc != NULL)
// // but extension_add is never called with desc=NULL so meh
// if (strcmp(insert_at->desc, desc) == 0) {
// break;
// }
// insert_at = insert_at->next;
// }
// ext->next = insert_at->ext_lst;
// insert_at->ext_lst = ext;
// ret = 0;
end:;
lck_rw_unlock_exclusive(sb->ext_lck);
return ret;
}
// inlined into extension_add
extension_set_t extension_set_create(sandbox_t sb) {
extension_set_t alloc = NULL;
alloc = smalloc_track(sbx, );
}