Terrapin-Attack-学习2

有了上篇的铺垫,这按开始正式的讲解Terrapin Attack

本文首发于破壳平台 https://mp.weixin.qq.com/s/axiHYAu54MdnMNKpJYGHbQ

Terrapin Attack

这个漏洞是由这边的几个安全研究员提出的漏洞。这个漏洞是一个基于MITM(Man-in-the-Middle 中间人,下文简称MITM)的攻击,这意味着,这个漏洞攻击的场景至少要存在如下的场合:

1
2
3
4
5
6
7
8

+----------+ +----------+ +----------+
| <------+ <---------+ |
| Alice | | Evil | | Bob |
| | | | | |
| | | | | |
| +----->| +---------> |
+----------+ +----------+ +----------+

这个漏洞是针对SSH的通信完整性发起的攻击,并且攻击者不具备对于会话相关密钥信息的知识,包含但不限于:

  • 加密内容的密钥
  • MAC使用的nonce
  • IV

所以这个问题不是一个简单的内存泄露或者逻辑漏洞。同时该攻击的一个重要的攻击面在于降低了SSH的防护措施,突破了SSH的通信完整性,从而使得之前出现过的攻击能够重新被利用。

通信完整性定义

When a secure channel between A and B is used, the data stream received by B should be identical to the one sent by A and vice versa

说白了,双方的通信都必须能够明确知道来自对方,这就是通信完整性

如何算是对SSH发起攻击

首先在讨论上述攻击的时候吗,首先要明确目标。SSH协议设计的时候是以通信完整为前提进行设计的,此时SSH通信的时候能够保证信息的完整性(integrity),这样就意味着其具备防御MITM的能力。
所以当SSH不再具备防御MitM攻击的时候,其实就可以认为对SSH完成了攻击。实际上,Terrapin提出的漏洞模型中,个人理解有一些仅需对SSH途径的路由进行控制,并且能进行Sniffer和重放,即可完成攻击。

SSH 历史问题

SSH历史上出现过很多问题,其中有几个比较有趣,这边就选取这几个进行讲解

SSH前置知识补充

SSH通信的大致流程如下:

SSH 通信使用的diffie-hellman

其中,这种通信协议被称为Binary Packet Protocol BPP,也就是二进制通信协议。并且在这里给出几个基础定义:

  • Packet: 网络数据发送的最小单位
  • Block:加密操作的最小单位

二者关系为:SSH的数据包以Packet为单位发送,每一个Packet中包含多个block。

1
2
3
4
5
Block0         Block1        Block2
+-------------+-------------+-------------+---
| Length |padding| Payload
+-------------+-------------+-------------+---
Packet0

CVE-2008-5161 针对M&E的攻击

注意:这个问题其实在后来被证实为可能存在一定的问题,可能只有在某些理想化状态下能够使用。不过这个攻击实在是有趣,这边介绍一下这个攻击思路

这个漏洞是一片发在IEEE的文章提出来的,这里有链接
漏洞发生在ssh对于之前提到的M&E实现过程中的问题,属于是协议的级别的问题。由于SSH通信过程为加密过程,其不能无限制的接受数据包,所以其在进行数据解析的时候,会按照Block解密。然而就是在其进行解密前的安全检查中,形成了这个漏洞的利用点。接下来来看一下这些错误检查点:

SSH与出错审计

整个安全检查分为好多步骤,我们着重观察以下步骤

  1. 进行长度check
1
2
3
4
if (packet_length < 1 + 4 || packet_length > 256 * 1024) {
buffer_dump(&incoming_packet);
packet_disconnect("Bad packet length%d.",packet_length);
}

这里的packet_length为从第一个数据包中解密的数据。这个检查用于防止写的过大导致的DDos问题。进行长度检测时,SSH允许的数据长度为[5,256 × 1024]之间。(这里注意很重要)

  1. 进行Block的check
1
2
3
4
need = 4 + packet_length - block_size

