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():将任务加入运行队列

任务阻塞机制

阻塞任务的基本流程:

  1. 设置任务状态为TASK_INTERRUPTIBLE
  2. 调用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

漏洞触发条件实现

漏洞触发三个条件

  1. 使netlink_attachskb()返回1
  2. 避免阻塞exp进程
  3. 第二次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);

唤醒路径选择:

  1. netlink_release():文件释放时调用,副作用大
  2. netlink_rcv_wake():通过recvmsg调用,流程复杂
  3. netlink_setsockopt():通过setsockopt调用,最简单

setsockopt唤醒路径

调用链:

SYSCALL_DEFINE5(setsockopt)
  -> netlink_setsockopt()
    -> wake_up_interruptible()

条件检查:

  1. optlen >= 0
  2. fd是有效套接字
  3. LSM允许setsockopt操作
  4. level != SOL_SOCKET
  5. level == SOL_NETLINK
  6. optlen >= sizeof(int)
  7. optname == NETLINK_NO_ENOBUFS
  8. val != 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;
}

完整攻击流程

  1. 主线程:

    • 创建netlink套接字
    • 创建解除阻塞线程
    • 调用mq_notify()触发漏洞路径
    • netlink_attachskb()中阻塞
  2. 解除阻塞线程:

    • 等待主线程阻塞
    • 调用setsockopt()唤醒主线程
  3. 主线程被唤醒后:

    • netlink_attachskb()返回1
    • 第二次fget()返回NULL(通过STAP脚本修改)
    • 进入netlink_detachskb()触发UAF

关键点总结

  1. 使用多线程控制竞态条件窗口
  2. 通过setsockopt()NETLINK_NO_ENOBUFS选项唤醒线程
  3. 使用自旋锁协调线程执行顺序
  4. 修改STAP脚本在正确时机删除文件描述符
  5. 阻塞机制延长了漏洞触发窗口,提高利用可靠性
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() 中的阻塞代码: 唤醒路径选择: netlink_release() :文件释放时调用,副作用大 netlink_rcv_wake() :通过recvmsg调用,流程复杂 netlink_setsockopt() :通过setsockopt调用,最简单 setsockopt唤醒路径 调用链: 条件检查: optlen >= 0 fd是有效套接字 LSM允许setsockopt操作 level != SOL_SOCKET level == SOL_NETLINK optlen >= sizeof(int) optname == NETLINK_NO_ENOBUFS val != 0 多线程实现 使用pthread创建解除阻塞线程: 主线程使用自旋锁等待线程就绪: System Tap脚本修改 修改为在 netlink_attachskb() 返回时才删除FDT中的fd: 完整攻击流程 主线程: 创建netlink套接字 创建解除阻塞线程 调用 mq_notify() 触发漏洞路径 在 netlink_attachskb() 中阻塞 解除阻塞线程: 等待主线程阻塞 调用 setsockopt() 唤醒主线程 主线程被唤醒后: netlink_attachskb() 返回1 第二次 fget() 返回NULL(通过STAP脚本修改) 进入 netlink_detachskb() 触发UAF 关键点总结 使用多线程控制竞态条件窗口 通过 setsockopt() 的 NETLINK_NO_ENOBUFS 选项唤醒线程 使用自旋锁协调线程执行顺序 修改STAP脚本在正确时机删除文件描述符 阻塞机制延长了漏洞触发窗口,提高利用可靠性