管道竞争-m0leCon CTF Teaser 2025-ducts
字数 2492 2025-08-20 18:18:10

Linux管道竞争漏洞分析与利用

管道(Pipe)基础

管道概述

管道是Linux系统中一种单向的进程间通信(IPC)机制,具有读端和写端。数据从写端写入,可以从读端读出。

管道类型

无名管道(Anonymous Pipes)

  • 创建:使用pipe(2)系统调用
  • 特点:
    • 返回两个文件描述符:pipefd[0](读端)和pipefd[1](写端)
    • 通常用于父子进程间通信

示例代码:

#include <unistd.h>
#include <stdio.h>

int main() {
    int pipefd[2];
    char buf[100] = "Hello, pipe!";
    
    if(pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }
    
    // 写入数据
    write(pipefd[1], buf, sizeof(buf));
    
    // 读取数据
    char read_buf[100];
    ssize_t n = read(pipefd[0], read_buf, sizeof(read_buf));
    if(n == -1) {
        perror("read");
        return 1;
    }
    read_buf[n] = '\0';
    printf("Read: %s\n", read_buf);
    
    // 关闭文件描述符
    close(pipefd[0]);
    close(pipefd[1]);
    return 0;
}

命名管道(FIFOs)

  • 创建:使用mkfifo(3)函数
  • 特点:
    • 具有文件系统中的名称
    • 任何进程都可以打开FIFO(权限允许时)

示例代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    const char *fifo_path = "/tmp/myfifo";
    
    // 创建FIFO
    if(mkfifo(fifo_path, 0666) == -1) {
        perror("mkfifo");
        return 1;
    }
    
    // 打开FIFO用于写入
    int fd = open(fifo_path, O_WRONLY);
    if(fd == -1) {
        perror("open");
        return 1;
    }
    
    // 写入数据
    const char *msg = "Hello, FIFO!";
    write(fd, msg, strlen(msg));
    
    // 关闭文件描述符
    close(fd);
    
    // 删除FIFO
    unlink(fifo_path);
    return 0;
}

I/O操作特性

  1. 读写操作

    • 读操作:
      • 空管道:read(2)会阻塞,直到有数据可读
      • 所有写端关闭:read(2)返回0(EOF)
    • 写操作:
      • 管道已满:write(2)会阻塞,直到有空间
      • 所有读端关闭:生成SIGPIPE信号,返回-1(EPIPE
  2. 非阻塞I/O

    • 通过fcntl(2)F_SETFL操作启用O_NONBLOCK标志
  3. 原子性

    • 写入≤PIPE_BUF字节的数据是原子的
    • 写入>PIPE_BUF字节的数据可能是非原子的
    • Linux中PIPE_BUF通常为4096字节

相关系统调用

类别 系统调用
创建和管理 pipe(2), mkfifo(3), open(2), fcntl(2), dup(2), close(2)
I/O操作 read(2), write(2), poll(2), select(2), splice(2), tee(2), vmsplice(2)
其他 stat(2), unlink(2), epoll(7)

管道竞争漏洞分析

竞争条件原理

  1. 初始状态

    • 多个进程同时尝试写入管道
    • 由于互斥锁,只有一个进程能获得锁并开始写入
  2. 管道满时

    • 第一个进程写满管道后释放锁
    • 其他进程获得锁,但也会因为管道满而阻塞
    • 第一个进程唤醒读进程
  3. 读进程唤醒

    • 读进程尝试获取锁,但被放入互斥等待队列
    • 读进程位于写进程之后
  4. 竞争点

    • 当管道有空间可写时,从wait_event_interruptible_exclusive退出的进程会尝试获取锁
    • 哪个进程先获取锁取决于从等待状态退出到上锁的速度

关键函数分析

pipe_write流程

  1. 获取互斥锁mutex_lock(&pipe->mutex)
  2. 检查管道状态:
    • 如果管道有空间,直接写入
    • 如果管道满,进入等待状态
  3. 释放锁mutex_unlock(&pipe->mutex)
  4. 唤醒读进程wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM)
  5. 等待可写状态wait_event_interruptible_exclusive(pipe->wr_wait, pipe_writable(pipe))
  6. 重新获取锁mutex_lock(&pipe->mutex)

