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, );
}