if (need % block_size != 0)
fatal("padding error: eed %d block %d mod %d", eed, block_size, need % block_size);

其中block_size会随着我们选择的加密算法变化而变化,但是总的来说为固定值且为8的倍数。这里的need表示仍然需要接受的数据长度;如果此时的数据不是对齐的状态,则此时认为传输数据有误,此时会放弃当前通信,而不进行数据返回

  1. 进行MAC的check
1
2
if (buffer_len(&input) < need + maclen)
return SSH_MSG_NONE;

buffer_len(&input)表示此时接受了的数据长度,maclen则是在SSH协议中的SSH MAC中指定的一个长度,根据使用的MAC不同而变化,例如hmac-sha1的长度为20
如果这个Check不通过,SSH则会抛出一个叫做Corrupted MAC on input.的错误信息。

总结一下这几个check的行为,可以得出如下的现象:

检查内容 检查未通过行为
Packet Length 连接断开,并且发送一个错误信息
Block 是否 对齐 连接断开
输入长度是否过长 返回错误信息
检查均通过 持续等待

SSH 中间人攻击

现在假设我们作为攻击者,能够从中间截获数据包。此时我们做出如下的假设

  • KK为每一个block加解密使用的key
  • FKF_K为加密操作,FK1F_K^{-1}为解密操作
  • LL为Block Size(3ds中L=8, aes中L=16)
  • pip_i为第i个明文block

对于CBC模式的加密算法,存在一系列的p1,p2,...pnp_1,p_2,...p_n,此时有:

ci=FK(pi)ci1,i=1,2,...nc_i = F_K(p_i) {\oplus} c_{i-1}, i=1,2,...n

其中c0c_0为IV,也可以是BBP 中获取的最后一个加密数据块cnc'_n
对于解密,则有

pi=ci1FK1(ci),i=1,2,...np_i = c_{i-1} {\oplus}F^{-1}_K (c_i), i=1,2,...n

前14bit猜测

我们假设作为攻击者,我们截获了一个加密数据包cic^*_i,此时我们有如下关系

pi=ci1FK1(ci)p^*_i = c^*_{i-1} {\oplus}F^{-1}_K (c^*_i)

假设我们把这个数据包插入到下一个Packet的开头,此时我们假设

  • cnc_n为当前连接中,上一个packet的最后一个block。这个block本来是要作为IV被使用的

那么此时的解密流程如下

p1=cnFK1(ci)p'_1 = c_{n} {\oplus}F^{-1}_K (c^*_i)

综合上述算式,我们能得到

pi=ci1p1cnp^*_i = c^*_{i-1}{\oplus}p'_1{\oplus}c_n

由于我们为中间人,因此可以假设我们能获取所有的cic_i。假设当我们插入数据包之后,出现如下的状态:

  • 状态一:SSH突然终止,没有出现任何错误信息
  • 状态二:SSH开始等待更多的数据输入

这均说明,程序已经通过了前文提到的长度检测。也就是说,p1p'_1的长度范围符合要求,也就是前14bit的数据一定为0。那么根据

pi=ci1p1cnp^*_i = c^*_{i-1}{\oplus}p'_1{\oplus}c_n

就能获得当前的某个pip^*_i的前14bit

Q:怎么找到下一个连接开头呢?
A:虽然没办法直接观察到Packet开头,但是可以通过观察数据通过的情况来判断什么时候有新的Packet进来

所有bit恢复

假设我们通过block长度check,也就是进入上述的状态二,那么此时会持续的接受block,直到下面的判断不满足要求:

1
2
if (buffer_len(&input) < need + maclen)
return SSH_MSG_NONE;

那么此时,我们就持续不断的插入[1,maclen]个数的LL长的blocks,观察ssh 触发MAC错误的那一刻。此时我们就能够根据我们发送的数据包,算出这个need的准确值。此时,这个p1p'_1的完整值我们就能由这个公式得到:

1
need = 4 + packet_length - block_size

