CYBerATtacK

cyber security related things


  • Home

  • Tags

  • Archives

  • About

  • Search

Automatic discovery of malicious websites in NOD

Posted on 2019-04-17 |

My Talk on FIRST-TC Amsterdam2019.

Our team operates China’s largest PDNS database. In the hundreds of billions of DNS requests per day, we can see more than 200k NOD (Newly Observed Domain), many of which are malicious websites, including but not limited to porno and gambling, malicious APP promotion, and so on.

These malicious websites deliberately use NOD to constantly change their identities to avoid blocking. At the same time, they will use various methods to evade detection, including simulating normal websites, filling irrelevant content, JS lazy loading, displaying content with pure images, etc.

After analysis, we found that these malicious websites are endless and varied, while the resources be used by them are relatively fixed, such as the same statistical links, same images, embedded JS code segments, download resources, three-party plug-ins with ID, etc. . All of the above can be tagged as “malicious resources”, and normal websites will never load them. Based on this feature, we can use the relationship between the websites and their loading resources to detect various malicious websites in the NOD.

At the same time, there are way too many malicious resources, and they are constantly being added. It is impossible for us to manually search and operate. We have also implemented an automatic discovery process for malicious resources. The malicious websites in NOD are tagged by the old malicious resources, and the new malicious resources are automatically discovered. The whole loop process requires only a small amount of manual review, and the automatic detection of most malicious websites in the full amount of NOD can be realized.

Download: Automatic discovery of malicious websites in NOD.PDF

Backbone Network Traffic Anomaly Detection

Posted on 2018-03-13 |

概述

目的

通过持续对流量数据的监测,不断丰富及提取合适的流量特征,以实时检测流量异常
几种主要结果:

  • scanmon
  • ddosmon
  • 暴力破解/垃圾邮件服务器等

Pivot模型

netflow数据是流量的摘要数据,因此通过netflow来检测流量异常也就只能通过摘要统计来提取特征。

一条原始netflow数据形如:

1
2018-03-12_10:56:18 1 TCP 111.75.213.6:80 -> 220.160.22.245:49948 ...PA... 0 5 6888

这样的数据,只有基本的发生时间,持续时长,来源/目的 的IP:端口等等基本信息。

一般情况下,单条数据无法形成有效的事件鉴别特征,如果我们想深入观察某个IP的状况,我们需要累积一定时间窗口内的与该IP相关的所有数据来观察,比如针对目的IP 189.203.188.074,我们累积一段时间的来源流量可以得出如下统计数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
来源Flow总计: 95(in_fsum)
来源Package总计: 95(in_psum)
来源Bytes总计: 113474(in_bsum)
来源包个数统计: _size=1;_nums=63;1=63;(in_pkgnums)
来源包大小统计: _size=28;_nums=63;1500=29;1476=3;1158=2;1063=2;1057=2;847=2;23=2;44=1;1234=1;63=1;1155=1;(in_pkgsize)
来源IP统计: _size=51;_nums=63;211.161.099.125=4;117.035.057.189=2;116.226.156.088=2;120.132.146.115=2;112.126.075.213=2;182.050.118.202=2;182.050.118.223=2;183.129.179.044=2;183.129.249.076=2;218.084.015.245=2;223.223.198.100=1;222.223.026.006=1;120.026.120.015=1;222.222.238.164=1;121.043.148.069=1;123.183.161.199=1;222.222.235.237=1;124.116.223.098=1;124.117.235.138=1;124.126.011.177=1;222.072.158.150=1;221.224.036.022=1;219.235.255.070=1;219.072.250.018=1;211.157.139.253=1;218.031.105.251=1;027.191.130.122=1;036.110.211.029=1;043.247.177.226=1;047.094.246.179=1;058.118.178.084=1;058.213.097.129=1;059.045.193.027=1;059.046.059.174=1;060.205.095.069=1;060.205.146.210=1;061.185.004.122=1;101.081.132.185=1;101.095.027.234=1;101.200.162.015=1;106.002.218.068=1;106.014.238.132=1;110.086.000.162=1;111.113.004.106=1;111.113.027.126=1;114.055.052.155=1;114.141.173.038=1;115.159.102.216=1;115.238.035.233=1;116.231.054.207=1;116.231.155.071=1;(in_ipv4)
来源端口统计: _size=2;_nums=63;0=33;53=30;(in_port)
PS:
1,数值型数据相加
2,枚举型数据组织为count-map
3,枚举类型count-map前面的 _size表示后面的list总长度,也即为unique(list.keys()), _nums表示sum(list.value())

我们可以看到,来源IP比较分散,但是来源Port固定为53和碎片0端口,同时,很多包都是1500 MTU的满包状态,我们就有充足的信心来说,这是一个DNS反射放大攻击事件,是典型的大量的被利用的反射节点给受害者目的IP发送超过1500的大包导致的流量形态。

双向

针对一个IP,我们要想观察它是否异常,我们就需要观察它进出两个方向的流量:

  • 如果是一个Scanner,可能一段时间内只看到它出去的流量,而看不到流向它的流量
  • 如果是一个被DDoS目标,可能一段时间内只看到它进入的流量,而看不到它出去的流量

多层

