kaspterio code as a hacker

01 January 2022

阅读前提:具备密码学基础,并了解TLS1.2的工作细节

主要内容来源:RFC6347

1. Overview of DTLS

DTLS设计的目标是为了构建TLS over UDP,但是TLS 依赖传输层提供有序可靠性保证,因此不能直接运行在UDP之上,需要对协议本身依赖传输层有序可靠的地方做一定修改。DTLS旨在以最小的改动做到最大程度上匹配TLS。

TLS依赖传输层有序的地方有:

  1. 首先在TLS握手阶段依赖消息有序可靠交换来进行密码学握手
  2. 在TLS握手完成后的数据加密保护传输阶段,block cipher中TLS依赖于双端内存中的seq_num来进行record的mac校验做到防止重放攻击,而seq_num在TLS是维护于双端内存中,依赖record交互来进行同步;stream cipher更是依赖于record的有序可靠交付进行加解密

针对TLS record协议的依赖问题(问题2),DTLS主要通过下面几个措施:

  • DTLS在record协议中增加显式的seq_number,并配合一个seq_number的滑动窗口机制来做到防重放
  • DTLS禁用了stream cipher

针对TLS handshake协议依赖问题(问题1),DTLS则通过以下措施来解决:

  • handshake消息有序性:DTLS 通过在Handshake消息里增加message_seq字段,标识消息的自增序号
  • handshake消息的可靠性:DTLS通过给TLS handshake状态机的状态切换增加超时重传机制来保证handshake消息可靠性(DTLS没有消息或者record级别的ACK机制,只状态机切换来触发超时重传)

由于传输层UDP,DTLS又引入了几个新的问题:

  • 我们知道UDP本身没有分片功能,如果packet大小超过PMTU,则在IP层会产生IP分片,然而IP分片的效率很低,一个分片丢失,这个packet的所有分片都将将需要重传。因此DTLS为了可靠传输Handshake消息,DTLS需要在Handshake协议层对大的握手消息进行分片,DTLS通过在Handshake协议里新增fragment offset和fragment length字段来支持Handshake分片。
  • 由于UDP易受DOS攻击的nature,DTLS采用stateless cookie exchange机制来验证未知的新Client地址的有效性(新增一个了HelloVerifyRequest 握手消息),因此当一个全新的client第一次和server建立DTLS连接时,往往需要一个额外的RTT来验证地址。

2. DTLS Record Layer

协议改动

首先从Record协议上,DTLS的Record 协议新增了两个字段,epoch和sequence_number,其他字段和TLS Record协议一致

struct {            
  ContentType type;            
  ProtocolVersion version;            
  uint16 epoch;                    // New field
  uint48 sequence_number;          // New field
  uint16 length;            
  opaque fragment[DTLSPlaintext.length];          
 } DTLSPlaintext;
  • 由于TLS1.2支持rehandshake解决同一个连接上record太多导致seq num回绕问题,DTLS中采用显式的epoch字段用来标识当前cipher state所处的阶段,值从0开始,每成功完成握手时自增1(tls1.2中通过ChangeCipherSpec消息标cipher状态切换)。
  • sequence_number是当前cipher state下的当前record序号,也是从0开始自增,rehandshake后重置0。

PMTU限制

DTLS的record层不支持分片(和UDP一样),需要上层来保证产生的record能fit within一个UDP包。因此,协议中规定了几个DTLS实现应当做的事:

  • DTLS应该提供给上层协议获取下层协议的预估PMTU的能力,
  • DTLS要给上层协议提供DTLS层本身会数据的扩充大小的预估,
  • 如果DTLS收到底层的协议的包不可达指示,需要通知给上层
  • 如果上层协议需要执行PMTU发现程序,DTLS层需要予以支持。(对于传输层是UDP时,DTLS需要直接将设置IP包属性的API暴露给上层)
  • 最后一点,DTLS Handshake协议其实也是Record协议的上层协议,因此Handshake协议层需要考虑PMTU大小,DTLS认为Handshake过程发送并不频繁而且也就那几个RTT,因此设计上选择轻量化的预估,而非更准确但是代价大的PMTU发现,具体来说DTLS的Handshake应当实现:
    • 如果Record层告知包因为大了不可达,那么Handshake层应当立即按照当前已知的PMTU大小进行分片
    • 如果多次重传Handshake消息后依旧没有收到响应,并且当前PMTU未知,那么后续重传应该对record 大小进行back off

