pwn入门-堆常见漏洞
pwn入门-堆常见漏洞
简介
常见漏洞
- 堆溢出
- DOUBLE FREE(USE AFTER FREE)
- UNLINK
常见利用方式:
- 修改MALLOC HOOK,FREE HOOK
- 覆写GOT表
堆溢出不像栈溢出可以直接劫持程序流,直接控制程序流程,也不像格式化字符串一样狙击式攻击。它主要是通过利用漏洞获取一个可以任意写的CHUNK,通过该CHUNK进行内存关键地址(GOT表,HOOK)的改写,进而达到一定目的。
UNLINK
该漏洞会导致你拥有可以在任意可读写地址进行读写的能力
GOT表改写
HOOK修改(MALLOC HOOKFREE HOOKREALLOC HOOK)
UAF
简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况
- 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
- 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
- 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。
原理
简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况
- 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
- 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
- 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。
这里给出一个简单的例子
1 |
|
运行结果如下
1 | ➜ use_after_free git:(use_after_free) ✗ ./use_after_free |
另一个例子:
1 | unsigned long long target[100]; |
用于伪造一个假的 chunk。
分配与释放内存
1 | unsigned long long *p = malloc(0x10); |
malloc(0x10)
: 分配 16 字节的堆内存,p
指向这个内存块。free(p)
: 释放这块内存,p
指向的 chunk 会进入 tcache 或 fastbin(具体行为取决于堆管理器的实现)。- 此时,
p
的元数据仍然留在堆中。
伪造堆块
1 | p[0] = target; |
修改 p
所在 chunk 的 fd
(forward pointer),将其指向伪造的 target
。
伪造 chunk 的元数据
1 | target[0] = 0; // prev_size (无用字段,值为 0) |
将 target的前两个字段伪造成堆块的元数据:
target[0]
: 伪造的 prev_size
,表示前一个 chunk 的大小(对 tcache 分配无效,可设置为任意值)。
target[1]: 伪造的 size,设置为 0x21(即 32 字节,包括元数据和用户数据)。
0x21
表示此 chunk 的大小为 16 字节(用户数据)+ 16 字节(元数据)。
第二次分配
1 | malloc(0x10); // Must be same size as p |
- 再次分配大小为
0x10
的堆内存。 - 因为释放的
p
对应的 chunk 已被伪造,分配的地址会指向target
。
写入数据
1 | char *q = malloc(0x10); // Must be same as target |
- 再次分配的
q
会指向target
(伪造的 chunk)。 - 使用
memcpy
向q
写入"hello"
字符串(包括空终止符,共 6 字节)。 - 因为
q
和target
是同一个地址,所以写入的数据会覆盖target[2]
开始的内存。
打印内容
1 | printf("%s\n", &target[2]); |
因为 "hello"
被写入 target[2]
开始的位置,所以程序输出:hello
问题 1: 为什么 malloc(0x10)
会分配的地址指向 target
,而不是新构造一个 chunk?
原因:tcache 的机制
释放后的 chunk: 当
free(p)
被调用时,p
指向的 chunk 会被放入 tcache(Thread-Cache)。tcache 是堆管理的一部分,用于存储小块内存(如 0x10)的空闲 chunk。修改
fd
指针: 在free(p)
后,代码修改了p
所指 chunk 的fd
,将其指向target
。堆管理器认为这是 chunk 的下一个空闲块。1
p[0] = target;
再次分配时: 当
malloc(0x10)
被调用时,堆管理器会优先从 tcache 中分配空闲块。由于 tcache 的链表头已经被伪造指向target
,分配的内存块就是target
。
问题 2: 为什么 char \*q = malloc(0x10)
会指向伪造的 target
?
伪造 tcache 链表
- 堆管理器在分配内存时,从 tcache 的链表头获取空闲块。
- 通过修改 tcache 链表(伪造
fd
指针),下一个分配的 chunk 地址被劫持到伪造的地址target
。 - 所以
malloc(0x10)
返回的q
就是target
的起始地址。
问题 3: 为什么写入的数据覆盖 target[2]
开始的内存,而不是覆盖 target[0]
和 target[1]
?
堆块结构和用户数据区域
- 堆块结构:
- 一个堆块包含元数据和用户数据。
- 元数据位于 chunk 的头部(
target[0]
和target[1]
)。 - 用户数据紧随元数据之后,通常从 chunk 的第 3 个
unsigned long long
开始(即target[2]
)。
- 写入数据:
memcpy(q, "hello", 6)
向q
指向的地址写入数据。- 因为
q
是用户数据区域的起始地址,数据覆盖从target[2]
开始的位置。
问题 4: 如果删掉 malloc(0x10)
直接执行 char \*q = malloc(0x10)
会怎么样?
情况分析
- 如果删掉
malloc(0x10)
,char *q = malloc(0x10)
仍会指向伪造的target
。 - 原因是
malloc(0x10)
的行为是从 tcache 分配一个 chunk,而伪造的target
是第一个空闲块,无论调用一次还是两次分配,结果相同。 - 删除前一个 malloc(0x10)后:
q
的值仍然是target
,功能不会受到影响。
CTF-WIKI的例子
1 | puts(" 1. Add note "); |
note 的结构体定义如下:
1 | struct note { |
注意,printnote在前,content在后
addnote:
1 | unsigned int add_note() |
print_note:
1 | unsigned int print_note() |
delete:
1 | unsigned int del_note() |
我们可以看到 Use After Free 的情况确实可能会发生,那么怎么可以让它发生并且进行利用呢?需要同时注意的是,这个程序中还有一个 magic 函数,我们有没有可能来通过 use after free 来使得这个程序执行 magic 函数呢?一个很直接的想法是修改 note 的printnote
字段为 magic 函数的地址,从而实现在执行printnote
的时候执行 magic 函数。 那么该怎么执行呢?
程序申请 8 字节内存用来存放 note 中的 printnote 以及 content 指针。
程序根据输入的 size 来申请指定大小的内存,然后用来存储 content。
1
2
3
4
5
6
7
8+-----------------+
| printnote |
+-----------------+
| content | size
+-----------------+------------------->+----------------+
| real |
| content |
+----------------+
我们必须想办法让某个 note 的 printnote 指针被覆盖为 magic 地址。由于程序中只有唯一的地方对 printnote 进行赋值。所以我们必须利用写 real content 的时候来进行覆盖。具体采用的思路如下
申请 note0,note1,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
释放 note0,note1
此时,大小为 16 的 fast bin chunk 中链表为 note1->note0
申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则,note2 其实会分配 note1 对应的内存块。real content 对应的 chunk 其实是 note0。
note1->note0 == note2(note1) -> note0(real content,magic的地址)
如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数。
1 | #!/usr/bin/env python |
[原创]Use-after-free之lab 10 hacknote-Pwn-看雪-安全社区|安全招聘|kanxue.com
note1->note0 == note2(note1) -> note0(real content,magic的地址)前还好,之后其实就有点懵了,为什么printnote(0)就会执行magic?虽然知道肯定是被修改了地址,但是它是怎么做到的?
note1->note0 == note2(note1) -> note0(real content,magic的地址
上面这个还是有点抽象,因此后面我参考了上面那篇文章,找到了更好的方式
chunk0就是put,chunk1就是content
因此现在的链是上面那样,首先原本的note1的chunk0会被当成note2的chunk0,而原本的note0的chunk0会被当成note2的chunk1
这时如果我们向note2的chunk1中填写数据,则相当于将note0的chunk0更改了
这样就比较好理解了,此时我们再执行print_note()就会执行magic函数,如下图
actf_2019_babyheap
我们再参考BUUCTF的一题,这个是B站星盟的视频讲解,跟上题差不多,区别就是不是直接的magic而是散装的binsh和system函数拼接
其它相似的我直接不介绍了
add:
ptint
delete都基本一样
有个地方不一样
content在前,func在后
最大疑问在于为什么binsh在前而system在后,其实也就是因为content在前而func在后而已
因为我先看星盟视频,理解太少了,后面先去看了wiki上简单的,理解完,下面那就简单了
UNLINK
一道题彻底理解 Pwn Heap Unlink-腾讯云开发者社区-腾讯云
pwn知识——unlink(smallbins) - Falling_Dusk - 博客园
fast_bin和small_bin不使用unlink
size的末位0表示空闲,1表示在使用
应该是BK+0x10地址中所存放的地址等于P的地址,按C语言的写法:*(BK+0x10)=p;括号里和p都是地址
FD+0X18=FD->bk=P BK+0X10=BK->fd=P
假如chunk在0x30,memory地址在0x40,假如在这伪造一个假的chunk
上图也就是下图:
在堆内存管理中,unlink 是释放操作的一部分。当一个内存块被释放时,堆分配器会检查其前后邻接的内存块是否为空闲状态。如果相邻块为空闲块,就会通过 unlink 将它们从空闲链表中取出,并尝试合并它们。
现在有物理空间连续的两个 chunk(Q,Nextchunk),其中 Q 处于使用状态、Nextchunk 处于释放状态。那么如果我们通过某种方式(比如溢出)将 Nextchunk 的 fd 和 bk 指针修改为指定的值。则当我们 free(Q) 时
- glibc 判断这个块是 small chunk
- 判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
- 判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
- 继而对 Nextchunk 采取 unlink 操作
那么 unlink 具体执行的效果是什么样子呢?我们可以来分析一下
- FD=P->fd = target addr -12 (FD是后一个chunk,P的fd给了FD,P的fd被设置为一个地址)
- BK=P->bk = expect value (BK是后一个chunk,P的bk给了BK)
- FD->bk = BK,即 *(target addr-12+12)=BK=expect value (后一个chunk的前一个 = BK,相当于删除这个P,改变指向)
- BK->fd = FD,即 *(expect value +8) = FD = target addr-12
但是不适用有检查的情况
- FD->bk = target addr - 12 + 12=target_addr
- BK->fd = expect value + 8
当满足以上条件时,才可以进入 Unlink 断链的环节:
因为 P 只可能从 smallbin 或者 largebin 中脱链,而这两个 bin 都是双向链表,因此脱链操作必须同时修改前后 chunk 的 fd 或者 bk 指针,即进行如下操作
1 | FD->bk = BK <=> P->fd->bk = p->bk <=> *(P->fd + 0x18) = P->bk //Ⅰ |
假设我们设置 P = free_got, *(&P-0x18) = system,那么当下一次free一个堆块的时候,就会调用system。