dnscat2协议

简介

本文描述dnscat2 协议

我称之为dnscat2协议,尽管严格来讲,它不仅仅局限于dnscat或DNS。我需要一种逻辑连接协议,它可以在不确保可靠性且带宽被极度限制的多种底层连接、数据报之上建立。

本协议是为dnscat设计的,采用轮询机制——客户端发送数据包,服务器作出回应,服务器无法知道是哪个客户端发送的数据包,也无法知道如何初始化一个连接,因此这些问题在dnscat2协议中都要考虑。

本协议基于数据报,包含一个16bits的session_id, 可用于追踪多种底层链路之上的连接,并处理底层的丢包、重复和乱序问题。

以下我会给出一些细节——我们需要什么来完成这些工作、连接如何工作的描述、消息中使用的常量和消息本身的结构

挑战

借助DNS协议很有挑战性!下面列举了一些问题:

  • 每个消息都需要某种类型的回复
  • 重传、丢包和乱序非常普遍
  • DNS数据包中只能包含数字和字母,不一定区分大小写

DNS传输协议和dnscat协议都将这些考虑在内,不像其它DNS隧道协议,我不依赖于使用TCP传输层来保证可靠性——dnscat协议有能力在DNS协议之上建立原始连接。

DNS运送协议

dnscat协议是一个独立的协议,可以被应用在任何轮询协议之上,例如DNS、HTTP、ICMP/Ping等——我将这些协议都看作运送协议。在这些不同的协议之上需要将数据稍稍包装一下。本节主要介绍如何将DNS协议作为一个运送通道。

编码

所有的数据被编码成字符串的16进制表示,例如: "AAA" 变为"414141"

域名中的所有点号都应被忽略,因此,"41.4141"、"414.141"和"414141"是完全一样的。

除此之外,不区分大小写,因此"5b"和"5B"也是相同的。客户端和服务器都要处理大小写的不敏感性,因为有些软件会改变请求的大小写!

发送/接收

客户端可以选择是否扩展域名(用户必须有此域名的权威DNS服务器)或者加一个"dnscat."前缀,消息格式如下:

.
or

.

任何不符合上述形式的数据,或者是不支持的记录类型,或者dnscat服务器无法识别的扩展域名,服务器可以丢弃或者转发至上层DNS服务器。

dnscat2服务器必须用正确格式的DNS应答报文回复,置位无错误位,并包含一个或多个answer。如果有多个answer,每个answer的每一个字节必须是一字节的序列号(中间DNS服务器很可能会改变记录的顺序)

应答记录类型必须和请求记录类型一致,应答的具体的编码方式取决于记录类型。

DNS记录类型

dnscat服务器支持绝大多数DNS消息类型:TXT, MX, CNAME, A和AAAA,这些类型的请求消息都被封装为DNS记录,不同类型的应答报文格式稍有不同。

TXT应答报文就是16进制编码的数据。理论上,TXT记录可以包含二进制数据,但是Windows DNS客户端会NULL字符截断,因此需要编码。

CNAME和MX记录在请求时是相同编码的:都有一个tag前缀或者域名后缀。这是必要的,因为中间DNS服务器不会转发不正确的域名。MX记录类型在DNS中也有额外的区域——优先级区域,可以随机设置,客户端应该忽略。

最后,A记录和AAAA记录,有点像TXT,就是原始数据,没有前缀后缀。但有两点补充:一是应答报文长度很短(A记录4B,AAAA记录16B),因此需要多个answer,不幸的是,DNS层次体系会
重新排列answer, 因此每个记录必须包含一个字节的序号作为前缀,数值并不重要,只要可以排列得到原始的顺序即可。

二是没有明确的方法可以获取应答报文的长度,因为应答报文实际上是分块的。因此,数据本身的长度也要用一个字节来存储,放在消息头部。如果消息结尾不够一个块的大小,需要填充。

A记录应答报文格式:

0.9.. 1. 2. 3...

0表示块序号, 9表示数据长度, 2和3表示块序号

Errors

如果server遇到错误,根据严重性会采取以下措施:

  • 可以忽略的错误(重复的SYN包),回复一个空消息;对于TXT/A/AAAA, 显然是不回复;对于CNAME/MX/NS,因为域名是必须的,所以只回复一个域名。
  • 致命错误(例如未处理的异常), 应该回复带有描述信息的FIN包。

dnscat协议

在上面我定义了一种DNS运送协议,这个协议解决了如何通过DNS报文传送数据。下面我正式介绍dnscat协议。

dnscat 协议

连接