Record 数据加密认证保护

  • DTLS 去掉stream cipher,只保留block cipher和AEAD cipher,cipher suite和TLS保持一致
  • 和TLS 类似,epoch + sequence_number 都将参与到Block cipher里的Mac计算或者AEAD里的additional data,但是和TLS 不一样的地方在于如果Mac/AEAD校验失败,DTLS直接丢弃包,而不是中断连接,从这点来说,DTLS连接要更resilient一些,而TLS连接则相对较脆。

Record防重放

其实这点属于Record 数据保护的一部分,但是内容值得拿出来单独写写。

首先一点,DTLS 在record中显式传输sequence_number导致record不在像TLS那样能在Mac校验时做到防重放了。

在TLS over TCP中,replay detection靠双端的seq number 同步做到,由于tls record的Mac计算涉及到seq number的参与,因此任何试图replay的消息都会由于Mac和当前双端内存中的seq number不匹配而导致Mac校验失败,从而做到replay detection。

到 DTLS这里,seq number需要作为header和数据一起显示传给对端,此时攻击者去replay消息,接收端收到后由于所有header都对,消息也都合法,MAC校验会通过,因此需要额外根据显示的seq number来去重做到防重放。在实现上维护一个收到records的滑动窗口,seq number太老或者已经在window里的record直接丢弃。协议规定滑动窗口的最小至少为32,最好一64为默认值。

最后,需要说明的是在DTLS里防重放是可选的,理由是基于UDP重放不一定是代表被攻击。上层可以选择不开启防重放,从而自己根据应用情况来实现。

3. DTLS Handshake Protocol

DTLS handshake过程基本上和TLS1.2 handshake一致,但是有下面几个重要的改动:

  • 为了防止DOS而引入的stateless cookie exchange机制
  • 为了保证Handshake消息的有序性在Handshake header里引入message_seq字段
  • 为了避免IP 分片,Handshake层需要支持分片,因此Header里引入分片offset和len
  • 为了保证 Handshake消息的可靠性,引入简单的超时重传机制

DTLS防DOS

UDP非常易受各种DOS攻击,DTLS协议主要关注两种攻击

  • 针对DTLS server本身的DOS攻击,攻击者可以发起大量DTLS握手请求来使得server分配资源进行代价很高的密码学计算(攻击者则不会进行真的握手)
  • 利用DTLS server进行反射攻击,攻击者用受害者的地址来伪造来源地址和端口,向DTLS server发起大量握手请求,利用DTLS server进反射攻击。由于一个很小的握手发起请求,就能引出server较大的数据响应,DTLS server在反射攻击中还起到了流量放大的作用。

为了应对这些攻击,DTLS引入的Photuris 、IKE等协议中使用的stateless cookie exchange机制,具体来说是:client发起ClientHello后,server回应一个HelloVerifyRequest消息,这个消息里包含了一个server根据Client的地址、ClientHello消息里的内容等计算的一个MAC(cookie),client收到HelloVerifyRequest消息后需要重发ClientHello消息,消息里需要带上这个cookie,server 收到后会验证这个ClientHello消息是否和cookie匹配(校验MAC),验证成功后server才进行后续的密码学握手。

      Client                                   Server
      ------                                   ------
      ClientHello           ------>

                            <----- HelloVerifyRequest
                                   (contains cookie)

      ClientHello           ------>
      (with cookie)

      [Rest of handshake]

显然这个机制可以防止放大反射攻击,但是不能有效防止针对DTLS server本身的从大量真实地址发起的DOS攻击,此外攻击者还可以通过事先收集真实的cookies, 做到复用ClientHello + cookie,形成和之前等效的DOS。

HelloVerifyRequest定义

消息体如下:

