文章目录
介绍
MulVAL是一个可以描述多主机、多阶段的基于逻辑推理的攻击图生成工具
官方网页
主要特点和功能:
- 多视图分析:MulVAL使用多视图方法,可以同时分析网络拓扑、系统配置和访问控制策略等多方面,以识别潜在的威胁和攻击路径。
- 自定义规则:可以定义自己的安全规则和策略,以适应其特定场景。
- 威胁建模:基于攻击图对网络攻击建模,可以帮助用户了解各种攻击威胁如何传播和影响后果。
- 漏洞分析:可以帮助用户识别网络和系统中的漏洞,以及这些漏洞可能被利用的方式。
- 可视化:MulVAL提供可视化工具,以便用户更容易地理解网络和系统的安全状况。
架构流程
核心文件
MulVAL目录:
├─bin/(src目录下功能文件编译后存放该目录下)
├─adapter/
├─metrics/
├─doc/
├─kb/(默认规则目录)
├─interaction_rules.P
├─interaction_rules_with_metrics.P
├─interaction_rules_with_metrics_artifacts.P
├─lib/(存放库文件)
├─libmulval.P
├─dom4j-1.6.1.jar
├─jaxen-1.1.1.jar
├─mysql-connector-java-5.1.8-bin.jar
├─src/(部分核心功能文件)
├─adapter/(一些java编写功能脚本,用于初始化数据库,连接数据库,获取漏洞信息等)
├─analyzer/(用于XSB推理的Prolog功能函数)
├─attack_graph/(绘制攻击图脚本文件)
├─attack_graph.cpp
├─attack_graph.h
├─Queue.h
├─graphit.l
├─graphit.y
├─metrics/(用于计算节点概率值)
├─independentAlgoSumm.java
├─node.java
├─testcases/(测试案例)
├─utils/(部分调用脚本,功能脚本)
├─compute_metrics.sh
├─dom.py(查找节点间支配与后支配关系)
├─graph_gen.sh(启动脚本)
├─load_policy.sh
├─render.sh(生成可视化攻击图)
├─riskAssess.sh
├─runRiskAssess.sh
├─trim.py(对图数据处理修剪)
├─risk_assessment.py
LICENSE
Makefile
README
技术细节
MulVAL的输入
将收集到的主机信息、漏洞信息、安全策略、网络配置等信息转换成相应谓词形式
输入的谓词predicate分为三类primitive(初始)、meta(元)、derived(派生)
例:
漏洞信息(primitive)
vulExists(_host, _vulID, _program).
//_host 主机/服务器上的 _program 存在漏洞,编号为 _vulID
漏洞影响(primitive)
vulProperty(_vulID, _range, _consequence)
//编号为 _vulID 的漏洞,利用方式为 _range ,影响后果为 _consequence
主机配置(primitive)
networkServiceInfo(_host, _program, _protocol, _port, _user)
//程序 _program 以用户权限 _user 在 _host 上运行,使用协议 _protocol,侦听端口 _port。
部分工具自带的三种类型谓词
primitive(clientProgram(_host, _programname)).
primitive(vulExists(_host, _vulID, _program)).
primitive(vulProperty(_vulID, _range, _consequence)).
primitive(hacl(_src, _dst, _prot, _port)).
primitive(networkServiceInfo(_host, _program, _protocol, _port, _perm)).
derived(execCode(_host, _perm)).
derived(netAccess(_machine,_protocol,_port)).
derived(canAccessHost(_host)).
derived(accessFile(_machine,_access,_filepath)).
meta(attackGoal(_)).
meta(cvss(_vulID, _ac)).
交互行为表示
对于derived类型的谓词,需要制定对应的交互规则(rule),将其编写为 Horn子句,其中第一行是结论,其余行是启用条件,例:
execCode(H, Perm) :-
vulExists(H, _VulID, Software, remoteExploit, privEscalation),
networkServiceInfo(H, Software, Protocol, Port, Perm),
netAccess(H, Protocol, Port)),
rule_desc('remote exploit of a server program',1.0)).
/**如果在主机 H 上运行的程序 Software,存在一个可远程利用(remoteExploit)的漏洞(VulID),
该漏洞的影响是权限提升(privEscalation),并且该程序 Software 在权限 Perm 下使用协议 Protocol 并且侦听端口 Port,
通过网络连接netAccess,则攻击者可以以权限 Perm 在机器 Host 上执行任意代码(execCode(Attacker, Host, Priv))。
此规则可应用于任何与模式匹配的漏洞。**/
由XSB推理环境根据输入的谓词文件以及定义的交互规则推理生成出新的derived谓词
这些派生出来新的谓词既可以作为最终的攻击表示,也可以用作其他交互规则的启用条件
graph_gen.sh:启动XSB环境进行推理
#创建XSB运行脚本
#这是一个Here文档(Here Document)的开始。
#它允许在脚本中嵌入多行文本,直到遇到结束标记 EOF 为止。
#所以整个 run.P 文件的内容将在 EOF 处结束。
cat > run.P <<EOF
:-['$MULVALROOT/lib/libmulval']. 导入Prolog库文件
:-['$MULVALROOT/src/analyzer/translate'].
:-['$MULVALROOT/src/analyzer/attack_trace'].
:-['$MULVALROOT/src/analyzer/auxiliary'].
:-dynamic meta/1. 创建动态事实
:-load_dyn('running_rules.P'). 加载规则
:-load_dyn('$INPUT'). 加载输入文件
:-assert(traceMode($trace_option)). 使用 assert 谓词在Prolog中插入一个 traceMode 事实,其值取自 $trace_option 变量。
EOF
#如果设置了 dynamic_file,则加载该文件,并执行 apply_dynamic_changes。
if test -n "$dynamic_file"; then
cat >> run.P <<EOF
:-load_dyn('$dynamic_file').
:-apply_dynamic_changes.
EOF
fi
#如果设置了 TRIM,加载相应的Trim模块,并执行与Trim相关的操作
if test -n "$TRIM"; then
cat >> run.P <<EOF
:-load_dyn('$MULVALROOT/src/analyzer/advances_trim.P').
:-tell('edges').
:-writeEdges.
:-told.
:-shell('rm -f dominators.P').
:-shell('dom.py edges dominators.P').
:-loadDominators('dominators.P').
EOF
else
cat >> run.P <<EOF
:-load_dyn('$MULVALROOT/src/analyzer/advances_notrim.P').
EOF
fi
#如果未设置 CVSS,插入一个 cvss(_, none) 事实
if test -z "$CVSS"; then
cat >> run.P <<EOF
:-assert(cvss(_, none)).
EOF
fi
#如果设置了 goal,插入一个 attackGoal 事实。
if test -n "$goal"; then
cat >> run.P <<EOF
:- assert(attackGoal($goal)).
EOF
fi
cat run.P > environment.P
#启动XSB Prolog系统,将其标准错误输出(2>)和标准输出(1>&2)重定向到名为 xsb_log.txt 的文件中,并使用Here文档传递Prolog脚本
xsb 2>xsb_log.txt 1>&2 <<EOF
[environment]. XSB环境中加载 environment.P 文件
tell('goals.txt'). 创建
writeln('Goal:').
iterate(attackGoal(G), 输出文件中列出攻击目标。
(write(' '), write_canonical(G), nl)).
told. 关闭输出文件。
EOF
cat goals.txt; rm goals.txt #读取并删除
cat >> run.P <<EOF
:-mulval_run.
EOF
#该条规则在调用的libmulval.P中,实现在XSB Prolog环境中运行攻击图生成过程。
#“:-” 用于表示执行一个查询或目标,
#mulval_run :-
#mulval_preprocess,
#writeln('Running attack simulation...'),
#attack_simulation_trace('trace_output.P'),
#mulval_postprocess.
# 在XSB执行正在运行的脚本
#[run]. 是一种在Prolog中运行脚本的方式
#在run.P文件中,已经定义了一系列的规则和查询,用于执行攻击图生成操作。
#执行[run].时,XSB环境会加载 run.P 文件并开始执行其中的规则和查询,这些规则和查询将调用其他规则,递归地生成攻击图,计算攻击目标等。
xsb 2>xsb_log.txt 1>&2 <<EOF
[run].
EOF
XSB推理原理
XSB是逻辑编程系统,使用基于逻辑的语言Prolog。
在逻辑编程中,程序由一组事实(facts)和规则(rules)组成,这些规则描述了问题的逻辑关系和约束。
程序中的查询会被推理引擎自动解释和求解,从而得出答案。
程序通常表示为Horn子句的形式,即:
A :- B, C, D 当B,C,D三种事实都为True时,即可得出A为True
graph_gen.sh:调用攻击图生成
mulval_run :-
mulval_preprocess,
writeln('Running attack simulation...'),
attack_simulation_trace('trace_output.P'),
mulval_postprocess.
由该条规则,XSB执行推理后会生成trace_output.P文件,检查是否存在后会开始生成攻击图
if [ -f trace_output.P ]; then #检查trace_output.P是否存在
if [ -f metric.P ]; then #检查metric.P
cat metric.P >> trace_output.P #追加内容到trace_output
fi
根据 $ATTACK_GRAPH_OPTS 中的选项(使用工具时输入)和输入文件 trace_output.P,将生成的攻击图输出到名为 AttackGraph.txt 的文件中。
#执行攻击图生成。
$MULVALROOT/bin/attack_graph $ATTACK_GRAPH_OPTS trace_output.P > AttackGraph.txt
trace_output.P部分内容
primitive(mitm,3).
primitive(noCheckAuth,3).
primitive(isDomainMember,3).
derived(execCode,2).
derived(netAccess,3).
derived(lanAccess,1).
meta(cvss,2).
meta(attackGoal,1).
attack(execCode(webServer,apache)).
possible_duplicate_trace_step(because(12,rule_desc('first access on the same LAN',1.0),netAccess(workstation,tcp,445),[vlanInterface(workstation,homeNetwork),attackerLocated(homeNetwork)])).
possible_duplicate_trace_step(because(0,rule_desc('remote exploit of a server program',1.0),execCode(workstation,root),[netAccess(workstation,tcp,445),networkServiceInfo(workstation,_h4499,smb,tcp,445,root),vulExists(workstation,'CVE-2020-0796',smb,remoteExploit,privEscalation)])).
possible_duplicate_trace_step(because(0,rule_desc('remote exploit of a server program',1.0),execCode(workstation,root),[netAccess(workstation,tcp,445),networkServiceInfo(workstation,_h4046,smb,tcp,445,root),vulExists(workstation,'CVE-2020-0796',smb,remoteExploit,privEscalation)])).
possible_duplicate_trace_step(because(10,rule_desc('multi-hop access',1.0),netAccess(webServer,tcp,80),[vlanInterface(webServer,serviceLAN),vlanInterface(workstation,userLAN),firewallRule(workstation,userLAN,webServer,serviceLAN,tcp,80),execCode(workstation,root)])).
possible_duplicate_trace_step(because(8,rule_desc('LAN access',1.0),lanAccess(homeNetwork),[vlanInterface(workstation,homeNetwork),execCode(workstation,root)])).
possible_duplicate_trace_step(because(8,rule_desc('LAN access',1.0),lanAccess(userLAN),[vlanInterface(workstation,userLAN),execCode(workstation,root)])).
/bin/attack_graph 该文件由 src/attack_graph/下的attack_graph.cpp、attack_graph.h、Queue.h、graphit.l、graphit.y共同编译而成。
- attack_graph.cpp为主文件,attack_graph.h和Queue.h为头文件,提供部分功能函数。
- graphit.l、graphit.y主要用于处理输入文件
- graphit.l用于将输入文本分解成标记,然后将这些标记传递给与之相关的Bison规则文件进行语法分析。它的目的是将输入文本中的各个元素(如关键字、标识符、数值等)转化为一系列标识符,以便进行后续语法分析。
- graphit.y 文件的主要功能是定义输入文件的语法结构,并将不同元素关联到相应的数据结构中。它是与词法分析器(如graphit.l)协同工作的一部分,用于完成整个编译过程。
attack_graph:main()
int main(int argc, char *argv[] )
{
if (argc < 2){
cout << "Usage attack_graph trace_file.\n";
return -1;
}
else{
process_args(argc, argv);
}
// 解析输入, 填充 facts, traceSteps and ruleList objects
#ifdef LINUX
yyin = fopen( tracefile_name,"r");
if (yyin == NULL) {
cout << "Cannot open trace file " << tracefile_name << endl;
return -1;
}
#else
*my_ptr = fopen( tracefile_name,"r");
if (*my_ptr == NULL) {
cout << "Cannot open trace file " << tracefile_name << endl;
return -1;
}
#endif
if (yyparse() != 0){
cerr << "Error in parsing trace_output.P" << endl;
return -1;
}
//如果没有攻击目标,打印找不到攻击路径
if (data.goals.size() == 0){
cerr << "No attack paths found.\n";
return 1;
}
if (build_graph())
return -1;
//调用build_visual()函数根据参数arc_and_node可视化攻击图
if (build_visual(arc_and_node))
return -1;
// If SAT-solver option selected and valid attack graph has been generated, write to files
//如果选择了SAT-solver选项并且生成了有效的攻击图,写入文件
//对于攻击图分析,SAT求解器通常用于验证攻击路径是否可行,即检查是否存在一种攻击方式,使得一组条件都满足。
//如果SAT求解器能够找到一组满足条件的变量赋值,那么攻击路径是可行的
if (buildCNF) {
cerr << "Convert graph nodes into CNF clauses, then write to clauses.cnf" << endl;
build_cnf();
}
return 0;
}
attack_graph:process_args()
- 调用process_args函数检查处理命令行参数并打开输入文件,例如是否输出节点和边的列表、是否只输出简单路径、是否运行测试模式、是否构建CNF表示等
void process_args(int argc, char *argv[]){
for (int i=1; i < argc; i++){
if (*argv[i] == '-'){
if (!strcmp(argv[i], "-l")){
arc_and_node = true;
}
else if (!strcmp(argv[i], "--arcNum")){
arc_mode = NUMBER;
}
else if (!strcmp(argv[i], "--arcMetric")){
arc_mode = METRICMODE;
}
else if(!strcmp(argv[i], "-h")){
print_usage();
}
else if(!strcmp(argv[i], "-s")){
buildCNF = true;
}
...
...
...
- 然后调用yyparse/yyin(linux)函数解析(由graphit.y graphit.l提供)有关facts、traceSteps、ruleList信息的输入文件并储存。
attack_graph:build_graph()
- 调用build_graph()函数构建攻击图,利用队列,循环遍历所有tracestep,根据谓词和事件关系创建相应的节点以及它们之间的边。
int build_graph(void)
{
// 循环遍历所有唯一的traceStep
traceStepMap::iterator i,j;
traceStepMap *Map;
Map = &data.all_trace_steps.traceSteps;
for( i=Map->begin(); i != Map->end(); )
{
string ts_key = i->first;
TraceStep *ts = i->second;
int num = ts->ruleNum;
Conjunct *c = ts->conjunct;//合取项
Fact *f = ts->fact;
float metric = ts->metric;
//释放TraceStep对象所占用的内存
delete ts;
j=i;
i++;
Map->erase( j );
创建一个fact_key用来获取事实节点的key属性
创建一个orNode的指针,指向OrNode类型对象,接受两个参数 事实节点的key和事实节点的label。
创建了一个andNode 指向 AndNode 类型对象的指针。并且将其添加到nodeList中
string fact_key = f->key;
OrNode *orNode = data.all_or_nodes.addOrNode(fact_key, f);//addOrNode(string &key, Fact *label);
AndNode *andNode = new AndNode(num, metric);//AndNode(int rulenum, float metric)
if( andNode == NULL || orNode == NULL) {
cerr << "Failed to create new node\n";
return -1;
}
data.all_and_nodes.nodeList.add( *andNode );
graph_data::nodeCount++;
接着将推理规则和推断节点之间建立边
andNode->nodeNum = graph_data::nodeCount;//将新创建的 AndNode 的节点编号设置为当前节点计数器的值。
andNode->parentNodeNum = orNode->nodeNum;//将当前 AndNode(规则)的父节点编号设置为关联的 OrNode(推断节点)的节点编号。
orNode->outGoing.add(*(new Arc(orNode, andNode)));//连边
for( Fact *fa= c->factList.gethead(); fa >0; fa = c->factList.getnext()) {//取事实
fact_key = fa->key;
Node *newNode;
Type factType = fa->predicate->type; //取事实的类型
if( factType == primitive) {//判断原始还是推断 创建节点 分类
newNode = data.all_leaf_nodes.addLeafNode(fact_key, fa);
}
else if( factType == derived) {
newNode = data.all_or_nodes.addOrNode(fact_key, fa);
}
if (factType == primitive || factType == derived){
andNode->outGoing.add(*(new Arc(andNode, newNode)));
//将新创建的节点(newNode)与当前的 AndNode 相连接,表示从当前的 AndNode 到新节点的边。
}
}
// 释放合取式c的空间
delete c;
}
攻击路径的终点为攻击目标goal,将goal作为头节点 反向处理
//为头节点添加数据
NodeMap::iterator k;
for (k = data.goals.begin(); k != data.goals.end(); k++) {//遍历数据结构 data.goals 中的所有攻击目标 data.goals为map映射
string fact_key = k->first;//获取事实的key
Node *headNode = data.all_or_nodes.nodes[fact_key];//OrNode推断结点中找
if (headNode != NULL){
data.goals[fact_key] = headNode;
}
else{
cerr << "Warning: attack goal "<<fact_key<<" was not computed."<<endl;
}
}
因为headNode以 Node *headNode = data.all_or_nodes.nodes[fact_key] 创建,故调用OrNode对应的函数
//执行修剪,删除非最短路径或非必要
switch(prune_option){
case noPrune:
break;
case nonSimple:
for (k = data.goals.begin(); k != data.goals.end(); k++) {
Node *headNode = k->second;
if (headNode != NULL){
headNode->allSimplePaths();
}
}
for (k = data.goals.begin(); k != data.goals.end(); k++) {
Node *headNode = k->second;
if (headNode != NULL){
headNode->pruneUselessEdges();
}
}
default:
break;
}
//修剪后重新分配节点编号
currentCounter++;
currentNodeNum=1;
currentArcNum = 1;
for (k = data.goals.begin(); k != data.goals.end(); k++) {
Node *headNode = k->second;
if (headNode != NULL){
headNode->dfs(reAssignNodeNum);
}
}
allSimplePaths()查找最短简单路径长度
查找从当前 OrNode 到攻击目标的所有简单路径中的最短路径长度。
如果节点已经在路径中(inPath 标志为 true),函数会返回 -1,表示存在循环。
函数首先将当前节点标记为在路径中,并初始化最短路径长度为 -1。
然后,它递归调用所有子节点的 allSimplePaths 函数,以查找从子节点到目标的最短路径长度。
如果子节点的路径长度大于等于 0,表示存在简单路径,函数会记录路径长度并更新最短路径长度。
最后,函数将当前节点标记为不在路径中,并返回最短路径长度。
int OrNode::allSimplePaths()
{
if (inPath){
return -1;
}
// 扩展DFS路径
inPath = true;
int shortestLength = -1;
// 递归调用所有子进程
for (Arc *arc=outGoing.gethead(); arc != NULL; arc=outGoing.getnext()) {
// 如果存在简单路径 返回该路径长度
int length = arc->getDst()->allSimplePaths();
if (length >= 0) {
if (arc->weight < 0 || length + 1 < arc->weight){
arc->weight = length + 1;
}
if (shortestLength < 0 || length + 1 < shortestLength){
shortestLength = length + 1;
}
}
}
inPath = false;
return shortestLength;
}
pruneUselessEdges()修剪无用边
剪除 OrNode 节点相连的无用边,以减少无效路径。
如果节点已经处理过(pruned 标志为 Useless),函数会直接返回。
函数首先将当前节点标记为已处理,然后遍历与当前节点关联的出边。
对于每个出边,如果边的权重小于 0,函数会从出边列表中删除该边。
否则,函数递归地调用子节点的 pruneUselessEdges 函数,以处理子节点的无用边。
void OrNode::pruneUselessEdges()
{
// 如果已经处理过该节点 返回
if (pruned == Useless){
return;
}
pruned = Useless;
QueueItem<Arc> *arcItemNext = NULL;
for (QueueItem<Arc> *arcItem = outGoing.getheadQitem();
arcItem != NULL ;
arcItem = arcItemNext) {
arcItemNext = outGoing.getnextQitem(arcItem);
Arc *arc = outGoing.getitem(arcItem);
if (arc->weight < 0) {
outGoing.remove(arcItem);
}
else{
arc->getDst()->pruneUselessEdges();
}
}
}
为AssetRank分配metrics
if (useMetrics){
cerr << "Computing metrics..." << endl;
for (k = data.goals.begin(); k != data.goals.end(); k++) {
Node *headNode = k->second;
if (headNode != NULL){
headNode->bestMetric();
}
}
}
return 0;
}
bestMetric()计算节点最佳度量值
查找从当前OrNode节点开始的所有简单路径中的最佳度量值。
如果节点已经在路径中(inPath 标志为 true),函数会返回 -1,表示存在循环。
如果节点的度量值已经计算过(nodeMetric >= 0),则直接返回存储的值。
函数首先将当前节点标记为在路径中,并初始化最佳度量值为 -1。
然后,它递归调用所有子节点的 bestMetric 函数,以查找从子节点到目标的最佳度量值。
如果子节点的度量值大于等于 0,函数会记录该值,并与当前边的度量值进行比较,保留较大的度量值。
最后,函数将当前节点标记为不在路径中,并返回最佳度量值。
float OrNode::bestMetric()
{
if (inPath){
return -1;
}
// 如果节点的度量已经计算过,则返回存储的值。
if (nodeMetric >= 0){
return nodeMetric;
}
// 扩展DFS路径
inPath = true;
float bestMetric = -1;
// 递归调用所有子项
for (Arc *arc=outGoing.gethead(); arc != NULL; arc=outGoing.getnext()) {
// 如果存在度量,记录。
float metric = arc->getDst()->bestMetric();
if (metric >= 0) {
if (arc->metric < 0 || betterMetric(metric, arc->metric)){
arc->metric = metric;
}
if (bestMetric < 0 || betterMetric(metric, bestMetric)){
bestMetric = metric;
}
}
}
inPath = false;
nodeMetric = bestMetric;
return bestMetric;
}
攻击图节点处理
节点名称对应:OrNode-推断节点 AndNode-推理规则节点 LeafNode-事实节点
对于三种类型节点的处理有细微差别
OrNode 推断节点:
bool WellFounded(int level);
//检查节点是否是良好状态,表示节点是否可达且没有未建立攻击路径。
void RemoveUnfoundedEdges();
//移除不良状态的节点的边
int allSimplePaths();
//计算到达节点的最短攻击路径的长度。
float bestMetric();
//获取到达节点的最佳度量值,用于评估攻击图的不同路径。
void pruneUselessEdges();
//修剪不必要的边。
int CountAndNodes();
//计算AndNode数。
void dfs(dfsAlgorithm alg);
//执行深度优先搜索算法,根据传入的 alg 参数进行不同的深度优先搜索操作。
int ReAssignNodeNum(int nodeNum);
//重新分配节点的编号。
void Render(renderMode mode, int indent);
//渲染节点,根据指定的渲染模式和缩进输出。
bool Render2(arcLabelMode mode);
//渲染另一种模式。返回布尔值。
void outputVertex(string description, float metric);
//输出节点的描述信息和度量值。
int TransformToCNF(int parent);
//转换为 CNF 形式。
AndNode推理规则节点:
float getMetric() {return metric;}
bool WellFounded(int level);
void RemoveUnfoundedEdges();
int allSimplePaths();
float bestMetric();
void pruneUselessEdges();
int CountAndNodes();
void dfs(dfsAlgorithm alg);
void Render(renderMode mode, int indent);
bool Render2(arcLabelMode mode);
void outputVertex(string description, float metric);
int TransformToCNF(int parent);
LeafNode事实节点:
bool WellFounded(int level);
void RemoveUnfoundedEdges();
int allSimplePaths();
float bestMetric();
void pruneUselessEdges();
void dfs(dfsAlgorithm alg);
void Render(renderMode mode, int indent);
bool Render2(arcLabelMode mode);
void outputVertex(string description, float metric);
int TransformToCNF(int parent);
attack_graph:build_visual()
- 调用build_visual函数,该函数根据(arc_and_node)参数选择不同的可视化输出方式。如果 (arc_and_node)为真,调用Render2()和outputVertex()函数输出节点和边信息。
int build_visual(bool arc_and_node)//arc_and_node为调用时接收的参数值 -l
{
NodeMap::iterator k;
for (k = data.goals.begin(); k != data.goals.end(); k++) {
string fact_key = k->first;
Node *headNode = k->second;
if (headNode != NULL){
if (arc_and_node){
//cout << "0," << headNode->nodeNum << ",1" << endl;
headNode->Render2(arc_mode);//根据输入参数选择渲染类型
//--arcNum arc_mode = NUMBER;
//--arcMetric arc_mode = METRICMODE;
}
else{
// 渲染图,并且使用 0 表示起始缩进。
headNode->Render(TEXT, 0);//文本形式渲染
cout << endl;
}
}
}
return 0;
}
Render(renderMode mode, int indent)
此函数用于渲染(输出)攻击图中的 OrNode 节点,根据给定的 renderMode 和缩进级别 indent 进行格式化输出。
如果节点已经被渲染过(rendered 标志为 true),调用draw_a_link()函数输出。
否则,标记节点为已渲染,调用输出节点的标签信息,包括标签的类型(label)和与该节点相连的出边数量。
然后,遍历所有与当前节点关联的出边,并递归调用子节点的 Render 函数,将 renderMode 和递增后的缩进级别传递给子节点。
void OrNode::Render(renderMode mode, int indent)
{
if(rendered) {
draw_a_link(mode, indent, nodeNum, label );
return;
}
rendered = true;
label->Render(mode, indent, nodeNum, outGoing.size());
for(Arc *arc=outGoing.gethead(); arc != NULL; arc=outGoing.getnext()) {
arc->getDst()->Render(mode, indent +1);
}
}
draw_a_link()
此函数绘制连接的信息,根据给定的 renderMode、缩进级别 indent、节点编号 nodeNumber 和标签信息 label 进行格式化输出。
函数创建缩进字符串(indentation)。
根据 renderMode 的不同,在文本模式下输出连接信息,包括标签的键值和目标节点的编号。
作者只给出了TEXT模式
void draw_a_link( renderMode mode, int indent, int nodeNumber, Fact *label)
{
string indentation ;
for ( int i =0; i< indent; i++) { indentation += indentStep; }
switch (mode) {
case TEXT:
cout << indentation << label->key
<< "==><" << nodeNumber << ">" << endl;
break;
case HTML:
break;
default:
break;
};
}
Render2(renderMode)
如果节点尚未被渲染过(rendered 为 false),将节点标记为已渲染(rendered = true)。
调用 outputVertex 函数,输出节点的标签(label->key)和度量值(label->metric)。
遍历所有与当前节点关联的出边,并递归调用子节点的 Render2 函数,传递给子节点相同的 arcLabelMode。
函数返回 true,表示已渲染。
bool OrNode::Render2(arcLabelMode mode)
{
if(!rendered) {
rendered = true;
outputVertex(label->key, label->metric);
for(Arc *arc=outGoing.gethead(); arc != NULL; arc=outGoing.getnext()){
if (arc->getDst()->Render2(mode))//调用arc由getDst得到的目标节点的 Render2 方法
arc->Render(mode);//调用Render()渲染弧
}
}
return true;
}
outputVertex(description,metric)
输出 包括节点编号、描述(description)、节点类型(“OR”)以及可选的度量值(metric)的信息。
如果 displayMetric 为真,会将度量值输出,否则只输出节点编号和描述。
输出样例:1,“execCode(webServer,apache)”,“OR”,0
void OrNode::outputVertex( string description, float metric )//outputVertex(label->key, label->metric)
{
if (displayMetric){
if (metric < 0){
metric = 0;
}
cout << nodeNum << ",\"" << description << "\",\"OR\"," << metric << endl;
}
else{
cout << nodeNum << ",\"" << description << "\",\"OR\"" << endl;
}
return;
}
- 如果buildCNF为true,程序将调用build_cnf函数,把图节点转换为CNF子句并写入文件 “clauses.cnf” 中。
CNF子句用于表示合取范式(Conjunctive Normal Form),通常用于描述布尔逻辑问题,特别是可满足性问题(SAT)的实例。在CNF文件中,每一行表示一个逻辑子句,子句由多个bool变量通过“与”、“或”连接而成。
例:(P ∨ Q) ∧ (R ∨ ¬Q)
如果选择了SAT-solver选项并且生成了有效的攻击图,将其写入相应文件
对于攻击图分析,SAT求解器用于验证攻击路径是否可行,检查是否存在一种攻击方式,使得一组条件都满足。
如果SAT求解器能够找到一组满足条件的变量赋值,那么攻击路径是可行的
int build_cnf()
{
NodeMap::iterator k;
//NodeMap k;
//k["key1"] = new Node();
//k["key2"] = new DerivedNode();
//k为NodeMap的一个对象 储存了两个节点和key1 key2关联
Node *headNode;
for (k = data.goals.begin(); k != data.goals.end(); k++) {
headNode = k->second;
if(headNode != NULL) {
headNode->TransformToCNF(0);
}
}
//遍历头节点并执行以下操作:
//如果 headNode 不为空,调用 headNode->TransformToCNF(0) 转换为合取范式(CNF)表示
// 写入原始事实 primitive_facts.P
filePrimitiveFacts.open("primitive_facts.P");
for(int i = 1; i <= primitiveCounter; i++) {
filePrimitiveFacts << mapPrimitives[i] << "." << endl;
}
filePrimitiveFacts.close();
// 写入派生事实 derived_facts.
fileDerivedFacts.open("derived_facts.P");
for(int i = 1; i <= derivedCounter; i++) {
fileDerivedFacts << mapDerived[i] << "." << endl;
}
fileDerivedFacts.close();
// 写入cnf子句 clauses.cnf
fileCNF.open("clauses.cnf");
fileCNF << "p cnf " << cnfCounter << " " << clauseCounter << endl; //写入 CNF 文件的头部。
//其中 "p cnf" 表示 CNF 的格式,cnfCounter 是 CNF 子句的数量,clauseCounter 是子句中的文字数量。这个头信息是 SAT 求解器期望的文件格式
for(int i = 1; i <= clauseCounter; i++) {
fileCNF << mapClauses[i] << endl;//将 CNF 子句写入文件
}
fileCNF << "0" << endl;//"0",表示 CNF 文件的结束
fileCNF.close();
// 将 CNF 编号/谓词字符串映射写入 mapping.cnf 文件
cerr << "Write mapping of node number to tuple to mapping.cnf" << endl;//节点号到元组的映射
fileMap.open("mapping.cnf");
for(int i = 1; i <= cnfCounter; i++) {
fileMap << i << "<<>>" << mapCNF[i] << endl;
}
fileMap.close();
return 0;
}
graph_gen.sh:CSV形式输出
#检查是否需要生成CSV格式的输出
if test -n "$CSVOutput"
then
#将 AttackGraph.txt 中的AND、OR和LEAF节点筛选到 VERTICES.CSV 文件中。
grep -E "AND|OR|LEAF" AttackGraph.txt > VERTICES.CSV
# AttackGraph.txt 中非AND、OR和LEAF节点筛选到 ARCS.CSV 文件中。
grep -Ev "AND|OR|LEAF" AttackGraph.txt > ARCS.CSV
含义:节点编号(Num),节点描述(Label),节点类型(Type),节点概率值(Metric)
含义:后继节点ID(successorID),前驱节点ID(predeccesorID),边的度量值(Metric)
graph_gen.sh:可视化攻击图
调用render.sh脚本可视化攻击图
#检查是否需要可视化攻击图。
if test -n "$VISUALIZE"; then
render.sh $VISUALIZATION_OPTS
else
echo "The attack graph data can be found in AttackGraph.txt."
fi
render.sh:基于CSV文件可视化攻击图
ac_prev=
for ac_option #迭代命令行参数
do
# 如果 ac_prev 非空表示需要参数,将当前选项ac_option(当前选项)的值分配给ac_prev(前一个选项)。
if test -n "$ac_prev"; then
eval "$ac_prev=\$ac_option"
ac_prev= #将ac_prev置空,表示前一个选项已经处理完毕
continue
fi
case "$ac_option" in
--arclabel)
arclabel=true ;;
--reverse)
reverse=true ;;
--nometric)
nometric=true ;;
--simple)
simple=true ;;
*)
# -h | --help)
cat <<EOF
Usage:render.sh [--arclabel]
[--reverse]
[--simple]
[-h|--help]
EOF
exit ;;
esac
done
echo "通过GraphViz生成攻击图"
echo digraph G { > AttackGraph.dot
#根据参数选择不同sed文件处理CSV文件
if test -n "$simple"; then
if test -n "$nometric"; then
vertice_sed_file=$MULVALROOT/utils/VERTICES_simple_no_metric.sed
else
vertice_sed_file=$MULVALROOT/utils/VERTICES_simple.sed
fi
else
if test -n "$nometric"; then
vertice_sed_file=$MULVALROOT/utils/VERTICES_no_metric.sed
else
vertice_sed_file=$MULVALROOT/utils/VERTICES.sed
fi
fi
sed -f $vertice_sed_file VERTICES.CSV >> AttackGraph.dot
if test -n "$reverse"; then
if test -n "$arclabel"; then
sed -f $MULVALROOT/utils/ARCS_reverse.sed ARCS.CSV >> AttackGraph.dot
else
sed -f $MULVALROOT/utils/ARCS_reverse_noLabel.sed ARCS.CSV >> AttackGraph.dot
fi
else
if test -n "$arclabel"; then
sed -f $MULVALROOT/utils/ARCS.sed ARCS.CSV >> AttackGraph.dot
else
sed -f $MULVALROOT/utils/ARCS_noLabel.sed ARCS.CSV >> AttackGraph.dot
fi
fi
echo } >> AttackGraph.dot
#使用 GraphViz 工具的 dot 命令将 AttackGraph.dot 转换为 PostScript 格式,并将结果存储在 AttackGraph.eps 文件中。
dot -Tps AttackGraph.dot > AttackGraph.eps
epstopdf AttackGraph.eps # 转换生成的 EPS 文件为 PDF 格式。
echo "如果成功生成, 攻击图将在AttackGraph.pdf中"
if test -n "$PDF_READER"; then
$PDF_READER AttackGraph.pdf&
fi
graph_gen.sh:输出XML格式
#检查 VERTICES.CSV 和 ARCS.CSV 文件是否存在且可读。
if [ -r VERTICES.CSV ] && [ -r ARCS.CSV ]; then
#设置 CLASSPATH 变量来包含MULVAL适配器的路径。
CLASSPATH=$CLASSPATH:$MULVALROOT/bin/adapter
#执行Java程序 XMLConstructor 来创建XML文件
java -cp $CLASSPATH XMLConstructor
else
exit 1
fi
XMLConstructor.java
public class XMLConstructor {
public static void main(String[] args) {
// TODO Auto-generated method stub
constructXML();
}
private static void constructXML() {
String node1 ="";
String node2 = "";
String line = "";
//节点在VERTICES.CSV
String id = "";
String fact = "";
String type = "";
String metric="";
String line_items [];
int line_len = 0;
try {
FileWriter fr = new FileWriter("AttackGraph.xml");
BufferedReader arcs= new BufferedReader(new FileReader("ARCS.CSV"));
fr.write("<attack_graph>\n");
fr.write("<arcs>\n");
//第一步,收集所有节点的前驱
while ((line = arcs.readLine()) != null) {
fr.write("<arc>\n");
//node here is the key
node1 = line.split(",")[0];
node2 = line.split(",")[1];
fr.write("<src>"+node1+"</src>\n");
fr.write("<dst>"+node2+"</dst>\n");
fr.write("</arc>\n");
}
fr.write("</arcs>\n");
BufferedReader vertices= new BufferedReader(new FileReader("VERTICES.CSV"));
fr.write("<vertices>\n");
while ((line = vertices.readLine()) != null) {
id = line.split(",")[0];
fact = line.split("\"")[1];
type = line.split("\"")[3];
line_items = line.split(",");
line_len = line_items.length;
//System.out.println(line);
metric = line_items[line_len-1];
// System.out.println(metric);
fr.write("<vertex>\n");
fr.write("<id>"+id+"</id>\n");
fr.write("<fact>"+fact+"</fact>\n");
fr.write("<metric>"+metric+"</metric>\n");
fr.write("<type>"+type+"</type>\n");
fr.write("</vertex>\n");
}
fr.write("</vertices>\n");
fr.write("</attack_graph>\n");
fr.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
其他功能文件
总结
MulVAL作为一款05年开发的攻击图生成工具,至13年不再更新。但基于MulVAL的可扩展性,仍有许多研究人员在其基础上做出新的拓展,例如根据不同风险场景的需求,开发出新的推理规则,或者框架改进等等。