想学习漏洞,但是博客资料看了一大堆,好像入门了,也好像没有入门,决定从一个漏洞入手,全面的分析一下。从零开始,我把所有我遇到的问题,以及解决或者绕过替代的方案跟大家分享一下。
作为一个新手,入手一个漏洞,首先要找个环境,测试漏洞是否存在,很不幸,如果顺利,我不会写环境搭建的问题。
首先百度搜的资料,i春情有一个讲解是,看雪也有一个大佬写过相关的资料了,我手上只有一个nexus6p,为了分析漏洞专门买了一个nexus 5x,都失败了,因为没有了最早的镜像文件,现在除非google源码编译6.0,否在,市面上的镜像基本修复过了,反正我是没有找到可以的,google官网已经把这部分有漏洞的rom下架了,而如果源码编译6.0(有驱动方面的问题),这部分的版本是要趟坑的,我嫌麻烦就没有做,而且这个漏洞是内核漏洞,不是android专有的,我完全可以编译一个linux内核啊,然后使用这个内核就可以了。
漏洞补丁日志
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=637b58c2887e5e57850865839cc75f59184b23d1
首先下载源码,因为这个漏洞是android和linux都有,所以我们下载android内核源码也可以,linux这个网站的源码也可以,一定要git下载,因为我们通过git回退到修复前的位置。
这个漏洞据说实在3.16以前的都有,反正版本那么多,随便下载吧,我也不懂,然后下载,我下载了一个3.16的(下载错了,搞了一大堆,找了找资料才知道,才想起来git回退的办法)。
git reset --hard 637b58c2887e5e57850865839cc75f59184b23d1
回退到这个提交的前面的版本,回退了之后,好像回退到了一个什么游标的位置,git这个我也不是很懂,然后我直接编译,最好是ubuntu14.04,我编译的是64位的,直接能编译过。(坑:我电脑是ubuntu20,gcc9,编译的时候,什么位置无光,编译选项,c语言版本什么的报错都解决了,装了一个gcc 5 ,编译过了,但是编译的内核不能跑,后来桩docker 编译过的,懂的大神,能不能解释下为什么)
内核搞定了,本想直接替换ubuntu14的内核试试,结果,系统直接崩溃了,折腾了一下,最后找了个qemu模拟内核启动的方案,然后去学了根文件系统的制作,编译了一个busybox。
https://www.secshi.com/19607.html
因为不太懂内核堆喷,所以去看了这个大神的博客,然后我直接把他搭建的环境,一起驱动那俩直接用了,我写的太简陋了,这里,感谢大神,非常感谢。
漏洞代码
https://bbs.pediycom/thread-210503.htm
这个大神也很感谢,我看了不下20遍,我把他的代码拿过来了,可以他是32位的,而且是arm,天真的我以为小改一下就能跑
https://github.com/mobilelinux/iovy_root_research
还有这个代码,他们的区别我下面会讲,这个工具,好像是个著名的提权工具
漏洞描述什么的,不好意思,看不懂,代码修改完了,提权不了,我去,蒙b了,心碎了。
第二天,天晴了,雨停了,我又行了。开始从头分析
readv函数,是一种高级I/O函数,就是加快读写速度的,每种I/O驱动都可以实现,也可以不实现,如果不实现,应该就没法使用readv。
这里使用pipe管道驱动的readv函数,最后调用到了pipe_read,这个函数有堆数组越界,导致任意读写,然后在进行一系列堆喷,溢出计算,等等看不懂的操作,据说就能提权,我们先看readv函数调用顺序。
read->SYSCALL_DEFINE3(readv...)->vfs_readv->do_readv_writev-> do_sync_readv_writev/do_loop_readv_writev -> pipe_read
(do_sync_readv_writev/do_loop_readv_writev ,这两个函数我没有具体研究。。。。)
代码我还是贴上吧,里面的注释,如果看不懂,可以看完在慢慢分析
static ssize_t pipe_read(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, loff_t pos) { struct file *filp = iocb->ki_filp; struct pipe_inode_info *pipe = filp->private_data; int do_wakeup; ssize_t ret; struct iovec *iov = (struct iovec *)_iov; size_t total_len; total_len = iov_length(iov, nr_segs); //读取iov->leng数据的长度的总长度 /* Null read succeeds. */ if (unlikely(total_len == 0)) return 0; do_wakeup = 0; ret = 0; __pipe_lock(pipe); for (;;) { int bufs = pipe->nrbufs; if (bufs) { int curbuf = pipe->curbuf; struct pipe_buffer *buf = pipe->bufs + curbuf; const struct pipe_buf_operations *ops = buf->ops; void *addr; size_t chars = buf->len; int error, atomic; if (chars > total_len) //chars 最大0x1000 也就是1页大小 chars = total_len; error = ops->confirm(pipe, buf); if (error) { if (!ret) ret = error; break; } //参数漏洞的位置主要在这里,这里有页错误然后goto的初衷,是因为高端内存映射不能一直驻留,pipe使用了高端内存,所以它本身会有页错误处理,而攻击者利用他的页错误处理 //将本该是驱动本身的页,没有映射到高端内存的错误,变成了用户空间接收数据的页的错误,强行goto atomic = !iov_fault_in_pages_write(iov, chars); //这一行也很很重要,堆喷的内存可以必须可读 redo: addr = ops->map(pipe, buf, atomic); error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic); //拷贝数据,无论怎么拷贝,抖不会超过一页的大小 //在第一次进入pipe_iov_copy_to_user这个函数以后,将界面释放,造成错误 ops->unmap(pipe, buf, addr); if (unlikely(error)) { /* * Just retry with the slow path if we failed. */ if (atomic) { atomic = 0; goto redo; //构造页错误,回强行执行到这个位置,导致 total_len 也就是接收数据的长度没更新 //第一次执行pipe_iov_copy_to_user,故意构造错误,返回,会执行到这里 } if (!ret) ret = error; break; } ret += chars; buf->offset += chars; buf->len -= chars; /* Was it a packet buffer? Clean up and exit */ if (buf->flags & PIPE_BUF_FLAG_PACKET) { total_len = chars; buf->len = 0; } if (!buf->len) { //这个页读完了,更新到下一个页,来接着读 buf->ops = NULL; ops->release(pipe, buf); curbuf = (curbuf + 1) & (pipe->buffers - 1); //从1-10的读取顺序 pipe->curbuf = curbuf; pipe->nrbufs = --bufs; do_wakeup = 1; } total_len -= chars; //第二次执行完 pipe_iov_copy_to_user 函数以后,会执行到这里更新长度,这个时候, //iov已经消耗完毕了,但是total_len,还没有读完 溢出部分 = total_len - chars - 第一次执行时消耗的个数 if (!total_len) break; /* common path: read succeeded */ } if (bufs) /* More to do? */ continue; if (!pipe->writers) break; if (!pipe->waiting_writers) { /* syscall merging: Usually we must not sleep * if O_NONBLOCK is set, or if we got some data. * But if a writer sleeps in kernel space, then * we can wait for that data without violating POSIX. */ if (ret) break; if (filp->f_flags & O_NONBLOCK) { ret = -EAGAIN; break; } } if (signal_pending(current)) { if (!ret) ret = -ERESTARTSYS; break; } if (do_wakeup) { wake_up_interruptible_sync_poll(&pipe->wait, POLLOUT | POLLWRNORM); kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT); } pipe_wait(pipe); } __pipe_unlock(pipe); /* Signal writers asynchronously that there is more room. */ if (do_wakeup) { wake_up_interruptible_sync_poll(&pipe->wait, POLLOUT | POLLWRNORM); kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT); } if (ret > 0) file_accessed(filp); return ret; } static int pipe_iov_copy_to_user(struct iovec *iov, const void *from, unsigned long len, int atomic) { unsigned long copy; while (len > 0) { while (!iov->iov_len) iov++; copy = min_t(unsigned long, len, iov->iov_len); if (atomic) { if (__copy_to_user_inatomic(iov->iov_base, from, copy)) return -EFAULT; } else { if (copy_to_user(iov->iov_base, from, copy)) return -EFAULT; } from += copy; len -= copy; iov->iov_base += copy; iov->iov_len -= copy; 拷贝过的项,iov_len=0 copy的值,看上面 } return 0; }
我们通过这个函数来分析,为什么产生漏洞,搞清楚,不是如何利用,如何进行任意地址写,只是分析为什么会产生
如果造成 atomic=1,pipe_iov_copy_to_user执行错误返回,这个时候,iov数组就会已经copy过的项的iov_len已经等于0了,但是这个时候,函数的逻辑是将atomic=0,然后继续goto到这个函数的位置,继续copy,就从这个地方看,再次进入iov函数,只有atomic和iov的数据产生了变化,拷贝的个数chars和源地址addr + buf->offset均未发生变化,不考虑漏洞的问题,这本身就是bug了,内存拷贝,目的地址变小了,但是拷贝的个数和源缓冲区大小都没变的情况下,必然导致溢出。
溢出的位置是哪里,我们看这个函数,iov是否数组,但是是通过指针使用的,如果要写入的数据不为0,iov数组就会越界,直到len小于0为止
这个漏洞是拷贝数据函数是pipe_iov_copy_to_user里面的__copy_to_user_inatomic,要达到地任意址写入任意大小的任意数据,我们必须同时控制他的三个参数(iov->iov_base,from, copy),copy大部分情况下等于iov->iov_le,from,是我们要读的缓冲区的数据,也就是我们写进来的数据,通过readv读出去。而iov是会发生数组越界的。
我们现在需要跟踪分析这个iov的前世今生了。看看能不能控制他。
readv -> vfs_readv -> do_readv_writev-> rw_copy_check_uvector
iov数组,只要大于8组,就可以在rw_copy_check_uvector这个函数通过kmalloc分配,然后将用户的数据拷贝过来的,小于8组看上个函数
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; if (nr_segs == 0) { ret = 0; goto out; } if (nr_segs > UIO_MAXIOV) { ret = -EINVAL; goto out; } if (nr_segs > fast_segs) { //如果大约8组就向堆申请,否在,不申请内存,使用上个函数的内部变量 iov = kmalloc(nr_segs*sizeof(struct iovec), GFP_KERNEL); if (iov == NULL) { ret = -ENOMEM; goto out; } } if (copy_from_user(iov, uvector, nr_segs*sizeof(*uvector))) { //传入的用户数据,拷贝到内核态的内存上 ret = -EFAULT; goto out; } ret = 0; for (seg = 0; seg < nr_segs; seg++) { void __user *buf = iov[seg].iov_base; ssize_t len = (ssize_t)iov[seg].iov_len; /* see if we we're about to use an invalid len or if * it's about to overflow ssize_t */ if (len < 0) { ret = -EINVAL; goto out; } if (type >= 0 && unlikely(!access_ok(vrfy_dir(type), buf, len))) { ret = -EFAULT; goto out; } if (len > MAX_RW_COUNT - ret) { len = MAX_RW_COUNT - ret; iov[seg].iov_len = len; } ret += len; } out: *ret_pointer = iov; return ret; }
64位内核,struct iovec大小=16,也就是说,我们传入多少个,iov的个数,拷贝到内核的数据,就是16*传入的个数
我们都知道这是个堆数组越界,但是我不这么分析,漏洞手段千百中,为什么这个漏洞要用堆喷那。
我们是要控制iov数组中的数据,让他达到任意写的目的,第一个想法,直接写进来,通过用户空间的iov传入(原谅我的天真和无知),因为这个iov是复制了我们用户空间传入的iov,我们直接在用户空间构造好iov行不行。抱歉,真不行!!!你以为写内核的大佬比你还菜吗!!
如果你传入类内核的地址,好像也可以啊,我试了一下,pipe_read都跑不到,你就被干掉了,就在rw_copy_check_uvector,这一行
if (type >= 0 && unlikely(!access_ok(vrfy_dir(type), buf, len)))
如果你传入内核地址,让他copy这里是过不了,所以这个方案,是不可行,也就是说,我们必须想办法,过掉这个函数的检测。而这个函数是基于数组的检测,也就是说,你传进来多少个数组,他就检测多少个,并且他的copy是基于你数组的大小的,多余的数据你是没办法传进来的,那就只能堆喷了,因为这个数组是在堆里面,而且溢出了,我们想办法让他溢出到我们的想让他溢出的数据上就行了,这样我们就能控制iov数组的数据了。
原理分析完了,该干活了(我是反向分析的,也就是说,上面写的,是我代码调试中,一步一步得出的结论,最后我总结了一个这个漏洞各个知识点的串联,不只问结果,更加问过程,为什么这样做,如果不明白为什么,不能举一反三,根本就是不会)
通过上面的知识点,我们得出结论
1.写入的数据,就是要改变的地址的内容,是源缓冲区的内容。
2.必须构造参数导致iov溢出,堆溢出
3.堆喷
太多没用的我就不写了,看雪大神另一篇博客上写的很清楚,我在写显得太罗嗦了。有些大神可能说了,有些可能觉得太简单了,就没写。我把一些边边角角的补充一下。
1.写入数据,这个为什么可以控制,如何控制,这个可以看,pipe驱动别的相关代码,pipe缓冲区,是个环形缓冲区,申请了16页的内存,也一种16页的内存这个,这个好象是可以调整的
ulimit -a
这条命令可以查看自己电脑上的pipe相关信息。
2.如何构造iov溢出,这个大神已经写过了,ummap就可以了,这个后续调试我会写遇到的问题,但是不只是ummap,必须写够4096个字节以上才行,因为,我们必须要调用三次pipe_iov_copy_to_user,如果没有读过4096可能一次就copy完了,即使中间ummap报错goto一次,也是两次,而第二次,atomic是等于0的,会导致写错误。理论上来说多几次应该也可以的。
3.要进行内核堆喷,就要了解内核堆,了解内核内存分配机制slub这一类的,可以看下下面的博客
https://blog.csdn.net/FreeeLinux/article/details/54754752
我用kmalloc进行过测试,申请相同大小的内存,内存之间是没有缝隙的,只限于组与组之间。而且内存申请释放之后,如果再次申请到相同的内存,如果没有人用过这个页面,数据极有可能没有改变。所以这里就出现了两种堆喷的方式,
(1)看雪大神,用的是,申请4k的一整个页面,数组在这个页里面全铺满,数组的溢出,就是在这一个页的相邻的下一个页,由于内核堆组内数据之间没有缝隙,可以申请很多页,总能碰到下一页是我申请的数据的情况。
(2)iovyroot,用的是堆块使用玩后不清理,再次申请的数据跟上次一样的机制,但是他使用一页,稍微大几个字节,这样内存申请内存的时候,一样返回的是两页,但是copy到内核内存中的字节却只有一页大几个字节,然后将iov溢出的数据,填在copy到内存中的数据的大小的后面,进行堆喷,这样,如果内核刚好申请到他的堆喷的页面,只copy了1页多几个自己的数据,剩下的数据,还是原来的,就可以用来控制堆喷了。
第二中方式,在这里可能更好一下,第一种方法,应该也会有一点点第二种方法的概率在里面,但是成功率,没有第二个高
环境上面写了,内核下载下来以后,git回退到了这个漏洞修复前的位置,内核突然变成3.14版本了。。。。
既然有源码,我这么菜,不如调试一下,改一下pipe驱动的代码。
问题1:我修改了pipe驱动,在pipe_read函数中增加了printk,但是在内核跑得时候,却打印不出来,没有任何显示
问题2:gdb调试内核跑飞,如何编译一个O0的内核,便于调试,我按照网上的单独增加pipe.o的编译标识未-O0还是没啥反映。
方法总比困难的,希望有大神,能解释一下我上面提出的问题,后来我又写了模仿pipe直接写了一个驱动。
wirte函数,我只设置了一个页的缓冲区,写入一次就行,多次使用,read函数只是实现了驱动的漏洞部分的代码,如果调试驱动有困难,可以试一试我这种方法。
1.堆喷,iov这个内存块,如果使用第一种方式堆喷,可以看一下他是什么时候释放的,我测试的时候,iov在使用完后释放了,单线程,完全没办法造成堆喷,也是我只跑了一个内核,任务少的原因,每次iov的地址,基本上跟上次地址都是相同的,想要进行堆喷,必须进行多线程。
2.munmap和mmap的时机问题,大神博客基本写过了,但是实际调试过程中,却发现还是有问题的,可能时间有些出入,每台机器毕竟都不相同,运行的时候
线程1 线程2 线程3
iov_fault_in_pages_write
unmap
pipe_iov_copy_to_user
mmap
然后goto调用下一次pipe_iov_copy_to_user
堆喷完成,数据布置位置准确
调用第三次pipe_iov_copy_to_user
我曾经双线程,就调试这一个问题,最后还是没办法把握好时间处理成功
3.x86有cr0位有内核写保护,我太傻了,还以为直接能跑,第16位,数组那种,0是第一位,而且,设一次不好使,cpu环境一旦切换还是不行,这好象是个基本问题吧。。。。但是我确实在这死过好几天。
native_write_cr0(0x80040033); 和 native_read_cr0();
这两个我是分析内核源码找到的。。
4.提权使用的是sys_call_table,由于是自己编译的内核,我是直接写死的地址,但是我去,调用号完全不一样,我去内核源码找,也没找到正常的,反正跟真实的都不对,最后我去查看sys_call_table的内存,然后一行一行的对函数地址才找到的。
5.gdb调试内核,多线程不好调试,动不动飞了 最后我用了set scheduler-locking step ,然后不跑了,关于这个命令,网上说的不是很明白,有没有大神,能给我通俗易懂的解释一下
我实现的驱动代码:
static int iov_fault_in_pages_write(struct iovec *iov, unsigned long len) { while (!iov->iov_len) iov++; while (len > 0) { unsigned long this_len; this_len = min_t(unsigned long, len, iov->iov_len); if (fault_in_pages_writeable(iov->iov_base, this_len)) { // printk(KERN_WARNING "iov_fault_in_pages_write iov_base = %p\n", iov->iov_base); // printk(KERN_WARNING "iov_fault_in_pages_write iov_len = %d\n", iov->iov_len); break; } len -= this_len; iov++; } return len; } static int pipe_iov_copy_to_user(struct iovec *iov, const void *from, unsigned long len, int atomic) { unsigned long copy; while (len > 0) { while (!iov->iov_len) iov++; copy = min_t(unsigned long, len, iov->iov_len); // printk(KERN_WARNING "iov_base = %p\n", iov->iov_base); // printk(KERN_WARNING "iov_len = %d\n", iov->iov_len); // printk(KERN_WARNING "from = %x\n",from); if (atomic) { if (__copy_to_user_inatomic(iov->iov_base, from, copy)) { return -EFAULT; } } else { if (copy_to_user(iov->iov_base, from, copy)) { return -EFAULT; } } from += copy; len -= copy; iov->iov_base += copy; iov->iov_len -= copy; } return 0; } static int pipe_iov_copy_from_user(void *to, struct iovec *iov, unsigned long len, int atomic) { unsigned long copy; while (len > 0) { while (!iov->iov_len) iov++; copy = min_t(unsigned long, len, iov->iov_len); if (atomic) { if (__copy_from_user_inatomic(to, iov->iov_base, copy)) return -EFAULT; } else { if (copy_from_user(to, iov->iov_base, copy)) return -EFAULT; } to += copy; len -= copy; iov->iov_base += copy; iov->iov_len -= copy; } return 0; } static ssize_t pipe_write(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, loff_t ppos) { printk(KERN_WARNING "vuln_write v1\n"); struct file *filp = iocb->ki_filp; struct pipe_inode_info *pipe = filp->private_data; ssize_t ret; int do_wakeup; struct iovec *iov = (struct iovec *)_iov; size_t total_len; ssize_t chars; int error, atomic = 1; char *src; struct page *page = pipe->tmp_page; int get_root = *((int *)(iov[0].iov_base)); printk(KERN_WARNING "get_root = %d\n", get_root); printk(KERN_WARNING "buffers = %d\n", iov[1].iov_base); printk(KERN_WARNING "buffers = %d\n", iov[1].iov_len); if (!page) { page = alloc_page(GFP_HIGHUSER); if (unlikely(!page)) { ret = ret ?: -ENOMEM; return ret; } pipe->tmp_page = page; } total_len = iov_length(iov, nr_segs); src = kmap_atomic(page); printk(KERN_WARNING "src = %p\n", src); error = pipe_iov_copy_from_user(src, iov, chars, atomic); kunmap_atomic(src); return 0; } static ssize_t pipe_read(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, loff_t pos) { printk(KERN_WARNING "vuln_read\n"); struct file *filp = iocb->ki_filp; struct pipe_inode_info *pipe = filp->private_data; struct iovec *iov = (struct iovec *)_iov; size_t total_len; size_t chars; int error, atomic; void *addr; ssize_t ret = 0; struct page *page = pipe->tmp_page; total_len = iov_length(iov, nr_segs); native_write_cr0(0x80040033); unsigned long cr0 = native_read_cr0(); printk(KERN_WARNING "vuln_open cr0 = %lx\n", cr0); printk(KERN_WARNING "vuln_read total_len = %d\n", total_len); printk(KERN_WARNING "vuln_read iov = %p\n", iov); // struct iovec *target = (struct iovec *)kzalloc(8192, GFP_KERNEL); // printk(KERN_WARNING "vuln_read iov = %p\n", target); // target[0].iov_base = 0xffffffff81801550; // target[0].iov_len = 0x8; // target[1].iov_base = 0xffffffff81801550; // target[1].iov_len = 0x8; // target[2].iov_base = 0xffffffff81801550; // target[2].iov_len = 0x8; while (1) { chars = 4096; if (chars > total_len) chars = total_len; printk(KERN_WARNING "pipe_read chars = %d\n", chars); atomic = !iov_fault_in_pages_write(iov, chars); redo: if (atomic) addr = kmap_atomic(page); else addr = kmap(page); printk(KERN_WARNING "pipe_read atomic = %d\n", atomic); // mdelay(300); error = pipe_iov_copy_to_user(iov, addr, chars, atomic); printk(KERN_WARNING "pipe_read error = %d\n", error); if (atomic) kunmap_atomic(addr); else kunmap(page); if (unlikely(error)) { if (atomic) { atomic = 0; goto redo; } if (!ret) ret = error; break; } ret += chars; total_len -= chars; if (!total_len) { break; } } // kfree(target); return ret; } static int pipe_close(struct inode *inode, struct file *filp) { printk(KERN_WARNING "vuln_close\n"); return 0; } static int fifo_open(struct inode *inode, struct file *filp) { struct pipe_inode_info *pipe; pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL); filp->private_data = pipe; pipe->nrbufs = 9; pipe->curbuf = 8; pipe->buffers = 7; native_write_cr0(0x80040033); unsigned long cr0 = native_read_cr0(); printk(KERN_WARNING "vuln_open cr0 = %lx\n", cr0); if (fault_in_pages_writeable(0xffffffff81801550, 8)){ printk(KERN_WARNING "fault_in_pages_writeable ture"); } return 0; } /** * The operations allowed by userspace applications. * We only really allow access through the ioctl interface. */ static struct file_operations vuln_ops = { .owner = THIS_MODULE, .unlocked_ioctl = do_ioctl, .release = vuln_release, .aio_write = pipe_write, .aio_read = pipe_read, .open = fifo_open, // .close = pipe_close, }; static int call_kernel_kmalloc(void) //用于堆喷,可以先调用这个函数,在readv { struct iovec *trash_object = kmalloc(8192, GFP_KERNEL); printk(KERN_WARNING "[x] call_kernel_kmalloc [x] =%p\n",trash_object); trash_object[257].iov_base = 0xffffffff81801550; trash_object[257].iov_len = 8; trash_object[258].iov_base = 0xffffffff81801550; trash_object[258].iov_len = 8; kfree(trash_object); return 0; }
资源:
把这个代码用linux跑就行,不过,没法提权,主要是因为x86有cr0的问题,但是漏洞利用成功是可以看到的,内核地址没法读写,它还提供了一个用户地址,用于测试,我们可以看到那个用户地址堆喷成功,数据改写了,一次可能不成功,多跑几次吧
https://github.com/mobilelinux/iovy_root_research
驱动,以及提权相关的我就不献丑了,看这个大神的吧,我也是用它的驱动改写的
https://www.secshi.com/19607.html
我的代码我思来想去还是不放了,免得误人子弟。