struct {
     ProtocolVersion server_version;
     opaque cookie<0..2^8-1>;
 } HelloVerifyRequest;
  • server_version字段和ServerHello的的version一样,用来告知client自己的version,但是注意和ServerHello不一样的是这个字段并非用于version协商,仅仅是告诉Client自己的version,以便于Client正确解析包。协议中规定DTLS1.2实现应当将server_version设值为DTLS1.0(1.0和1.2的HelloVerifyRequest格式一样的),Client则不要根据这个字段来进行版本协商。
  • cookie字段根据Client的地址、ClientHello包里的参数等进行MAC计算得到:Cookie = HMAC(Secret, Client-IP, Client-Parameters)。将cookie通过HelloVerifyRequest返回给Client使得server无需要存储任何Client state。这里采用的Secret可以是DTLS server启动时就初始化好的随机Secret

ClientHello定义

DTLS给ClientHello新增了cookie字段,用于client将收到HelloVerifyRequest消息后在新的ClientHello里带上cookie,其他字段必须要和初始的ClientHello一致

   struct {
     ProtocolVersion client_version;
     Random random;
     SessionID session_id;
     opaque cookie<0..2^8-1>;                             // New field
     CipherSuite cipher_suites<2..2^16-1>;
     CompressionMethod compression_methods<1..2^8-1>;
   } ClientHello;

什么时候需要发起HelloVerifyRequest

  • 协议规定DTLS server在收到一个全新的握手请求(ClientHello的cookie字段为空)时应该去进行cookie exchange。
  • 如果server部署在一个可信的环境中,那么可以将server配置为不需要进行cookie exchange。这将会消耗一个额外的RTT
  • 当收到client的resume session的ClientHello时(对于tls1.2而言,sessionId不为空或者带有session ticket扩展),server可以选择不进行cookie exchange

最后还有一点:初始ClientHello以及HelloVerifyRequest不算做handshake transcipt的一部分,在CertificateVerify和Finished消息中不用考虑这部分

Handshake协议的消息有序性保证

DTSL Handshake协议为了保证有序性、并且支持分片,在协议层加了三个字段:

struct {
     HandshakeType msg_type;
     uint24 length;
     uint16 message_seq;                               // New field
     uint24 fragment_offset;                           // New field
     uint24 fragment_length;                           // New field
     select (HandshakeType) {
       case hello_request: HelloRequest;
       case client_hello:  ClientHello;
       case hello_verify_request: HelloVerifyRequest;  // New type
       case server_hello:  ServerHello;
       case certificate:Certificate;
       case server_key_exchange: ServerKeyExchange;
       case certificate_request: CertificateRequest;
       case server_hello_done:ServerHelloDone;
       case certificate_verify:  CertificateVerify;
       case client_key_exchange: ClientKeyExchange;
       case finished: Finished;
     } body;
   } Handshake;

  • message_seq:用于给每个handshake消息进行按序编号,收到乱序的消息后根据message_seq重排序来做到按序交给tls状态机处理。和record层的seq不一样,如果某个消息丢了,重传时这个message_seq保持不变
  • fragment_offset & fragment_length: 为了支持对同一个handshake消息进行分片,多个分片的message_seq一致。实现上需要注意一点是:收到的分片可能有重叠,DTLS需要能够处理重叠的情况(PMTU预估值发生改变)
  • 和TLS一样,一个handshake message会跨多个DTLS record,一个DTLS record中也可能聚合了多个DTLS handshake message(聚合多个消息的message到一个 record,减少record开销)。区别于TLS的地方在于,DTLS拆分handshake message是为了避免IP分片,TLS好像没有必要在record层拆分message(除非消息特别大,一个record都装不下),因为TCP层直接给做了

Handshake协议的消息可靠性保证

DTLS Handshake协议采用简单的超时重传机制来保证消息可靠性,但是并没有引入TCP、QUIC等协议中的常见的显式ACK机制来进行loss detect,而是利用握手过程的握手消息交互机制做到隐式ACK。

