很久之前学习的堆相关的东西,里面的堆结构与利用方式都有点忘记了。这次当复习吧,同时学习一下以前学堆没有学到的一些东西。同时多做点有难度的赛题。
堆基础
先常见堆处理机制是glibc 集成的 ptmalloc2,其基本内存管理思想是:只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。
堆基本操作
malloc
malloc 函数返回对应大小字节的内存块的指针,并对一些异常情况进行了处理:
- 当 n = 0 时,返回当前系统允许的堆的最小内存块
- 当 n 为负数时,由于在大多数系统上, size_t 是无符号数,所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配。
free
free函数会释放由 p 所指向的内存块,这个内存块有可能通过 malloc 函数得到,也可能通过 realloc 得到。
异常处理为:
- 当 p 为空指针时,函数不执行任何操作
- 当 p 已经被释放之后,再次释放会出现乱七八糟的效果,这就是
doubel free
- 除了被禁用(mallopt)的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便减小程序所使用的内存空间
内存分配后的系统调用
无论 malloc 和free 动态申请释放内存时,都会调用系统函数 (s)brk 函数以及 mmap,munmap 函数。
(s)brk
- 不开启 ASLR 时,start_brk 以及 brk 会指向 data/bss 段的结尾
- 开启 ASLR 时,start_brk 以及 brk 也会指向同一位置,只是这个位置是在 data/bss 段结尾后的随机偏移处
mmap
malloc 会使用 mmap 来创建独立的匿名映射段,目的是可以申请以 0 为填充的内存,并且这块内存仅被调用进程所使用
unlink
unlink 用来将一个双向链表(只存储空闲的 chunk)中的一个元素取出来,使用条件:
- malloc
- 从恰好大小何时的 Large bin 中获取 chunk
- fastbin 和 small bin 没有使用 unlink
- 依次遍历处理 unsorted bin 时也没有使用 unlink
- 从恰好大小何时的 Large bin 中获取 chunk
从比请求的 chunk 所在的 bin大的 bin 中取 chunk
free
- 后向合并,合并物理相邻低地址空闲的 chunk
- 前向合并,合并物理相邻高地址空闲 chunk(除了top chunk)
malloc_consodlidate
- 后向合并,合并物理相邻低地址空闲chunk
- 前向合并,合并物理相邻高低地址空闲 chunk(除了 top chunk)
realloc
- 前向扩展,合并物理相邻高地址空闲 chunk(除了top chunk)
malloc_printerr
在 glibc_malloc 时检测到错误的时候,会调用 malloc_printer函数
1 | static void malloc_printerr(const char *str) { |
会调用 __libc_message来执行 abort 函数,如下:
1 | if ((action & do_abort)) { |
在 abort 函数里,在 glibc 还是 2.23 版本时,会 fflush stream。
1 | /* Flush all streams. We cannot close them now because the user |
申请内存块
__libc_malloc
首先会检查是否有内存分配函数的钩子函数 (__malloc_hook),这个主要用于用户自定义的堆分配函数,方便用户快速修改堆分配函数并进行测试。用户申请的字节一旦进入申请内存函数中就变成了无符号整数。
会寻找一个 arena 分配内存
调用 _int_malloc 函数去申请对应的内存
如果分配失败,ptmalloc 会尝试再去寻找一个可用的 arena,并分配内存
如果申请到了 arena,则在退出之前还得解锁
判断目前的状态是否满足以下条件:
- 要么没申请到内存
- 要么是 mmap 的内存
- 要么申请到的内存必须在其所分配的 arena 中
1 | assert(!victim || chunk_is_mmapped(mem2chunk(victim)) || |
最后返回内存
__int_malloc
__int_malloc 是内存分配的核心函数,其核心思路如下:
- 根据用户申请的内存块大小以及相应大小 chunk 通常使用的 频度(fastbin chunk, small chunk, large chunk),依次实现了不同的分配方法
- 由小到大依次检查不同的 Bin 中是否有相应的空闲块可以满足用户请求的内存
- 当所有的空闲 chunk 都无法满足时,他会考虑 top chunk。
- 当 top chunk 也无法满足时,堆分配器才会进行内存块申请
多线程支持
在 glibc 的 ptmalloc 实现中,支持了多线程的快速访问,所有线程共享多个堆。
堆相关数据
malloc_chunk
chunk无论大小和状态,其都统一使用一个数据结构。不过根据是否被释放,其含义会有不同。
1 | /* |
prev_size,前一堆块空闲时会记录前一块的size大小,如果前一堆块在使用则可以被前一堆块使用记录数据。(前一堆块指地址较低的chunk)
size,该chunk 的大小,大小必须是 2*SIZE_SZ 的整数倍,如果不是整数倍,也会分配匹配的最小整数倍。SIZE_SZ 32位是 4字节,64位是 8字节。字段的低三个比特位为:
- NON_MAIN_ARENA,记录当前堆块是否属于主线程,是为 1,不是为0
- IS_MAPPED,记录当前 chunk 是否是 由 mmap 分配的
- PREV_INUSE,记录前一个 chunk 块是否被分配。堆分配的第一个 堆块其 标志为 1,以防止访问前面的非法内存。当一个 chunk 的size 的 P位为0 时,通过 prev_size 字段来获取上一个 chunk 的大小以及地址,便于 对空闲堆块的合并。
fd,bk。分配时,这两个含有 堆块的数据。空闲时,fd 指向 下一个(非物理相邻)空闲的 chunk,bk指向 上一个(非物理相邻)空闲的 chunk
fd_nextsize,bi_nextsize,是只有 chunk 空闲的时候才使用,用于较大的 chunk(large chunk):fd_nextsize 指向前一个 与当前 chunk 大小不同的第一个 空闲块,不包含 bin 头指针。bk_nextsize 指向后一个 大小不同空闲堆块。 空闲 large_chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列,可以避免在分配chunk时遍历查找。
ZCTF-2015-note1
程序分析
程序总体实现了一个 note 的创建、展示、修改和删除,和一个正常题目差不多。note 的结构体如下:
1 | note{ |
程序的漏洞在于,修改时发生了堆溢出,可以输入的content大小变为了 0x200:
漏洞分析
有一个堆溢出漏洞,并且堆结构中存放了两个指针用于遍历 note。如果溢出修改next指针,那么通过show 和 edit 函数,我们就能对指针处的值进行泄露或修改。
还有 delete 函数有一个 释放时 如同链表一样的操作,可以实现修改 note_list 这个链表的头指针,这也是可以利用的地方。
EXP
这里我就是使用的常规方法,泄露 libc 地址,修改atoi_got 地址为system地址。
1 | from pwn import * |
ZCTF-2015-note2
程序分析
这道题大概逻辑和上一题差不多,区别在于漏洞点有点不易发现。是无符号整数 导致的整数溢出。当 无符号整数 0 - 1 后,其会变成最大的整数,就造成了我们可以堆溢出。同时当申请堆大小为 0 时,其会分配最小的 堆块(64位为0x20,32位为 0x10)。
漏洞分析
我们可以构造三个堆块,其中第一个和第三个大小为 0x80,而第二个大小为 0x0。然后再通过第 2 个堆块的堆溢出去覆盖第三个堆块的 堆头,同时在第一个堆块中伪造一个 假的 fastbin块,修改其 fd 和 bk 指针为我们想控制的 地址。然后通过释放第三块,造成第一块的 unlink,最终控制 一个地址,此处地址选择 note_list 地址 0x602120。
其中,最重要的就是要伪造好 fastbin 和 下一个堆块的堆头。
1 | 第一个堆块real_chunk: |
其中注意是 fastbin 中的 fake_pre_size 是我们当前伪造的 fastbin 的大小,其 in_use 位要置为 0,
而 下一个堆头中 pre_size 位大小要满足: 此时地址堆地址 - pre_size = fake_fast_bin_head_addr ,所以此处为 0xa0
还有下一个堆块的 size 位要把 pre_in_use 位 置为0,这样 free() 时,系统才会向前去指向 unlink。
EXP
1 | from pwn import * |
hack-lu-2014-oreo
Arbitrary Alloc
Arbitrary Alloc 其实与 Alloc to stack 是完全相同的,唯一的区别是分配的目标不再是栈中。 事实上只要满足目标地址存在合法的 size 域(这个 size 域是构造的,还是自然存在的都无妨),我们可以把 chunk 分配到任意的可写内存中,比如 bss、heap、data、stack 等等。
在非栈上构造一个 fastbin,除了需要伪造 size 满足 fastbin 范围之内外。通过下面这道题,还有一个重要点,就是 我们伪造的堆块的地址的 后面这个紧接着的 size 地址 数 不能太大,一般可设为 0x100。
漏洞分析
漏洞点,在 输入 name 和 content 时都存在溢出。也就是 可以修改 堆块结构里的 Next 指针 以及下一个堆块头。这里 修改下一个 堆块头没有用到,主要是利用修改 next 指针使得我们能够在 bss上伪造一个堆块。
然后就是选择伪造的地址,上图中位于 bss 地址内,其中 order_num 和 Rifle_num 都是随着我们 增加提交等动作而变化。而 notice 则存储的是 notice 的指针,其指向 0x804a2c0。如果我们提交 了 0x41个堆块,那么 Rifle_num 将为 0x41,然后我们再将 最后一个堆块的 Next 溢出修改为 0x804a2a8。那么随后调用 free 函数,将会对整个链条依次 free,所以就可以将我们伪造的 0x804a2a8 这个堆块放进 fastbin中。
最后我们再将该堆块申请出来,则可以实现控制notice 这里指向的指针,再通过 提交 Notice 来修改任意地址数据。
EXP
1 | from pwn import * |
RCTF-2015 Shaxian
这道题和上一题一样,都是伪造 fast chunk 到一个指针处,然后通过修改指针的值,修改任意地址。
漏洞分析
漏洞原因就在我们输入内容时,能够覆盖结构体中的 next 指针。导致我们可以free 一个我们伪造的 fastbin chunk。而 gouwuche 这是一个链表指针,指向最新的一个 chunk地址,如果我们伪造的chunk能够覆盖该指针。那么就可以实现 修改任意地址的值。
这道题,主要是 伪造 chunk 的堆头,要注意 地址对齐吧,我试了一下大概需要都是 0x10 的整数倍。
还有 atoi() 输入地址时,需要 减去 0x100000000 地址才行。
EXP
这里先放上一个简单版,即我们能够得到 Libc库基址。这样就可以常规的 覆盖 got 表就行。
1 | from pwn import * |
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2020/08/11/堆漏洞练习/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!