文章目录
引言
现在大型企业逐渐开始依赖于威胁检测软件来发现可疑行为。这些软件会产生许多告警,网络空间安全分析人会去分析这些告警是否是真正的攻击。不幸的是,在实际使用中,需要处理的告警数量远远多于分析人员的数量。这就导致了一个威胁警报疲劳或者信息过载问题的产生,从而可能会造成漏掉真正的攻击告警。
本文提出的NoDoze联合使用了告警相关的上下文以及历史信息来解决上述问题。NoDoze首先生成一个告警事件的因果依赖图;然后给图中每一个边打一个分,代表这个边的一个异常情况,这个分数和相应边所关联的事件出现的频率有关。然后NoDoze使用一个新的扩散算法,将这个分数通沿着相邻的边进行传播,这最终会生成一个聚合的分数以用来进行分类。在最后的实验结果中,该方法将误报降低了$84\%$。
背景
现在大型企业中的一些威胁检测系统(TDS)的误报警告频繁发生。根据FireEye的一项最近的研究,大多数机构每周会收到17000个告警,其中一半以上的告警都是误报,而且仅有其中的$4\%$被妥善处理了。分析人员被这些告警所淹没,面临着威胁告警疲劳的问题,重要攻击的告警也被淹没在误报警告中。其中一个著名的案例就是2013年Target公司的数据泄露,4000万用户银行卡记录被盗。尽管系统给出了大量的告警,但是由于大量相似的告警已经司空见惯,Target的雇员并没有及时处理这个攻击威胁,安全团队认为这个告警是个误报。
威胁告警疲劳问题至少部分是由于学术界和工业界的TDS使用启发式以及依赖于单个事件来匹配的方法来进行告警造成的。然而在许多情况下,如果仅仅使用单个事件来进行告警时,一些错误的告警和真正的攻击看起来极其相似。数据溯源分析可能是解决这个问题的一种方法。数据溯源可以提供告警的上下文信息,从而构造导致告警时间产生的事件链。有了上下文信息后就更容易区分事件是误报还是真的告警。
虽然基于溯源的方法听起来很有说服力,但是它有两个重要的局限性:
- 劳动力密集型:就目前的技术水平而言,仍然需要安全分析人员手动进行溯源。
- 依赖爆炸问题:由于现代系统的复杂性,目前的溯源追踪技术会包含大量的错误的依赖关系 --- 一个时间会依赖于所有之前发生的事件。如果使用这个构造图,那么图会变的非常庞大,这会导致即使是安全分析人员也很难去进行分析。
本文提出了一种基于溯源图分析的自动告警分类与调查系统 --- NoDoze。NoDoze解决了上述提到的两个问题,从而可以帮助安全专家更好地理解攻击、快速发现漏洞、加速意外事件的响应。
威胁模型与设定
攻击目标
攻击者的目标是泄漏敏感数据、篡改系统数据或者将数据移动到网络中其它主机上去。
攻击者能力
攻击者可以在目标系统中安装恶意软件,可以利用现有正在运行的进程,可以注入后门。
系统设定
- 攻击者无法修改或删除我们构建溯源图所用的事件记录(比如日志);
- 不考虑攻击者使用不经过系统调用或者溯源跟踪模块无法捕捉的一些方法来进行攻击;
- 不跟踪利用内核漏洞进行的攻击。
TDS设定
- TDS检测率是完备的,即真正攻击的威胁总是可以被检测到;
- 如果攻击事件真的发生,那么在告警出现时,至少存在一个事件(告警事件的前置事件或者后续事件)可以证明这个告警关联着一个真的攻击。也就是不考虑那种看起来所有事件都正常但是实际是攻击的攻击类型。
NoDoze 详解
NoDoze的设计认为:溯源图中每个事件的可疑程度应该与相邻事件的可疑程度进行调整,一个由可疑进程创建的进程应该比正常进程创建的进程更加可疑。基于此认识,本文提出的打分方法是一种无需训练的无监督方法。为此,NoDoze构建了一个事件频率数据库,用来存储所有已经发生的事件,用来帮助打分。
为了解决依赖爆炸问题,文中提出了行为执行划分的概念,基本思想是:将一个程序根据正常和异常行为进行划分,然后生成一个真实告警最为异常的依赖图。这样安全分析人员就可以将精力用在最异常的事件上,从而加速了调查过程。
NoDoze总体工作流程如上图所示。威胁检测模块检测到告警之后发送给NoDoze进行进一步分析。NoDoze根据对各个告警所得的异常分对告警进行排序,并给出精确的告警事件依赖图,用于进一步调查分析。
下面我们详细介绍NoDoze。
定义
依赖事件
操作系统级别的系统日志用到两类实体:主体和客体。主体指的是运行的进程,可以指的是文件、socket连接、IPC等等。依赖(因果)事件$\varepsilon$定义为一个三元组$ $,其中$SRC \in \text{ {process} }$实体发起信息流,$DST \in \text{ {process, file, socket} }$接收信息流,$REL$代表关系。事件依赖关系取值如下表所示:
依赖路径
一个依赖事件$\varepsilon_a$的依赖路径$P$表示导致$\varepsilon_a$发生以及$\varepsilon_a$引起的事件链,$P := \{ \epsilon_1, \epsilon_i, \ldots, \epsilon_a, \ldots, \epsilon_n \}$,$P$长度为$n$。每一个依赖事件可以有多个依赖路径,每一个路径都代表了一个经过$\varepsilon_a$的信息流。我们将依赖路径分成两类:
- 控制依赖路径(CD),$P_{CD} = {\epsilon_1, \epsilon_2, \ldots, \epsilon_n}$,满足$\forall REL \in \{Pro\_Start, Pro\_End\}$。
- 数据依赖路径(DD),$P_{DD} = \{\epsilon_1, \epsilon_2, \ldots, \epsilon_n \}$,满足$\forall REL \in \{ Pro\_Start, Pro\_End \}$。
依赖图
一个事件所有的依赖路径组合在一个图中就组成了一个事件的依赖图。
真告警依赖图 (True Alert Dependency Graph)
真告警依赖图指的是仅包含最异常的依赖路径的图。相比于完整的依赖图来说,真告警依赖图更加精确,并且可以加速攻击的调查进程。
问题描述
给定一个告警事件的列表$\epsilon_1, \epsilon_2, \ldots, \epsilon_n$以及两个用户指定的阈值$\tau_l, \tau_d$,我们想要
- 通过事件的异常得分来对这些告警事件进行排序以排除那些异常得分小于$\tau_d$的,并将其分类为误报。
- 生成真告警依赖图,满足依赖路径路径长度不超过$\tau_l$。
下面,我们介绍如何打分以及如何生成真告警依赖图。
异常分值计算算法描述
异常分值是用来衡量一个事件在一条依赖路径中的异常程度。一种简单的打分方法就是:出现频率低的事件的异常值较高。但是有很多攻击事件会依赖一些经常发生的事件,比如解压。因此,我们的目标是定义一个不仅仅依赖于单个事件而是依赖于整个依赖路径的异常值。下面我们给出如何根据整条路径来计算一条依赖路径的异常值。算法python伪代码如下:
def get_path_anomaly_score(alert_event, max_path_len):
"""获取告警事件所以依赖路径及其异常分值
输入:
alert_event: 告警事件
max_path_len: 路径最大深度
输出:
依赖路径及其异常分值对的列表
"""
output = []
# 1. 获取告警事件的依赖图
graph = get_dependency_graph(alert_event)
src = alert_event.SRC
dst = alert_event.DST
# 2. 使用深度优先搜索,获取事件的上下游路径
back_paths = dfs_traversal_backward(graph, src, max_path_len)
forward_paths = dfs_traversal_forward(graph, dst, max_path_len)
# 3. 合并上下游路径
total_path_list = combine_paths(back_paths, forward_paths)
# 4. 获取事件的转移概率矩阵
transition_matrix = get_transition_matrix(graph)
# 5. 根据转移概率矩阵计算每条路径的异常值
for path in total_path_list:
anomaly_score = calculate_score(path, transition_matrix)
output.append((path, anomaly_score))
return output
从上面的代码中,我们可以看出算法主要分为5个步骤,下面我们介绍下步骤4和5的计算方法。
概率转移矩阵算法
转移矩阵为一个$N \times N$的矩阵(记为$M$),$N$为$graph$中节点的数量(节点记为 $V_1, V_2, \ldots, V_N$),那么一个从节点$V_i$到节点$V_j$的事件$\epsilon = (V_i, V_j, relation)$的转移概率的计算方法为:
分子为事件$\epsilon$在特定时间窗口内发生的次数,分母为从$V_i$节点发出到任意一个节点的$relation$事件事件发生的次数($*$表示可以匹配任何节点)。如果不存在从节点$V_i$到$V_j$的事件,那么$M[i, j] = 0$。
根据转移矩阵计算异常分值
定义一个长度为$l$的路径为:$P = (\varepsilon_1, \varepsilon_2, \ldots, \varepsilon_l)$,其异常值计算公式如下:
其中$\varepsilon_i.SRC$表示$\varepsilon_i$事件的$SRC$节点,其中$\varepsilon_i.DST$表示$\varepsilon_i$事件的$DST$节点,$M(\varepsilon_i)$表示转移概率。$IN$和$OUT$是两个维度为$N$的向量。这两个向量为节点在作为$DST$($IN$)和$SRC$($OUT$)时的两种度量。
进程(程序)节点$v$的度量方式如下:
其中,$|T|$为事件窗口的数量,$|T_{to}^{\prime}|$为$v$作为目的节点时,稳定窗口的数量;$|T_{from}^{\prime}|$为$v$作为源节点时,稳定窗口的数量;时间窗口大小$m$为一个超参数,文中取值为24小时(也就是一天)。稳定窗口的定义为:如果在一个事件窗口内没有以节点$v$为目的节点($to$)或者没有以节点$v$为源节点($from$)的事件$\varepsilon$出现,那么我们称这个时间窗口是稳定的。
举例来说:一个节点如果5天时间内,即没有调用任何进程,也没有被任何进程调用,那么显然其非常稳定,其$IN$和$OUT$的得分都为$\frac{5}{5} = 1$。
对于数据节点(文件)其度量方式为:
- 临时文件(只写不读),这种文件基本没有啥威胁,因此给一个较大的值,表示其很稳定
- 可执行文件,这种在攻击中经常被用到,因此给一个较小值
- 已知的一些恶意的扩展文件,十分危险,给一个较小值
对于$socket$连接的节点,文中通过查询一个恶意IP的在线数据库,给恶意IP一个较低的分数。
异常分值的计算还有一个问题:从异常分值计算公式我们可以看出,路径越长会导致$\prod_{i}^{l}$计算所得值越小,从而导致异常值越大。因此文中还为各个长度的路径分别设置了一个衰减常数,用来对不同长度的异常分值进行归一化,具体大家可以参考论文。记长度为$l$的路径$P$的衰减常数为$\alpha$,那么计算公式变为:
至此,我们已经知道如何计算一个告警事件的异常分值(结果包含多个依赖路径的分值)。显然,如果告警事件所在的所有依赖路径的异常分值都很小,那么就可以将该告警归类为误报。
阈值选择
确定一个合适的异常分值的阈值作为区分误报与真正的攻击,需要根据实际情况来确定。也就是说,需要有实际TDS的告警及误报数据集进行训练才能得到。
真告警依赖图生成
一个攻击可能包含很多个攻击路径,那么我们的真告警依赖图就是为了能够将那些与攻击确实相关的攻击路径合并,而将无关的依赖路径去掉,以降低分析人员分析的工作量。
真告警依赖图的生成基于这样一个发现:良性的路径的异常得分与真的异常路径的异常得分通常会相差一个数量级。假设我们将一个告警事件$\varepsilon$的依赖路径按照异常分值从大到小排列:
如果$k$为异常路径和正常路径的分割点($P_k为异常,P_{k+1}为正常$),那么$S_{k+1} - S_k$的这个值一定较大,而$S_{i} - S_{i-1}, (i\lt k)$的值较小。
这样我们只需要确定一个阈值$\tau_m$,并且将路径按照异常值排序,然后找到满足$S_{k+1} - S_k > \tau_m$的那个$k$值,然后将$P_1, P_2, \ldots, P_k$合并到一个图中,作为我们最终的真告警依赖图即可。$\tau_m$可根据实际的实验数据取值。
实验与验证
论文中的想法在NEC实验室(美国)进行了验证工作。此处,我们给出一个简化告警依赖图的实验结果,详细结果可以参考论文。
其中$(a)$为由传统工具生成的依赖图,而$(b)$为NoDoze生成的精确的依赖图,显然NoDoze生成的简化了很多。
引用
[1] Hassan, Wajih Ul, et al. "Nodoze: Combatting threat alert fatigue with automated provenance triage." Network and Distributed Systems Security Symposium. 2019.
更多推荐