同时针对一个IP,观察需要按照IP/Proto/Port来分层观察流量:

  • 一个IP上TCP 80端口是正常服务,但是被攻击的是90端口,如果不根据端口来区分观察流量可能会miss
  • 一个IP上TCP 80端口正常服务,但是被攻击的是UDP流量,如果不根据协议来区分观察流量可能会miss

多触发

一般情况下,聚合10分钟的数据,然后整理成所需的格式push到后续流程做检测就够了,但为了数据的时效性以及查全查准,一共有三种检测触发器:

  • STWPop:STW for Sliding Time Window,固定时间窗口流量spike触发检测
  • EarlyPop:一个IP一个时间窗口内最开始出现的部分流量
  • AETWPop: AETW for Auto-Extend Time Window, 当一个固定时间窗口内流量太小,自动扩展更多个时间窗口的数据来触发检测

规则

特征概述

根据我们上述“双向-多层-多触发”模型,任何一个规则条件都可以简单概括为如下:

针对一个IP/IP-Proto/IP-Proto-Port,其:

  • 来源/目的
  • IP/Proto/Port/Flow/PackageNum/PackageSize/Duration
  • 的unique/sum/dispersion/top

是什么

比如上面我们给出的UDP反射放大攻击的case,我们提到“来源IP比较分散,但是来源Port固定为53和碎片0端口”

这里其实需要映射为三个“与关系”的条件:

  • 针对IP 189.203.188.074,其来源IP的dispersion介于[0-5]之间(这里的[0-5]是离散的数值表示)
  • 针对IP 189.203.188.074,其来源Port的Top1等于53
  • 针对IP 189.203.188.074,其来源Port的Top2等于0

规则条件说明

规则条件基本上都是 Calc_DirectionORPosition_Item 的形式。

DirectionPosition

  • in: 进入的流量
  • self_as_dst: 自身作为目的的流量
  • self_as_src: 自身作为源的流量
  • ot: 出去的流量

比如如下5条流量:

1
2
3
4
5
1.1.1.1:1111 -> 2.2.2.2:2221
1.1.1.1:1112 -> 2.2.2.2:2222
1.1.1.1:1113 -> 2.2.2.2:2223
2.2.2.2:2225 -> 3.3.3.3:3331
2.2.2.2:2226 -> 3.3.3.3:3332

整理成我们的Pivot模型后,DirectionPosition的位置如下:

1
2
3
4
5
6
7
8
9
|<--IP 2.2.2.2 Info-->|
in self_as_dst|self_as_src ot
1.1.1.1:1111 -> 2221:2.2.2.2
1.1.1.1:1112 -> 2222:2.2.2.2
1.1.1.1:1113 -> 2223:2.2.2.2
2.2.2.2:2225 -> 3.3.3.3:3331
2.2.2.2:2226 -> 3.3.3.3:3332

Item基本概念:

  • duration: flow持续时长,一个5元组flow数据的持续时长
  • pkgnums:一个flow中的package数,数字越大,说明同一个flow中传输的package越多
  • pkgsize: 一个flow中的package的平均大小,数字越大说明同一个flow中传输的package越大
  • peer: IP+Port是一个peer
  • ipv4:IP
  • ip_b:IP/16
  • ip_c:IP/24
  • port:port
  • tf: TFlags整体
  • fin/ack/syn/psh/rst/urg/nul: TFlags中对应的单独的一位

所有枚举型都会被整理为ordered-count-map,类如{A:100, B:80, … N:1},表示A出现100次,B出现80次,。。。 N出现1次,并且按照大小排序,Calc基本都是根据ordered-count-map的计算

Calc基本概念:

  • lens:unique(ordered-count-map)
  • diss:离散度,0-9999,数字越小说明越分散,5为比较分散,10为比较聚集,20以上为非常聚集。这个数字的step要根据现实流量的态势来调整
  • tops: ordered-count-map的最大的key
  • top2: ordered-count-map第二大的key
  • avgs: ordered-count-map中value的均值,或者sum([k * v for k, v in ordered-count-map])/len(ordered-count-map)
  • span: ordered-count-map中最大最小的key的跨度
  • rate: 比率,往往是和TFlags结合来确定进出tflags的比率

举例:

  • lens_in_ipv4: 进入流量中ip的unique个数
  • diss_ot_port: 出去流量中port的离散程度
  • tops_in_port: 进入流量中最多的port
  • avgs_ot_duration: 出去流量持续时长的均值
  • rate_in_syn: 进入流量的SYN的比例

规则实例

上面说的规则条件都是单个的条件,一条规则实际是多个条件的“与”结合

1
2
3
4
5
6
7
8
9
1 2024->attr tag udp@attack@amp_flood_target
2 2024->cond prot equal 17
3 2024->cond in_locals_spike in_span 15-999999999
4 2024->cond in_global_spike in_span 15-999999999
5 2024->cond diss_in_port in_span 9-999999999
6 2024->cond tops_in_port equal 11211;27960;389;1701;69;111;520;19;1900;123;53;161;137;17;0
7 2024->cond top2_in_port equal 11211;27960;389;1701;69;111;520;19;1900;123;53;161;137;17;0
8 2024->cond tops_in_pkgsize in_span 1000-999999999
9 2024->cond avgs_in_pkgsize in_span 600-999999999

