CVE-2017-11176: 一步一步linux内核漏洞利用 (二)(阻塞)
字数 1782 2025-08-27 12:33:23
Linux内核漏洞CVE-2017-11176分析与利用(第二部分)
核心概念:Linux调度子系统
任务状态
task_struct中的state字段表示任务状态- 主要状态:
TASK_RUNNING(0):进程正在运行或准备就绪TASK_INTERRUPTIBLE(1):进程可中断的等待状态
- 状态设置方式:
- 直接修改
state字段 - 使用
__set_current_state()宏
- 直接修改
运行队列(run queue)
- 每个CPU有自己的运行队列(
struct rq) - 包含:
- 可运行任务列表
- 统计信息(nr_running, nr_switches等)
- 当前运行任务指针(curr)
- 关键操作:
deactivate_task():将任务移出运行队列activate_task():将任务加入运行队列
任务阻塞机制
阻塞任务的基本流程:
- 设置任务状态为
TASK_INTERRUPTIBLE - 调用
schedule()
schedule()函数关键行为:
- 检查当前任务状态
- 如果状态非0且无挂起信号,调用
deactivate_task() - 选择下一个要运行的任务
等待队列
- 表示阻塞任务的双链表(
wait_queue_head_t) - 等待队列元素(
wait_queue_t)包含:- flags
- private指针(通常指向
task_struct) - func回调函数(默认为
default_wake_function) - task_list链表节点
关键操作:
DECLARE_WAITQUEUE():创建等待队列元素add_wait_queue():将元素加入等待队列remove_wait_queue():从等待队列移除元素
任务唤醒机制
- 通过
__wake_up()函数唤醒任务 - 遍历等待队列,对每个元素调用其func回调
- 默认回调
default_wake_function会调用try_to_wake_up() try_to_wake_up()关键操作:- 调用
activate_task()将任务加入运行队列 - 设置任务状态为
TASK_RUNNING
- 调用
漏洞触发条件实现
漏洞触发三个条件
- 使
netlink_attachskb()返回1 - 避免阻塞exp进程
- 第二次
fget()调用返回NULL
阻塞主线程的优势
- 延长竞态条件窗口期
- 提供控制点("断点")
- 使攻击更可靠
解除阻塞机制
netlink_attachskb()中的阻塞代码:
DECLARE_WAITQUEUE(wait, current);
__set_current_state(TASK_INTERRUPTIBLE);
add_wait_queue(&nlk->wait, &wait);
schedule_timeout(*timeo);
__set_current_state(TASK_RUNNING);
remove_wait_queue(&nlk->wait, &wait);
唤醒路径选择:
netlink_release():文件释放时调用,副作用大netlink_rcv_wake():通过recvmsg调用,流程复杂netlink_setsockopt():通过setsockopt调用,最简单
setsockopt唤醒路径
调用链:
SYSCALL_DEFINE5(setsockopt)
-> netlink_setsockopt()
-> wake_up_interruptible()
条件检查:
optlen >= 0- fd是有效套接字
- LSM允许setsockopt操作
level != SOL_SOCKETlevel == SOL_NETLINKoptlen >= sizeof(int)optname == NETLINK_NO_ENOBUFSval != 0
多线程实现
使用pthread创建解除阻塞线程:
struct unblock_thread_arg {
int fd;
bool is_ready;
};
static void* unblock_thread(void *arg) {
struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;
int val = 3535;
uta->is_ready = true; // 通知主线程
sleep(5); // 给主线程时间阻塞
_setsockopt(uta->fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val));
return NULL;
}
主线程使用自旋锁等待线程就绪:
while (uta.is_ready == false)
; // 自旋等待
System Tap脚本修改
修改为在netlink_attachskb()返回时才删除FDT中的fd:
function force_trigger_after:long (arg_sock:long)
{
struct files_struct *files = current->files;
struct fdtable *fdt = files_fdtable(files);
fdt->fd[3] = NULL;
}
完整攻击流程
-
主线程:
- 创建netlink套接字
- 创建解除阻塞线程
- 调用
mq_notify()触发漏洞路径 - 在
netlink_attachskb()中阻塞
-
解除阻塞线程:
- 等待主线程阻塞
- 调用
setsockopt()唤醒主线程
-
主线程被唤醒后:
netlink_attachskb()返回1- 第二次
fget()返回NULL(通过STAP脚本修改) - 进入
netlink_detachskb()触发UAF
关键点总结
- 使用多线程控制竞态条件窗口
- 通过
setsockopt()的NETLINK_NO_ENOBUFS选项唤醒线程 - 使用自旋锁协调线程执行顺序
- 修改STAP脚本在正确时机删除文件描述符
- 阻塞机制延长了漏洞触发窗口,提高利用可靠性