[更新中]MulVAL:基于逻辑推理的攻击图生成及风险评估工具

介绍

MulVAL是一个可以描述多主机、多阶段的基于逻辑推理的攻击图生成工具
官方网页

主要特点和功能:

  1. 多视图分析:MulVAL使用多视图方法,可以同时分析网络拓扑、系统配置和访问控制策略等多方面,以识别潜在的威胁和攻击路径。
  2. 自定义规则:可以定义自己的安全规则和策略,以适应其特定场景。
  3. 威胁建模:基于攻击图对网络攻击建模,可以帮助用户了解各种攻击威胁如何传播和影响后果。
  4. 漏洞分析:可以帮助用户识别网络和系统中的漏洞,以及这些漏洞可能被利用的方式。
  5. 可视化: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()

  1. 调用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;
      }
...
...
...

  1. 然后调用yyparse/yyin(linux)函数解析(由graphit.y graphit.l提供)有关facts、traceSteps、ruleList信息的输入文件并储存。

attack_graph:build_graph()

  1. 调用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()

  1. 调用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;
}
  1. 如果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

AttackGraph.txt

VERTICES.CSV
含义:节点编号(Num),节点描述(Label),节点类型(Type),节点概率值(Metric)
ARCS.CSV
含义:后继节点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的可扩展性,仍有许多研究人员在其基础上做出新的拓展,例如根据不同风险场景的需求,开发出新的推理规则,或者框架改进等等。

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
贝叶斯攻击(Bayesian Attack Graph)是用于评估网络安全的一种方法,通过将网络的各个节点、攻击路径和漏洞等信息建模为贝叶斯网络,然后利用贝叶斯推理的方法进行分析和预测。 在MATLAB创建贝叶斯攻击的代码可以分为以下几个步骤: 1. 导入相关库和数据:首先,需要导入MATLAB的贝叶斯网络工具箱(Bayes Net Toolbox),以及相关的数据文件。可以使用`import`命令导入贝叶斯网络工具箱,使用`load`命令导入数据文件。 2. 定义贝叶斯网络结构:根据实际情况,定义贝叶斯网络的节点和边的连接关系,并创建一个空的贝叶斯网络对象。可以使用`addNode`和`addEdge`命令定义节点和边。 3. 定义节点的概率:为每个节点定义先验概率和条件概率表。先验概率是指在没有其他节点信息的情况下,每个节点发生的概率。条件概率表则是指每个节点在给定其父节点的情况下的概率。可以使用`setPrior`和`setCPT`命令定义节点的概率。 4. 进行推理计算:使用贝叶斯推理的方法计算给定节点信息的条件下,其他节点的概率分布。可以使用`propagate`命令进行推理计算。 5. 绘制攻击:根据推理计算结果,绘制贝叶斯攻击。可以使用MATLAB的绘函数进行绘制,例如`plot`命令可以绘制节点和边的连接。 以上就是用MATLAB创建贝叶斯攻击的大致步骤。具体代码实现会依赖于贝叶斯网络工具箱的使用方法以及数据的具体情况,可以参考相关的MATLAB文档和示例代码进行具体实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值