上面是一条完整的规则,attr表示为规则属性,cond表示为规则条件,按行依次解释为:

1
2
3
4
5
6
7
8
9
1 属性:编号为2024的规则是用来匹配“udp@attack@amp_flood_target”事件
2 条件:proto=17 UDP 协议
3 条件:短时间周期内(1小时)流量spike 15倍以上
4 条件:长时间周期内(1天)流量spike 15倍以上
5 条件:来源端口比较聚集
6 条件:来源端口top1是常见反射放大端口其一
7 条件:来源端口top2是常见反射放大端口其一
8 条件:来源包大小top是1000字节以上的大包
9 条件:来源包大小的均值是600以上

规则集

如上一条规则只能捕获一种或几种类型的“udp@attack@amp_flood_target”事件,但是不能捕获所有的“udp@attack@amp_flood_target”事件。同一种事件,在不同的场景下可能有不同的数据形态,在不同的阶段可能有不同的数据形态,因此针对某一特定事件,我们需要多条规则来覆盖所有的场景,以期达到更高的检出率。多条规则之间是“或”的关系,一次匹配,是匹配所有可疑的规则,只要能命中其中任意一条,我们便可判定为真。

同时,存在某些场景下,不同的事件类型但是数据形态非常相似,只有细微的差别,这时候为了规则运维的方便,有一个priority属性,一条数据匹配到多个规则,只会取priority最大的结果。

架构

整体数据流程可以简单的分为“分发 - 汇集 - 匹配 - 输出”四个步骤。

  • 分发:接受不同采集节点的数据,按照一定规则hash(ip)的方式分发数据到汇集的节点
  • 汇集:持续累积给定时间窗口的数据,整理为上述的模型数据,并依照上述模型中的触发条件,将数据POP出来,给到后续匹配流程做检测
  • 匹配:接受数据,根据给定的规则,检测具体的事件
  • 输出:最终结果事件的输出记录

原始数据分发

我们的模型是要针对某一个IP采集进/出两个方向的流量,而一个IP要么出现在src的位置,要么出现在dst的位置,这就意味着,对于一个 A->B 的flow,我们既要将其发送给A对应的汇集点,也要将其发送给B对应的汇集点。

原始数据聚合

聚合的过程是根据数据到达的时间实时进行的,几个要点:

  • 默认聚合过程是汇聚到IP-Proto-Port级别,而IP/IP-Proto级别的数据,是在STWPop的时候动态计算的,这样可以节省内存资源消耗
  • 由于有AETWPop的存在,所以一个固定时间窗口过去后,“即没有多到可以直接判断,又没有少到可以忽略的”这部分数据需要留存下来参与下一次的聚合过程
  • 由于数据分布不均的原因,同一个聚合点缓存数据的Flush实际有两个触发条件,一个是配置的TimeWindow的大小,一个是聚合key的最大值。正常情况下都是前者起作用,但后者可以保证即便某个点数据爆炸,系统还能保持稳定运行

模型数据匹配

匹配过程是一个 “white-black-gray” 串联匹配过程

  • white: 用以初步过滤一些明显的不管新的数据
  • black: 用以匹配出我们已知的恶意事件
  • gray: 对white/black以外的数据进行历史流量建baseline,然后找出可疑的异常

三个节点都是可选的,动态配置。

结果数据输出

略。

附录

规则可用属性

  • tag: 事件类型,基本为proto@event_type@detailed_type 格式
  • index: 规则组,如果想针对不同的数据过不同的规则集
  • priority: 优先级,规则默认优先级是0,如果确认一条规则最精确最准,可以设置一个高优先级,这样会覆盖其他规则的匹配

规则可用条件

大部分规则都符合“规则-规则说明”中的格式,部分特殊的加了额外说明

  • accu: 数据聚合级别,3是IP-PROTO-PORT,2是IP-PROTO,1是IP
  • prot: protocol 协议
  • port: 端口
  • proc_step: 处理阶段,1是STWPop和AETWPop,2是EarlyPop
  • time_win: AETW扩展了几个时间窗口
  • in/ot_fsum: 进/出的flow总计
  • in/ot_psum: 进/出的package总计
  • in/ot_bsum: 进/出的bytes总计
  • diff_flow_io: 进出flow差值
  • diff_flow_oi: 出进flow差值
  • in/ot_locals_spike: 进/出短时间窗口(1小时)spike幅度
  • in/ot_global_spike: 进/出长时间窗口(1天)spike幅度
  • in/ot_spike: in/ot_max(locals_spike/globals_spike)
  • in/ot_spike_type: flow spike/packages spike/bytes spike
  • in/ot_ipv4_count_avg: 进/出IP的平均出现次数
  • in/ot_ipv4_count_top: 进/出IP中出现最多的次数
  • rate_io_avgs_pkgsize: 进/出包大小比率
  • tops_nzport_io_equal: 进/出流量中最大的非0端口是否一致
  • 此处省略100条条件

PDNS 系统设计实现之二:PDNS白画像

Posted on 2017-05-26 |

在上一篇《PDNS系统设计实现总结》中,简单记录了一下我们当前PDNS系统的结构关键点。但是,那篇里面主要提及的点,都是如何找出异常,对于PDNS系统而言,还有一个重要功能是对正常业务的刻画,今儿补上。

