# 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 | _IO_vtable_check (void) |
但是,在 2.34 的早期版本又是可以写的(glibc-2.34-0ubuntu1_amd64)
这个时候我们可以尝试攻击 vtable 结构体,达到 getshell 的目的。
但是在后面的几次更新中,又将修复了这个漏洞,在 (Ubuntu GLIBC 2.34-0ubuntu3.2) 2.34 版本中,就不可以修改(目前已知在 2.340ubuntu3 版本以及之前的版本依旧有可写的权限)
我们可以找到很多关于如何绕过 vtable check 的办法进行劫持 IO 流,其中最主流的还是利用 _IO_str_jumps 和 _IO_wstr_jumps 两个虚表,
二者利用几乎一样。我们在源码 /libio/strops.c 可以看到相关的 vatable 的内容,以下我以_IO_str_jumps 作主要说明。
1 | const struct _IO_jump_t _IO_str_jumps libio_vtable = |
这里面有两个很有用的函数
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
相关源码如下
# _IO_str_finish
1 | //Glibc 2.34 |
这里我们值得注意的是_IO_str_finish,在之前版本中,函数中其实是存在任意函数执行的漏洞的
1 | //Glibc 2.31 |
但是在新版本的函数中,将这部分删除了,所以我们无法通过这里 getshell.
# _IO_str_overflow
2.34 对比之前的版本,这里并没有太大的变化,但是因为没有了 free_hook 事情变得不容乐观
1 | int |
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 | static void |
从源码中可以看到这个断言中调用了 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.34 0ubuntu3.2 |
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 的链表。伪造整个链表,进行劫持。相关的全局变量是可以写的。后面会解释这个变量的用处。
另外一个与之相比破坏性比较小,更容易成功。由于 link_map 通过链表链接,但是在加载 exit 的时候,相关函数智慧通过 link_map->l_next 指针进行相关的检查。我们可以在某个特定的位置,更改 next 指针,将下一以链表节点转为我们控制的地方,比如 heap 上。
很多朋友看了上面的可能会比较蒙,下面我具体说一参数。
关于 link_map, 我们攻击 exit 时,会使用到一个 link_map 的链表,链表的一些信息保存在 struct rtld_global 结构体中,这个结构体信息很多,很繁杂,但是 banana 只用到了几个关键的点。
1 | pwndbg> p &_rtld_global |
我们需要关注的是,
#1,_ns_loaded = 0x7f56e43ba220, 这是整个链表的头节点,
#2, _ns_nloaded = 4, 这里知名个这个链表的节点个数,在 exit 后面加载的检查中,会要求_ns_nloaded 链表的节点不少于 3 个
(后面我会给出相关的源码)
然后对于每个节点,都是 link_map 结构体,我们利用第一个节点做一下简单说明 (省略了部分无关的数据)
1 | pwndbg> p *(struct link_map *)0x7f56e43ba220 |
我们需要关注的:
#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 | void |
总结下我们需要绕过那些检查
-
判断
_ns_loaded
链表中至少有三个节点(dl-fini 开始部分通过循环遍历链表,做检查,) -
检查
l == l->l_real
-
检查
l->l_init_called > 8
这个其实跟数据的处理有关1
2
3
4
5
6
7unsigned 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 的数据
以及一个不为 1 的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17pwndbg> 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> -
检查
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 | pwndbg> distance &_rtld_global &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next) |
由此我们就知道了需要向哪里写入 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 | pwndbg> distance _rtld_global._dl_ns._ns_loaded &_rtld_global._dl_ns._ns_loaded->l_info[26] |
重点来了,info 这连个位置我们写入什么数据
1 | 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}, |
这是一个比较通用的 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 | pwndbg> tel 0x55a656f072a0(fake) 40 |
最后我们就是利用 onegadget 获得 shell 了。
利用 gdb 万能必挂点,结合 one_gadget 工具帮助我们快速找到合适的 one_gadget
# 一些注意点:
因为_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
- *(fake+0x28)=fake。
- *(fake +0x48)=fake+0x58, *(fake+0x50) = 0x8
- *(fake+0x58) = shell
- *(fake+0x110) = fake+0x40
- *(fake+0x120) = fake+0x48
- (int)*(fake+0x31c) = 0x9
最后笔者在这里提出一个未完成的验证,house of emma 在 3.2 版本下的利用.
因为个人实力依旧比较菜,文章出可能会出现错误及不足,欢迎斧正。也希望能和对此文感兴趣的师傅进一步交流关于新版本的利用姿势。
[参考]:
想到验证各种姿势,感谢 ru7n 师傅
3.2 下攻击 exit_hook 的思考,感谢 Ayaka 师傅
house of banana 的最初构想 感谢 ha1vk 师傅