此时,根据 上文推到的

pi=ci1p1cnp^*_i = c^*_{i-1}{\oplus}p'_1{\oplus}c_n

我们就能获得所有的明文信息!
根据总结,满足如下加密算法的传输都能够被这种方式进行攻击:

  • 加密数据包中使用了长度字段
  • 使用CBC的加密模式
  • 允许攻击者少量传输数据
  • 在不同的数据包错误下,返回不同的错误信息

攻击场景

实际上,SSH也不可能允许一个用户反复的执行上述操作,其必将导致连接中断。但是,我们可以根据某些已知位置的数据进行攻击。例如,用户在进行远程登陆的时候,我们只需要将ssh通信过程中登录密钥相关逻辑进行破译,并不需要获取整个ssh通信数据。

侧信道攻击

这个攻击其实比较神奇,但是神奇之中透露着合理。这也是来自一篇USENIX的论文。论文提到说,人们在敲击键盘的时候,会有一定的倾向性。某些特定的字符或者字符组合敲击的时候,时间间隔可能会变得很长或者很短。

那么,通过观察数据包的特定格式,就能够猜测此时的输入内容。例如上述的通信中,通过传输数据的长度特征,就能推断出是否是正在输入SU指令,以及猜测当前用户的输入长度。有些时候还能够通过一些特殊的观察看到额外的值。这边要给出作者之前打比赛遇到过的一道题,在这个题目中,虽然给出的是SSH的加密后数据包,并且给出了keylog,但是其实log中存在数据修改。通过一个叫做packetStrider的脚本,实际上在加密状态下,根据数据包特征依然能准确的获取部分特定的输入内容(例如回车,或者删除)。这篇论文似乎也是提出了一个类似的方案。(不过具体内容太长,没有深入理解)这边贴出一个分析Terrapin-Attack的博客,这其中提到了在未提供防护的情况下,SSH数据包的特征:

可以看到,在未使用防护技巧的时候,SSH的数据包时间间隔和数据大小是有明显差异的,而在SSH新修复的场景中

可以看到,时间间隔变成一致的。这是因为SSH提供了一种基于时间的混淆技巧,从而让数据包的传输没有时间特征,从而避免了侧信道

Terrapin Attack

好了,完成了之前那么多的前情提要,终于可以开始介绍这个攻击了。这个攻击针对的是SSH握手阶段发起的,这边要仔细介绍一下SSH的握手阶段发生的事情:

SSH握手阶段

SSH从握手到建立加密通信信道的流程如下:

其中黑色的部分表示当前信道已经是加密信道了,从黑色部分开始,中间人就完全无法解析SSH通信的具体内容。

上图有几个细节:

  • SSH使用的是前文提到的二进制包协议(Binary Packet Protocol,之后简称BPP)
  • Client和Server端的序列号都从0开始计数
  • 通信在Newkeys之后才正式进入加密处理

SSH加密的时候,会交换加密中用到的密钥,以及用于保护秘密信息的nonce。注意这里的SSH通信过程中,使用的一般是椭圆曲线的交换方式,也就是使用形如(gx)ymodD=(gy)xmodD(g^x)^y mod D =(g^y)^x mod D的特性,完成密钥K=gxyK=g^{x*y} 以及一些必要的nonce等数据的获取。
这里生成的MAC值适用于检查信息的完整性,然而单纯生成普通的MAC值(例如,对明文进行hmac计算),攻击者很容易的就能使用各种方法对数据进行伪造。此时就需要引入刚刚提到的nonce数据,以及计数器Counter。
SSH会使用SndRcv两种不同的counter,前者会在发出数据包的时候自增,用于计算发出的数据包的MAC;后者会在接收到数据包的时候自增, 用于验证数据包的MAC,从而保证信道不被篡改。
由于SSH是基于TCP协议的,所以被认为是不发生丢包的稳定通信,因此使用的counter为隐式counter。

KEXINIT