做安全分析,着眼点都是异常是什么,为什么要通过DNS来对业务做画像呢?其实这个道理和杀毒软件的演进思路大同小异,因为异常是多变的,难以完全描述,甚或有时候难以捕捉,因此直接做黑名单黑规则,总有漏网之鱼,且规则要紧跟异常,否则漏的就越来越多。而从白名单的角度,业务的稳定性远远高于异常,因此如果能较为精确的梳理出业务的白画像,那异常的捕捉就能变得更为简单。

直接上图。

数据流程图

上述数据流程图中,从最开始数据接入归一化,自上而下分三路:左路是一般数据处理入库,大部分流程在上一篇中已经提及,是上一篇的重点;右路是实时异常检测,在上一篇中略有提及;中路就是今天的主角,如何利用PDNS系统来形成对业务的精确画像(Profile)。而中路中,数据处理入库和左路没有太大区别,业务的白画像数据,和左路数据的最大的改进就是过滤掉所有黑数据,因此重点就是图中着重标出的 Dynamic Filter 部分。

先说一下睡在 Dynamic Filter 左边的兄弟 Static Filter。

  • 无效数据: 比如*.arpa等无意义数据
  • 无效纪录:比如SOA等无意义记录
  • Pairing 失败:一条有效的PDNS数据比如要query和response的有效pairing,否则数据可能是伪造的
  • 响应为error:数据无用
  • 请求域名无效:有些请求的域名是无效的,但是有些open resolver仍旧会回复有效数据,这些意外的数据也应该过滤掉
  • 请求域名有效,响应看似有效,但是实际无效:比如一个example.com CNAME 到 not.exist.bla,如果不仔细教研rdata部分,很容易就会混入很多无效数据
  • CDN第二跳域名: 有些CND的子域名不是固定的,变动特别频繁,这部分数据如果完全记录,那也将是数据灾难

上述之所以归为Static Filter,是因为这些过滤的条件是简单易判断可以写死的条件,无需其他过程的介入和交互。那 Dynamic Filter 要过滤哪些数据呢?

  • Abused DNS:spamhaus, dans tunnle CC指令,数据传送等等非正常DNS使用的
  • Disposable domain:两个关键 cloudfront greencompute
  • Sinkhole: 被sinkhole的数据只对安全分析有作用,绝对不可能是正常业务
  • DGA / FustFlux等黑产域名
  • DDNS动态域名: 这些域名数数count就好了
  • 黑帽SEO流量站以及站群带来的批量二级子域名
  • 子域名暴力破解 + 泛解析
  • 域名暴力破解 + gTLD泛解析
  • DNS benchmark
  • Chrome 随机域名探测
  • 随机前缀攻击
  • DNS反射放大之A纪录填充
  • 安全防护DNS流量引流, CNAMER到防护域名甚或无效域名,A到内网地址等
  • DNS suffix
  • DNS劫持结果,黑产+你熟悉的G/F/W
  • 各种异常:互联网是程序员写出来的,有程序员的地方就会有BUG,举个栗子:有些域名做防护,会将请求的域名CNAME到一个随机子域名,比如把example.com CNAME 到 aaaabbbbcccc.example.com,后续根据客户端有没有请求该子域名,来判断客户端是不是正常。如果有了该子域名的请求,则认为客户端是真实的,再将 aaaabbbbcccc.example.com CNAME 回真正的地址,实现正常请求。但是现实中,实现的方法以及判读逻辑可能有问题,导致子域名 aaaabbbbcccc.example.com 的请求过来,又一次被CNAME到更深层级的子域名,比如 ddddeeeeffff.aaaabbbbcccc.example.com, 又要进行一次判断,如此往复。因此,我们能看到一个 ping-pang 的DNS请求,请求的域名越来越长,包越来越大,直到超出 DNS 域名规范的限制。这个过程,防护设备不但没有起到防护的作用,甚或间接的造成了DDoS攻击,整个请求过程的域名都是我们要过滤掉的。

上面这一坨,每个点拿出来想做好,都可能是一项烦耗的工程。这些过滤,依赖于右路实时异常检测的结果,需要实时监测到这些异常,然后实时通知 Dynamic Filter过滤掉。这对实时异常检测的要求比较高。当然,这部分工作完全可以在最终入库前做,将入库时间延后,等各种异常汇集后再做,但是这样,对应的业务白画像的DB White也就不是实时可用的了,这个可以根据自己的需求权衡。

而且,实时异常检测的时候,不可避免会有漏判误判,最下方的 Off-Line Analysis 的产出是立足于黑白灰全局数据的安全分析,也是定期复盘,一要纠正数据,而要提供更多的模型及规则给实时异常检测,形成一个 检测-过滤-验证-检测 的环路,不断优化模型,精细梳理规则,以期实现一个精准度不断提高的PDNS白画像。

pymongo find操作的limit限制对返回的cursor.count() 默认不生效

Posted on 2017-04-27 |

我们有一个web服务的mongo连接池,对每一个mongo find查询,我们会在日志中记录查询条件及其返回数据的count,从后可以方便的后续定位问题。
最开始使用的count方法是

1
2
result = monger.find(cond);
count = result.count()

出问题了,mongo后台记录了很多command: count慢查日志,在有这些慢查的时候,mongo库性能急剧下降,几乎不可用。

