binder内核模块引用计数的漏洞。

Root Cause Analysis

binder_node采用引用计数。正常的逻辑在使用一个binder_node时需要首先增加引用计数,再减少引用计数。然而,有一条错误处理的路径可以让node不经过任何处理后被dec掉,但其垂悬的引用仍然存在于binder_buffer->target上,导致了UAF

binder background

此处先介绍一点背景知识。主要都是参考这篇非常详尽的原作者博客。

binder是安卓上的ipc框架。一个进程想和其他目标服务通信,首先需要取得该服务的一个handle。如何取得该handle呢?安卓有一个全局的service manager,其对所有进程可见,handle值就为0 。如果要获取其他进程的handle,就需要调用获取服务的API,即IServiceManager.aidl接口中的getService。当某个进程创建一个服务时,将一个binder对象传给service managerbinder内核会在该进程处创建一个binder_node,而其他进程使用该服务时就会在本地创建一个binder_ref

Environment Setup

Android Kernel Compilation

看所有的内核版本:

git ls-remote https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/common | grep android12-5.10-2022-10
987d4745ed78ca0f958513bab62fa9de303f2676	refs/heads/deprecated/android12-5.10-2022-10

需要在.repo/manifests/default.xml中,改动这一行

  <project path="common" name="kernel/common" revision="deprecated/android12-5.10-2022-03" upstream="android12-5.10-2022-03" dest-branch="android12-5.10-2022-03" />

加上deprecated后,可以fetch内核代码。

编译需要修复一些编译问题。

需要安装ncurses5库。

yay -S ncurses5-compat-libs

由于subcmd这里采用的c99语法,不支持for内声明,需要做一些改动。

diff --git a/tools/bpf/resolve_btfids/Makefile b/tools/bpf/resolve_btfids/Makefile
index 35f092065c4f..21a17ad1e10d 100644
--- a/tools/bpf/resolve_btfids/Makefile
+++ b/tools/bpf/resolve_btfids/Makefile
@@ -44,7 +44,7 @@ $(OUTPUT) $(OUTPUT)/libbpf $(OUTPUT)/libsubcmd:
        $(Q)mkdir -p $(@)
 
 $(SUBCMDOBJ): fixdep FORCE | $(OUTPUT)/libsubcmd
