libc 源码下载:
http://ftp.gnu.org/gnu/glibc/
# 1.Unlink
这里参考了博客:
https://hollk.blog.csdn.net/article/details/108481889?spm=1001.2014.3001.5502
unlink
是一个宏,定义在 malloc.c 里
#define unlink(AV, P, BK, FD) { | |
FD = P->fd; | |
BK = P->bk; | |
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) | |
malloc_printerr (check_action, "corrupted double-linked list", P, AV); | |
else { | |
FD->bk = BK; | |
BK->fd = FD; | |
if (!in_smallbin_range (P->size) | |
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { | |
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); | |
if (FD->fd_nextsize == NULL) { | |
if (P->fd_nextsize == P) | |
FD->fd_nextsize = FD->bk_nextsize = FD; | |
else { | |
FD->fd_nextsize = P->fd_nextsize; | |
FD->bk_nextsize = P->bk_nextsize; | |
P->fd_nextsize->bk_nextsize = FD; | |
P->bk_nextsize->fd_nextsize = FD; | |
} | |
} else { | |
P->fd_nextsize->bk_nextsize = P->bk_nextsize; | |
P->bk_nextsize->fd_nextsize = P->fd_nextsize; | |
} | |
} | |
} | |
} |
unlink
是在进行 free
操作时执行的,看上面的源码知道是对 链表
的操作,这里是修改指针的效果(unlink 目的
就是将一个空闲块 (在链表中) 拿出来,例如 free 时和目前物理相邻的 free chunk 进行合并),我的理解是 unlink
是将空闲的块在有新释放的块满足条件要合并时进行的操作,因为想要合并,就需要先将空闲的块从链表里取下来, unlink
就是在进行这个取下来的操作
执行 unlink
的函数(在 free函数
执行了 int_free()函数
,其中调用了 unlink
宏)
#define unlink(AV, P, BK, FD) | |
static void _int_free (mstate av, mchunkptr p, int have_lock) | |
free(){ | |
_int_free(){ | |
unlink(); | |
} | |
} |
ctfwiki 上的图片:
这里就能发现是将中间的 P
给取了出来,修改了 BK
和 FD
的指针
图里面的执行顺序为:
- P->fd=FD
- P->bk=BK
- FD->bk=BK
- BK->fd=FD
上面的释放顺序为:
-
free(FD)
-
free(P)
-
free(BK)
所以在 bin 中为:
BK->P>FD
的顺序
# 2.unlink 检查机制
由于 unlink 是在 free 函数中调用的,所以只需要检查 chunk 是否为空闲
其检查机制有三个:
- 检查被释放的
chunk
的size
的值是否与相邻高地址的chunk
的pre_size
的大小相同(这里忽略p
标志位,p标志位
为 size 最低位)【一个块为空闲时,相邻高地址块的pre_size
为前一个块的大小(=size)】 - 检查被释放 chunk 与相邻高地址的
chunk
的size
的P标志位
是否为 0,p为0则表示前一个chunk空闲
- 检查前后被释放 chunk 的 fd 和 bk
# 3.unlink 绕过
我们想要利用 unlink,就需要绕过其检查,而上面的三个检查机制里前面两个通过溢出直接修改即可,后面的则需要我们进行一番操作
# 1. 关键检查:
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) | |
malloc_printerr ("corrupted double-linked list"); |
上面的是检查在 空闲的链表
中前后释放的 chunk的指针
是否对应正确
# 2. 绕过
下面的图里我们可以看到空闲链表的结构:
这里我们可以知道各个指针指向的位置:
P->fd=FD | |
P->bk=BK | |
FD->bk=P // 关键检查 | |
BK->fd=P // 关键检查 |
1. 这里我们知道 FD
从 pre_size
位到 bk
位需要 0x18
个大小(32 位是 12 个),也就是说 &FD+0x18=&bk
, 而 P->fd=FD
并且 FD的bk
的值为 P 的地址,所以 P->fd->bk=P<=>*(P->fd+0x18)=P
这里 *(P->fd+0x18)=P
>P->fd+0x18=&P> P->fd=&P-0X18
2. 同理知道 BK 的 pre_size
位到 fd
位需要 0x10
个大小(32 位是 8 个),即 &BK+0X10=&fd
,而 P->bk=BK 并且 BK 的 fd 的值为 P,所以 P->bk->fd=P<=> *(P->bk+0x10)=P
这里 *(P->bk+0x10)=P
>P->bk+0x10=&P> P->bk=&P-0X10
也就是说我们最终通过:
P->fd->bk == P <=> *(P->fd + 0x18) == P | |
p->bk->fd == P <=> *(p->bk + 0x10) == P |
得到了:
P->fd = &P - 0x18 | |
P->bk = &P - 0x10 |
这意味着当我们的 P 中的 fd=&P - 0x18
, bk=&P - 0x10
就能绕过检查 (这里是将 fd 的 内容
设置为 (&p-0x18),将 bk 的 内容
设置为 (&p-0x10) )
# 3. 不同链表的 unlink(fd_nextchunk 和 bk_prechunk 分别为了方便在 large bins 中快速地管理 chunk 块)
因为 P 的脱链操作只能在 smallbin
和 largebin
中(无法在 fastbin 中)进行,而这两个 bin 都是 双向链表
,所以我们必须修改 前后
chunk 的 fd 和 bk 指针
上面已经得出了结论,这里有别人更详细的推导:
https://cloud.tencent.com/developer/article/1557872
# 对于 smallbin 来说:
smallbin
通过上面的方式直接就可以完成脱链,因为 smallbin 中的 chunk 的 fd_nextsize
和 bk_nextsize
是没有意义的
# 对于 largebin 来说:
对于 smallbin 来说,脱链操作上面就已经完成了,但是对于 largebin 来说,还有未完成的工作,因为 largebin 中还有 fdnextsize 以及 bknextsize 指针需要修改。
在 largebin 中,也只有在相同尺寸的同一组 chunks 中的第一个 chunk 中 fd_nextsize
以及 bk_nextsize
才有意义。
if (!in_smallbin_range (P->size) \ | |
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { | |
... | |
} |
当 P->fdnextsize!=null
时才需要修改(这里意味着 P 是 这一组
相同尺寸 chunk 的第一块 chunk),如果 P->fdnextsize == null
,说明 P 是尺寸相同的一组 chunks 的非第一个 chunk,此时 P 的 fdnextsize
和 bknextsize
是没有意义的,自然没有修改的必要
if (FD->fd_nextsize == NULL) { | |
if (P->fd_nextsize == P) | |
FD->fd_nextsize = FD->bk_nextsize = FD; | |
else { | |
FD->fd_nextsize = P->fd_nextsize; | |
FD->bk_nextsize = P->bk_nextsize; | |
P->fd_nextsize->bk_nextsize = FD; | |
P->bk_nextsize->fd_nextsize = FD; | |
} | |
} | |
else { | |
P->fd_nextsize->bk_nextsize = P->bk_nextsize; | |
P->bk_nextsize->fd_nextsize = P->fd_nextsize; | |
} |
如果 FD->fd_nextsize == NULL ,那么 P 脱链(P 为 这一组
第一个 chunk)后 FD 即成为当前尺寸相同的 chunks 的第一个 chunk。
接着判断 P->fd_nextsize == P ,因为当 P 为仅有的唯一一组尺寸相同的 chunks 的第一个 chunk 的话,是需要特别对待的,P 不为第一个时 FD 直接继承 P 的 fdnextsize 以及 bknextsize 即可。
如果 FD->fd_nextsize != NULL ,说明 FD 是下一组尺寸相同的 chunks 的第一个 chunk。(这里是每一个组的第一个都是满足这个条件)
# 4.Unlink 利用
由上面的绕过可以得知:
P->fd = &P - 0x18 | |
P->bk = &P - 0x10 |
而在 unlink 宏中:
#define unlink(AV, P, BK, FD) { | |
FD = P->fd; | |
BK = P->bk; | |
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) | |
malloc_printerr (check_action, "corrupted double-linked list", P, AV); | |
else { | |
FD->bk = BK; // 在这里 BK=P->bk | |
BK->fd = FD; // 在这里 FD=P->fd | |
.... | |
} | |
} |
通过我们进行的绕过手段导致这里也就相当于:
FD->bk = BK 并且 BK=P->bk
所以FD->bk=P->bk
BK->fd = FD 并且 FD=P->fd
所以BK->fd =P->fd
因此这也就是:
FD->bk =&P - 0x10 // 事实上 FD->bk=BK (这里实际上是因为已经在链表中去除 P 后会根据之前 P 的 fd 和 bk 来确定修改后的指针的指向) | |
BK->fd =&P - 0x18 // 事实上 BK->fd=FD |
前面构造的为了进行绕过:
P=&P-0x10 //P->bk | |
P=&P-0x18 //P->fd |
这里的执行是有顺序的,所以 BK->fd = FD
一定是后执行的,所以结果就为: P=&P-0x18
也就是说可以往 P 里写入值去修改 &P-0x18
的内容!(这里是我们往 P
里写值,其会去修改 &P-0x18
地方的值)
# P 地址的寻找:
然而这里还有一个问题是我们如何去找到 P 的地址,这里我们就需要找到堆的管理数组 (一般在 bbs 段处,会有个数组来记录每个 chunk 的地址),我们可以在这里找到我们伪造的堆块的 数组
的地址,通过这个地址来减 0x18 放入伪造的堆块的 fd 中即可
这样我们后续改变就可以根据这个数组来修改地址内部的值
【个人理解】
我对于 unlink 漏洞的理解是认为他其实是对 对应的堆块的管理数组进行了修改,将我们想要修改的地址作为堆块给添加了上去,其中的一个数组会存放这个想要修改值的地址,这就会导致我们后面可以对其当作堆块一样进行修改
参考:
https://www.bilibili.com/video/BV1Uv411j7fr/?p=20
https://cloud.tencent.com/developer/article/1557872
https://hollk.blog.csdn.net/article/details/108481889?spm=1001.2014.3001.5502
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/
https://www.secpulse.com/archives/115388.html
堆的数据结构:
https://www.cnblogs.com/L0g4n-blog/p/12965187.html