H4lo's blog


IOT 安全研究爱好者


cve-2020-8597 pppd stackoverflow

前言

在 3月6号,国外安全研究员 Ilja Van Sprundel(IOActive) 发现了 pppd 组件的 EAP 协议中一个存在了 17 年的严重的栈溢出漏洞,导致所以使用 pppd 组件的系统都受影响,包括 Ubuntu、Debian、Fedora 等,有潜在的远程代码执行的风险,CVSS 评分为 9.8。

前置知识

  1. EAP 协议概念:
> EAP协议是使用可扩展的身份验证协议的简称,全称Extensible Authentication Protocol。是一系列验证方式的集合,设计理念是满足任何链路层的身份验证需求,支持多种链路层认证方式。

因为 EAP 协议主要用于认证,因此这个漏洞影响了众多协议,如 pppoe、pptp 等。
  1. EAP 协议帧格式
字段 占用字节数 描述
Code 1个字节 表示EAP帧四种类型:1.Request;2.Response 3.Success;4.Failure
Identifier 1个字节 用于匹配Request和Response。Identifier的值和系统端口一起单独标识一个认证过程
Length 2个字节 表示EAP帧的总长度
Data 0或更多字节 表示EAP数据
  • code 用于标识 eap 协议为请求或者响应包,或者认证成功或者认证失败
  • length 字段用于表示 eap 帧 的长度,漏洞产生的原因就是因为对这个字段处理不当造成的。

漏洞分析

查看 github 上的 commit,发现 eap.c 文件中, 1420 行和 1846 行处的长度处理不当导致的一处栈溢出:

image.png-157.2kB

这两段代码分别位于 eap_request()eap_response() 函数中,且都位于 EAPT_MD5CHAP 分支,很明显这两个是用来处理 EAP 协议的数据包请求和响应的函数。

  • BCOPY 函数的定义:
1
#define BCOPY(s, d, l)		memcpy(d, s, l) //memcpy 函数的封装

也就是第一个参数的指针指向的内存区域的字符,复制到第二个参数的内存空间中。因为第二个参数(rhostname)位于函数的栈上,导致复制完数据之后导致栈溢出,且可控制返回地址。

1
char rhostname[256];

接着分析一下触发的条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
if (vallen < 8 || vallen > len) {
error("EAP: MD5-Challenge with bad length %d (8..%d)",vallen, len);
...
break;
}

/* Not so likely to happen. */
if (vallen >= len + sizeof (rhostname)) {
...
BCOPY(inp + vallen, rhostname, sizeof (rhostname) - 1);
rhostname[sizeof (rhostname) - 1] = '\0';
} else {
BCOPY(inp + vallen, rhostname, len - vallen);
rhostname[len - vallen] = '\0';
}
...
  • vallen 表示的是 MD5CHAP 的长度字段,为一个常量。

如果要执行下面的代码段,必定要满足 vallen <= len,所以这里必定会进入 else 的分支,只要 eap 数据包的长度 len 足够大,且 len - vallen 的长度大于 rhostname (/etc/hostname 中的内容)字符串的话,就会产生栈溢出。

因为请求和响应两个函数都存在栈溢出,因此在 client 端和 server 端均存在漏洞,在 server 端溢出就能够控制函数的返回地址,导致潜在的远程代码执行漏洞。

漏洞复现

因为受影响的协议包括了 ppp、pppoe(Point-to-Point Protocol Over Ethernet),这里就直接拿 pppd 这个二进制程序来进行漏洞复现。

环境搭建

因为搭建 ppp 服务或者 pppoe 服务的话通常需要硬件环境,这里采取一种不依靠硬件的方法,即搭建两个虚拟机,用虚拟串口连接的方式进行通信(就像是对端的实体端到端连接)。

具体步骤如下:

  1. 使用 virtual box 运行两台 ubuntu 虚拟机,一台搭建好了之后可以直接复制到另一台。
    image.png-211.2kB
  2. 将作为 server 虚拟机的 “设置 -> 端口” 选项下,勾选启用串口,选择 COM1 (在 linux 下 COM1 可以看作是 /dev/ttyS0 设备)
    image.png-172kB
  3. client 端的虚拟机也是同样的设置方法,但是需要注意的是,这里一定要勾选上 “连接至现有的通道或者套接字”!!,不勾选上的话虚拟机的串口是无法通信的。
    image.png-176.2kB
  4. 先将 server 端的虚拟机启动,之后再启动 client 端虚拟机。注意顺序,否则会报错。