-       $(Q)$(MAKE) -C $(SUBCMD_SRC) OUTPUT=$(abspath $(dir $@))/ $(abspath $@)
+       $(Q)$(MAKE) -C $(SUBCMD_SRC) EXTRA_CFLAGS="$(CFLAGS)" OUTPUT=$(abspath $(dir $@))/ $(abspath $@)
 
 $(BPFOBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
        $(Q)$(MAKE) $(submake_extras) -C $(LIBBPF_SRC)  OUTPUT=$(abspath $(dir $@))/ \
diff --git a/tools/lib/subcmd/parse-options.c b/tools/lib/subcmd/parse-options.c
index 39ebf6192016..cbcd8598d3e7 100644
--- a/tools/lib/subcmd/parse-options.c
+++ b/tools/lib/subcmd/parse-options.c
@@ -637,10 +637,11 @@ int parse_options_subcommand(int argc, const char **argv, const struct option *o
        /* build usage string if it's not provided */
        if (subcommands && !usagestr[0]) {
                char *buf = NULL;
+    int i;
 
                astrcatf(&buf, "%s %s [<options>] {", subcmd_config.exec_name, argv[0]);
 
-               for (int i = 0; subcommands[i]; i++) {
+               for (i = 0; subcommands[i]; i++) {
                        if (i)
                                astrcat(&buf, "|");
                        astrcat(&buf, subcommands[i]);
@@ -666,7 +667,8 @@ int parse_options_subcommand(int argc, const char **argv, const struct option *o
                exit(130);
        case PARSE_OPT_LIST_SUBCMDS:
                if (subcommands) {
-                       for (int i = 0; subcommands[i]; i++)
+      int i;
+                       for (i = 0; subcommands[i]; i++)
                                printf("%s ", subcommands[i]);
                }
                putchar('\n');
diff --git a/tools/objtool/Makefile b/tools/objtool/Makefile
index 5cdb19036d7f..66ed50126bea 100644
--- a/tools/objtool/Makefile
+++ b/tools/objtool/Makefile
@@ -63,7 +63,7 @@ $(OBJTOOL): $(LIBSUBCMD) $(OBJTOOL_IN)
 
 
 $(LIBSUBCMD): fixdep FORCE
-       $(Q)$(MAKE) -C $(SUBCMD_SRCDIR) OUTPUT=$(LIBSUBCMD_OUTPUT)
+       $(Q)$(MAKE) -C $(SUBCMD_SRCDIR) EXTRA_CFLAGS="$(CFLAGS)" OUTPUT=$(LIBSUBCMD_OUTPUT)
 
 clean:
        $(call QUIET_CLEAN, objtool) $(RM) $(OBJTOOL)

执行完这两步骤即可编译。

默认每次build都会执行mproper,会清空所有的编译产物,因此每次都需要从头开始编译。

开启增量编译:

export SKIP_MRPROPER=1  SKIP_EXT_MODULES=1

SKIP_MRPROPER=1 LTO=thin BUILD_CONFIG=common/build.config.gki.x86_64 LLVM=1 build/build.sh


LTO=thin BUILD_CONFIG=common-modules/virtual-device/build.config.virtual_device.x86_64 build/build.sh -j16 LTO=thin

Aosp

测试用emulator无法启动,因此选择编译aosp,再用launch_cvd来启动内核。

这里还碰到一个crossvm的问题,由于拉取的代码比较老,当时还没有修复。手动patch这段代码即可。

Exploitation

Channel Creation

该漏洞需要自己控制的结点互相之间通信。然而,结点想要发现对方需要第三方的参与。安卓上,service_manager可以用作服务发现。常见的各种服务都是在此注册的,通过建立在binder之上的aidl进行通信。

最初实现了基于ServiceManager的注册和发现;

frameworks/native/libs/binder/aidl/android/os/IServiceManager.aidl

    /**
     * Retrieve an existing service called @a name from the
     * service manager.
     *
     * This is the same as checkService (returns immediately) but
     * exists for legacy purposes.
     *
     * Returns null if the service does not exist.
     */
    @UnsupportedAppUsage
    @nullable IBinder getService(@utf8InCpp String name);
 
 
 
    /**
     * Place a new @a service called @a name into the service
     * manager.
     */
    void addService(@utf8InCpp String name, IBinder service,
        boolean allowIsolated, int dumpPriority);
 

aidl在传递数据之前,还有一些消息格式需要注意。相关代码可以查看frameworks/native/libs/libbinder/Parcel.cpp

  writeInterfaceToken(&trdata, "android.os.IServiceManager");
  trdata_put_str16(&trdata, name);
  trdata_put_binder(&trdata, 0xcafebabe, true);
  trdata_put_u32(&trdata, 0xc000001);
  trdata_put_u32(&trdata, 0);

最大的问题是该操作本身需要权限。如何实现无特权的信道建立呢?那就要搜索其他的第三方注册服务。

badspin公开的exp所采用的hwbinder中有一个token_manager,其不需要任何权限,因此改用hwbinder

system/libhidl/transport/token/1.0/ITokenManager.hal

/**
 * This facilitates converting hidl interfaces into something that
 * are more easily transferrable to other processes.
 */
interface ITokenManager {
 
    /**
     * Register an interface. The server must keep a strong reference
     * to the interface until the token is destroyed by calling unregister.
     *
     * Must return empty token on failure.
     *
     * @param store Interface which can later be fetched with the returned token.
     * @return token Opaque value which may be used as inputs to other functions.
     */
    createToken(interface store) generates (vec<uint8_t>token);
 
    /**
     * Destory a token and the strong reference to the associated interface.
     *
     * @param token Token received from createToken
     * @return success Whether or not the token was successfully unregistered.
     */
    unregister(vec<uint8_t> token) generates (bool success);
 
    /**
     * Fetch an interface from a provided token.
     *
     * @param token Token received from createToken
     * @return store Interface registered with createToken and the corresponding
     *               token or nullptr.
     */
    get(vec<uint8_t> token) generates (interface store);
};
 

这里的通信格式与aidl有所不同;实际上要简单些。

  writeInterfaceTokenHw(&tr, TOKEN_MANAGER);
  trdata_put_binder(&tr, ptr, true);

Triggering Vulnerability

具体触发的方法:

  • client A, B, C创建通信- client A创建一个binder对象,并通过一个transaction发送给client B。此时,client A有一个binder_node,记为0xdeadbeaf,实际上为一个用户创建的对象。而client B有一个binder_ref,指向0xdeafbeafnode
  • client Bclient A发送一个transactiontarget.handle0xdeadbeaf
  • client B关闭binderfd,此时binder_ref会被销毁。现在,binder_node的引用仅有该transaction。此时,client Cclient B发送0xdeadbeaf对象,设置偏移错误,此时在错误处理路径会free掉该binder_node,但实际上仍然可以通过binder_buffer->target来访问到该node
  • 通过binder_thread_read方法实现UAF读取。

Write Primitive

在这一步,实现有限的写原语,实现向任意可写内核地址写入一个合法指针(可写)。

Leak Primitive

后续的利用需要一个已知地址的binder_node

binder_nodefree掉之后可以通过read操作将ptrcookie值泄露出来,可以看到如果我们用binder_ref去占位,就可以泄露一个binder_node的地址。

❯ pahole binder_ref           
struct binder_ref {
	struct binder_proc *       proc;                 /*    80     8 */
	struct binder_node *       node;                 /*    88     8 */
	struct binder_ref_death *  death;                /*    96     8 */
};

❯ pahole binder_node
struct binder_node {
	binder_uintptr_t           ptr;                  /*    88     8 */
	binder_uintptr_t           cookie;               /*    96     8 */
};

我们前面得到的是一个泄露内核地址的原语,还需要结合一个写的原语。 在执行binder_dec_node_nilocked时,有机会触发一个unlink操作。unlink会把链表prev->next字段改成next,同时将next->prev改成prev

*pprev = next
*(next + 8) = pprev

这就提供了一种受限的写原语,能够去写一个合法的内核指针或者0。

我们可以在释放掉binder_node后通过堆喷sendmsg来修改binder_node的内容,从而控制要修改的指针。可以通过检查cookie字段来看是否喷到了。

这里有一个最不稳定的因素就是unlink会去检查是否list为空,如果不为空直接会BUGON,导致手机重启。这当然不是我们想要的结果。list为空的条件就是里面的指针指向binder_node自身,因此,想要漏洞稳定的复现必须要确保泄露的binder_node地址是正确的。在我的exp中暂时还没碰到过地址出错的情况。

最后,调用一次binder命令BINDER_FREE_BUFFER即可调用unlink操作。

Read Primitive

主要思路是改写file->inode的指针到我们的epitem上。这里的epitem有个字段的8字节我们可以完全控制。后面通过FIGETBSZ实现任意写,流程:file->inode->i_sub->s_blocksize。这里i_sub是我们完全控制的,也就可以泄露任意内核地址。

由于epitem现在和binder_node不在一个cache上,需要做crosscache攻击,可以通过喷大量的binder_node,同时释放掉,再喷epitem和file结构体。

后面就能够去泄露fileepitem的地址。利用写原语将fileinode改成我们控制的epitem,后面通过EPOLL_CTL_MODepitemdata字段写入指针值,再通过ioctl即可任意读取内核地址。

这里碰到一个问题,selinux模块可能会出现一个非法指针访问,x86架构下可以加一个额外的写,将前面部分泄露的指针找到一个PRIVATE位为1的即可。

LPE Primitive

Gain Root

泄露binder_nodeproc,从而泄露proc->tsktask_struct,进而泄露cred,再将相关id字段写0,就可以提权。

Selinux Bypass

selinuxlinux提供了基于MAC的保护,即便是root也无法执行很多操作。基于内核写原语,可以通过覆盖掉enforcing来设置permissive模式,从而绕过selinux

Seccomp Bypass

安卓限制了一部分syscall的使用,如果使用了这些被block掉的syscall进程就会被杀死。

这里可以覆盖掉task_struct中的一个mask字段来关闭seccomp

Pixel 6

需要修复一些偏移字段。对taskcred的泄露可以通过扫描,但是selinux暂时没想到好的方法,因此从ROM中获取硬偏移。

提权成功截图:

Misc

Binder Debugging

内核提供了一组binderfs接口,里面暴露了很多信息(当然查看的前提是root,可以编译eng或者userdebug版本的aosp

emu64a:/dev/binderfs/binder_logs # ls
failed_transaction_log  proc  state  stats  transaction_log  transactions