排查代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print "0x00 len(list(mongor.table.find(cond))) COUNT: ", len(list(mongor.table.find(cond)))
for i, v in enumerate(mongor.table.find(cond, fields=["types"])):
print "\t", i, v
print
print "0x01 mongor.table.find(cond, limit=1).count() COUNT: ", mongor.table.find(cond, limit=1).count()
for i, v in enumerate(mongor.table.find(cond, fields=["types"], limit=1)):
print "\t", i, v
print
print "0x02 mongor.table.find(cond).limit(1).count() COUNT: ", mongor.table.find(cond).limit(1).count()
for i, v in enumerate(mongor.table.find(cond, fields=["types"]).limit(1)):
print "\t", i, v
print
print "0x03 mongor.table.find(cond, limit=1).count(True) COUNT: ", mongor.table.find(cond, limit=1).count(True)
print
print "0x04 mongor.table.find(cond).limit(1).count(True) COUNT: ", mongor.table.find(cond).limit(1).count(True)

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
0x00 len(list(mongor.table.find(cond))) COUNT: 2
0 {u'_id': ObjectId('58fcb2c52d616749494d9cb9'), u'types': u'udp@attack@simple_flood_target'}
1 {u'_id': ObjectId('58fcb0ef2d616749494d98df'), u'types': u'udp@attack@simple_flood_target'}
0x01 mongor.table.find(cond, limit=1).count() COUNT: 2
0 {u'_id': ObjectId('58fcb2c52d616749494d9cb9'), u'types': u'udp@attack@simple_flood_target'}
0x02 mongor.table.find(cond).limit(1).count() COUNT: 2
0 {u'_id': ObjectId('58fcb2c52d616749494d9cb9'), u'types': u'udp@attack@simple_flood_target'}
0x03 mongor.table.find(cond, limit=1).count(True) COUNT: 1
0x04 mongor.table.find(cond).limit(1).count(True) COUNT: 1

可以看到

  • 0x00 的情况,就是针对结果集数据进行在业务层自己算count,一共两条数据
  • 0x01 的情况,limit在find内,对cursor的count不起作用,尽管最终输出一条结果,但是count仍然是2条
  • 0x02 的情况,limit在find外,结果同0x01, limit对count不起作用
  • 0x03 & 0x04, 不管limit在哪,count的时候,执行加上True参数,limit条件就可以起作用了, True参数对应的是applySkipLimit, 是否考虑skip和limit的影响,默认为False,具体参见 文档 cursor.count

因此,我们之前的查询,尽管find的时候设置了limit,但是由于cursor.count(default: False)的存在,仍然相当于把cursor对应的满足条件的所有数据都遍历一遍。假定一个查询条件,库中满足的结果有很多很多,那这个count就带来了慢查,拖慢了整个mongo库。

总结:

  • 尽管count提供了applySkipLimit参数,但是默认为False为败笔,因为count在limit之后,这个默认和前后顺序关系逻辑不一致
  • 如果结果注定是要返回的,最好就直接对结果集算count,不要用curosr.count(),省一次command: count操作

加速正则表达式匹配过程

Posted on 2017-04-05 |

我们有个小系统,要load几十亿域名,支持字符串查询/正则模式匹配等等各种查找方式,以期找到满足模式的域名进行后续分析。

普通的正则过程会比较慢,尤其正则越复杂,性能下降会很严重。当前load数据有50亿到100亿之间,一个复杂正则可能需要耗费几十分钟才能跑完。

想到一个优化方法

  • load数据的过程中,预先计算每个域名的组成,【0-9a-z._-】分别对应到一个bit,一个int64的整数足够
  • 查询时,把正则中固定字符串提取出来,算固定字符串的组成,正则匹配之前,先看要检查的域名的对应的组成int64的位与是否能cover正则表达式的组成int64

域名的组成很少会把所有的字符都用到,对于特定的模式,能有overlap的域名只是一小部分,而上述的位与检查会非常迅速,所以可以大大提高匹配过程的效率。粗略估算,提升在5倍左右,对于正则表达式,如果能提供的固定字符串越多,匹配越快,如果完全不包含固定字符串,那就退化到和之前一样了。

问题来了,python里面 re.compile(pattern, 128)会把一个pattern对应的解析后结果展示出来,re2没有这个接口,我们得自己添加。

re2/re2.h 给 RE2 类添加

1
2
3
4
5
6
7
8
9
10
11
... ...
+class Regexp {
+ public:
+ string Dump();
+};
class RE2{
... ...
+ string RegexpDump() const { return Regexp()->Dump(); }
... ...
};

Dump方法在re2/regex.h是有的,不过没有给出实现。在re2/testing/下有类似的dump实现,不过dump出来的字符串是一坨不好看,我们修改为如下

