从零开始的Linux堆利用1 -- House of Force

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只显示源代码部分

image-20210217212638040

这个程序主要做的事情就是调用了几次malloc,之后return

vmmap可以显示出进程当前的内存空间,在这一句malloc还没有执行的时候,程序内存空间不存在堆的区域

image-20210217212950029

当第一次malloc执行之后,查看vmmap会发现多出来了一个堆的空间

image-20210217213700269

在pwndbg中的命令vis_heap_chunk简写为vis可以查看堆的chunk分布

image-20210217214128093

我们虽然是执行了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

image-20210217214720913

图中几个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的空间

image-20210217215150684

虽然最后16个字节只用到了1个字节,但还是按照16字节对齐进行分配的。

最后就是Top chunk,可以看到在我们自己申请的Chunk之后有一个Top Chunk的meta data;

并且随着一次次的申请新的堆空间,这个Top Chunk的大小会发生变化。

这是因为内核在分配堆的内存空间时是创建一块大的Top Chunk,每一次用户执行的malloc就是压缩top chunk分配给用户,直到Top Chunk的空间不足以分配,就会再向下拓展Top Chunk

image-20210217221525951

在Top Chunk中有一个值得注意的地方是,在Glibc的很多版本中,Top Chunk的Size字段都是没有完整性检查的,这就是The House of Force的基本原理

在2005年,第一次出现了一篇名为The Malloc Maleficarum的论文,其中写了5种堆利用的技巧;

  1. Houses of Prime
  2. Houses of Mind
  3. Houses of Force
  4. Houses of Lore
  5. 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中堆题的结构

image-20210226160340641

为了方便学习漏洞本身,程序运行前输出了puts的地址以及heap开始的地址

选项1是malloc,之后可以输入要申请的大小以及输入的内容

例如上面申请了24,但是输入了24个a以及7个b最后和一个\n

这是我们<C-c>后回到pwndbg,可以用vis看到现在的堆

image-20210226160534387

可以看到由于溢出了7个字节的b和1个字节的\x0a,top chunk处的size已经被覆盖了。

在GDB中使用vmmap libc可以查看程序调用的libc信息

这个程序由于增加了Runpath,连接的是特定的libc

image-20210226193254435

这里使用的是没有开启tcache的程序,但是实际上house of force是可以在tcache存在的libc使用的;

这里使用这样没有开启tcache的libc是为了在还没有学过tcache机制的情况下就可以了解如何使用这个漏洞利用方式

任意地址写

程序的第二个选项可以输出一个变量target

正常来说这个变量的值是一串X

image-20210226162944191

在pwndbg中可以使用dq &target以四个字为单位查看这个变量附近的值

image-20210226163057701

dq的全称是dump qwords,另外也有类似的dwdddb

堆起始地址是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看到

image-20210304153132039

top chunk已经是全f了

第二步就是申请一个特别大的chunk,正好到target前面一点点的位置;

这个程序前面输出了heap的地址,在pwntools中读取之后,计算差值

需要分配的是(0xffffffff-0x603000)+0x602010-0x20-0x20这么大的内容

malloc之后用vis查看

image-20210304155216574

可以看到这时正好在0x602010上方

这之后再malloc申请内存覆盖的就是target的地方,再申请20的空间,在里面随便输入一些内容

发现vis后0x602010处的值就已经不再是XXXXXX了

image-20210304155430523

在菜单里面输出target发现值也变了

image-20210304155406422

这就实现了一个任意地址写的效果

任意代码执行

通过一个任意地址写转换成代码执行的利用有这样几个思路:

  1. 修改栈,但是这个程序中栈采用了ASLR;
  2. 修改Binary段,修改PLT中的项或修改fini_array,程序中的每一个函数在退出时会运行这个fini_array中的,但是这个程序开启了full-RELRO,在binary加载完成之后原本的二进制区段会变成只读,无法对其进行修改;
  3. 修改堆,但是这个程序中除了我们自己的数据,没有影响控制流的数据,所以没用;
  4. 修改libc,__exit_funcstls_dtors_list这两个指针会在特定情况下调用,比较类似于PLT,但是都被指针完整性保护,并且在这个程序中没有可以触发的地方,所以难以实现;
  5. 修改__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的位置

image-20210308212201801

由于分配时减了0x20,这里看一下__malloc_hook - 2

image-20210308212444377

运行top_chunk看到top_chunk的位置就在这上方

image-20210308212604069

所以我们接下来再用malloc申请一段空间,将__malloc_hook这里的函数指针修改为指向system函数的地址

libc.sym['system']

这时用p __malloc_hook看一下可以发现这个函数指针已经变成了__ libc_system

image-20210308213641372

下面就是想办法执行system("/bin/bash")

由于system的参数是一个指向字符串的指针,我们可以在前面几轮malloc填充数据时就直接填充/bin/bash在这里填写当时分配出来的地址

image-20210308214212027

比如把第二轮的malloc中填充的字符串改成/bin/bash

这样最后调用时要填写的字符串地址就是最初的heap+0x30

这次再执行就不需要后面的GDB NOASLR了,直接运行就可以拿到shell

image-20210308214407682

总结一下,这里的house_of_force只对2.28以下的GLIBC有效,再新的GLIBC就增加了top chunk的完整性保护了;