管道竞争-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操作特性
-
读写操作:
- 读操作:
- 空管道:
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
- 修改GOT表项(如
利用代码关键部分
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)
防御措施
-
正确同步:
- 使用适当的同步机制确保读写顺序
- 避免依赖不可控的竞争条件
-
输入验证:
- 验证从管道读取的数据结构和长度
- 检查指针有效性
-
权限控制:
- 限制管道访问权限
- 使用命名管道时设置适当的文件权限
-
资源限制:
- 控制管道缓冲区大小
- 监控管道使用情况
总结
Linux管道竞争漏洞源于多个进程同时写入管道时的未正确同步问题。通过精心构造的竞争条件,攻击者可以控制管道数据的接收顺序和内容,进而实现信息泄露和任意内存写入。理解管道的工作原理和内核同步机制对于发现和防御此类漏洞至关重要。