测试联通性

测试串口的联通性,即一端将数据输入到 /dev/ttyS0 设备中;一端进行读取。

image.png-595.4kB

发现这里的串口是通的,可以互相访问。

编译 pppd 组件

从 github 上 clone 代码到本地(server 端和 client 端都需要),并编译、安装:

1
2
3
4
5
6
git clone https://github.com/paulusmack/ppp.git
cd ppp/
git checkout ppp-2.4.8 // 切换到存在漏洞的分支
./configure
make -j8
make install

查看 pppd 的版本:

image.png-71.5kB

测试 pppd 通信

在 server 端运行下面的命令:

1
sudo pppd /dev/ttyS0 9600 noauth local lock defaultroute debug nodetach 172.16.1.1:172.16.1.2 ms-dns 8.8.8.8
  • 参数的介绍:
1
2
3
4
5
6
7
8
/dev/ttyS0      // 连接到的串口
9600 // 波特率
noauth // 无密码认证
local // 不要使用数据机控制线路
lock // 在串口上锁定并使用互斥存取
defaultroute // 采用默认路由
debug // 显示连接过程中的封包内容
nodetach // 不脱离终端

运行命令之后,会将生成的 ppp0 端口绑定到 /dev/ttyS0 串口,这样 client 端可以通过访问 /dev/ttyS0 串口来访问 ppp 服务。

image.png-274.9kB

然后在 client 端运行下面的命令:

1
sudo pppd noauth local lock defaultroute debug nodetach /dev/ttyS0 9600

当获取到 IP 地址之后,就相当于客户端和服务端的端到端连接成功了,接着就可以测试漏洞点。

image.png-2150.2kB

触发栈溢出漏洞

接着我们来测试 EAP 协议的栈溢出漏洞,所以这里前提就是需要服务端开启 eap 认证。

先在服务端运行命令,开启 eap 认证:

1
sudo pppd /dev/ttyS0 9600 auth local lock defaultroute debug nodetach 172.16.1.1:172.16.1.2 ms-dns 8.8.8.8 require-eap

为了方便复现,在客户端的 eap.c 源代码 eap_request() 的函数中,在 EAPT_MD5CHAP 分支下,手动 patch 代码,加入发送到服务端的 payload:

image.png-441.1kB

重新编译客户端的 pppd 程序:

1
2
3
4
make clean
./configure
make -j8
make install

在客户端运行命令:

1
sudo pppd noauth local lock defaultroute debug nodetach /dev/ttyS0 9600 user test password test

运行起来之后很快会发现服务端的进程崩溃:

image.png-2257.7kB

程序保护机制

使用 checksec 命令查看程序保护机制,发现这里开启了 canary 保护,无 pie 保护,所以这里只需要绕过 canary 机制即可。

image.png-55.2kB

漏洞补丁

将判断改成 len - vallen >= sizeof (rhostname),当包的长度大于 sizeof (rhostname) 时,就会进入 if 判断,最多只会复制 sizeof (rhostname) 大小的数据,防止了栈溢出漏洞的发生。

1
2
3
4
5
6
7
8
9
10
11
...
- if (vallen >= len + sizeof (rhostname)) {
+ if (len - vallen >= sizeof (rhostname)) {
dbglog("EAP: trimming really long peer name down");
BCOPY(inp + vallen, rhostname, sizeof (rhostname) - 1);
rhostname[sizeof (rhostname) - 1] = '\0';
} else {
BCOPY(inp + vallen, rhostname, len - vallen);
rhostname[len - vallen] = '\0';
}
...

参考资料

https://www.kb.cert.org/vuls/id/782301/
https://github.com/paulusmack/ppp
https://github.com/marcinguy/CVE-2020-8597
https://gist.github.com/nstarke/551433bcc72ff95588e168a0bb666124