pipe_read流程

  1. 获取互斥锁mutex_lock(&pipe->mutex)
  2. 检查管道状态:
    • 如果管道为空且无写者,返回
    • 如果管道为空但有写者,等待数据
  3. 释放锁mutex_unlock(&pipe->mutex)
  4. 等待可读状态wait_event_interruptible_exclusive(pipe->rd_wait, pipe_readable(pipe))
  5. 读取数据
  6. 唤醒写进程wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM)

互斥锁机制

mutex_init

  • 初始化互斥锁为未锁定状态
  • 设置计数器为1
  • 初始化自旋锁和等待队列

mutex_lock

  1. 尝试快速路径:原子地将计数器从1减到0
  2. 如果失败,进入慢速路径:
    • 将任务加入等待队列
    • 使任务进入睡眠状态

mutex_unlock

  1. 清除所有者信息
  2. 尝试快速路径:原子地将计数器从0加到1
  3. 如果失败,进入慢速路径:
    • 验证所有者信息
    • 唤醒等待队列中的下一个任务

漏洞利用

利用思路

  1. 触发竞争

    • 多个进程同时写入超过管道缓冲区的数据
    • 导致接收流程中断,未完全接收数据
    • 唤醒另一个进程写入管道,使剩余接收的数据来自另一个进程
  2. 信息泄露

    • 利用残留的next指针(包含PIE地址)
    • 通过print_messages函数泄露地址
  3. 任意写

    • 控制message结构体的next字段
    • 指向目标地址(如GOT表)
    • 使用redact_message修改目标内容

利用步骤

  1. 泄露PIE基址

    • 利用管道竞争使接收的message包含残留的NULL_MESSAGE地址
    • 该地址与PIE基址有固定偏移
  2. 泄露libc地址

    • 构造message使next指向GOT表附近
    • 通过print_messages读取GOT表内容
  3. 劫持控制流

    • 修改GOT表项(如fwrite)为system
    • 构造message使destory_buf为"/bin/sh"
    • 触发fwrite调用执行shell

利用代码关键部分

def leak_text(r):
    payload = {0: command_print()}
    send_stage(payload)
    # 解析泄露的地址计算PIE基址

def leak_libc(r):
    payload = {
        0: write_payload(exe.got.fwrite-0x8, 0x0) + read_payload()
    }
    send_stage(payload)
    # 解析泄露的地址计算libc基址

def rewrite_got(r):
    payload = {
        0: write_payload(exe.got.fwrite, libc.sym.system) + 
           build_message(b"/bin/sh", b"Master pwner", 0x0)
    }
    send_stage(payload)

防御措施

  1. 正确同步

    • 使用适当的同步机制确保读写顺序
    • 避免依赖不可控的竞争条件
  2. 输入验证

    • 验证从管道读取的数据结构和长度
    • 检查指针有效性
  3. 权限控制

    • 限制管道访问权限
    • 使用命名管道时设置适当的文件权限
  4. 资源限制

    • 控制管道缓冲区大小
    • 监控管道使用情况

总结

Linux管道竞争漏洞源于多个进程同时写入管道时的未正确同步问题。通过精心构造的竞争条件,攻击者可以控制管道数据的接收顺序和内容,进而实现信息泄露和任意内存写入。理解管道的工作原理和内核同步机制对于发现和防御此类漏洞至关重要。