这里的"连接"指的是客户端和服务器之间的逻辑会话。一个连接以SYN包开始,之后包含若干个MSG包,最后以FIN包结束。每个连接用一个唯一的16bits的session_id标识,注意不要将SYN/FIN和tcp连接中的概念混淆,这里指的完全是dnscat中的概念。

总结一下:客户端发送SYN包给server, server回复SYN包,这样一个连接就建立了。客户端发送MSG包给server, server回复MSG包。当客户端决定终止连接,客户端发送FIN,server回复之;当server决定终止连接,它使用FIN包回复来自客户端的MSG包,客户端不再回复。

SYN包中的flags区域在client和server之间交换,这些flag影响整个会话过程。

大部分情况下,意外的数据包会被忽略.

client和server都允许处理多个会话,client经常和server同时展开多个会话;server则可以和不同的client同时展开会话。

一个完整的连接过程如图:

+----------------+
| Client  Server |
+----------------+
|  SYN -->  |    |
|   |       v    |
|   |  <-- SYN   |
|   v       |    |
|  MSG -->  |    |
|   |       v    |
|   |  <-- MSG   |
|   v       |    |
|  MSG -->  |    |
|   |       v    |
|   |  <-- MSG   |
|  ...     ...   |
|  ...     ...   |
|  ...     ...   |
|   |       |    |
|   v       |    |
|  FIN -->  |    |
|           v    |
|      <-- FIN   |
+----------------+

server决定终止连接:

+----------------+
| Client  Server |
+----------------+
|  SYN -->  |    |
|   |       v    |
|   |  <-- SYN   |
|   v       |    |
|  MSG -->  |    |
|   |       v    |
|   |  <-- MSG   |
|   v       |    |
|  MSG -->  |    |
|   |       v    |
|   |  <-- FIN   |
|   v            |
| (nil)          |
+----------------+

收到意外的MSG,server回复FIN:

+----------------+
| Client  Server |
+----------------+
|  MSG -->  |    |
|   |       v    |
|   |  <-- FIN   |
|   v            |
| (nil)          |
+----------------+

server收到意外的FIN,忽略之。

+----------------+
| Client  Server |
+----------------+
|  FIN -->  |    |
|           v    |
|         (nil)  |
+----------------+

SEQ/ACK号

SEQ(sequence 序列)和ACK(acknowledgement确认)号和TCP中的概念十分相似。在连接初始阶段,client和server都选择一个随机的ISN(initial sequence number初始序列号),并发送给对方。

client的SEQ号就是server的ACK号,反之亦然。这样双方都知道下一个应收到的序号是多少。

在一个会话过程中,双方会相互发送数据,当更多的数据排队等待发送,想像你正在将这些数据移动到已发送数据列表中,当消息发送出去之后,系统应当注意自己的序列号和字符队列来决定应该发送什么。如果有还未被对方确认的数据在等待,这些数据应该被重传,直到和当前的序列号匹配。

当收到消息后,接收方必须将消息中的序列号和自己的确认号进行对比,如果序列号比确认号小,说明收到的是重复数据,ACK可能丢失了,没有被对方收到,必须重新发送ACK。如果序列号比确认号大,数据应该被缓存或是悄悄丢弃(当对方发送多个数据包进行测速时),如果相等,则进一步处理。

当消息进一步处理时,接收方根据收到的字节数增加ACK号,并将新的ACK、SEQ和等待的数据发送出去。

当发送方收到对方的ACK,会增加自己的SEQ,从新的SEQ处开发发送数据。

你要知道双方都会持续确认对方发送的数据(通过增加对方的SEQ号),同时发送自己的数据并更新自己的SEQ(通过对方的ACK)

命令协议

在dnscat协议之上有一个称为命令协议的协议。如果在SYN头部设置了OPT_COMMAND位,所有的消息都会被当作命令消息,必须遵循命令协议。

关于命令协议的详细信息请查看command_protocol.md.

加密/签名

待完成

常量

/* Message types */
#define MESSAGE_TYPE_SYN    (0x00)
#define MESSAGE_TYPE_MSG    (0x01)
#define MESSAGE_TYPE_FIN    (0x02)
#define MESSAGE_TYPE_ENC    (0x03)
#define MESSAGE_TYPE_PING   (0xFF)

/* Encryption subtypes */
#define ENC_SUBTYPE_INIT    (0x00)
#define ENC_SUBTYPE_AUTH    (0x01)

/* Options */
#define OPT_NAME            (0x01)
#define OPT_COMMAND         (0x20)

Messages

本节解释了如何为消息类型编码,所有的区域采用大端编码方式,整个数据包通过DNS运送协议传送。运送协议负责处理数据包大小,数据包大小是已知的。

