Sandbox 101

Digging into macOS sandbox for fun and my profit

I put /usr/lib/system/libsystem_sandbox.dylib into radare2 by chance.

[0x000026d8]> pdf @ sym._rootless_mkdir_protected
│    │ ││   0x00002672      488d3d4d1400.  lea rdi, str.Sandbox        ; 0x3ac6 ; "Sandbox"
│    │ ││   0x00002679      be23000000     mov esi, 0x23               ; '#' ; 35
│    │ ││   0x0000267e      e8b70e0000     call sym.imp.__sandbox_ms
│    │ ││   0x00002683      89c3           mov ebx, eax
│    │ ││   0x00002685      85db           test ebx, ebx
│    │┌───< 0x00002687      750d           jne 0x2696
│    ││││   0x00002689      4489ef         mov edi, r13d
│    ││││   0x0000268c      4489f6         mov esi, r14d
│    ││││   0x0000268f      e80c0f0000     call sym.imp.fchflags

It looks like __sandbox_ms function does something important.
It takes a string Sandbox as arg1, and a fascinating integer 35 as arg2.
When we look into the /usr/include/sys/syscall.h, 35 corresponds to fchflags UN*X system call.
Therefore, this assembly snippet seems to try calling fchflags in sandbox-way first, then it calls normally if it fails.

Since this function is imported from somewhere, I tried to figure out where it has come from...

$ grep -lr "sandbox_ms" /usr/lib 2>/dev/null
/usr/lib/dyld
/usr/lib/libsandbox.1.dylib
/usr/lib/libsandbox.dylib
/usr/lib/system/libquarantine.dylib
/usr/lib/system/libsystem_kernel.dylib
/usr/lib/system/libsystem_sandbox.dylib
/usr/lib/system/libsystem_secinit.dylib

radare2 disassembly of /usr/lib/dyld shows the results below:

[0x00000000]> afl~sandbox_ms
0x000314b4    3 21           sym.___sandbox_ms
[0x00000000]> s sym.___sandbox_ms
[0x000314b4]> pdf
        ╎   ;-- ___mac_syscall:
        ╎   ;-- func.000314b4:
┌ (fcn) sym.___sandbox_ms 21
│   sym.___sandbox_ms (int32_t arg4);
│       ╎   ; arg int32_t arg4 @ rcx
│       ╎   ; CALL XREF from sym._sandbox_check_common (0x2f0d9)
│       ╎   0x000314b4      b87d010002     mov eax, 0x200017d
│       ╎   0x000314b9      4989ca         mov r10, rcx                ; arg4
│       ╎   0x000314bc      0f05           syscall
│      ┌──< 0x000314be      7308           jae 0x314c8
│      │╎   0x000314c0      4889c7         mov rdi, rax
│      │└─< 0x000314c3      e9e0f5ffff     jmp sym._cerror_nocancel
│      │    ; CODE XREF from sym.___sandbox_ms (0x314be)
└      └──> 0x000314c8      c3             ret

Thus, it is actually a __mac_syscall UN*X system call
Moreover, as we can see from /usr/include/sys/syscall.h, syscall number 0x17d = 381 corresponds to __mac_syscall.

According to xnu-4903.221.2/security/mac.h, the function prototype is as follows:

int	 __mac_syscall(const char *_policyname, int _call, void *_arg);

Since ___sandbox_ms was called with two arguments ("Sandbox", 35), we can at least deduce that _policyname = "Sandbox" and _call = 35.

By the way, what does ms mean in ___sandbox_ms?
When we look in xnu-4903.221.2/libsyscall/Platforms/syscall.map,

___sandbox_me	___mac_execve
___sandbox_mm	___mac_mount
___sandbox_ms	___mac_syscall
___sandbox_msp	___mac_set_proc
__exit	___exit
_accessx_np	___access_extended
_getsgroups_np	___getsgroups
_getwgroups_np	___getwgroups
# initgroups wrapper is defined in Libinfo
_initgroups
_posix_madvise	___madvise
_pthread_getugid_np	___gettid
_pthread_setugid_np	___settid
_setsgroups_np	___setsgroups
_setwgroups_np	___setwgroups
_wait4	___wait4_nocancel

It seems that the function name on the left hand side is to become an alias of the function on the right hand side for each column.
So, I assume that ms stands for mac_syscall.

__mac_syscall

An implementation of __mac_syscall can be found at xnu-4903.221.2/security/mac_base.c.

/*
 * __mac_syscall: Perform a MAC policy system call
 *
 * Parameters:    p                       Process calling this routine
 *                uap                     User argument descriptor (see below)
 *                retv                    (Unused)
 *
 * Indirect:      uap->policy             Name of target MAC policy
 *                uap->call               MAC policy-specific system call to perform
 *                uap->arg                MAC policy-specific system call arguments
 *                
 * Returns:        0                      Success
 *                !0                      Not success
 *
 */
int
__mac_syscall(proc_t p, struct __mac_syscall_args *uap, int *retv __unused)
{
    struct mac_policy_conf *mpc;
    char target[MAC_MAX_POLICY_NAME];
    int error;
    u_int i;
    size_t ulen;

    error = copyinstr(uap->policy, target, sizeof(target), &ulen);
    if (error)
        return (error);
    AUDIT_ARG(value32, uap->call);
    AUDIT_ARG(mac_string, target);

    error = ENOPOLICY;

    for (i = 0; i < mac_policy_list.staticmax; i++) {
        mpc = mac_policy_list.entries[i].mpc;
        if (mpc == NULL)
            continue;

        if (strcmp(mpc->mpc_name, target) == 0 &&
            mpc->mpc_ops->mpo_policy_syscall != NULL) {
            error = mpc->mpc_ops->mpo_policy_syscall(p,
                uap->call, uap->arg);
            goto done;
 		}
    }
    if (mac_policy_list_conditional_busy() != 0) {
        for (; i <= mac_policy_list.maxindex; i++) {
            mpc = mac_policy_list.entries[i].mpc;
            if (mpc == NULL)
                continue;

            if (strcmp(mpc->mpc_name, target) == 0 &&
                mpc->mpc_ops->mpo_policy_syscall != NULL) {
                error = mpc->mpc_ops->mpo_policy_syscall(p,
                    uap->call, uap->arg);
                break;
            }
        }
        mac_policy_list_unbusy();
    }

done:
    return (error);
}