Linux管道竞争漏洞分析与利用 管道(Pipe)基础 管道概述 管道是Linux系统中一种单向的进程间通信(IPC)机制,具有读端和写端。数据从写端写入,可以从读端读出。 管道类型 无名管道(Anonymous Pipes) 创建:使用 pipe(2) 系统调用 特点: 返回两个文件描述符: pipefd[0] (读端)和 pipefd[1] (写端) 通常用于父子进程间通信 示例代码: 命名管道(FIFOs) 创建:使用 mkfifo(3) 函数 特点: 具有文件系统中的名称 任何进程都可以打开FIFO(权限允许时) 示例代码: I/O操作特性 读写操作 : 读操作: 空管道: read(2) 会阻塞,直到有数据可读 所有写端关闭: read(2) 返回0(EOF) 写操作: 管道已满: write(2) 会阻塞,直到有空间 所有读端关闭:生成 SIGPIPE 信号,返回-1( EPIPE ) 非阻塞I/O : 通过 fcntl(2) 的 F_SETFL 操作启用 O_NONBLOCK 标志 原子性 : 写入≤ PIPE_BUF 字节的数据是原子的 写入> PIPE_BUF 字节的数据可能是非原子的 Linux中 PIPE_BUF 通常为4096字节 相关系统调用 | 类别 | 系统调用 | |------|----------| | 创建和管理 | pipe(2) , mkfifo(3) , open(2) , fcntl(2) , dup(2) , close(2) | | I/O操作 | read(2) , write(2) , poll(2) , select(2) , splice(2) , tee(2) , vmsplice(2) | | 其他 | stat(2) , unlink(2) , epoll(7) | 管道竞争漏洞分析 竞争条件原理 初始状态 : 多个进程同时尝试写入管道 由于互斥锁,只有一个进程能获得锁并开始写入 管道满时 : 第一个进程写满管道后释放锁 其他进程获得锁,但也会因为管道满而阻塞 第一个进程唤醒读进程 读进程唤醒 : 读进程尝试获取锁,但被放入互斥等待队列 读进程位于写进程之后 竞争点 : 当管道有空间可写时,从 wait_event_interruptible_exclusive 退出的进程会尝试获取锁 哪个进程先获取锁取决于从等待状态退出到上锁的速度 关键函数分析 pipe_write 流程 获取互斥锁 mutex_lock(&pipe->mutex) 检查管道状态: 如果管道有空间,直接写入 如果管道满,进入等待状态 释放锁 mutex_unlock(&pipe->mutex) 唤醒读进程 wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM) 等待可写状态 wait_event_interruptible_exclusive(pipe->wr_wait, pipe_writable(pipe)) 重新获取锁 mutex_lock(&pipe->mutex) pipe_read 流程 获取互斥锁 mutex_lock(&pipe->mutex) 检查管道状态: 如果管道为空且无写者,返回 如果管道为空但有写者,等待数据 释放锁 mutex_unlock(&pipe->mutex) 等待可读状态 wait_event_interruptible_exclusive(pipe->rd_wait, pipe_readable(pipe)) 读取数据 唤醒写进程 wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM) 互斥锁机制 mutex_init 初始化互斥锁为未锁定状态 设置计数器为1 初始化自旋锁和等待队列 mutex_lock 尝试快速路径:原子地将计数器从1减到0 如果失败,进入慢速路径: 将任务加入等待队列 使任务进入睡眠状态 mutex_unlock 清除所有者信息 尝试快速路径:原子地将计数器从0加到1 如果失败,进入慢速路径: 验证所有者信息 唤醒等待队列中的下一个任务 漏洞利用 利用思路 触发竞争 : 多个进程同时写入超过管道缓冲区的数据 导致接收流程中断,未完全接收数据 唤醒另一个进程写入管道,使剩余接收的数据来自另一个进程 信息泄露 : 利用残留的 next 指针(包含PIE地址) 通过 print_messages 函数泄露地址 任意写 : 控制 message 结构体的 next 字段 指向目标地址(如GOT表) 使用 redact_message 修改目标内容 利用步骤 泄露PIE基址 : 利用管道竞争使接收的 message 包含残留的 NULL_MESSAGE 地址 该地址与PIE基址有固定偏移 泄露libc地址 : 构造 message 使 next 指向GOT表附近 通过 print_messages 读取GOT表内容 劫持控制流 : 修改GOT表项(如 fwrite )为 system 构造 message 使 destory_buf 为"/bin/sh" 触发 fwrite 调用执行shell 利用代码关键部分 防御措施 正确同步 : 使用适当的同步机制确保读写顺序 避免依赖不可控的竞争条件 输入验证 : 验证从管道读取的数据结构和长度 检查指针有效性 权限控制 : 限制管道访问权限 使用命名管道时设置适当的文件权限 资源限制 : 控制管道缓冲区大小 监控管道使用情况 总结 Linux管道竞争漏洞源于多个进程同时写入管道时的未正确同步问题。通过精心构造的竞争条件,攻击者可以控制管道数据的接收顺序和内容,进而实现信息泄露和任意内存写入。理解管道的工作原理和内核同步机制对于发现和防御此类漏洞至关重要。