DNS请求数据包解析

DNS请求数据包图示

  1. 如上图所示,DNS数据包由DNS头部和DNS数据部分组成,DNS头部包含了DNS数据包的基本信息,DNS数据部分包含了DNS数据包的具体数据。
  2. DNS头部(在截图左侧上红框内)包含了12个字节,具体如下:
    1. 2字节的标识符(Transaction Id),用于标识DNS请求和响应数据包,响应数据包的标识符和请求数据包的标识符一致。
    2. 2字节的标志位(Flags),用于标识DNS数据包的类型,如下:
      Flags标志位截图
      1. QR(1bit) 查询应答标志,0表示这是查询报文,1表示这是应答报文。
      2. opcode(4bit) 查询应答类型,0表示标准查询,1表示反向查询,2表示请求服务器状态。
      3. AA(1bit) 表示权威回答( authoritative answer ),意味着当前查询结果是由域名的权威服务器给出的,仅由应答报文使用。
      4. TC(1bit) 位表示截断( truncated ),使用 UDP 时,如果应答超过 512 字节,只返回前 512 个字节,仅当DNS报文使用UDP服务时使用。DNS 协议使用UDP服务,但也明确了 『当 DNS 查询被截断时,应该使用 TCP 协议进行重试』 这一规范。
      5. RD(1bit) 表示递归查询标志 ( recursion desired ),在请求中设置,并在应答中返回。该位为 1 时,服务器必须处理这个请求:如果服务器没有授权回答,它必须替客户端请求其他 DNS 服务器,这也是所谓的 递归查询; 该位为 0 时,如果服务器没有授权回答,它就返回一个能够处理该查询的服务器列表给客户端,由客户端自己进行 迭代查询。
      6. RA(1bit) 位表示可递归 ( recursion available ),如果服务器支持递归查询,就会在应答中设置该位,以告知客户端。仅由应答报文使用。
      7. zero(3bit) 这三位未使用,固定为0。
      8. rcode(4bit) 表示返回码(reply code),用来返回应答状态,常用返回码:0表示无错误,2表示格式错误,3表示域名不存在。
    3. 2字节的DNS请求查询域名数量,标识DNS请求数据包中的问题数,通常一次查询查一个域名(为1)。
    4. 2字节的DNS响应记录数,标识DNS响应数据包中的资源记录数,一般为1,分区域解析或负载均衡等情况下会有多个结果。
    5. 2字节的授权资源记录数,标识DNS响应数据包中的授权资源记录数,通常为0。
    6. 2字节的额外资源记录数,标识DNS响应数据包中的额外资源记录数,通常为0。

DNS响应数据包解析

响应包和请求包没有太大的差别

  1. 响应包中的Answer RRs会根据实际结果展示数量,这个值在请求包中是。
  2. 响应包中的尾部追加了响应数据,如下图所示:
    DNS响应数据包图示

由上述请求包和响应包描述可知:响应包因为含有响应数据,所以会比请求包大。并且响应结果越多的域名相应的响应包的大小也会越大。这里就是DNS反射放大攻击的放大 原理,通过构造一个小的请求包,然后通过DNS服务器响应一个大的响应包,从而达到放大的效果。

DNS请求数据包构造

✨温馨提示:

配合博文配套项目食用更佳!