所有的消息包含一个16位的packet_id, 每个packet_id应该不同,这是为了缓存而设置的。

数据类型

正如上面提到的,所有的区域大端编码(网络字节序)。以下数据类型被用到:

  • uint8_t - an 8-bit (one-byte) value
  • uint16_t - a 16-bit (two-byte) value
  • uint32_t - a 32-bit (four-byte) value
  • uint64_t - a 64-bit (eight-byte) value
  • ntstring - a null-terminated string (that is, a series of bytes with a NUL byte ("\0") at the end
  • byte[] - an array of bytes - if no size is specified, then it's the rest of the packet

MESSAGE_TYPE_SYN [0x00]

  • (uint16_t) packet_id
  • (uint8_t) message_type [0x00]
  • (uint16_t) session_id
  • (uint16_t) initial sequence number
  • (uint16_t) options
  • If OPT_NAME is set: (ntstring) 7.
Notes
  • 每个连接的初始化,都是通过一个客户端发送SYN开始,包含一个随机的session_id和随机的初始化序列号,以及请求选项。

  • 以下选项被定义:

    • OPT_NAME - 0X01[C->S]

      • 数据包包含一个额外的区域:session名,一个自由区域,可以包含可读数据
    • OPT_COMMAND - 0X20[C->S]

      • 命令会话,表示是一个命令隧道消息
    • OPT_ENCRYPTED - 0x40 [C-C]

      • 协商加密方式
      • crypto_flags 未定义,为0
      • 公钥x和y为大数,直接转为16进制值,左侧填充0
  • 服务器回复SYN,包含初始序列号和选项

    • 如果客户端请求中包含OPT_ENCRYPTED,服务员也必须包含
  • session_id和初始号必须随机化,使得连接劫持攻击更加困难(两个序列号和session_id每个连接给我们大约48bits的熵)

  • packet_id对每个packet来说都应该不同,完全是为了阻止缓存。可以是递增的,双方都应该忽略这个值。

  • 如果服务器收到多个完全相同的SYN,则每个都要回复。

  • 如果服务器收到了有相同session_id的不同SYN,应该忽略。

错误状态
  • 如果客户端没有收到SYN应答,意味着请求包或者应答包丢失了,客户端可以选择重传或者生成新的SYN包或会话。

  • 如果客户端在收到MSG消息之前收到第二个相同session的SYN,需要当作有效来回复。

    • 有效意味着包含相同选项、序列号、相同名称、相同密钥。
  • 如果客户端和服务器在建立连接时收到SYN,应该丢弃。

MESSAGE_TYPE_MSG: [0x01]

  • (uint16_t) packet_id
  • (uint8_t) message_type [0x01]
  • (uint16_t) session_id
  • (uint16_t) seq
  • (uint16_t) ack
  • (byte[]) data
Notes

如果SYN包含OPT_COMMAND,数据区域使用命令协议

MESSAGE_TYPE_FIN: [0x02]

  • (uint16_t) packet_id
  • (uint8_t) message_type [0x02]
  • (uint16_t) session_id
  • (ntstring) reason
Notes

一旦FIN发送,client or server就不太回应任何消息。

MESSAGE_TYPE_ENC: [0x03]

  • (uint16_t) packet_id
  • (uint8_t) message_type [0x03]
  • (uint16_t) session_id
  • (uint16_t) subtype
  • (uint16_t) flags
  • If subtype is ENC_SUBTYPE_INIT:
    • (byte[32]) public_key_x
    • (byte[32]) public_key_y
  • If subtype is ENC_SUBTYPE_AUTH:
    • (byte[32]) authenticator
Notes
  • 如果要使用加密连接,客户端应该立即发送ENC|INIT数据包
  • 服务器必须用包含相同子类型的ENC包回复ENC请求
    • 如果客户端选择加密,服务器也必须加密;客户端选择认证,服务器也必须认证。
  • 服务器必须用相同的密钥回复ENC|INIT包
  • 公钥和认证器编码为32字节的16进制字符串,左侧填充0

下面的Ruby代码用于转换整数到字符串:

[bn.to_s(16).rjust(32*2, "\0")].pack("H*")

反之:

[bn.to_s(16).rjust(32*2, "\0")].pack("H*")

MESSAGE_TYPE_PING: [0xFF]

  • (uint16_t) packet_id
  • (uint8_t) message_type [0xFF]
  • (uint16_t) ping_id
  • (ntstring) data

Notes

ping_id应该和请求一致

你可能感兴趣的:(dnscat2协议)