binder
内核模块引用计数的漏洞。
- Attacking Android Binder: Analysis and Exploitation of CVE-2023-20938 - Android Offensive Security Blog
- OffensiveCon24 - Eugene Rodionov,Zi Fan Tan and Gulshan Singh - YouTube
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 manager
,binder
内核会在该进程处创建一个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
,指向0xdeafbeaf
的node
。client B
向client A
发送一个transaction
,target.handle
即0xdeadbeaf
。client B
关闭binder
的fd
,此时binder_ref
会被销毁。现在,binder_node
的引用仅有该transaction
。此时,client C
向client B
发送0xdeadbeaf
对象,设置偏移错误,此时在错误处理路径会free
掉该binder_node
,但实际上仍然可以通过binder_buffer->target
来访问到该node
。- 通过
binder_thread_read
方法实现UAF
读取。
Write Primitive
在这一步,实现有限的写原语,实现向任意可写内核地址写入一个合法指针(可写)。
Leak Primitive
后续的利用需要一个已知地址的binder_node
。
binder_node
被free
掉之后可以通过read
操作将ptr
和cookie
值泄露出来,可以看到如果我们用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 */
};
Unlink Primitive
我们前面得到的是一个泄露内核地址的原语,还需要结合一个写的原语。
在执行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结构体。
后面就能够去泄露file
和epitem
的地址。利用写原语将file
的inode
改成我们控制的epitem
,后面通过EPOLL_CTL_MOD
往epitem
的data
字段写入指针值,再通过ioctl
即可任意读取内核地址。
这里碰到一个问题,selinux
模块可能会出现一个非法指针访问,x86
架构下可以加一个额外的写,将前面部分泄露的指针找到一个PRIVATE
位为1的即可。
LPE Primitive
Gain Root
泄露binder_node
的proc
,从而泄露proc->tsk
的task_struct
,进而泄露cred
,再将相关id
字段写0,就可以提权。
Selinux Bypass
selinux
为linux
提供了基于MAC
的保护,即便是root
也无法执行很多操作。基于内核写原语,可以通过覆盖掉enforcing
来设置permissive
模式,从而绕过selinux
。
Seccomp Bypass
安卓限制了一部分syscall
的使用,如果使用了这些被block
掉的syscall
进程就会被杀死。
这里可以覆盖掉task_struct
中的一个mask
字段来关闭seccomp
。
Pixel 6
需要修复一些偏移字段。对task
和cred
的泄露可以通过扫描,但是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