re2/regexp.cc,添加到文件最后

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
static const char* kOpcodeNames[] = {
"bad ",
"no ",
"emp ",
"CHR ",
"STR ",
"PATTERN ",
"alt ",
"0-ANY ",
"1-ANY ",
"0-1 ",
"REPEAT ",
"SUB",
"dot ",
"byte ",
"bol ",
"eol ",
"wb ", // kRegexpWordBoundary
"nwb ", // kRegexpNoWordBoundary
"BOT ",
"EOT ",
"IN ",
"match ",
};
static void DumpRegexpAppending(Regexp* re, string* s, int level) {
if (re->op() < 0 || re->op() >= arraysize(kOpcodeNames)) {
StringAppendF(s, "op%d", re->op());
} else {
switch (re->op()) {
default:
break;
case kRegexpStar:
case kRegexpPlus:
case kRegexpQuest:
case kRegexpRepeat:
if (re->parse_flags() & Regexp::NonGreedy)
s->append("n");
break;
}
if(! s->empty()){
s->append("\n");
}
for(int i=0; i<level; ++i){
s->append(" ");
}
s->append(kOpcodeNames[re->op()]);
if (re->op() == kRegexpLiteral && (re->parse_flags() & Regexp::FoldCase)) {
Rune r = re->rune();
if ('a' <= r && r <= 'z')
s->append("fold");
}
if (re->op() == kRegexpLiteralString && (re->parse_flags() & Regexp::FoldCase)) {
for (int i = 0; i < re->nrunes(); i++) {
Rune r = re->runes()[i];
if ('a' <= r && r <= 'z') {
s->append("fold");
break;
}
}
}
}
s->append("{");
switch (re->op()) {
default:
break;
case kRegexpEndText:
if (!(re->parse_flags() & Regexp::WasDollar)) {
s->append("\\z");
}
break;
case kRegexpLiteral: {
Rune r = re->rune();
char buf[UTFmax+1];
buf[runetochar(buf, &r)] = 0;
s->append(buf);
break;
}
case kRegexpLiteralString:
for (int i = 0; i < re->nrunes(); i++) {
Rune r = re->runes()[i];
char buf[UTFmax+1];
buf[runetochar(buf, &r)] = 0;
s->append(buf);
}
break;
case kRegexpConcat:
case kRegexpAlternate:
for (int i = 0; i < re->nsub(); i++){
DumpRegexpAppending(re->sub()[i], s, level+1);
}
break;
case kRegexpStar:
case kRegexpPlus:
case kRegexpQuest:
DumpRegexpAppending(re->sub()[0], s, level+1);
break;
case kRegexpCapture:
if (re->name()) {
s->append(*re->name());
s->append(":");
}
DumpRegexpAppending(re->sub()[0], s, level+1);
break;
case kRegexpRepeat:
s->append(StringPrintf("%d,%d ", re->min(), re->max()));
DumpRegexpAppending(re->sub()[0], s, level+1);
break;
case kRegexpCharClass: {
string sep;
for (CharClass::iterator it = re->cc()->begin();
it != re->cc()->end(); ++it) {
RuneRange rr = *it;
s->append(sep);
if (rr.lo == rr.hi)
s->append(StringPrintf("%#x", rr.lo));
else
s->append(StringPrintf("%#x-%#x", rr.lo, rr.hi));
sep = " ";
}
break;
}
}
if((*s)[s->size()-1] == '}'){
s->append("\n");
for(int i=0; i<level; ++i){
s->append(" ");
}
}
s->append("}");
}
string Regexp::Dump() {
string s;
DumpRegexpAppending(this, &s, 0);
return s;
}

这样做个小程序

