「PWN」CVE-2023-4911 复现
最近接触到了这个洞,感觉利用面还是挺广的,虽然国内绝大多数机子似乎 libc 版本都挺低的(),总而言之先调调看看吧。
环境搭建
测试环境
OS: Ubuntu 22.04.1 LTS on Windows 10 x86_64
Kernel: 5.15.123.1-microsoft-standard-WSL2
Glibc: 2.35-0ubuntu3.3
测试效果
漏洞点
在执行 set-user-ID
和 set-group-ID
程序时,代码会以特权模式执行。在 commit 2ed18c("Fix SXID_ERASE behavior in setuid programs (BZ #27471)") (glibc2.34) 中引入了一个处理 GLIBC_TUNABLES 环境变量时的缓存区溢出漏洞。
我们可以观察一下 glibc
的源码
------------------------------------------------------------------------
269 void
270 __tunables_init (char **envp)
271 {
272 char *envname = NULL;
273 char *envval = NULL;
274 size_t len = 0;
275 char **prev_envp = envp;
...
279 while ((envp = get_next_env (envp, &envname, &len, &envval,
280 &prev_envp)) != NULL)
281 {
282 if (tunable_is_name ("GLIBC_TUNABLES", envname))
283 {
284 char *new_env = tunables_strdup (envname);
285 if (new_env != NULL)
286 parse_tunables (new_env + len + 1, envval);
287 /* Put in the updated envval. */
288 *prev_envp = new_env;
289 continue;
290 }
------------------------------------------------------------------------
在 __tunables_init() 函数定义这里,我们可以观察到它首先对 envname 进行了判断(#282),判断条件是 orig == envname
,且后一位 envname == '=' && orig == '\0'
。
static __always_inline bool
tunable_is_name (const char *orig, const char *envname)
{
for (;*orig != '\0' && *envname != '\0'; envname++, orig++)
if (*orig != *envname)
break;
/* The ENVNAME is immediately followed by a value. */
if (*orig == '\0' && *envname == '=')
return true;
else
return false;
}
此后,它会对找到的 envname 进行 tunables_strdup
深拷贝(#284)
static char *
tunables_strdup (const char *in)
{
size_t i = 0;
while (in[i++] != '\0');
char *out = __sbrk (i);
/* For most of the tunables code, we ignore user errors. However,
this is a system error - and running out of memory at program
startup should be reported, so we do. */
if (out == (void *)-1)
_dl_fatal_printf ("sbrk() failure while processing tunables\n");
i--;
while (i-- > 0)
out[i] = in[i];
return out;
}
对副本进行拷贝后,它将原本的 GLIBC_TUNABLES
过滤后替换为副本(利用 parse_tunables()
,其中第一个参数是环境变量副本的值对应的指针,第二个参数是原始的环境变量的值的指针)
在 parse_tunables()
中,它会移除所有 arg1 里的危险 tunables( SXID_ERASE
),但会保留 SXID_IGNORE
和 NONE
,关于这点我们后面再说。 GLIBC_TUNABLES
的格式为 GLIBC_TUNABLES=aaa=111:bbb=222
,在前面都是对一些情况的判断,如没有赋值的情况等,但我们考虑一种情况:当我们的 GLIBC_TUNABLES=AAA=AAA=BBB
时,我们查看程序流:
// 第一次
static void
parse_tunables (char *tunestr, char *valstring)
{
if (tunestr == NULL || *tunestr == '\0')
return;
char *p = tunestr; // >> [A]AA=AAA=BBB
size_t off = 0;
while (true)
{
char *name = p;
size_t len = 0;
/* First, find where the name ends. */
while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
len++;
// >> len = 3
/* If we reach the end of the string before getting a valid name-value
pair, bail out. */
if (p[len] == '\0')
{
if (__libc_enable_secure)
tunestr[off] = '\0';
return;
}
/* We did not find a valid name-value pair before encountering the
colon. */
if (p[len]== ':')
{
p += len + 1;
continue;
}
p += len + 1; // >> AAA=[A]AA=BBB
/* Take the value from the valstring since we need to NULL terminate it. */
char *value = &valstring[p - tunestr]; // >> AAA=[A]AA=BBB
len = 0;
while (p[len] != ':' && p[len] != '\0')
len++; // >> len=7
/* Add the tunable if it exists. */
for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
{
tunable_t *cur = &tunable_list[i];
if (tunable_is_name (cur->name, name))
{
/* If we are in a secure context (AT_SECURE) then ignore the
tunable unless it is explicitly marked as secure. Tunable
values take precedence over their envvar aliases. We write
the tunables that are not SXID_ERASE back to TUNESTR, thus
dropping all SXID_ERASE tunables and any invalid or
unrecognized tunables. */
if (__libc_enable_secure)
{
if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
{
if (off > 0) // off = 0
tunestr[off++] = ':';
const char *n = cur->name;
while (*n != '\0')
tunestr[off++] = *n++; // [AAA]=AAA=BBB
tunestr[off++] = '='; // >> AAA[=]AAA=BBB
for (size_t j = 0; j < len; j++)
tunestr[off++] = value[j]; // >> AAA=[AAA=BBB]
}
if (cur->security_level != TUNABLE_SECLEVEL_NONE)
break; // >> 触发
}
value[len] = '\0';
tunable_initialize (cur, value);
break;
}
}
if (p[len] != '\0') // AAA=[A]AA=BBB(), 所以没进入
p += len + 1;
}
}
// 第二次
static void
parse_tunables (char *tunestr, char *valstring)
{
if (tunestr == NULL || *tunestr == '\0')
return;
char *p = tunestr;
size_t off = 0;
while (true)
{
char *name = p; // AAA=[A]AA=BBB
size_t len = 0;
/* First, find where the name ends. */
while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
len++;
// >> len = 3
/* If we reach the end of the string before getting a valid name-value
pair, bail out. */
if (p[len] == '\0')
{
if (__libc_enable_secure)
tunestr[off] = '\0';
return;
}
/* We did not find a valid name-value pair before encountering the
colon. */
if (p[len]== ':')
{
p += len + 1;
continue;
}
p += len + 1; // AAA=AAA=[B]BB
/* Take the value from the valstring since we need to NULL terminate it. */
char *value = &valstring[p - tunestr]; // AAA=AAA=[B]BB
len = 0;
while (p[len] != ':' && p[len] != '\0')
len++; // >> len=3
/* Add the tunable if it exists. */
for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
{
tunable_t *cur = &tunable_list[i];
if (tunable_is_name (cur->name, name))
{
/* If we are in a secure context (AT_SECURE) then ignore the
tunable unless it is explicitly marked as secure. Tunable
values take precedence over their envvar aliases. We write
the tunables that are not SXID_ERASE back to TUNESTR, thus
dropping all SXID_ERASE tunables and any invalid or
unrecognized tunables. */
if (__libc_enable_secure)
{
if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
{
if (off > 0)
tunestr[off++] = ':'; // >> AAA=AAA=BBB[:]
const char *n = cur->name;
while (*n != '\0')
tunestr[off++] = *n++; // >> AAA=AAA=BBB:[AAA]
tunestr[off++] = '='; // >> AAA=AAA=BBB:AAA[=]
for (size_t j = 0; j < len; j++)
tunestr[off++] = value[j]; // >> AAA=AAA=BBB:AAA=[BBB]
}
if (cur->security_level != TUNABLE_SECLEVEL_NONE)
break; // >> 触发
}
value[len] = '\0';
tunable_initialize (cur, value);
break;
}
}
if (p[len] != '\0') // AAA=AAA=BBB[:]AAA=BBB,所以没进入
p += len + 1;
}
}
注意到此时我们已经溢出了 tunestr,但是 还没完
// 第三次
static void
parse_tunables (char *tunestr, char *valstring)
{
if (tunestr == NULL || *tunestr == '\0')
return;
char *p = tunestr;
size_t off = 0;
while (true)
{
char *name = p; // AAA=AAA=BBB[:]AAA=BBB
size_t len = 0;
/* First, find where the name ends. */
while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
len++;
// >> len = 0
/* If we reach the end of the string before getting a valid name-value
pair, bail out. */
if (p[len] == '\0')
{
if (__libc_enable_secure)
tunestr[off] = '\0';
return;
}
/* We did not find a valid name-value pair before encountering the
colon. */
if (p[len]== ':')
{
p += len + 1;
continue;
}
// 第四次
static void
parse_tunables (char *tunestr, char *valstring)
{
if (tunestr == NULL || *tunestr == '\0')
return;
char *p = tunestr;
size_t off = 0;
while (true)
{
char *name = p; // AAA=AAA=BBB:[A]AA=BBB
size_t len = 0;
/* First, find where the name ends. */
while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
len++;
// >> len = 3
/* If we reach the end of the string before getting a valid name-value
pair, bail out. */
if (p[len] == '\0')
{
if (__libc_enable_secure)
tunestr[off] = '\0';
return;
}
/* We did not find a valid name-value pair before encountering the
colon. */
if (p[len]== ':')
{
p += len + 1;
continue;
}
p += len + 1; // AAA=AAA=BBB:AAA=[B]BB
/* Take the value from the valstring since we need to NULL terminate it. */
char *value = &valstring[p - tunestr]; // AAA=AAA=BBB\0UNEX[P]ECTED_STRING\0
len = 0;
while (p[len] != ':' && p[len] != '\0')
len++; // >> len=3
/* Add the tunable if it exists. */
for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
{
tunable_t *cur = &tunable_list[i];
if (tunable_is_name (cur->name, name))
{
/* If we are in a secure context (AT_SECURE) then ignore the
tunable unless it is explicitly marked as secure. Tunable
values take precedence over their envvar aliases. We write
the tunables that are not SXID_ERASE back to TUNESTR, thus
dropping all SXID_ERASE tunables and any invalid or
unrecognized tunables. */
if (__libc_enable_secure)
{
if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
{
if (off > 0)
tunestr[off++] = ':'; // >> AAA=AAA=BBB:AAA=BBB[:]
const char *n = cur->name;
while (*n != '\0')
tunestr[off++] = *n++; // >> AAA=AAA=BBB:AAA=BBB:[AAA]
tunestr[off++] = '='; // >> AAA=AAA=BBB:AAA=BBB:AAA=
for (size_t j = 0; j < len; j++)
tunestr[off++] = value[j]; // >> AAA=AAA=BBB:AAA=BBB:AAA=[PEC]
}
if (cur->security_level != TUNABLE_SECLEVEL_NONE)
break; // >> 触发
}
value[len] = '\0';
tunable_initialize (cur, value);
break;
}
}
if (p[len] != '\0') // AAA=AAA=BBB:AAA=[B]BB{:}AAA=PEC,进入
p += len + 1; // AAA=AAA=BBB:AAA=BBB:[A]AA=PEC
}
}
// 第五次
static void
parse_tunables (char *tunestr, char *valstring)
{
if (tunestr == NULL || *tunestr == '\0')
return;
char *p = tunestr;
size_t off = 0;
while (true)
{
char *name = p; // AAA=AAA=BBB:AAA=BBB:[A]AA=PEC
size_t len = 0;
/* First, find where the name ends. */
while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
len++;
// >> len = 3
/* If we reach the end of the string before getting a valid name-value
pair, bail out. */
if (p[len] == '\0')
{
if (__libc_enable_secure)
tunestr[off] = '\0';
return;
}
/* We did not find a valid name-value pair before encountering the
colon. */
if (p[len]== ':')
{
p += len + 1;
continue;
}
p += len + 1; // AAA=AAA=BBB:AAA=BBB:AAA=[P]EC
/* Take the value from the valstring since we need to NULL terminate it. */
char *value = &valstring[p - tunestr]; // AAA=AAA=BBB\0UNEXPECTED_S[T]RING\0
len = 0;
while (p[len] != ':' && p[len] != '\0')
len++; // >> len=3
/* Add the tunable if it exists. */
for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
{
tunable_t *cur = &tunable_list[i];
if (tunable_is_name (cur->name, name))
{
/* If we are in a secure context (AT_SECURE) then ignore the
tunable unless it is explicitly marked as secure. Tunable
values take precedence over their envvar aliases. We write
the tunables that are not SXID_ERASE back to TUNESTR, thus
dropping all SXID_ERASE tunables and any invalid or
unrecognized tunables. */
if (__libc_enable_secure)
{
if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
{
if (off > 0)
tunestr[off++] = ':'; // >> AAA=AAA=BBB:AAA=BBB:AAA=PEC[:]
const char *n = cur->name;
while (*n != '\0')
tunestr[off++] = *n++; // >> AAA=AAA=BBB:AAA=BBB:AAA=PEC:[AAA]
tunestr[off++] = '='; // >> AAA=AAA=BBB:AAA=BBB:AAA=PEC:AAA[=]
for (size_t j = 0; j < len; j++)
tunestr[off++] = value[j]; // >> AAA=AAA=BBB:AAA=BBB:AAA=PEC:AAA=TRI
}
if (cur->security_level != TUNABLE_SECLEVEL_NONE)
break; // >> 触发
}
value[len] = '\0';
tunable_initialize (cur, value);
break;
}
}
if (p[len] != '\0') // AAA=AAA=BBB:AAA=BBB:[A]AA={P}EC:AAA=TRI,进入
p += len + 1; // AAA=AAA=BBB:AAA=BBB:AAA=[P]EC:AAA=TRI
}
}
下一次由于 tunable_is_name (cur->name, name)
不再满足,退出。
我们可以进行简单的测试:
利用思路
既然我们已经找到了漏洞点,那么我们就要思考如何利用它了。首先来看 tunestr,它是由 tunables_strdup
分配的。在这里,根据 commit 33237fe,已经将 sbrk
更换为了 __minimal_malloc() 函数(因此 glibc2.34 实际上无法使用这个 exp),观察函数可以知道它只是一个 mmap
的包装实现而已。此时我们观察 vmmap,发现此时可写的地方很少,只有 ld.so 本身的可读可写段(第一页是 RELRO 段,在后面会被 mprotect
改写为只读;不过第二页可以用),但是我们观察 __minimal_malloc
,可以知道它至少分配两页,因此我们的缓存区不可能分配在 ld.so 的第二页。
Insufficient space left; allocate another page plus one extra page to reduce number of mmap calls.
因此我们只能溢出到 __minimal_malloc
分配出的页上。注意到 __tunables_init
可以处理多个环境变量,因此我们可以考虑这样:构建两 个 GLIBC_TUNABLES
环境变量,第一个不溢出(长度大到不能被 ld.so 的读写段装下,而是使用 mmap
分配),第二个溢出(也同样长到使用 mmap
分配,由于 mmap
是从高到低分配的,所以这个会在第一个的下方),使其覆盖第一个 GLIBC_TUNABLES
。
这里看原文+调试的时候稍微有点没理解,因为观察到第二个确实在 ld 下方,但是溢出不到第一个,是因为其实 EXP 并没有使用这种方法。EXP 的第一个
GLIBC_TUNABLES
变量单纯用来填充满 ld.so 的读写段,而并非像这里提到的溢出。
然而这个思路是不可行的。如果覆盖第一个 GLIBC_TUNABLES
,我们只有两个利用思路:
- 将其覆盖为
LD_PRELOAD
或者LD_LIBRARY_PATH
使其指向我们的恶意 ld 路径。但这是不可行的,因为这些环境变量后面在 ld 初始化时会经由process_envvars()
被清除。 - 将其覆盖为
SXID_ERASE
的 tunables 。根据漏洞点里提到的内容,它会清除属于SXID_ERASE
的 tunables,而我们此时溢出时它已经被清理过了,因此写它是完全没有问题的。然而问题是在 Linux 下不存在这样一个 SUID-root 程序(既运行setuid(0)
再execve
其它程序,又在运行时保留之前的环境变量)(例如在 OpenBSD 上,/usr/bin/chpass
会首先setuid(0)
然后执行execve(/usr/sbin/pwd_mkdb)
)
但是在 _dl_new_object()
中(真不知道他们是怎么找到的这个调用链的,fuzz?),存在一个链表 link_map
,它的内存其实是由 calloc
分配的,也就是我们的 __minimal_calloc