Somehow, the arguments taken by the function is different from its prototype listed above.
I assume that the second argument uap is equivalent to those arguments listed in a fucntion prototype as it can be guessed from the comment block.

copyinstr

The function stands for Copy In String.
It seems that the string given in uap->policy is copied to the kernel-mapped memory (it doesn't look like so in this case) target, by the definition of the function.
Thus, the string Sandbox is put into an array, target.

AUDIT_ARG

A macro AUDIT_ARG checks whether audit is enabled or not, then generate the function name, starting from the string, audit_arg_.

AUDIT_ARG(value32, uap->call); will generate a function, audit_arg_value32.
uap->call should be the second argument (%rsi) of ___sandbox_ms, thus 35.

The definition can be found at xnu-4903.221.2/security/audit/audit_arg.c.

void
audit_arg_value32(struct kaudit_record *ar, uint32_t value32)
{

    ar->k_ar.ar_arg_value32 = value32;
    ARG_SET_VALID(ar, ARG_VALUE32);
}

Another macro, ARG_SET_VALID is defined in xnu-4903.221.2/security/audit/audit_private.h.

/*
 * Arguments in the audit record are initially not defined; flags are set to
 * indicate if they are present so they can be included in the audit log
 * stream only if defined.
 */
#define	ARG_IS_VALID(kar, arg)	((kar)->k_ar.ar_valid_arg & (arg))
#define	ARG_SET_VALID(kar, arg) do {					\
    (kar)->k_ar.ar_valid_arg |= (arg);				\
} while (0)

So, this macro sets the validity flag.

The second AUDIT_ARG generates the function audit_arg_mac_string.
The definition is at xnu-4903.221.2/security/audit/audit_mac.c.

void
audit_arg_mac_string(struct kaudit_record *ar, char *string)
{

    if (ar->k_ar.ar_arg_mac_string == NULL)
        ar->k_ar.ar_arg_mac_string =
            kalloc(MAC_MAX_LABEL_BUF_LEN + MAC_ARG_PREFIX_LEN);

    /*
     * XXX This should be a rare event. If kalloc() returns NULL,
     * the system is low on kernel virtual memory. To be
     * consistent with the rest of audit, just return
     * (may need to panic if required to for audit).
     */
    if (ar->k_ar.ar_arg_mac_string == NULL)
        if (ar->k_ar.ar_arg_mac_string == NULL)
            return;

    strncpy(ar->k_ar.ar_arg_mac_string, MAC_ARG_PREFIX,
        MAC_ARG_PREFIX_LEN);
    strncpy(ar->k_ar.ar_arg_mac_string + MAC_ARG_PREFIX_LEN, string,
        MAC_MAX_LABEL_BUF_LEN);
    ARG_SET_VALID(ar, ARG_MAC_STRING);
}

The first half of the function is some NULL check, so we skip here.
Another half of the function does the simple stuff, it just copies the string by strncpy function.
Both MAC_ARG_PREFIX and MAC_ARG_PREFIX_LEN are fixed values, a string "arg: " and an integer 5, respectively.

After copying a predefined string "arg: ", it is followed by a given string, i.e. target char array, which originally came from uap->policy.
Therefore, it becomes "Sandbox" in this case, and ar->k_ar.ar_arg_mac_string eventually becomes "arg: Sandbox".
Finally, it calls ARG_SET_VALID again to set the validity flag.

mpo_policy_syscall

            error = mpc->mpc_ops->mpo_policy_syscall(p,
                uap->call, uap->arg);

There is a relevant struct defined in xnu-4903.221.2/security/mac_policy.h:

/**
  @brief Policy extension service
  @param p Calling process
  @param call Policy-specific syscall number
  @param arg Pointer to syscall arguments

  This entry point provides a policy-multiplexed system call so that
  policies may provide additional services to user processes without
  registering specific system calls. The policy name provided during
  registration is used to demux calls from userland, and the arguments
  will be forwarded to this entry point.  When implementing new
  services, security modules should be sure to invoke appropriate
  access control checks from the MAC framework as needed.  For
  example, if a policy implements an augmented signal functionality,
  it should call the necessary signal access control checks to invoke
  the MAC framework and other registered policies.

  @warning Since the format and contents of the policy-specific
  arguments are unknown to the MAC Framework, modules must perform the
  required copyin() of the syscall data on their own.  No policy
  mediation is performed, so policies must perform any necessary
  access control checks themselves.  If multiple policies are loaded,
  they will currently be unable to mediate calls to other policies.

  @return In the event of an error, an appropriate value for errno
  should be returned, otherwise return 0 upon success.
*/
typedef int mpo_policy_syscall_t(
    struct proc *p,
    int call,
    user_addr_t arg
);

In brief, it tries to invoke a system call.
As we know that call = 35, mpo_policy_syscall is trying to trigger fchflags system call.

The content may be updated after I finish reading MOXiI III's corresponding chapter...
This .md is written without any knowledge of Apple's sandbox