PDNS系统设计实现总结

从14年中开始,我们团队开始做中国最大的PassiveDNS系统,并在基本的PDNS系统之上,衍生了很多的额外的功能服务。当前我们有:

  • flint: 基本的passive dns系统,domain-ip ip-domain的映射关系的历史记录查询. passivedns.cn,只对安全公司可信分析人员开放
  • flint.real: flint是所有历史记录的数据,这个flint.real则是最近一小时内的domain-ip/ip-domain的映射关系,数据实时分析的时候,实时的关系更重要
  • domain_stat: 域名访问统计,可以区分不同的请求类型,不同的返回类型,不同的数据节点
  • pdns_capture: 给定过滤条件,实时抓取最新的DNS记录数据
  • dtree:域名查找服务,给定一个模式,可以是子域名,可以是wildcard,可以是正则表达式,快速的在所有FQDN中查找符合模式的域名。很多安全分析文章都会对敏感域名打码,对我们而言,几乎天下无码。

这是我在当前团队做的第一个服务,2年多来也一直在改进维护,除了前端接入原始数据是同事在做,中间的数据流,处理分析,入库,查询接口,都是我在做。其中艰辛很多,也感觉学到很多,资源不够如何权衡妥协,网络不如意服务如何调度分派,数据放量如何动态扩展,很多细节如果不看代码都要忘记了。刚好年初数据放量,重新梳理代码优化了一下性能,趁机聊作记录以备忘。

考虑到用户隐私,所有涉及client ip的地方要做混淆, 混淆后的数据可用于数据的关联

出于保密的需求,具体架构不能说太细,数据库设计不会涉及,更多的是偏重功能+场景+实现妥协技巧这些容易遗忘的东西

数据采集点的区别

数据分析的前提是要懂数据,懂数据的前提就是要知道数据从哪来,是什么样子的,从而可以知道对于得到的数据,那些能做,哪些不能做。不同的采集点,采集到的数据不一样,量有大小的区别,覆盖范围有区别,可以提取出来的特征不一样,因此对于既定的分析目标,可能一个采集点的数据可以轻而易举的完成,而另外一个采集点可能做起来会非常费力,甚或天然的就无法做到。

由于懒,我直接抠我在FloCon2017上的talk的PPT的一页来说明。

上图是一个最简单的DNS请求的全路径。一个用户在运营商提供的一个子网内,发起的DNS请求通过运营商的边界路由,到一个OpenResolver/RecursiveServer,OpenResolver/RecursiveServer 负责完整的递归查询,将最终的IP返回给用户

open resolver 之上

图中的点【1】处。

这里的数据是DNS服务商的递归查询数据。理论上,只有当

  • 一个查询的域名之前没有查询过
  • 一个已经缓存的域名结果TTL已经过期

两种情况下,才会有递归数据产生。

域名的请求永远是大头长尾的数据形态,大部分的查询都会落在缓存中,有效的TTL时间范围内,一般来说,这里的数据量比点【2】处的客户端查询要小2个数量级。

加之一般的DNS服务商都会有基本的数据过滤,不合法的请求,错误的数据包,基本都可以很轻松的清洗掉,所以这里的数据也会比较干净。

数据量小,而且干净,拥有所有的递归过程的数据,因此这里的数据最适合用于构建一个PDNS系统,用以记录历史上domain-ip的映射关系及其变化。

open resolver 之下

图中的点【2】处。

这里的数据包括所有的点【1】的数据,不过数据量至少至少上升了2个数量级。

此外,在这里我们看得到客户端的数据,我们可以知道一个client ip在什么时间请求了什么域名。一个client ip频繁的请求比如cpsc.gov ANY,这极有可能是反射放大。一个client ip请求同一个SLD的不同的子域名,这又分至少两种情况:子域名如果构成比较规律,比如一个单词,那可能是子域名暴力破解;子域名如果构成是杂乱的随机字符串,那可能是RSD攻击。

而且,我们在此还可以知道query数据包中的src port(sport),transaction id(tid)数据。真实的DNS请求都是伴随着随机的sport/tid,因此,在一段时间内,针对同一个domain的所有query,或者一个client发出去的针对任意domain的query,其sport/tid的统计肯定是离散的,当统计显示针对某domain/某client的sport/tid是聚集的时候,我们就有相当大的把握断定这部分数据是伪造的query。

