感觉栈相关简单的漏洞基本原理学的差不多了,准备学一学堆相关的;
这个主要是学习Linux Heap Exploitation时的笔记,具体的课可以去Udemy上看,感觉讲的蛮不错的;
然后内容都是自己的博客,原文在https://hack1s.fun/,欢迎大家去看
introduction
Glibc
ldd
是list dymic dependencies,可以显示出二进制程序运行时需要加载的动态链接库
libc是linux中最基本的动态链接库,绝大多数程序都需要用到libc,如果删除libc的链接,关机都会关不掉;
malloc
堆是程序在执行中可以使用malloc向内核请求一段连续的内存空间;
I/O、文件读写等等都是通过堆来实现的;
堆与malloc
首先需要对堆有一个基本的理解,堆通过malloc分配chunk,通过free来释放chunk
首先是一个demo例子用于理解堆
在pwndbg中执行
set context-sections code
这样可以让之后每一次显示context只显示源代码部分
这个程序主要做的事情就是调用了几次malloc,之后return
vmmap
可以显示出进程当前的内存空间,在这一句malloc还没有执行的时候,程序内存空间不存在堆的区域
当第一次malloc执行之后,查看vmmap会发现多出来了一个堆的空间
在pwndbg中的命令vis_heap_chunk
简写为vis
可以查看堆的chunk分布
我们虽然是执行了malloc(9)
,申请了9的空间,但是实际上给了我们3*8=24字节的空间(蓝色部分的第一个8字节是头部,不是用户可以用的)
也就是说malloc(9)
分配给了24字节的user data以及8个字节的meta data,这个chunk一共占了32字节;
malloc
分配的最小chunk就是这样0x20的大小,即24字节的user data和8个字节的meta data
即使执行的是malloc(0)
、malloc(1)
仍然会分配一个0x20大小的chunk
图中几个chunk分别是malloc(9)
、malloc(1)
、malloc(0)
、malloc(24)
分配的;
可以看到实际上内容都是一样的占了0x20字节;
但是也可以注意到,其中meta data部分并不是存储了0x20,而是0x21;
这是因为meta data这里两个字段,一是chunk size,表示整个chunk(包含user和meta两部分)的大小,另外由于chunk分配时是按照16字节对齐的,最低位就可以用来表示其他信息;这个字段就是previous_inuse,用来表示这一个chunk相邻的前一个chunk是否是在使用的状态,如果是就为1,否则为0;
下面如果继续执行malloc(25)
会分配一个0x30的空间
虽然最后16个字节只用到了1个字节,但还是按照16字节对齐进行分配的。
最后就是Top chunk,可以看到在我们自己申请的Chunk之后有一个Top Chunk的meta data;
并且随着一次次的申请新的堆空间,这个Top Chunk的大小会发生变化。
这是因为内核在分配堆的内存空间时是创建一块大的Top Chunk,每一次用户执行的malloc就是压缩top chunk分配给用户,直到Top Chunk的空间不足以分配,就会再向下拓展Top Chunk
在Top Chunk中有一个值得注意的地方是,在Glibc的很多版本中,Top Chunk的Size字段都是没有完整性检查的,这就是The House of Force的基本原理
在2005年,第一次出现了一篇名为The Malloc Maleficarum的论文,其中写了5种堆利用的技巧;
- Houses of Prime
- Houses of Mind
- Houses of Force
- Houses of Lore
- Houses of Spirit
从此之后的堆利用技巧也因此都叫"house of XX"这样的形式
House of Force
原理
house of force的原理就是前面提到的,没有对Top Chunk的size字段进行完整性检查;
这导致在分配了一个比较小的chunk后,如果输入的内容大于chunk的大小,进而溢出到top chunk的size 字段,就可以伪造控制top chunk的大小;
之后再一次使用malloc
分配chunk,可以达到一个任意地址写的效果,运用得当也可以实现RCE的效果;
漏洞程序本身
程序本身是一个类似CTF中堆题的结构
为了方便学习漏洞本身,程序运行前输出了puts
的地址以及heap
开始的地址
选项1是malloc
,之后可以输入要申请的大小以及输入的内容
例如上面申请了24,但是输入了24个a
以及7个b
最后和一个\n
这是我们<C-c>
后回到pwndbg,可以用vis看到现在的堆
可以看到由于溢出了7个字节的b和1个字节的\x0a
,top chunk处的size已经被覆盖了。
在GDB中使用vmmap libc
可以查看程序调用的libc信息
这个程序由于增加了Runpath
,连接的是特定的libc
这里使用的是没有开启tcache的程序,但是实际上house of force是可以在tcache存在的libc使用的;
这里使用这样没有开启tcache的libc是为了在还没有学过tcache机制的情况下就可以了解如何使用这个漏洞利用方式
任意地址写
程序的第二个选项可以输出一个变量target
正常来说这个变量的值是一串X
在pwndbg中可以使用dq &target
以四个字为单位查看这个变量附近的值
dq
的全称是dump qwords
,另外也有类似的dw
、dd
、db
堆起始地址是0x603000
,但是可以看到这里其实target的位置是在堆的上方的0x602010
使用malloc只能继续往高地址申请空间,没有办法摸到target
所以我们需要溢出top chunk
这边由于虚拟机的环境有写问题,换了一台虚拟机
首先申请24的空间,然后输入b"Y"*24+b"\xff"*8
这样可以把top chunk覆盖为0xffffffff
,在python脚本里面用gdb调试
这里给出的脚本中有几个函数是可以直接辅助在VIM中运行的,在vim输入
:!./% GDB
这个功能的实现是通过这一块代码
gs = '''
continue
'''
def start():
if args.GDB:
return gdb.debug(elf.path,gdbscript=gs)
else:
return process(elf.path)
相当于直接启动GDB附加这个程序,使用vis
看到
top chunk已经是全f了
第二步就是申请一个特别大的chunk,正好到target前面一点点的位置;
这个程序前面输出了heap的地址,在pwntools中读取之后,计算差值
需要分配的是(0xffffffff-0x603000)+0x602010-0x20-0x20
这么大的内容
malloc之后用vis查看
可以看到这时正好在0x602010上方
这之后再malloc申请内存覆盖的就是target的地方,再申请20的空间,在里面随便输入一些内容
发现vis后0x602010处的值就已经不再是XXXXXX了
在菜单里面输出target发现值也变了
这就实现了一个任意地址写的效果
任意代码执行
通过一个任意地址写转换成代码执行的利用有这样几个思路:
- 修改栈,但是这个程序中栈采用了ASLR;
- 修改Binary段,修改PLT中的项或修改
fini_array
,程序中的每一个函数在退出时会运行这个fini_array
中的,但是这个程序开启了full-RELRO,在binary加载完成之后原本的二进制区段会变成只读,无法对其进行修改;
- 修改堆,但是这个程序中除了我们自己的数据,没有影响控制流的数据,所以没用;
- 修改libc,
__exit_funcs
、tls_dtors_list
这两个指针会在特定情况下调用,比较类似于PLT,但是都被指针完整性保护,并且在这个程序中没有可以触发的地方,所以难以实现;
- 修改
__malloc_hook
,在GLIBC中的数据段,修改__malloc_hook
可以使程序在调用malloc时调用这里被修改的函数;
这里面修改__malloc_hook
是可行的,我们首先将top chunk溢出为全f
之后申请一个空间,从堆中目前top chunk所在的位置到libc的__malloc_hook
这么长;
由于libc
的区段在堆的下面,不需要像获取任意地址写那样滚一圈内存空间了;
distance = libc.sym.__malloc_hook - 0x20 - (heap + 0x20)
调试的时候使用:!./% GDB NOASLR
暂时关掉ASLR
分配之后查看__malloc_hook
的位置
由于分配时减了0x20,这里看一下__malloc_hook - 2
运行top_chunk
看到top_chunk的位置就在这上方
所以我们接下来再用malloc申请一段空间,将__malloc_hook
这里的函数指针修改为指向system函数的地址
libc.sym['system']
这时用p __malloc_hook
看一下可以发现这个函数指针已经变成了__ libc_system
下面就是想办法执行system("/bin/bash")
由于system的参数是一个指向字符串的指针,我们可以在前面几轮malloc填充数据时就直接填充/bin/bash
在这里填写当时分配出来的地址
比如把第二轮的malloc中填充的字符串改成/bin/bash
这样最后调用时要填写的字符串地址就是最初的heap+0x30
这次再执行就不需要后面的GDB NOASLR
了,直接运行就可以拿到shell
总结一下,这里的house_of_force只对2.28以下的GLIBC有效,再新的GLIBC就增加了top chunk的完整性保护了;