使用 cgroups + etcd + kafka 开发而成的hids的架构,agent 部分使用go 开发而成, 会把采集的数据写入到kafka里面,由后端的规则引擎(go开发而成)消费,配置部分以及agent存活使用etcd。关于agent 使用cgroups限制资源以及使用etcd做配置管理agent存活已经在前文介绍了一下。下面介绍一下agent抓取DNS请求和异常分析的部分。
DNS 请求的格式和响应格式差不多,DNS 定义了一个用于查询和响应的报文格式。上图显示这个报文的总体格式。这个报文由 12 字节长的首部和 4 个长度可变的字段组成。
DNS报文里的名字 说明
DNS 报文 12 字节长的首部格式如下图所示。
标识字段由客户程序设置并由服务器返回结果。 客户程序通过它来确定响应与查询是否匹配。
16 bit 的标志字段被划分为若干子字段,如下图所示。
Transaction ID:这是由 client 端指定的标识数据,DNS server 会将这个字段原样返回,client 端可以用来区分不同的 DNS 请求
RR:Resource Record 的缩写
Flags
16 bits 的值,各部分按顺序如下(按顺序:位号、Ethereal 名称、说明):
Bit 15,QR(Query/Response Flag) :0 表示查询,1 表示响应(query / response)
Bit 14~11, Opcode:查询类型——请求和响应包都适用:
0:普通查询(最常用的)
1:反向查询
2:服务器状态请求
3:通知
4:更新
Bit 10, Authoritative: AA 用于响应包,判断服务器是否一个认证的域服务器。
Bit 9, Truncated: TC表示 “可截断的(Truncated)”。使用 UDP 时,它表示当应答的总长度超过 512 字节时,只返回前 512 个字节。
Bit 8, Recursion desired:收发包都用,表示是否需要用递归。作为 client 端,最好置 1,要不然 DNS 不执行递归查询,将有很多数据没能查到。该比特能在一个查询中设置,并在响应中返回。 这个标志告诉名字服务器必须处理这个查询,也称为一个递归查询。 如果该位为 0,且被请求的名字服务器没有一个授权回答,它就返回一个能解答该查询的其他名字服务器列表,这称为迭代查询。
Bit 7, Recursion available:响应包用,表示 “可用递归(Recursion Available)”。 如果名字服务器支持递归查询,则在响应中将该比特设置为 1。 在后面的例子中可看到大多数名字服务器都提供递归查询,除了某些根服务器。
Bit 6, 0
Bit 5, 0
Bit 4, 0
Bit 3~0, Reply code:响应状态码,是一个 4 bit 的返回码字段。通常的值为 0(没有差错)和 3(名字差错)。 名字差错只能从一个授权名字服务器上返回,它表示在查询中指定的域名不存在。 下面列出rcode
0:OK 1:查询格式错误 2:服务器内部错误 3:名字不存在 4:这个错误码不支持 5:请求被拒绝 6:name 在不应当出现时出现 7:RR 设置不存在 8:RR 设置应当存在但是却不存在 9:服务器不具备改管理区的权限 10:name 不在管理区中
随后的 4 个 16 bit 字段说明最后 4 个变长字段中包含的条目数。
对于查询报文,问题(question)数通常是 1,而其他 3 项则均为 0。
类似地,对于应答报文,回答数至少是 1,剩下的两项可以是 0 或非 0。
问题部分中每个问题的格式,通常只有一个问题。
查询名(Name)是要查找的名字,它是一个或多个标识符的序列。
每个标识符以首字节的计数值来说明随后标识符的字节长度, 每个名字以最后字节为 0 结束,长度为 0 的标识符是根标识符。 计数字节的值必须是 0 ~ 63 的数,因为标识符的最大长度仅为 63 (我们会看到计数字节的最高两比特为 1,即值 192 ~ 255,将用于压缩格式)。 不像我们已经看到的许多其他报文格式,该字段无需以整 32 bit 边界结束,即无需填充字节。
每个问题有一个查询类型(Type),而每个响应(也称一个资源记录,我们下面将谈到)也有一个类型。 大约有 20 个不同的类型值,其中的一些目前已经过时。 下面列出:
1 A 由域名获得IPv4地址 2 NS 查询域名服务器 5 CNAME 查询规范名称 6 SOA 开始授权 11 WKS 熟知服务 12 PTR 把IP地址转换成域名 13 HINFO 主机信息 15 MX 邮件交换 28 AAAA 由域名获得IPv6地址 252 AXFR 传送整个区的请求 255 ANY 对所有记录的请求
最常用的查询类型是 A 类型,表示期望获得查询名的 IP 地址。 一个 PTR 查询则请求获得一个 IP 地址对应的域名。查询类(Class)通常是 1,指互联网地址(某些站点也支持其他非 IP 地址)。
DNS 报文中最后的三个字段,回答字段、授权字段和附加信息字段, 均采用一种称为资源记录RR(Resource Record)的相同格式。 图显示了资源记录的格式。
域名是记录中资源数据对应的名字。它的格式和前面介绍的查询名字段格式相同。
类型说明 RR 的类型码。它的值和前面介绍的查询类型值是一样的。类通常为 1,指 Internet 数据。
生存时间字段是客户程序保留该资源记录的秒数。资源记录通常的生存时间值为 2 天。
资源数据长度说明资源数据的数量。该数据的格式依赖于类型字段的值。对于类型 1(A 记录)资源数据是 4 字节的 IP 地址。
实例代码
抓取dns记录,可以hook udp_recvmsg() udpv6_recvmsg() 函数,也可能直接用libpcap。libpcap的工作原理可以描述为,当一个数据包到达网卡时,通过网络分接口(即旁路机制)将数据包发给BPF过滤器,匹配通过的数据包可以被libpcap利用创建的套接字PF_PACKET从链路层驱动程序中获得。进而在用户空间提供独立于系统的用户级API接口。我们使用谷歌的包github.com/google/gopacket, gopacket构建在libpcap之上。配合 audit 或者hook 相关函数,可以清楚看到对应的uid pid。
样例:
import ( "fmt" "log" "errors" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" ) var ( SrcIP string DstIP string ) func getDnsPcapHandle(ip string) (*pcap.Handle, error) { devs, err := pcap.FindAllDevs() if err != nil { return nil, err } var device string for _, dev := range devs { for _, v := range dev.Addresses { if v.IP.String() == ip { device = dev.Name break } } } if device == "" { return nil, errors.New("find device error") } h, err := pcap.OpenLive(device, 65535, true, pcap.BlockForever) if err != nil { return nil, err } log.Println("StartDnSMonitor") err = h.SetBPFFilter("udp and port 53") if err != nil { return nil, err } return h, nil } func StartDNSNetSniff(resultChan chan map[string]string) { var eth layers.Ethernet var ip4 layers.IPv4 var udp layers.UDP var dns layers.DNS var payload gopacket.Payload var resultdata =make(map[string]string) h, err := getDnsPcapHandle("10.10.16.1") if err != nil { fmt.Println("get pcaphandle failed, err:", err) return } parser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, ð, &ip4,&udp, &dns, &payload) decodedLayers := make([]gopacket.LayerType, 0, 10) for { data, _, err := h.ReadPacketData() if err != nil { fmt.Println("Error reading packet data: ", err) continue } err = parser.DecodeLayers(data, &decodedLayers) for _, typ := range decodedLayers { switch typ { case layers.LayerTypeIPv4: SrcIP = ip4.SrcIP.String() DstIP = ip4.DstIP.String() case layers.LayerTypeDNS: if !dns.QR { for _, dnsQuestion := range dns.Questions { resultdata["source"] = "dns" resultdata["src"] = SrcIP resultdata["dst"] = DstIP resultdata["domain"] = string(dnsQuestion.Name) resultdata["type"] = dnsQuestion.Type.String() resultdata["class"] = dnsQuestion.Class.String() resultChan <- resultdata } } } } } }
上面给出了一个抓取dns请求的样例,比如,看下面这个DNS隧道:
特征很明显:
1, 请求的Type一般都是TXT(为了返回的时候能够加入更多的信息)。】
2, payload部分一般都会编码(可能为base64、2进制或16进制)后放到子域名里面,而且多变,不一致
3, DNS发生频率很高,短时间为了发送大量数据,会产生大量请求
我们也可以联动威胁情报
在server 端做流式分析,挖掘 C2 、APT、 botnet 等等。