路由器边界

图中的点【3】处。

乍看起来,点【3】的数据等于把所有点【2】的数据都汇聚到一起。现实的问题在于:1)点【2】的数据都属于各大DNS厂商,这部分数据不可能完全汇总分享 2)正常的用户往往都会使用一个DNS服务器,但是和DNS相关的攻击流量都会和很多的open resolver相关 3)有很多DNS流量和open resolver无关,在点【2】也看不到,因此点【3】是非常必要的。

举例来说,RSD攻击一般来说会通过多个open resolver来打,但是也可能伪造流量直接请求到authoritative server,如果是后者,那在点【2】的位置就完全看不到,但是在点【3】可以看到。

再者,点【3】是client-focused,比如反射放大攻击,一次攻击可能动用上万个open resolver,在点【2】的单个open resolver处很可能被忽略掉,但是点【3】看到的是一个client ip接受来自上万个open resolver的响应,想故意漏掉都比较难吧。

甚或,这里我们可以看到query without response 有去无回数据, response without query无中生有数据,看到一个DNS服务器将任意domain的query响应为一个固定的IP等等,具体有什么用,think~

其他

上述三个数据采集点是做DNS数据分析一般的,常规的,最有效的数据采集位置。 我们很富,我们都有 。此外前后两端的数据采集点也要注意。

authority server 边界

麻烦各个NS管理员,如果闲了,看看自己的DNS服务器处理的请求都是什么,有没有开泛解析,有没有配置错误将一个权威服务器开启了递归查询功能。我们的数据表明,常被利用的DNS反射节点中,至少2%是开了递归查询功能的权威服务器,无意间就给反射放大攻击添柴助力。

客户端网卡

说一个场景,用户电脑,没人操作的时候,恶意软件可没闲着,各种可疑的黑网站,DGA这时候都会突兀的出现。

不可说

实际上,任何可以获取DNS解析记录的地方,其数据可能都有独到可用之处。尤其考虑到数据获取的场景context,简单来说,场景越黑,数据越黑。你有黑场景的数据可分享么?如果有,请联系我~


接入点处理

当前我们的数据在白天忙时平均有700w records/s,record指得是query-response pairing之后提取出来的数据记录。多个数据节点数据并不是平均分配的,最大的点超过150w/s,接入的千兆网卡是打满的状态。针对这么大的数据量,系统架构,或者说数据流设计,都要依赖一个高效稳固的接入和足够灵活的数据分发方式,所以接入点的处理单独拎出来说明一下。

  • sensor:负责原始DNS流量的抓取,解析,配对过程,形成最终的record,然后以hash(client ip/24)为key将数据publish出来
  • hasher:接受从sensor的record数据,然后以hash(SLD)的形式将数据publish出来