具体来说,DTLS将握手消息按照flight分组,下面是full handshake时双方的握手过程交互:

   Client                                          Server
   ------                                          ------

   ClientHello             -------->                           Flight 1

                           <-------    HelloVerifyRequest      Flight 2

   ClientHello             -------->                           Flight 3

                                              ServerHello    \
                                             Certificate*     \
                                       ServerKeyExchange*      Flight 4
                                      CertificateRequest*     /
                           <--------      ServerHelloDone    /

   Certificate*                                              \
   ClientKeyExchange                                          \
   CertificateVerify*                                          Flight 5
   [ChangeCipherSpec]                                         /
   Finished                -------->                         /

                                       [ChangeCipherSpec]    \ Flight 6
                           <--------             Finished    /

对于session-resuming的握手过程,情况有些不同,最后一个fight是client发出的。

 Client                                           Server
   ------                                           ------

   ClientHello             -------->                          Flight 1

                                              ServerHello    \
                                       [ChangeCipherSpec]     Flight 2
                            <--------             Finished    /

   [ChangeCipherSpec]                                         \Flight 3
   Finished                 -------->                         /

总的来说:一方发出一个flight后,如果握手过程还没结束,另一方需要去回应一个flight。如果一方在一段时间内没有收到对方的下一个flight(很可能对方的下一个flight有丢失),或者收到上对方重传的上一个flight(很可能自己刚发的flight有丢失,导致对方的超时重传)。那么他都需要重传这个flight的所有handshake消息。基于这个原则,DTLS将握手过程的超时重传抽象了一个握手状态机:

                      +-----------+
                      | PREPARING |
                +---> |           | <--------------------+
                |     |           |                      |
                |     +-----------+                      |
                |           |                            |
                |           | Buffer next flight         |
                |           |                            |
                |          \|/                           |
                |     +-----------+                      |
                |     |           |                      |
                |     |  SENDING  |<------------------+  |
                |     |           |                   |  | Send
                |     +-----------+                   |  | HelloRequest
        Receive |           |                         |  |
           next |           | Send flight             |  | or
         flight |  +--------+                         |  |
                |  |        | Set retransmit timer    |  | Receive
                |  |       \|/                        |  | HelloRequest
                |  |  +-----------+                   |  | Send
                |  |  |           |                   |  | ClientHello
                +--)--|  WAITING  |-------------------+  |
                |  |  |           |   Timer expires   |  |
                |  |  +-----------+                   |  |
                |  |         |                        |  |
                |  |         |                        |  |
                |  |         +------------------------+  |
                |  |                Read retransmit      |
        Receive |  |                                     |
           last |  |                                     |
         flight |  |                                     |
                |  |                                     |
               \|/\|/                                    |
                                                         |
            +-----------+                                |
            |           |                                |
            | FINISHED  | -------------------------------+
            |           |
            +-----------+
                 |  /|\
                 |   |
                 |   |
                 +---+

              Read retransmit
           Retransmit last flight
  • PREPARING state:这个state用于给endpoint准备将要发送的flight的所有Handshake消息,TLS的密码学计算发生在这个state,一旦数据准备好,DTLS将进行状态切换至SENDING state
  • SENDING state: 这个state用于endpoint将准备好的数据发送出去,涉及到消息的封帧(Handshake messages → DTLS record)、打包(DTLS record → UDP packet),设置超时timer。一旦数据发送完成,DTLS将进行状态切换至WAITING state。如果发送的flight 是最后一个flight ,那么将切换为FINISHED state。
  • WAITING state: 这个state 用于等待对端的响应flight,会有四种case:
    • timer超时前收到了对端的回应的flight,成功进入下一轮握手flight,状态切换到PREPARING state
    • timer超时收到了对端的回应的flight,并且是最后一个flight,状态切换到FINISHED state,此时握手完成。
    • timer未超时,但是收到了对端重传的上一个flight,可能是由于对端的timer超时导致重发,此时也需要重发当前flight,状态切换到SENDING state
    • timer超时,需要重发当前flight,状态切换为SENDING state
  • FINISHED state,这个state标识从当前enpoint视角来看,握手已经完成。接下来可能的状态切换有:
    • 当前连接需要进行rehandshake,状态切换至PREPARING state
    • 收到了对端重传的flight,说明本端的最后一个flight对端没有收到,因此需要进行重传本段的最后flight。状态保持为FINISHED state。