KEXINIT阶段(如图未加密),SSH会使用椭圆加密等手段进行nonces以及支持的算法列表进行交换。这里交换的四条算法列表包括

  • 用于密钥交换的算法列表
  • 用于服务端签名的算法
  • 双边各使用的加密算法

KEXDHINIT

此时使用Diff-Hellman密钥交换算法进行数据交换。(也有可能使用ECDH或者PQC等算法)服务端会使用握手阶段中的数字签名对此时的数据信息进行校验。这个数字签名为之前提到的那些信息以某种固定的顺序进行计算的结果。

交换用hash:仅对部分数据校验

这里提到的数字签名只会对通信数据中的部分数据进行计算。具体来说,这个hash如下:

H=HASH(VCVSICISKSXK)H=HASH(V_C ||V_S||I_C||I_S||K_S||X||K)

这些值含义如下:

  • HASH:一种hash算法
  • VCVSV_C V_S 客户端 服务器端的版本信息
  • ICISI_C I_S 发生在KEXINIT阶段的基本信息
  • KSK_S 来自服务器端的公钥
  • KK 密钥交换得到的私钥
  • XX 一个由多个密钥值决定的数值

上述的每一个值都有一个编码定义的长度域
注意,这里并没有把形如IGNORE MESSAGE这类消息,或者其他的消息进行编码。这就给MitM创造了机会

序列码

为了对每一个数据包进行唯一性标记,这里使用了SndRcv两种序列码共同标记。注意,在前几个序列中并不适用MAC对发送数据进行校验,而是等整个安全信道建立的时候,MAC才会参与数据校验。并且此时发送端的Snd必须要和接收端的Rcv相等,否则会直接抛弃当前数据包

Terrapin Attack 攻击细节

文章提出的是一种叫做prefix truncation attacks前缀截断攻击的一种攻击形式。这个攻击核心即为:

The SSH Binary Packet Protocol is not a secure channel because a MitM attacker can delete a chosen number of integrity-protected packets from the beginning of the channel in either or both directions without being detected

攻击的核心在于能够删除SSH通信过程中的Counter Number,从而能够突破其完整性校验,然后强迫SSH使用低安全性的加密算法,完成完整的漏洞攻击流程。在攻击中,会使用如下的术语:

  • IGNORE数据包:在SSH中,部分协议支持使用IGNORE数据包,即由一方发往另一方,但是无需对方回显的数据包
  • UNKNOWN数据包:在SSH中,如果当前数据包格式正常,但是却无法识别其类型,那么就会当成UNKNOWN数据包,对放则会回复一个UNIMPLEMENT的数据包作为回应
  • IVkdfIV_{kdf}:派生密钥,也就是例如CBC模式中,IV,或者Enc(pi)Enc(p_i)那个值,就叫做派生密钥

核心漏洞成因

这个漏洞的核心成因为如下两点:

  • 未对握手阶段进行仔细校验。SSH在握手阶段使用了一个签名来校验完整性,但是并没有对所有的副本进行校验,而只是校验了某一个特定序列的信息
  • SSH的安全会话序列号是从握手阶段开始算起的,而非建立起真正的安全信道的时候。这就会导致,在安全信道真的建立起来之前的序列号本质是不受到保护的

通过上述结论,我们可以使用如下的方式对目标进行攻击

修改序列数

通过在握手阶段插入一个数据包,我们可以增加Rcv的计数器。换句话说,攻击者可可以动态的修改这个SndRcv

BBP上进行前缀截断攻击

核心攻击技巧:攻击者可以通过使用序列号控制来动态的删除一个安全信道建立之初的数据包

在SSH通信过程中,如果接收方的Rcv与发送方的Snd不匹配,此时就会抛弃这个数据包。这个攻击就是利用了这个机制,使得SSH会将关键的数据包进行抛弃。
通过上述操作,可以发动如下的攻击:
1. BBP上进行多段前缀截断攻击
攻击者可以通过往Client或者Server段一次性发送多个特殊的IGNORE数据包,从而引发多个数据包丢失,造成多段截断攻击。

