CVE-2019-19726 OpenBSD dynamic loader Local Privilege Escalation Vulnerability
0x01. 漏洞介绍CVE-2019-19726 是 OpenBSD dynamic loader 在清理 LD_LIBRARY_PATH 环境变量时存在的一个本地提权漏洞,由 Qualys Research Team 发现。该漏洞获得 2020 年 Pwnie Awards Best Privilege Escalation Bug 提名。漏洞补丁可参考 libexec/ld.so/loader.c 。
0x02. 漏洞分析 2.1 setrlimit / RLIMIT_DATAint getrlimit (int resource, struct rlimit *rlim) ;int setrlimit (int resource, const struct rlimit *rlim) ;
The getrlimit () and setrlimit () system calls get and set resource limits respectively. Each resource has an associated soft and hard limit, as defined by the rlimit structure:
struct rlimit { rlim_t rlim_cur; /* Soft limit */ rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */ };
The soft limit is the value that the kernel enforces for the corresponding resource. The hard limit acts as a ceiling for the soft limit: an unprivileged process may only set its soft limit to a value in the range from 0 up to the hard limit, and (irreversibly) lower its hard limit. A privileged process (under Linux: one with the CAP_SYS_RESOURCE capability) may make arbitrary changes to either limit value.
The value RLIM_INFINITY denotes no limit on a resource (both in the structure returned by getrlimit () and in the structure passed to setrlimit ()).
简单来说,进程可以通过 setrlimit
来限制自身的资源使用上限:
对于普通进程而言,soft limit 只能位于区间 [0, hard limit]
,而 hard limit 则只能进行下调操作
对于特权进程而言,soft limit 可以随意设置
RLIM_INFINITY
表示没有任何限制
第一个参数 resource
表示资源类型,RLIMIT_DATA
表示内存资源。
RLIMIT_DATA
The maximum size of the process’s data segment (initialized data, uninitialized data, and heap). This limit affects calls to brk (2) and sbrk (2), which fail with the error ENOMEM upon encountering the soft limit of this resource.
测试代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/time.h> #include <sys/resource.h> void show_rlimit () { struct rlimit rl ; getrlimit(RLIMIT_DATA, &rl); printf ("cur=%lu, max=%lu\n" , rl.rlim_cur, rl.rlim_max); } void try_malloc (size_t size) { printf ("malloc %lu bytes: " , size); void *p = malloc (size); if (p == NULL ) { printf ("%s\n" , strerror(errno)); } else { printf ("Succeeded\n" ); free (p); } } int main (int argc, char **argv) { const unsigned long val = 1024 * 1024 ; const struct rlimit rl = { val, val}; show_rlimit(); setrlimit(RLIMIT_DATA, &rl); show_rlimit(); try_malloc(1024 ); try_malloc(val); return 0 ; }
测试结果:
$ ./a.outcur=18446744073709551615, max=18446744073709551615 cur=1048576, max=1048576 malloc 1024 bytes: Succeeded malloc 1048576 bytes: Cannot allocate memory
2.2 ARG_MAXARG_MAX 定义于 limits.h
头文件中,表示程序命令行参数(包含环境变量参数)的最大大小。
ARG_MAX
Maximum length of argument to the exec functions including environment data. Minimum Acceptable Value: {_POSIX_ARG_MAX}
_POSIX_ARG_MAX
Maximum length of argument to the exec functions including environment data. Value: 4096
2.3 CVE-2019-19726对于 SUID-root 程序,链接器 ld.so
需要去除危险的环境变量(比如 LD_LIBRARY_PATH
等),以防止通过环境变量来实现提权。
比如,glibc 中的 _dl_non_dynamic_init
函数负责清理此类危险的环境变量:
void _dl_non_dynamic_init (void ) { // ...... if (__libc_enable_secure) { static const char unsecure_envvars[] = UNSECURE_ENVVARS #ifdef EXTRA_UNSECURE_ENVVARS EXTRA_UNSECURE_ENVVARS #endif ; const char *cp = unsecure_envvars; while (cp < unsecure_envvars + sizeof (unsecure_envvars)) { __unsetenv (cp); cp = (const char *) __rawmemchr (cp, '\0' ) + 1 ; } #if !HAVE_TUNABLES if (__access ("/etc/suid-debug" , F_OK) != 0 ) __unsetenv ("MALLOC_CHECK_" ); #endif } // ...... }
UNSECURE_ENVVARS
的定义如下:
/* Environment variable to be removed for SUID programs. The names are all stuffed in a single string which means they have to be terminated with a '\0' explicitly. */ #define UNSECURE_ENVVARS \ "GCONV_PATH\0" \ "GETCONF_DIR\0" \ GLIBC_TUNABLES_ENVVAR \ "HOSTALIASES\0" \ "LD_AUDIT\0" \ "LD_DEBUG\0" \ "LD_DEBUG_OUTPUT\0" \ "LD_DYNAMIC_WEAK\0" \ "LD_HWCAP_MASK\0" \ "LD_LIBRARY_PATH\0" \ "LD_ORIGIN_PATH\0" \ "LD_PRELOAD\0" \ "LD_PROFILE\0" \ "LD_SHOW_AUXV\0" \ "LD_USE_LOAD_BIAS\0" \ "LOCALDOMAIN\0" \ "LOCPATH\0" \ "MALLOC_TRACE\0" \ "NIS_PATH\0" \ "NLSPATH\0" \ "RESOLV_HOST_CONF\0" \ "RES_OPTIONS\0" \ "TMPDIR\0" \ "TZDIR\0"
FreeBSD 相关处理代码如下(参考 src/libexec/ld.so/loader.c ):
/* * grab interesting environment variables, zap bad env vars if * issetugid, and set the exported environ and __progname variables */ void _dl_setup_env(const char *argv0, char **envp) { static char progname_storage[NAME_MAX+1 ] = "" ; /* * Get paths to various things we are going to use. */ _dl_debug = _dl_getenv("LD_DEBUG" , envp) != NULL ; _dl_libpath = _dl_split_path(_dl_getenv("LD_LIBRARY_PATH" , envp)); _dl_preload = _dl_getenv("LD_PRELOAD" , envp); _dl_bindnow = _dl_getenv("LD_BIND_NOW" , envp) != NULL ; _dl_traceld = _dl_getenv("LD_TRACE_LOADED_OBJECTS" , envp) != NULL ; _dl_tracefmt1 = _dl_getenv("LD_TRACE_LOADED_OBJECTS_FMT1" , envp); _dl_tracefmt2 = _dl_getenv("LD_TRACE_LOADED_OBJECTS_FMT2" , envp); _dl_traceprog = _dl_getenv("LD_TRACE_LOADED_OBJECTS_PROGNAME" , envp); /* * Don't allow someone to change the search paths if he runs * a suid program without credentials high enough. */ _dl_trust = !_dl_issetugid(); if (!_dl_trust) { /* Zap paths if s[ug]id... */ if (_dl_libpath) { _dl_free_path(_dl_libpath); _dl_libpath = NULL ; _dl_unsetenv("LD_LIBRARY_PATH" , envp); } if (_dl_preload) { _dl_preload = NULL ; _dl_unsetenv("LD_PRELOAD" , envp); } // ...... } // ...... }
对 LD_LIBRARY_PATH
的处理有点特殊,额外调用了 _dl_split_path
函数,对应的代码如下(参考 src/libexec/ld.so/path.c ):
char **_dl_split_path(const char *searchpath) { int pos = 0 ; int count = 1 ; const char *pp, *p_begin; char **retval; if (searchpath == NULL ) return (NULL ); /* Count ':' or ';' in searchpath */ pp = searchpath; while (*pp) { if (*pp == ':' || *pp == ';' ) count++; pp++; } /* one more for NULL entry */ count++; retval = _dl_reallocarray(NULL , count, sizeof (*retval)); if (retval == NULL ) return (NULL ); // ...... }
这里调用 _dl_reallocarray
分配内存,如果分配失败,_dl_split_path
将返回 NULL
。
回到 _dl_setup_env
函数,可以发现当 _dl_split_path
返回 NULL
时,环境变量 LD_LIBRARY_PATH
将不会被清理,此时可以通过 so 动态库加载劫持来实现 root 提权。
_dl_libpath = _dl_split_path(_dl_getenv("LD_LIBRARY_PATH" , envp)); // ...... _dl_trust = !_dl_issetugid(); if (!_dl_trust) { /* Zap paths if s[ug]id... */ if (_dl_libpath) { _dl_free_path(_dl_libpath); _dl_libpath = NULL ; _dl_unsetenv("LD_LIBRARY_PATH" , envp); }
0x03. 漏洞利用原作者挑选了 /usr/bin/chpass
来实现漏洞利用,其 main
函数做了以下操作:
调用 setuid(0)
调用 pw_init
把 RLIMIT_DATA
重置成了 RLIM_INFINITY
调用 pw_mkdb
通过 vfork / execv
执行 /usr/sbin/pwd_mkdb
,其中 execv
会继承环境变量
当 /usr/sbin/pwd_mkdb
执行时,会重新触发 ld.so
中的逻辑,但由于此时已经没有了 RLIMIT_DATA
资源限制,以下代码会成功执行:
_dl_libpath = _dl_split_path(_dl_getenv("LD_LIBRARY_PATH" , envp));
而由于 /usr/sbin/pwd_mkdb
并不是 SUID-root 程序,LD_LIBRARY_PATH
不会被清理,因此最终可以通过 so 动态库加载劫持来实现 root 提权。
另外需要注意的是,对 /usr/bin/chpass
而言,由于 _dl_libpath
为 NULL
,所以 LD_LIBRARY_PATH
对 chpass
本身是不会起作用的。
综上,应该只有极少数刚好满足条件的 SUID-root 程序,才可以完成这个提权漏洞的利用。
0x04. References
Local Privilege Escalation in OpenBSD’s dynamic loader (CVE-2019-19726) / Wayback Machine
https://pwnies.com/qualys-security-advisory-team-2/
https://github.com/openbsd/src/commit/eee3c75f9abd5ea51e066dd0fe6b1efa470e4d0c
https://linux.die.net/man/2/setrlimit
https://pubs.opengroup.org/onlinepubs/009695399/basedefs/limits.h.html
https://codebrowser.dev/glibc/glibc/elf/dl-support.c.html
https://codebrowser.dev/glibc/glibc/sysdeps/generic/unsecvars.h.html