看起来这样虽然简单但是效率不高,一个flight中任何消息的任何分片丢失都将导致整个flight重传。

最后,重传的时间和次数取决于实现,协议建议从1s开始指数退避到60s截断。

4. Connection IDs机制

内容源自[I-D.ietf-tls-dtls-connection-id],目前还是草案,主要内容是在DTLS 1.2中引入cid机制,用来标识连接,而非采用至之前的IP + 端口 标识连接。理由是DTLS常被用于IOT通信,对于IOT设备而言往往会因为低功耗要求导致连接保活策略比较保守,NAT rebinding发生的会比较频繁,也就导致DTLS连接失效。CID机制的引入就是为了DTLS连接能发生NAT rebind后依旧能够存活下来,QUIC也有类似的机制。

改动主要包括几个部分:

  • 新增connection_id扩展,用于双方协商是否支持CID、CID初始化
  • Record协议的改动:一旦双方都支持CID,握手完成后将采用新的Record协议进行通信,这也将导致Record protection的一些改动
  • 连接迁移处理:协议规定当地址发生变更时需要怎么处理。

connection_id 扩展

  struct {
      opaque cid<0..2^8-1>;
  } ConnectionId;
  • Client在ClientHello消息中需要带上connection_id扩展,里面的CID是client端对这个连接的标识,希望server端后续用这个CID来给client发消息,如果CID是空,表示Client不希望server用CID来给自己发消息
  • Server也需要在ServerHello里带上connection_id扩展,里面的CID则是server端对这个连接的标识,同样也是用于让Client给自己发消息所使用,如果CID为空,也表示Server不希望client使用CID给自己发消息。
  • Client和Server独立选择两个方向上的CID。通常而言Client不需要让Server通过带CID的record来给自己发消息。为了解决Client NAT rebind后地址迁移问题,Server端则需要让Client通过带CID的record来给自己发消息,以便于在Client 地址发生迁移时Server还能通过CID认出Client
  • DTLS不支持CID更新,因此CID在整个连接生命周期内都将保持不变。

新的Record Layer

当DTLS连接启用了CID,那么在握手完成后的应用数据将采用新的Record协议进行通信,新的 Record协议采用和TLS1.3类似的做法,先用一个DTLSInnerPlaintext结构wrap下DTLSPlaintext结构:

     struct {
         opaque content[length];
         ContentType real_type;
         uint8 zeros[length_of_padding];
     } DTLSInnerPlaintext;
  • DTLSInnerPlaintext 里的content就是原来的DTLSPlaintext里的fragment内容,即payload的明文
  • real_type是DTLSPlaintext的type,即payload的类型
  • zeros是对payload的长度进行混淆引入的padding

有了DTLSInnerPlaintext后,对其整个DTLSInnerPlaintext结构进行加密,并构建新的带CID的DTLSCiphertext:

     struct {
         ContentType outer_type = tls12_cid;
         ProtocolVersion version;
         uint16 epoch;
         uint48 sequence_number;
         opaque cid[cid_length];               // New field
         uint16 length;
         opaque enc_content[DTLSCiphertext.length];
     } DTLSCiphertext;
  • 其中outer_type固定取值为tls12_cid
  • 新增了cid字段
  • enc_content是对DTLSInnerPlaintext结构整体进行加密保护的结果
  • 计算MAC / AEAD 的Additional data需要将cid纳入其中。

连接迁移处理

当收到一个带CID、并且地址和该CID表示的连接之前采用的地址不同时,可能表示对端发生了地址迁移,也可能是攻击者伪造的迁移。因此,协议规定了接收方在什么情况下需要进行地址替换。

  • 收到的包能够成功进行deprotect,这是最基本的要求,因为如果不能成功deprotect ,那么这个包很可能就不是对方发的
  • 收到的包比连接上之前收到的所有包都要新(更大的epoch + seq_number),这么做一方面是旧包的地址很可能不对了,另一方面也在一定程度上防止攻击者用一个假的地址去replay
  • DTLS应该将地址迁移事件上报给应用层,应用层可以进行地址验证程序。

最后,CID的引入也造成了一定的隐私泄漏风险。尤其是DTLS1.2的CID缺少更新机制。