2. 扩展协议降级攻击
在SSH通信中,会使用EXTINFO来标注当前的SSH支持的扩展协议。然而攻击者可以通过丢弃这个EXTINFO,造成Extension Negotiation,迫使安全信道降级,让服务端以为客户端无法支持这几年的安全的协议,从而迫使服务端改用可以被键盘输入时间攻击keystroke timing attack的老旧协议

3. 恶意扩展攻击和恶意会话攻击
在例如AsyncSSH这类SSH实现端上。当攻击者拥有受害者的用户名的时候,可以通过插入一个带有用户认证信息的数据包,此时受害者会直接登陆到攻击者的shell环境上,实现整个会话的劫持。

适用范围

攻击对于ChaCha20Poly1305这种AEAD的加密方式比较好,因为其使用的是近似于EtM的完整性校验(实际上更为松散),同样可以用于部分CBC-EtM模式中。但是,CBC-EaM,CTR-EaM,GCM这三种模式都是不受到这个攻击影响的。
理由:
这个理由牵扯到我们上一篇文章提到的AEAD的加密方式。因为这个攻击牵扯到遗弃数据包的动作,当我们尝试丢弃数据包的后,EaM这种完整性校验会察觉到数据包被丢弃,而EtM有可能不会,因为EaM其实是对明文做的完整性校验,而EtM其实是对密文做的完整性校验,尤其ChaCha20Poly1305这种算法,它将序列号也放在了派生密钥的过程中,这样就意味着,Key4Key_4就只能解开Snd4Snd_4的数据包,这与我们想法一致,丢弃数据包的动作完全不会影响它的解密。

CBC-EtM 与出错处理

实际上,某些算法中的EtM未就能够绕过check,例如CTR-EtM中,由于Counter的介入,当我们丢弃数据包的时候,Counter会发生错位,从而导致出错。所以,这里特指CBC-EtM。而CBC-EtM也并非完全可靠。我们举个例子,在CBC加密模式下,明文计算公式为:

p1=Dec(c1)IVkdfp_1 = Dec(c_1){\oplus}IV_{kdf}

那么假设此时,我们使用扩展协议降级攻击,使得前面k个数据包丢失,那么此时的计算为:

p1=Dec(ck+1)IVkdfp'_1 = Dec(c_{k+1}){\oplus}IV_{kdf}

此时我们的p1值就是未知的了,而且可能是无效的。然而根据CBC的特性可知,此后的值都是没问题的:

p2=pk+2=Dec(ck+2)ck+2p'_2 = p_{k+2} = Dec(c_{k+2}){\oplus}c_{k+2}

所以这里就产生了一个疑问,SSH究竟会如何处理这个可能有问题的数据包呢?这里有三种可能:

  • 数据包出错过于离谱,直接将数据包抛弃
  • 数据包虽然出错,但是关键部分的数据是可以识别的,此时SSH会将这个包当作正常的数据包进行使用
  • 数据包虽然能解析,但是无法解析,此时将数据包视为UNKOWN数据包,并且给出UNIMPLEMENT回显

接下来,就会展示一些可能的攻击场景,描述当前攻击的可行性

具体实例: ChaCha20-Poly1305

SSH算法会在NEWKEYS阶段后,建立加密隧道,并且在加密信道中发送EXTINFO相关信息,提供一些扩展加密策略,从而防止各种形如keystroke timing attack的攻击策略。此时我们可以使用单个包的丢失阶段技巧后,可以使其丢弃对应的扩展加密策略,从而迫使其使用不太安全的通信策略:

具体实例: CBC-EtM.

此攻击同样是逼迫SSH丢弃EXTINFO相关信息。然而正如前文所说,对于类似CBC这种模式,其解密逻辑原先如下:

P1=IVDec(C1)P_1 = IV{\oplus}Dec(C_1)