DNS请求数据包的构造,主要是按照DNS协议的规定构造出DNS头部和DNS请求数据部分。

  1. 构造DNS数据包需要的结构体
    // DNSData DNS数据包结构体
    type DNSData struct {
    TransactionId uint16 // 属于header 客户端随机生成的一个无符号整数,范围是0~2^16(0~65536)。在响应头里面也会返回这个值作用是校验。如果值不相等,丢弃响应内容。
    Flags uint16 // 属于header 16位标志位,包含QR、opcode、AA、TC、RD、RA、zero、rcode。一般查询flags为 00000001 00000000
    Queries []dnsQuestion // 本身不属于header 表示查询请求记录内容数据,他的组数长度值为Questions(属于header)
    // 下面几个是应答记录中的内容,只有在应答消息中才会出现
    Answers []dnsAnswer // 本身不属于header 应答资源记录数据(answer resource record, answer RR)此项只在DNS应答消息中存在,他的数组长度值为AnswerRRs(属于header)
    AuthorityRRs uint16 // 属于header 授权资源记录数量(authority resource record, authority RR)此项只在DNS应答消息中存在
    AdditionalRRs uint16 // 属于header 附加资源记录数量(additional resource record, additional RR)此项只在DNS应答消息中存在
    }

    // Flags赋值方法 因为zero这3bit位固定为0,所以不需要赋值
    func (dDNSData *DNSData) SetFlag(QR uint16, Opcode uint16, AA uint16, TC uint16, RD uint16, RA uint16, Rcode uint16) {
    dDNSData.Flags = QR<<15 + Opcode<<11 + AA<<10 + TC<<9 + RD<<8 + RA<<7 + Rcode // 采用位运算符进行赋值到对应bit位
    }

    // 定义请求数据的结构体
    type dnsQuestion struct {
    QueriesName string `net:"domain-name"` // 要查询的域名
    QueriesType uint16 // 查询类型 1:A 2:NS 5:CNAME 6:SOA 12:PTR 15:MX 16:TXT 28:AAAA
    QueriesClass uint16 // 查询类 1:IN 2:CS 3:CH 4:HS 通常为1表示为TCP/IP互联网地址
    }

    // 定义响应数据的结构体
    type dnsAnswer struct {
    AnswerName uint16 // 同dnsQuestion.QueriesName 要查询的域名
    AnswerType uint16 // 应答记录的类型 1:A 2:NS 5:CNAME 6:SOA 12:PTR 15:MX 16:TXT 28:AAAA
    AnswerClass uint16 // 同dnsQuestion.QueriesClass
    AnswerTTL uint32 // 32位生存时间(有效期)单位是秒
    AnswerDataLength uint16 // 16位无符号整数,表示应答资源记录中数据的长度
    AnswerCNAME string `net:"domain-name"` // 别名
    }
  2. 写入DNS头部数据
    // 写入DNS协议头部数据
    func (dDNSData *DNSData) WriteHeader() []byte {
    // DNS协议定义Header为12个字节的固定长度
    bs := make([]byte, 12)
    binary.BigEndian.PutUint16(bs[0:2], dDNSData.TransactionId)
    binary.BigEndian.PutUint16(bs[2:4], dDNSData.Flags)
    binary.BigEndian.PutUint16(bs[4:6], uint16(len(dDNSData.Queries))) // Queries的数组长度值为Questions
    binary.BigEndian.PutUint16(bs[6:8], uint16(len(dDNSData.Answers))) // Answers的数组长度值为AnswerRRs
    binary.BigEndian.PutUint16(bs[8:10], dDNSData.AuthorityRRs)
    binary.BigEndian.PutUint16(bs[10:12], dDNSData.AdditionalRRs)
    // 填充Question数据,要将域名进行转换。用.分割域名字符串
    ds := strings.Split(dDNSData.Queries[0].QueriesName, ".")
    // 循环遍历域名的每一部分,将其长度和内容写入到字节切片中。例如:list.eber.vip,写入的内容为4list4eber3vip0 末尾用0来表示结束。
    for _, d := range ds {
    bs = append(bs, byte(len(d)))
    bs = append(bs, []byte(d)...)
    }
    bs = append(bs, 0)
    // 添加查询类型和分类
    temp := make([]byte, 2)
    binary.BigEndian.PutUint16(temp, dDNSData.Queries[0].QueriesType)
    bs = append(bs, temp...)
    binary.BigEndian.PutUint16(temp, dDNSData.Queries[0].QueriesClass)
    bs = append(bs, temp...)
    return bs
    }
  3. 测试发送请求
    // 测试发送DNS请求
    func TestDNSDemo1(t *testing.T) {
    dnsServer := "114.114.114.114:53"
    dnsProtocol := "udp"
    dnsType := uint16(1)
    dnsClass := uint16(1)
    udpAddr, err := net.ResolveUDPAddr(dnsProtocol, dnsServer)
    if err != nil {
    fmt.Println(err)
    return
    }
    conn, err := net.DialUDP(dnsProtocol, nil, udpAddr)

    question := dnsQuestion{"dns.eber.vip", dnsType, dnsClass}
    out := DNSData{}
    out.TransactionId = 2015 // 这里随便给个值 0~65536
    out.SetFlag(0, 0, 0, 0, 1, 0, 0)
    out.Queries = append(out.Queries, question)
    header := out.WriteHeader()
    fmt.Println(header)
    _, err = conn.Write(header)
    var buf []byte
    buf = make([]byte, 512)
    n, err := conn.Read(buf[0:])
    fmt.Println(buf[0:n])
    }
    不出意外的话执行后Wireshark会捕获到该请求的DNS数据包。OK,这样我们就完成了一个简单的DNS请求。