简介
本文描述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."前缀,消息格式如下:
<encoded data>.<domain>
or
<tag>.<encoded data>
任何不符合上述形式的数据,或者是不支持的记录类型,或者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.<byte1>.<byte2> 1.<byte3><byte4><byte5> 2.<byte6><byte7><byte8> 3.<byte9>.<pad>.<pad>
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-<S and S->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应该和请求一致