如果我们用IGNORE丢弃一个数据包的话,数据会变成

P1(?)=IVDec(C2)P2(?)=IVDec(C3)......Pi(?)=IVDec(Ci+1)P_1(?) = IV{\oplus}Dec(C_2) \\ P_2(?) = IV{\oplus}Dec(C_3) \\ ...... \\ P_i(?) = IV{\oplus}Dec(C_{i+1})

这样有生成的所有密文都会被影响,从而使攻击失效。于是此时我们可以使用另一种策略强行让其丢弃EXTINFO,那就是使用一种服务器无法解析的UNKNOWN信息。此时服务端返回UNIMPLEMENT。这种办法可以迫使Server端使用UNIMPELEMENT数据包替换EXTINFO,这样办法就能保证往后的密文解析没问题

如图,首先通过在Client端发送UNKNOWN,使其能够保持对齐,然后通过在合适位置往Server端插入UNKNOWN信息,即可保证在通信过程中依然能够截获EXT_INFO。然而UNIMPLEMENT信息通常较短,可能会导致数据错位(没能填满Block,或者因为EXT_INFO导致错位等等)使得数据解密发生错误。然而,在部分SSH客户端中,我们可以使用PING-PONG包代替这种包,通过在PING数据包中塞入大量的数据,此时返回的PING将很有可能能够符合SSH客户端接受数据的要求,此时准确率就会提升非常多

具体攻击 AsyncSSH

如果说之前的说法都是理论上的泛泛而谈,这边就要举一个实际的例子:asyncssh,这个库是一个python里面的有名的库。并且其就受到这种攻击的影响。这里介绍两种实际的攻击形式

恶意使用EXTINFO

这里的打法和ChaCha20-Poly1305类似,不过将IGNORE替换成了指定的EXTINFO。理论上来说,EXTINFO应该在加密信道中进行接收,但是AsyncSSH可以接受任何时候发送的EXTINFO,于是配合前面提到的前缀截断攻击,可以将原先的安全的SSH协议替换成我们指定的SSH协议

劫持SSH会话,要求有一个能够SSH的用户信息

这种攻击需要攻击者在这个SSH服务端上也有一个登陆凭证。这种攻击能够让用户以攻击者指定的用户登录,但是毫无察觉。在这种场景下,攻击者能轻易的获取受害者的所有输入,甚至作为一个伪造的SSH Agent存在。这种攻击如图

首先在客户的NEWKEYS之前,插入一个USER_AUTHREQUEST请求,这个请求中包含了攻击者指定的认证信息(最好使用password或者publickey机制)。此时,AsyncSSH的服务器端会认为其接收到了认证信息,但是由于还没有完成握手机制(NEWKEYS未完成),所以其仍会等待对应的流程完成。之后用户端发起SERVICE_REQUEST,要求进行认证后,服务端此时发送SERVICE_ACCEPT,表示可以进行认证。然而我们之前已经伪造了一个USER_AUTHREQUEST请求,此时AsyncSSH的服务端会认为我们已经完成了请求,于是返回USERAUTH_SUCCESS,表示可建立通讯通道。

期间为了防止Client的正常行为从而导致攻击者的登录被取代,以及防止Client察觉,这里故意将真正的USER_AUTHREQUEST滞后,此时当服务器端返回请求后,攻击者再将这个请求发往对面。然而此时因为认为通道已经建立,这个登录请求就被抛弃了。

Q:为什么这个时候还要发送真正的USER_AUTHREQUEST呢?
A:因为丢弃数据会引发CBC-MtE解密错误,所以只能延后,不能丢弃

总结

这个攻击模型非常有趣,其中无论是利用SSH机制的部分,还是通过替换数据包 or 丢弃数据包从而绕过MAC完整性的办法都是非常有趣的地方。在今后的安全研究中,需要试着从不同的角度去考虑攻击场景以及防护场景,才能更好的对安全有一个广泛的认知。