Binder Secctx Patch Analysis

In the beginning of 2019, a new feature was added in the Binder kernel module. This patch allows to send the caller SElinux context in a Binder transaction. This feature was in fact a fix for CVE-2019-2023. This vulnerability is related to an unsafe use of the getpidcon function, leading to ACL bypass.

This article studies details of this patch and its impact on security.

getpidcon ACL Bypass

Issues with getpidcon() usage is a long story in Android. The Jann Horn's issue report details that the hardware servicemanager was vulnerable and several similar bugs were already reported because of unsafe getpidcon() usage. This function is not a safe way to get the SELinux context of a calling process as explained by Jann Horn in one of his issues:

This is problematic because getpidcon($pid) is only safe to use if the caller either knows that the process originally referenced by $pid can't transition from zombie to dead (normally because it is the parent or ptracer of $pid) or if the caller can validate that the process referenced by $pid can not have spawned before $pid referred to the correct process based on the age of the process that $pid points to after the getpidcon() call. (The same thing applies to pretty much any API that refers to processes using PIDs.)

In other words, there is a race condition: The process referred by a given pid can change between the time a transaction was received and the call to getpidcon.

A clean way to fix this design issue is to send the SELinux context with the binder transaction. This is the purpose of the patch we will analyze today.

Patch overview

The patch details were available on lore.kernel.org. The commit title is "create node flag to request sender's security context".

Let's analyze the main part of this patch:

//@@ -3020,6 +3027,20 @@  static void binder_transaction(struct binder_proc *proc,
    t->flags = tr->flags;
    t->priority = task_nice(current);
+   if (target_node && target_node->txn_security_ctx) {
+       u32 secid;
+
+       security_task_getsecid(proc->tsk, &secid);
+       ret = security_secid_to_secctx(secid, &secctx, &secctx_sz);
+       if (ret) {
+           return_error = BR_FAILED_REPLY;
+           return_error_param = ret;
+           return_error_line = __LINE__;
+           goto err_get_secctx_failed;
+       }
+       extra_buffers_size += ALIGN(secctx_sz, sizeof(u64));
+   }

+   if (secctx) {
+       size_t buf_offset = ALIGN(tr->data_size, sizeof(void *)) +
+                   ALIGN(tr->offsets_size, sizeof(void *)) +
+                   ALIGN(extra_buffers_size, sizeof(void *)) -
+                   ALIGN(secctx_sz, sizeof(u64));
+       char *kptr = t->buffer->data + buf_offset;
+
+       t->security_ctx = (uintptr_t)kptr +
+           binder_alloc_get_user_buffer_offset(&target_proc->alloc);
+       memcpy(kptr, secctx, secctx_sz);
+       security_release_secctx(secctx, secctx_sz);
+       secctx = NULL;
+   }

When a binder transaction is sent, the kernel checks if the destination process needs the SELinux context (target_node->txn_security_ctx). This option can be specified on binder object init with the flag FLAT_BINDER_FLAG_TXN_SECURITY_CTX.

For instance, the hardware service manager enables this feature:

    //platform/system/hwservicemanager/service.cpp
    int main() {
        // [...]

        sp<ServiceManager> manager = new ServiceManager();
        setRequestingSid(manager, true);
    }

With this feature enabled, the kernel increases the size of extra_buffers_size to store the security context just after. A received transaction looks as following:

Binder receive transaction buffer

Then the receiver can retrieve the security context in the transaction using getCallingPid().

    pid_t IPCThreadState::getCallingPid() const
    {
        return mCallingPid;
    }

    // /platform/system/libhwbinder/IPCThreadState.cpp
    status_t IPCThreadState::executeCommand(int32_t cmd)
    {
    // [...]
    struct binder_transaction_data_secctx {
            struct binder_transaction_data transaction_data;
            binder_uintptr_t secctx;
    };

    binder_transaction_data_secctx tr_secctx;
    binder_transaction_data& tr = tr_secctx.transaction_data;

    if (cmd == (int) BR_TRANSACTION_SEC_CTX) {
        result = mIn.read(&tr_secctx, sizeof(tr_secctx));
    } else {
        result = mIn.read(&tr, sizeof(tr));
        tr_secctx.secctx = 0;
    }
    // ...
    mCallingSid = reinterpret_cast<const char*>(tr_secctx.secctx);

Vulnerability 1: Integer Overflow

A quick review allows to identify an integer overflow.

    extra_buffers_size += ALIGN(secctx_sz, sizeof(u64));

extra_buffers_size is fully controlled by user with BC_TRANSACTION_SG transaction. If sum of extra_buffers_size, tr->data_size and tr->offsets_size are equal to zero, the following instructions will copy the security context before t->buffer->data (buf_offset is negative).

    size_t buf_offset = ALIGN(tr->data_size, sizeof(void *)) +
                ALIGN(tr->offsets_size, sizeof(void *)) +
                ALIGN(extra_buffers_size, sizeof(void *)) -
                ALIGN(secctx_sz, sizeof(u64));
    char *kptr = t->buffer->data + buf_offset;

    t->security_ctx = (binder_uintptr_t)kptr +
        binder_alloc_get_user_buffer_offset(&target_proc->alloc);
    memcpy(kptr, secctx, secctx_sz);

This vulnerability was identified as CVE-2019-2181 and patched in commit 0b0509508beff65c1d50541861bc0d4973487dc5.

It was published in the Android Security Bulletin of September, 2019. https://source.android.com/security/bulletin/2019-09-01.html#kernel-components

Vulnerability 2: Security context overwrite

When a binder transaction contains a BINDER_TYPE_PTR object, the kernel copies the buffer of sender process in the extra part. We can see above that to store the security context, the extra_buffers_size is increased. However the kernel does not make any distinction between standard data and security context in extra buffer. A user can craft a binder transaction which overwrites the security context.

BINDER_TYPE_PTR overwrite security context

This is an easier way to bypass ACL than the getpidcon() race condition!

This vulnerability was patched by the commit named : binder: Set end of SG buffer area properly

Conclusion

This Binder kernel patch purpose was to improve security and provides another way to retrieve the context of a calling process to avoid usage of getpidcon().

Paradoxically, the initial patch version added more security flaws and made ACL bypass easier than the former vulnerability!