unlink知识点和手法

前置:

堆物理结构:

unlink知识点和手法_第1张图片

prev_size(记录前一个堆块)

  • 若前一个堆块处于释放状态(P为0),prev_size启用,存储前一个堆块大小
  • 若前一个堆块处于使用状态(P为1),prev_size不启用,存储前一个堆块数据

size(记录自身堆块大小)

后三个字节作为标志

  • PREV_INUSE (P) :前一个块是否已分配。
    • 位置:最低位(第 0 位)。
    • 值为 1:表示前一个堆块 已被分配(正在使用),当前块的头部与前一个块的数据区邻。
    • 值为 0:表示前一个堆块 未被分配(空闲),此时当前块的头部可以与前一个块的头部合并,形成更大的空闲块。
  • IS_MMAPPED (M) :是否通过 mmap 分配。
    • 位置:次低位(第 1 位)。
    • 值为 1:表示该堆块通过 mmap 系统调用 分配(而非从主堆中分配),通常用于较大的内存块(如超过 mmap_threshold 的块)。
    • 值为 0:表示该堆块从 主堆(heap) 中分配。
      • 注意
        • mmap 分配的块在释放时直接通过 munmap 系统调用释放,不返回给堆分配器。
        • 该标志位仅在 64 位系统 中有效,32 位系统可能不使用此位。
  • NON_MAIN_ARENA (N) :是否属于主分配区。
    • 位置:第三位(第 2 位)。
    • 值为 1:表示该堆块属于 非主分配区(non-main arena),即由多线程环境下的其他线程分配器创建。
    • 值为 0:表示该堆块属于 主分配区(main arena),即由主线程的分配器管理。

fd和bk指针

仅当块处于空闲状态时有效,用于将块链入空闲链表(如 fastbinstcacheunsorted bin 等)

fd(指针)

在bin中指向下一个(非物理相邻)
只存在于空闲的Chunk

bk(指针)

在bin中指向上一个(非物理相邻)
只存在于空闲的Chunk

主要:

unlink

在堆内存管理(如 glibc 的 ptmalloc)中,空闲内存块通过双向链表组织。当一个内存块被释放或合并时,需要从链表中移除,unlink 用来将一个双向链表(只存储空闲的 chunk)中的一个元素取出来

unlink常用于free()中进行 chunk 的整理,可以对空闲 chunk 进行前向合并和后向合并。

当被free 的 chunk 的 P 位为 0 时,说明被 free 的 chunk 的前一个 chunk 为空,于是对前一个 chunk 进行 unlink 操作,将前一个 chunk 与被 free 的 chunk 进行后向合并。
后向合并的操作首先将两个 chunk 的大小相加,然后对前一个 chunk 进行 unlink。

unlink正常情况下的执行流程:
假设当前块 P 位于链表中间,执行 unlink(P) 后:

  • P 的前一个块(FD)的 bk 指针会指向 P 的后一个块(BK)
  • P 的后一个块(BK)的 fd 指针会指向 P 的前一个块(FD)
  • 最终效果:P 被从链表中移除,链表保持连续

unlink 宏的核心代码句:

FD = P->fd; 
BK = P->bk;    //获取P这个内存块的前驱节点(fd)和后继节点(bk)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))  
//检查链表的完整性    确保下一个块的 bk 必须指向当前块,上一个块的 fd 也必须指向当前块
FD->bk = BK;  
BK->fd = FD;   //链表删除

![[Pasted image 20250523214657.png]]
总的就是由A <-> B <-> C这样变成 A <-> C这样

堆溢出漏洞的unlink手法

64位
目的:将堆指针伪造到目标地址,实现任意地址写或代码执行
1.先创建两个连续的chunk0和chunk1(设一个大小为0x30,一个大小为0x80)
2.选择目标地址(也就是这里&p)
(一般是题目中add函数中的创建堆块时的一个参数,一般在bss段)
3.在0块中构造一个fake_chunk(伪造堆块)
在chunk0只差一个chunk头处开始,fake_chunk大小为0x30
![[Pasted image 20250523215757.png]]
要将fake_chunk伪装成一个空闲的chunk,这样我们在下面释放1块就可以触发unlink,所以控制chunk1的prev_size和size位
注意点:区分p和&p

p

指针变量的值,指向 fake_chunk 头部

&p

指针变量 p 本身存储的地址

# fake_chunk(伪造堆块)
&p #这里写的是p本身存储的地址,目标地址
payload=p64(0) #prev_size
payload+=p64(0x31) #size
(PREV_INUSE(P)=1,表示前一个堆块未释放,不启用prev_size,存放前一个堆块数据(这里prev_size就写0))
payload+=p64(&p-0x18) #fd 
payload+=p64(&p-0x10) #bk
payload = payload.ljust(0x30, b'a')  # padding 
(填充满剩余fack_chunk空间,使下面的值正确填入)
payload+=p64(0x30) #next_prev_size
payload+=p64(0x90) #next_size
(PREV_INUSE(P)=0,表示前一个堆块以释放,启用prev_size,存放前一个堆块大小)

现在我们把1块释放
系统会先检查与1块相邻的fake_chunk,发现fake_chunk已经释放,是空闲状态
所以会触发unlink(fake_chunk)

看这里unlink的核心代码句

FD=fake_chunk->fd;     
BK=fake_chunk->bk;       
FD->bk=BK;              
BK->fd=FD;                

先获取fake_chunk这个内存块的前驱节点(fd)和后继节点(bk
1.FD=fake_chunk->fd
fake_chunk 的 fd 被我们设计成 &p-0x18

 FD=fake_chunk->fd=&p-0x18 

2.BK=fake_chunk->bk
fake_chunk 的 bk 被我们设计成 &p-0x10

BK=fake_chunk->bk=&p-0x10

在进行链表删除
3.FD->bk=BK
FD(FD=&p-0x18)的bk,起始距fd是0x18,所以加0x18

*(&p-0x18+0x18)=BK=&p-0x10

4.BK->fd=FD
BK(BK=&p-0x10)的fd,起始距fd是0x10,所以加0x10

*(&p-0x10+0x10)=FD=&p-0x18

最后得到的就是*(&p)=&p-0x18

这里选取-0x18和-0x10这两个值
也是考虑unlink前会检查链表的完整性:

if (FD->bk != fake_chunk || BK->fd != fake_chunk) 

要确保下一个块的 bk 必须指向当前块,上一个块的 fd 也必须指向当前块
用了这两个值到最后可以看到FD->bk=&p-0x18+0x18=&p ,BK->fd=&p-0x10+0x10=&p,成功绕过检查

我们在下面想要往目标地址写入数据,先填充0x18就行,这样就可以任意地址写

补充一点:unlink后,目标地址&p[]和chunk是对应的,从一开始创建堆块后,看&p处的具体数据和堆情况就能看出,&p存储的数据就是每一个堆块的相关地址

例题:

NSSCTF [HGAME 2022 week3]changeable note

https://blog.csdn.net/2401_88087539/article/details/148218987

NSSCTF [SUCTF 2018 招新赛]unlink

https://blog.csdn.net/2401_88087539/article/details/148218314?spm=1001.2014.3001.5502

你可能感兴趣的:(unlink专栏,pwn,堆,unlink)