应对超大量数据

  • 数据水平切分,client/domain两大维度,后续详细分析
  • 传输使用Zmq,pub/sub模式,单ctx足够
  • 数据格式为protobuf。另:注意所有字段都为optional,不明白原因的去google
  • 批量合并压缩数据,zlib的Z_BEST_SPEED模式下压缩率为30%左右
  • 无锁队列,zmq的push/pull (inproc://addr)。注意,一定是消费者同质的时候才可以。消费者不同质,老老实实上lock-queue,否则一个慢消费者会拖死整个队列
  • log要异步多线程flush
  • 打点统计尽可能避免锁,可以使用__sync_XXX系列函数,也可以考虑thread::local单独打点,合并dump

数据分割

这一点尤为关键,接入的数据量巨大,不可能根据不同的需求重复传输多次,只有水平切分做好,后续的处理过程才能非常方便的扩展。

我的架构里,sensor和hasher作为公共的数据获取接口,其中sensor是以hash(client ip)为key的获取接口,hasher是以hash(SLD)为key的获取接口。

根据需求,如果后续的分析过程是以domain为核心的,那就从hasher来获取,所有*.test.domain都会被分发到一个同一个key下。如果想看某个client ip的情况,那就从sensor直接获取,那同一个client ip的访问行为会集中发布在同一个key上。

提取SLD的过程,不要简单的从后向前数点,com.cn等多级的TLD和com等单级的TLD判断起来会比较麻烦费力。先将所有TLD数据load成为一个trie tree,来的每一条数据,将FQDN从后向前遍历到最深,然后接着遍历到结尾或者下一个.的位置即可,考虑到SLD的长度基本都会在10个字符以内,这样的算法可以认为是O(1)的。

LRU 去重

DNS原始请求按照域名来看 永远是大头长尾 的数据形态,top 100网站的访问量占据了所有访问请求的半壁江山,因此一个放置在足够靠前位置的LRU cache,就可以有效的对大头数据进行去重缩量。一个百万size的大小的cache,可以将原始数据缩减一个数量级。

Disposable domain

处理过程的数据量缩减了就OK了么?NO。
当前,DNS服务被滥用的非常厉害,很多的域名查询已经不是原始的domain-ip映射关系的作用,有的用来做数据上报,有的用来做request-response服务,还有更多的不知道在干什么。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
141.131.152.01.zen.spamhaus.org
128.185.146.01.zen.spamhaus.org
161.101.124.01.zen.spamhaus.org
125.166.112.01.zen.spamhaus.org
112.147.171.01.zen.spamhaus.org
187.115.108.01.zen.spamhaus.org
115.118.145.01.zen.spamhaus.org
145.119.155.01.zen.spamhaus.org
2az8s49ydcfmtjk.q13795113801.pw
2iu5dsf679glqjp.q13795113801.pw
2icpazfdh3ewl59.q13795113801.pw
298gc7pfdy5a6jh.q13795113801.pw
29oiqrcle3ymg4u.q13795113801.pw
243twlvqkovhmye.q13795113801.pw
28xmi5p63gskjr9.q13795113801.pw
27n8pqgihzl6ts9.q13795113801.pw
257dsqmhzvx3n8w.q13795113801.pw
27lvywg4t8eocm3.q13795113801.pw
0.209d801.1033.14b4.15c2.3e9b.10.0.00c674b565ce48ea12966d1b82b66522.avqs.mcafee.com
0.60ab089.41.14b4.15c2.3e9b.10.0.087321142026ece602c83ab04528010c.avqs.mcafee.com
0.7092081.d960073.14b4.15b4.3e9a.10.0.09f2c55b594c11a6562f932ce8654ef0.avqs.mcafee.com
0.7091081.1033.14b4.15b4.3e9a.10.0.05441f6ba15c1f633a3968332fe9b9a9.avqs.mcafee.com
0.400001.d960073.14b4.15a2.3e9a.10.0.0c44e2b096e7c988856103b74d5b4766.avqs.mcafee.com
0.60fa021.c871031.14b4.15b4.3e9a.10.0.0ac236f269070480252be0b92f545d59.avqs.mcafee.com
0.70f6801.c051031.14b4.15e0.3e9a.10.0.0c406d44050caac10e17eaa6c33e1f4f.avqs.mcafee.com
0.70f2008.20033.14b4.15b4.3e9a.10.0.10a2f663fdc511fd52bfcfd0a8837549.avqs.mcafee.com
x-0.19-23000809.0.16a8.1ff0.6592.200.0.1113g9hwlzvrnims8qbe2ublbi.avqs.mcafee.com
x-0.19-23000809.0.16a8.1fcd.6592.200.0.1113g9hwlzvrnims8qbe2ublbi.avqs.mcafee.com
123456789123456789.3218721830764663770520984289166039746476607721908141260485540.5020666520264355071788964119716022440607979911188348837802609.11111111111111111111111111111111111111111111111111111111111111.top
123456789123456789.3218721830764663770520984289166039746476607721908141260485540.5020666520264355071788964119716022440607979911188348837802609.11111111111111111111111111111111111111111111111111111111111111.top
www.123456789123456789.3218721830764663770520984289166039746476607721908141260485540.5020666520264355071788964119716022440607979911188348837802609.11111111111111111111111111111111111111111111111111111111111111.top
02609.11111111111111111111111111111111111111111111111111111111111111.top
123456789123456789.3218721830764663770520984289166039746476607721908141260485540.5020666520264355071788964119716022440607979911188348837802609.11111111111111111111111111111111111111111111111111111111111111.top

这种“用一次就丢的”域名,可能已经不属于我们想要的domain-ip mapping关系,不在我们想要分析的范围内。
现实中,尽管他们占据所有原始请求数据的比例不高,远远比不上top domain的请求量,但是如果按照unique(FQDN)来统计,这些类型的FQDN至少占据了最终入库的70%(刚随手统计了我们新增域名最近一个月的数据。也就是说,这部分数据可能不会对实时处理过程有严重影响,但是对最终的数据集的大小影响比较大。

因此,我们可以选择一个合适的环节丢弃;即便不全部丢弃,也可以采样来降低数据集大小;或者选择性丢弃,只保留例如spamhaus结果为黑的部分。

“阶梯”采样

但是,并不是所有处理环节都能使用LRU 去重,比如我要统计请求一个特定域名的客户端的分布的时候; 也并不是所有环节都能干掉disposable domain,比如我想看一个client ip都访问了哪些域名的时候。

以前者为例,此时要分析的数据中心就是一个域名,client ip是作为属性存在的,如果全部记录,数据量会非常庞大,但是如果针对全局数据采样,那可能长尾数据就被采丢了,而大头还是大头。正确的做法是,针对要分析的key,domain,对其属性进行采样,例如100个以内的client ip全记录, 100-1000个的时候只记录1/10, 1000个往上只记录1/100。这样,长尾数据会被完好的保留下来,而大头数据会被有效缩减,且能保留它的统计特征。


基本架构/数据流

PDNS system

从hasher开始,接入LRU cache做去重(deduper),然后

  • cached_count_limit
  • cached_times_limit

两个判定条件将数据pop out(worker),最终入库 rrset/rdata

real-time data query system

从hasher开始,接入去掉disposable domain的数据,a.baidu.com -> com.baidu.a .分割,类似trie tree的结构遍历节点存储count(stater)
5分钟为时间单位,遍历count tree,把结果入库,形成domain的访问统计

cross-access system

从stater开始,以domain为key对client阶梯采样,可以查询一个client在给定时间都访问过哪些域名,也可以查询一个域名在给定时间都被哪些client访问

real-time analysis system

从stater开始,分别对domain/client ip/dns server进行建模统计,实时检测DGA-client, DNS反射放大攻击, RSD攻击,子域名暴力破解等等异常行为

dtree

dtree存在的意义就是天下无码,这里所有disposable domain都可以去掉。
当前我们dtree集群加载数据为百亿FQDN,加上时间类型等基本信息,文件为500G大小,新的FQDN除去disposable domain外产生的速度比较慢,因此这部分数据还是允许都加载到内存的。


实时分析处理Points

归一化及数据预处理

  • 大小写归一化
  • 域名合法性判定,有返回结果的不一定是好域名, 不符合域名规范的也可能是真实使用的域名
  • rdata排序
  • response error判定,比如被sinkhole的域名,比如本来是NXDOMAIN但是返回一个劫持IP等情况

Profile[domain/client/dnserv]

特征越全越好,参考上面不同采集点的数据特点,可利用的数据属性也不一致,不过domain/client/dnserv尽管是不同维度,但是在同样的数据采集点,特征大部分还是相似的。

Profile的意义不仅仅是可以直接判断各种异常,而更重要的意义是做为history snapshot,可以为后续的判断提供证据基础。

特征选择

特征除了原始数据,一定要尽可能泛化,len/top/avg/diss/sequ/compose/pattern 各个维度的统计数据在特定场景下可以发挥决定性的作用。

随机程度的度量:熵?

域名随机性往往都喜欢用熵来做,不过我觉得不好。

域名的作用本意是为了好记,正规的域名往往都是英文单词/词根/拼音/惯用简写的组合,看到一个随机字母的或者数字的,不是dga就是赌博色情这种常常变化的,那为什么不直接来判断域名的组合特征?

所以,做个分词器,动态加载不同的词表就能做各种组合的判断了。拼音就那么几百个,英文词根也很少,单词不要用词典,去搜索一个google top words就很好用了,里面还包含常用的缩略语。一个比较长的串,可能会有多种组合形式,选择的时候,优选覆盖原始字串最长的,次选组合成分最少的,几乎就没错了。


匆匆忙忙,蜻蜓点水,权作备忘。

一个一个子系统的做过来没什么感觉,回顾的时候发现要想写一个全景的流程图PPT都写不下,任何一个子系统的工程实践,几乎都有意料之外的约束。计算机艺术是妥协的艺术,满足了需求的就是好的。

立志成为低碳程序员的我,相当一部分成就感就是来自于“只加需求,不加机器”。