main.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(int argc, char* argv[])
{
for(int i=1; i<argc; ++i){
auto line = std::string(argv[i]);
re2::RE2 p(line);
if(p.ok()){
std::cout << ">>> " << line << std::endl;
std::string pcc = p.RegexpDump();
std::cout << pcc << std::endl;
}else{
std::cout << ">>> " << line << " Parse error." << std::endl;
}
}
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[xuyang@dev:/secret/path/]$ ./repattern.exe '^hello[0-9]{1,3}\.[abc_.-]{5,6}world$'
>>> ^hello[0-9]{1,3}\.[abc_.-]{5,6}world$
PATTERN {
BOT {}
STR {hello}
REPEAT {1,3
IN {0x30-0x39}
}
CHR {.}
REPEAT {5,6
IN {0x2d-0x2e 0x5f 0x61-0x63}
}
STR {world}
EOT {}
}

上述STR/CHR的部分就是我们关心的“一个正则中固定字符串”的部分,这里是“hello.”,将其映射到一个uint64 for pattern。
正则匹配之前,先通过一个位与运算判断上面的uint64 for pattern是否被包含与uint64 for string, 不被包含的就说明原始字符串肯定不会含有hello.对应的所有字符,也就肯定不可能被正则匹配过程命中,直接忽略掉匹配过程,加速整体匹配性能。

RSD 随机子域名攻击

Posted on 2017-03-25 |

RSD, random sub domain [attack], 也可称为 PRSD, P for Pseudo,因为所谓的随机数生成算法是“伪随机”。

攻击原理

假定我们想攻击example.com,让其不能正常提供服务,我们可以构造大量的虚假子域名请求,比如:

1
2
3
4
5
6
7
aaaaaaa.example.com
aaaaaab.example.com
...
aaaaaaz.example.com
aaaaaba.example.com
...
zzzzzzz.example.com

这些FQDN都是构造出来的,真实业务不存在的域名,因此接收到这些请求的公共DNS服务器, 例如 114.114.114.114,会根据请求开始递归请求过程,将上述请求转到 example.com 的权威NS服务器, 例如 ns.example.com。

以上伪造的子域名模式是[a-z]7个字母的定长组合,一共有 26 ** 7 = 8031810176个,想象一下,这么多伪造请求如果在10分钟内发出去,可能发生的事情:

  • 公共DNS服务器 114.114.114.114 缓存可能被撑爆,可能无法处理正常请求
  • 114.114.114.114 到 ns.example.com 的路由线路中间某节点带宽被占满
  • ns.example.com 处理不了如此多的请求,导致无法处理正常请求
  • 甚或,到达example.com 的流量只有20G,但是所在IDC认为流量太大,强制将example.com下线来自保(没开玩笑)

这些可能只要满足一个,就有相当大部分或者所有人,在一定时间内都无法正常访问 example.com,攻击成功。

上面伪造请求我用了连续字符串空间的子域名来说明,实际攻击中会用各种随机算法来生成定长/变长的,字母/数字/混合的各种子域名来进行攻击,所以被称为 RSD 。

另:DNS 请求是基于 UDP 的,UDP 没法验证来源,也就是说 DNS 的请求是可以伪造来源的,如果攻击者伪造子域名请求的来源 IP 为 ns.example.com 的 IP 或者同 IDC 的 IP 会如何?

攻击现状

天天有,几乎时时刻刻都有。按照我们当前检测到的结果,每天少则几个,多则十几个站点会遭受这种攻击,持续时长从十几分钟到十几天甚或更久都有可能。

随机子域名模式生成基本就两个维度,固定pattern + 随机pattern,随机pattern又被两个因素来决定:长度 + 字符集。长度是固定的还是随机的?长度如果随机,随机范围是多少?字符随机范围是什么?字母还是数字还是混合,甚或是特定的自选字符集?

最常见的前缀模式

  • 两次rand:一次rand选择一个随机长度,后续每次rand选择一个随机字符
  • 固定长度,随机字母/字母+数字
  • 特定模式:比如d(-[0-9]{2,7}){3,3}-d

攻击规模

攻击发生时,基本都是 百万/s 的请求量。看题图,上面这个攻击有意思在于分了几个阶段:

  • 攻击前:5分钟访问量只有几百
  • 攻击阶段1: 5分钟访问量迅速升到百万, 并且持续了8个小时
  • 攻击阶段2: 攻击者发现没有达到预想的效果,攻击升级,5分钟访问量升到了150w,持续了1个小时
  • 攻击暂停: 攻击者可能发现了自己的一个操作错误,暂停了一下
  • 攻击阶段3: 攻击重新开始,此时5分钟访问量稳步升到了200w,并持续中
  • 后续:写这行字时,正在发生。。。

攻击目标

利益驱动,so…

实时检测方法

知道了RSD的原理,做检测更多的是需要两个前提: 有数据 + 工程能力。

数据参照前一篇 《PDNS系统设计实现总结》,我们需要的是3,至少是2的位置。

检测方法:

  • 0x00 请求计数:对所有级别的FQDN做计数,类似trie-tree的结构,域名反转, www.baidu.com 反转为 com.baidu.www,按点分割,记录各级的count。RSD如果发生,父级域名的count必然有一个明显的spike,比如攻击为 xxxxx.www.baidu.com,那www.baidu.com的访问量会有一个明显的上升。这一步不是必须,但之所以要做这个判断是因为后续过程计算会很重,而这一个前置步骤,可以将大部分正常数据放过,省去不必要的计算
  • 0x01 子域名计数:RSD一大特征就是构造很多很多随机子域名,因此统计子域名个数是最重要的特征。可以使用hyperloglog算法,redis中源码写的不错,可以借鉴
  • 0x02 子域名模式判断:子域名多不见得是RSD攻击,还有可能是各种数据通道,子域名爆破等等情况。要判定为RSD,那就必须要对子域名模式进行分析判定。每分钟几十万的子域名,没必要都记录,阶梯抽样缩减到千级别即可。判断构成的随机性,也可以根据已知的模式判断匹配程度,前者可以来检测所有未知的RSD,后者的好处是可以和已知的bot家族关联到一起。随机性的判定,建议使用分词,不要用熵。

防御方法

我们没有防御的责任和位置,因此防御我们没有做,就算有想法,可能有坑,所以暂且不表。

等哪天如果做了,有了被证实的经验,我可能会回来填坑【doge脸】


我们部门小伙伴还专门分析过 Bot Elknot 的RSD的情况,做的很好,感兴趣的可以看看 vb-2016-TheElknotDDoSBotnetsWeWatched.pdf

PDNS系统设计实现总结

Posted on 2017-03-13 |

从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都写不下,任何一个子系统的工程实践,几乎都有意料之外的约束。计算机艺术是妥协的艺术,满足了需求的就是好的。

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

使用[hexo + github + next]来构建个人博客

Posted on 2017-03-11 |

博客已经过时许久,不过我年纪渐长,记性渐差,还是需要个这么个东西来记录。
之前blog是在自己的vps,不过时不时被gfw干掉,操作不便,学习了下当今潮流,发现hexo+github+next半天就可以搞定,而且足够满足我的需求,于是搞起。

  • hexo;一个基于node的静态博客生成发布引擎,简单来说,就是用户只用写markdown的文稿,后续生成页面、渲染、发布的过程都由hexo来搞定的
  • github:实际是github的page,静态博客的载体,hexo会将内容发布到用户自己github的page,然后通过page来查看。如果有自己的域名,也可以cname到自己的github的page,这样就能通过自己的域名来访问。
  • next:只是依赖hexo框架的一个主题样式,比较漂亮,而且还提供根据markdown的标题自动生成outline等辅助功能。

hexo的环境配置

直接贴脚本

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
mkdir your.dir
cd your.dir
npm install hexo-cli --save
npm install hexo --save
hexo init
npm install hexo-generator-index --save
npm install hexo-generator-archive --save
npm install hexo-generator-category --save
npm install hexo-generator-tag --save
npm install hexo-server --save
npm install hexo-deployer-git --save
npm install hexo-deployer-heroku --save
npm install hexo-deployer-rsync --save
npm install hexo-deployer-openshift --save
npm install hexo-renderer-marked@0.2 --save
npm install hexo-renderer-stylus@0.2 --save
npm install hexo-generator-feed@1 --save
npm install hexo-generator-sitemap@1 --save
npm install hexo-util --save
npm install hexo-generator-searchdb --save
git clone https://github.com/iissnan/hexo-theme-next themes/next
cd themes/next
npm install
npm install -g bower
npm install -g grunt-cli
cd -

此时,如果所有安装都没有意外,执行 hexo server 就可以启动http服务器在本机4000端口,有一个默认的hello world的主页面

省略github的配置以及github page的部署过程

省略自己域名 到 github page的映射过程

hexo 配置

两个配置文件:
1, yourdir/_config.yml: 这个是hexo的配置文件,包括使用哪个主题,这里我们使用next,就需要在该配置中设置。同时,还需要在这里配置要同步到的github page的地址。
2, yourdir/themes/next/_config.yml: 这个是主题相关的配置,包括页面布局,第三方评论统计接口等等,看看就明白了
3, 我的配置参见:https://github.com/xuy1202/blog

注意:
hexo deploy 的时候,是从source文件夹生成为public,然后上传到github。而自己的域名绑定到xxx.github.io的时候,xxx.github.io下面第一级必须有一个CNAME文件对应为自己的域名。所以,必须将对应的CNAME文件放到source一份,这样才能保证每次deploy之后,从自己域名转到github的访问是正确的。

C++ Tail Call Optimization

Posted on 2017-03-09 |

一直没有验证过C++对尾递归的优化,同事讨论起来,写个代码验证下

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
#include <iostream>
#include <time.h>
#include <unistd.h>
int tailrecsum(int x, int& running_total)
{
if(x == 0){
return running_total;
}
usleep(100);
// GOOD
running_total += x;
return tailrecsum(x - 1, running_total);
// BAAD
//return x + tailrecsum(x - 1, running_total);
}
int main(void)
{
int r = 0;
std::cout << tailrecsum(10 , r) << std::endl;
std::cout << tailrecsum(100 , r) << std::endl;
std::cout << tailrecsum(1000 , r) << std::endl;
std::cout << tailrecsum(10000 , r) << std::endl;
std::cout << tailrecsum(100000 , r) << std::endl;
std::cout << tailrecsum(2000000000, r) << std::endl;
return 0;
}

上述代码,GOOD的两行就是尾递归模式,BAAD的一行是普通递归的模式
如果把usleep睡眠屏蔽,直接g++编译,执行立马会 “Segmentation fault: 11”
把usleep放开,会观察到内存持续上升,这就是栈调用占用的内存

但是只要开启了 -O1/2/3 优化模式编译,把usleep屏蔽掉,结果会很快返回(当然,结果是错的,溢出成负数)。把usleep放开,可以观察到内存会恒定不变,此时递归调用不会随着调用过程占用额外的内存。

如果是BAAD的代码,-O1/2/3 编译也是没用的。

结论:

  • 编译器能够针对尾递归代码进行执行优化,此时递归调用基本相当于一个循环,前提是要 -O1/2/3 开启编译优化
  • 编译器不会把非尾递归的代码优化成尾递归的效果,绿色低碳程序员请多留心,提高自己姿势水平,代码一小行,烧掉一棵树

FloCon 2017

Posted on 2017-01-15 |

Here is the slide of my talk on FloCon 2017.

DOWNLOAD: Backbone Network DRDoS Attack Monitoring and Analysis.pdf

  • DRDoS accounts for over 60% of all DDoS, hard to track, annoying bandwidth consumption, larger & larger
  • DNS + NTP + CharGEN reflection account for over 77% of all DRDoS events
  • DRDoS amplifiers has been bing used heavily, over 30% of our detected DNS amplifiers are bing used for DRDoS right now
  • DNS reflection using ANY query, NTP reflection using MONLIST command, CharGEN …, all of little practical use
  • Kill top amplifiers’ in-traffic, solve the majority problem, no effect to normal network, hands together, let’s DO it.

Happy time at San Diego.

12
YANG XU

YANG XU

18 posts
31 tags
RSS
Linkedin GitHub Twitter Weibo
Friends Links
  • 0x00 RootKiter | Reverse Engineer/Botnet
© 2015 - God knows - 2019 YANG XU
Powered by Hexo
Theme - NexT.Mist