
CVE-2022-0185에 대한 교육용 사례 연구 및 익스플로잇 개발 워크스루입니다. 로컬 권한 상승을 가능하게 하는 Linux 커널 힙 기반 버퍼 오버플로우 취약점입니다. POC, QEMU 디버깅, 그리고 상세 기술 분석이 포함된 우분투 익스플로잇을 제공합니다.
이 사례 연구는 ECE 9069: 해킹 입문 과제의 결과물입니다: https://whisperlab.org/introduction-to-hacking/
CVE-2022-0185는 Linux 커널의 Filesystem Context 기능에서 legacy_parse_param 함수가 제공된 매개변수 길이를 검증하는 방식에서 발견된 힙 기반 버퍼 오버플로 취약점입니다. (unprivileged user namespaces가 활성화된 경우 비특권, 그렇지 않으면 namespaced CAP_SYS_ADMIN 권한 필요) 로컬 사용자가 Filesystem Context API를 지원하지 않는 파일시스템을 열어 (따라서 레거시 처리로 대체) 이 결함을 이용하여 시스템에서 권한을 상승시킬 수 있습니다. [1]
이 취약점이 보고된 후, 이 버그를 수정하기 위한 패치가 릴리스되었습니다:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=722d94847de2
https://ubuntu.com/security/CVE-2022-0185#impact-score
explorer의 상세 분석 글: https://www.hackthebox.com/blog/CVE-2022-0185:_A_case_study
이 저장소에서는 이 취약점을 재현하기 위한 기본 단계와 관련 배경 정보를 설명합니다. 또한, 이해가 안 되는 부분이 있으면 exmail:[email protected]로 문의해 주세요. 기꺼이 답변해 드리겠습니다.
CVE-2022-0185 취약점은 2022년 2월 11일에 공개되었으며, CVSS 3.x 기본 점수는 8.4(높음)입니다.[1] 이 취약점은 부호 없는 정수 언더플로우로 인해 발생하는 힙 기반 버퍼 오버플로입니다.
이 취약점은 Linux v5.1 커널에서 도입되어, 커널 버전 5.1 이상의 모든 Linux 배포판에 영향을 미쳤습니다. 예를 들어, Ubuntu 20.04 LTS (focal)가 이 버그에 취약했습니다. 그러나 패치가 릴리스되어 버전 5.4.0-96.109부터 사용 가능합니다.[3]
이 취약점을 악용하면 비특권 로컬 사용자가 시스템에서 권한을 상승시켜 잠재적으로 전체 시스템을 손상시킬 수 있습니다.[1][2]
다음은 CVSS 점수에 대한 상세 분석입니다: 기본 점수: 8.4, 즉각적인 주의가 필요한 심각한 보안 위험을 나타냅니다. 영향 점수: 5.9, 악용될 경우 상당한 잠재적 피해를 시사합니다. 높은 기밀성, 무결성, 가용성 값이 이 점수에 기여합니다. 악용 가능성 점수: 2.5, 비교적 높은 악용 가능성을 시사합니다. 로컬 공격 벡터, 높은 무결성, 높은 가용성 값이 이 점수에 기여합니다.
| CVSS v3.1 심각도 | 값 |
|---|---|
| 기본 점수 | 8.4 높음 |
| 영향 점수 | 5.9 |
| 악용 가능성 점수 | 2.5 |
표 1.1 CVSS 심각도 점수[1]
| CVSS v3.1 메트릭 | 값 |
|---|---|
| 공격 벡터(AV) | 로컬 |
| 필요 권한(PR) | 없음 |
| 사용자 상호작용(UI) | 없음 |
| 기밀성(C) | 높음 |
| 무결성(I) | 높음 |
| 가용성(A) | 높음 |
표 1.2 CVSS 벡터[1]
현대 컴퓨터에는 signed와 unsigned 두 가지 정수 유형이 있습니다. signed 숫자의 표현은 일반적으로 2의 보수(2's complement)라는 연산을 포함합니다.[4] "2의 보수는 가장 큰 자릿값을 가진 이진수를 부호로 사용하여 이진수가 양수인지 음수인지 나타냅니다."[4]
2의 보수를 도입하면 뺄셈 계산을 덧셈으로 변환하여 CPU의 설계와 구현을 단순화합니다. 정수의 2의 보수를 생성하는 단계는 다음과 같습니다:[4]
그림 2.1.1.1은 변환 과정을 "-6"을 2의 보수 형식으로 변환하는 실제 예제와 함께 다이어그램으로 보여줍니다.

그림 2.1.1.2 2의 보수를 사용한 덧셈
그림 2.1.1.2는 '-6'의 2의 보수를 '+6'에 더하는 과정을 보여줍니다. 이는 2의 보수를 사용하면 덧셈이 뺄셈을 대체할 수 있음을 보여줍니다.

그림 2.1.1.2 2의 보수를 사용한 덧셈
2.1.1절에서 우리는 이미 2의 보수가 무엇인지 이해했습니다. 이제 컴퓨터에서 부호 없는 숫자 언더플로우 시나리오를 살펴보겠습니다. 현대 컴퓨터에서 부호 없는 숫자를 사용할 때, 최상위 비트는 부호 비트로 처리되지 않고, 대신 부호 없는 숫자 자체의 일부입니다. 이 상황은 부호 없는 숫자로 뺄셈을 수행할 때 주의해야 함을 의미하며, 부호 없는 숫자 언더플로우라고 알려진 상태로 이어질 수 있습니다.[5]
그림 2.1.2.1은 8비트 부호 없는 숫자에 대해 5에서 6을 빼는 상황을 보여줍니다. 부호 없는 숫자 래핑으로 인해 최종 결과는 255입니다. 이 언더플로우가 조건문에서 발생하면, 해당 문의 기능을 방해할 수 있습니다.

그림 2.1.2.1 부호 없는 숫자 언더플로우
리눅스 커널에서 슬랩 할당기(Slab Allocator)는 작은 메모리 청크를 효율적으로 할당 및 해제하는 데 사용되는 메모리 관리 메커니즘입니다. 각각 고정 크기 메모리 블록을 포함하는 여러 슬랩 캐시를 유지함으로써 성능을 제공합니다. 일반적으로 kmalloc-32는 32바이트 메모리를 할당하며, 이는 kmalloc-32 슬랩입니다. 반면 kmalloc-4k는 4096바이트 메모리를 할당하며, 이는 kmalloc-4k 슬랩입니다.[6]
또한, 리눅스 커널의 슬랩 할당은 일반적으로 커널 힙 메모리 영역 내의 연속적인 주소 공간에서 메모리를 할당하는 것을 포함합니다. 이 연속적인 주소는 커널에 의해 관리되며 다양한 커널 객체 및 데이터 구조에 메모리를 할당하는 데 사용됩니다. 그림 2.2.1.1은 리눅스 커널 메모리의 슬랩 레이아웃을 보여줍니다.

그림 2.2.1.1 리눅스의 슬랩 할당기 [7] (이 그림의 작성자는 https://leviathan.vip/입니다.)
자체 컴파일된 리눅스 커널로 이 과정을 재현하려면 다음 마크다운 파일을 읽어 배경 정보를 얻으십시오:
참고: 모든 마크다운 파일과 코드 및 스크립트는 이 저장소의 다른 폴더에 있으며, 각 폴더에는 자체 마크다운 파일이 있으므로 무언가를 시도하기 전에 읽으십시오!
2.1절에서 우리는 부호 없는 언더플로우의 작동 방식을 설명했습니다. 이제 이 취약점을 포함하는 커널 함수를 살펴보겠습니다.
사용자 "clubby789"이 커널 함수 legacy_parse_param에서 취약점을 발견했습니다. 이 함수는 주로 커널에 전달된 매개변수를 구문 분석하는 역할을 합니다. CVE-2022-0185에서는 fsopen을 사용하여 파일 디스크립터를 연 후, fsconfig 함수를 사용하여 구성 키-값 쌍을 커널에 전달한 후에 호출되었습니다. legacy_parse_param의 단순화된 버전은 다음 코드에 나와 있습니다. [2]```c static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param) { struct legacy_fs_context *ctx = fc->fs_private; // [1] unsigned int size = ctx->data_size; // [2] size_t len = 0; int ret; [ ... ] switch (param->type) { case fs_value_is_string: len = 1 + param->size; // [3] 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"); // [4] [ ... ] if (!ctx->legacy_data) { ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); // [5] if (!ctx->legacy_data) return -ENOMEM; } ctx->legacy_data[size++] = ','; // [6] 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; }
위 코드 조각에서 볼 수 있듯이, [1]번째 줄과 [2]번째 줄은 코드의 컨텍스트를 설정하고, [4]번째 줄에는 부호 없는 언더플로우가 발생하는 문장이 포함되어 있습니다. [5]번째 줄은 힙 슬랩 할당을 처리하며, [6]번째 줄과 [7]번째 줄은 할당된 슬랩에 데이터를 채우는 역할을 합니다. 특히 [6]번째 줄에서는 쉼표(',')를 별도의 구분자로 추가하고 등호('=')도 추가되어 실제 데이터 크기를 초과하는 2바이트가 더 발생합니다.
[4]번째 줄에서 if 문 내의 변수에는 PAGE_SIZE (4096으로 설정된 매크로)와 size (부호 없는 64비트 숫자)가 포함됩니다. 부호 없는 숫자가 4095까지 누적되면 언더플로우가 발생하여 if 문이 항상 false로 평가됩니다. 이로 인해 인접한 슬랩에 대한 경계를 벗어난 쓰기가 가능해집니다. 언더플로우는 부호 없는 숫자를 뺄셈하여 발생하며, 4096 - 4095의 결과로 부호 없는 숫자 18446744073709551615가 생성됩니다.[2]
`18446744073709551615`의 결과는 다음 텍스트에서 설명됩니다:```c
if (len > PAGE_SIZE-2-size) return invalf(fc, "VFS: Legacy: Cumulative options too large");
Notice that, here the PAGE_SIZE equals to 4096 bytes, and the 2 equals to the character , and = added to separate each key-value pair. The issue is that size is an unsigned value, thus when size reach 4095, the statement of PAGE_SIZE-2-size will equals to signed value: -1, however for unsigned value: 18446744073709551615 because the 2's complement as shown in the following diagram! [3]

따라서 위의 if 문은 항상 거짓이 되며, 이는 나머지 데이터가 할당된 slab을 넘어 힙에 복사된다는 것을 의미합니다!
이 부호 없는 숫자 언더플로우가 어떻게 발생할 수 있는지 이해한 후, 취약점을 입증하기 위한 개념 증명(POC) 코드를 작성할 수 있습니다.
사용자 “clubby789”가 다음 코드 조각에 표시된 상세한 POC 코드를 제공합니다. 코드는 간결합니다. 먼저 ext4라는 파일 디스크립터를 열고, 그런 다음 fsconfig를 여러 번 사용하여 데이터를 커널에 채웁니다.
여기서 주목할 점 두 가지: