• C/C++源码扫描系列- Joern 篇


    文章首发于

    https://xz.aliyun.com/t/9277
    

    概述

    codeqlFortify 相比 Joern不需要编译源码即可进行扫描,适用场景和环境搭建方面更加简单。

    环境搭建

    首先安装 jdk ,然后从github下载压缩包解压即可

    https://github.com/ShiftLeftSecurity/joern/releases/
    

    解压之后执行 joern 即可

    image.png

    然后我们 进入sca-workshop/joern-example 通过 importCode 分析我们的示例代码

    joern-example$ ~/sca/joern-cli/joern
    Compiling /home/hac425/sca-workshop/joern-example/(console)
    creating workspace directory: /home/hac425/sca-workshop/joern-example/workspace
    
         ██╗ ██████╗ ███████╗██████╗ ███╗   ██╗
         ██║██╔═══██╗██╔════╝██╔══██╗████╗  ██║
         ██║██║   ██║█████╗  ██████╔╝██╔██╗ ██║
    ██   ██║██║   ██║██╔══╝  ██╔══██╗██║╚██╗██║
    ╚█████╔╝╚██████╔╝███████╗██║  ██║██║ ╚████║
     ╚════╝  ╚═════╝ ╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝
    
    Type `help` or `browse(help)` to begin
    joern> importCode(inputPath="./", projectName="example")
    

    然后使用 cpg.method.name(".*get.*").toList 可以打印出所有函数名中包含 get 的函数

    joern> cpg.method.name(".*get.*").toList
    res15: List[Method] = List(
      Method(
        id -> 1000114L,
        code -> "char * get_user_input_str ()",
        name -> "get_user_input_str",
        fullName -> "get_user_input_str",
        isExternal -> false,
        signature -> "char * get_user_input_str ()",
    	...........
    

    除了使用 importCode 解析代码外,还可以通过 joern-parse 工具解析代码

    joern-example$ ~/sca/joern-cli/joern-parse ./
    joern-example$ ls
    cpg.bin  example.c  Makefile  system_query  test.sc  workspace
    

    解析完成后默认会将解析结果保存到 cpg.bin 中,可以通过 --out 参数指定保存的文件名。

    $ ~/sca/joern-cli/joern-parse --help
    Usage: joern-parse [options] input-files
    
      input-files              directories containing: C/C++ source | Java classes | a Java archive (JAR/WAR)
      --language <value>       source language: [c|java]. Default: c
      --out <value>            output filename
    

    然后进入 joern 的命令行使用 importCpg 函数即可导入之前的解析结果。

    joern> importCpg("cpg.bin")
    Creating project `cpg.bin8` for CPG at `cpg.bin`
    Creating working copy of CPG to be safe
    Loading base CPG from: /home/hac425/sca-workshop/joern-example/workspace/cpg.bin8/cpg.bin.tmp
    res0: Option[Cpg] = Some(value = io.shiftleft.codepropertygraph.Cpg@5b2ac349)
    

    然后就可以开始对代码进行检索了。

    Joern语法简介

    Joern 的规则脚本的开发语言是 scala ,其在代码分析阶段会将代码转换成抽象语法树、控制流图、数据流图等结构,然后在规则解析阶段会将这些图的属性、节点都封装成 Java对象,我们开发的 scala 规则脚本主要就是通过访问这些对象以及 Joern提供的API来完成查询。

    Joern 使用 cpg 这个全局对象表示目标源码中所有信息,通过这个对象可以遍历源码中的所有函数调用、类型定义等,比如使用 cpg.call 获取代码中的所有函数调用

    joern> cpg.call
    res4: Traversal[Call] = Traversal
    

    joern 很多方法的返回值都是 Traversal 类型,可以使用 toList 方法( l 方法是这个的缩写)转成 List方便查看.

    joern> cpg.call.toList
    

    或者

    joern> cpg.call.l
    

    这里有一点需要注意,Joern会把 =, +, && 等逻辑、数学运算都转换为函数调用(形式为 <operator>.xxx)保存到语法树中

      Call(
        id -> 1000529L,
        code -> "*xx = user",
        name -> "<operator>.assignment",
        order -> 4,
        methodFullName -> "<operator>.assignment",
        argumentIndex -> 4,
        signature -> "TODO",
        lineNumber -> Some(value = 259),
        columnNumber -> Some(value = 9),
        methodInstFullName -> None,
        typeFullName -> "",
        depthFirstOrder -> None,
        internalFlags -> None,
        dispatchType -> "STATIC_DISPATCH",
        dynamicTypeHintFullName -> List()
      )
    

    上面可以看到 = 赋值符号使用 <operator>.assignment 表示。

    Joern 的查询结果转成 List 后(使用 Traversal 也可以进行过滤),我们就可以使用 Scala 的语法来对结果进行过滤,比如 filter 方法

    joern> cpg.call.l.filter(c => c.name == "system")
    res7: List[Call] = List(
      Call(
        id -> 1000419L,
        code -> "system(cmd)",
        name -> "system",
        order -> 1,
        methodFullName -> "system",
        argumentIndex -> 1,
    	................................
    	................................
    

    这里就是对 call 的结果进行过滤,返回调用 system 函数的位置。

    在开发规则的时候可以查看代码的ast, cpg, ddg等图形来帮助调试

    joern> var m = cpg.method.name("call_system_safe_example").l.head
    joern> m.dotAst.l
    res19: List[String] = List(
      """digraph call_system_safe_example {
    "1000522" [label = "(METHOD,call_system_safe_example)" ]
    "1000523" [label = "(BLOCK,,)" ]
    "1000524" [label = "(LOCAL,user: char *)" ]
    "1000525" [label = "(<operator>.assignment,*user = get_user_input_str())" ]
    "1000526" [label = "(IDENTIFIER,user,*user = get_user_input_str())" ]
    "1000527" [label = "(get_user_input_str,get_user_input_str())" ]
    "1000528" [label = "(LOCAL,xx: char *)" ]
    "1000529" [label = "(<operator>.assignment,*xx = user)" ]
    "1000530" [label = "(IDENTIFIER,xx,*xx = user)" ]
    "1000531" [label = "(IDENTIFIER,user,*xx = user)" ]
    "1000532" [label = "(CONTROL_STRUCTURE,if (!clean_data(xx)),if (!clean_data(xx)))" ]
    "1000533" [label = "(<operator>.logicalNot,!clean_data(xx))" ]
    "1000534" [label = "(clean_data,clean_data(xx))" ]
    "1000535" [label = "(IDENTIFIER,xx,clean_data(xx))" ]
    "1000536" [label = "(RETURN,return 1;,return 1;)" ]
    "1000537" [label = "(LITERAL,1,return 1;)" ]
    "1000538" [label = "(system,system(xx))" ]
    "1000539" [label = "(IDENTIFIER,xx,system(xx))" ]
    "1000540" [label = "(RETURN,return 1;,return 1;)" ]
    "1000541" [label = "(LITERAL,1,return 1;)" ]
    "1000542" [label = "(METHOD_RETURN,int)" ]
      "1000522" -> "1000523"
      "1000522" -> "1000542"
      "1000523" -> "1000524"
      "1000523" -> "1000525"
      "1000523" -> "1000528"
      "1000523" -> "1000529"
      "1000523" -> "1000532"
      "1000523" -> "1000538"
      "1000523" -> "1000540"
      "1000525" -> "1000526"
      "1000525" -> "1000527"
      "1000529" -> "1000530"
      "1000529" -> "1000531"
      "1000532" -> "1000533"
      "1000532" -> "1000536"
      "1000533" -> "1000534"
      "1000534" -> "1000535"
      "1000536" -> "1000537"
      "1000538" -> "1000539"
      "1000540" -> "1000541"
    }
    """
    )
    

    然后找个在线Graphviz绘图网站绘制一下即可

    image.png

    除了AstJoern 还对代码构建一下几种结构

    joern> m.dot
    dotAst    dotCdg    dotCfg    dotCpg14  dotDdg    dotPdg
    

    本节只对基础语法进行介绍,其他的语法请看下文或者官方文档。

    system命令执行检测

    本节涉及代码

    https://github.com/hac425xxx/sca-workshop/tree/master/joern-example/system_query
    

    漏洞代码如下

    int call_system_example()
    {
        char *user = get_user_input_str();
        char *xx = user;
        system(xx);
        return 1;
    }
    

    代码通过 get_user_input_str 获取外部输入, 然后传入 system 执行。

    def getFlow() = {
        val src = cpg.call.name("get_user_input_str")
        val sink = cpg.call.name("system").argument.order(1)
        sink.reachableByFlows(src).p
    }
    

    代码解释如下:

    1. 首先根据 cpg.call.name 对所有的 call 过滤,获取所有的 get_user_input_str 函数调用,保存到 src
    2. 然后获取所有 system 函数调用,并将其第一个参数(从 1 开始索引)保存到 sink
    3. 最后使用 sink.reachableByFlows(src) 检索出所有从 srcsink 的结果,然后对结果使用 .p 方法,把结果打印出来。

    image.png

    可以看到对于每个搜索到的结果,Joern会把数据的流动过程打印出来,结果中存在一个误报

    image.png

    clean_data 函数会对数据进行校验,不会产生命令执行,所以需要把这个结果过滤掉

    def clean_data_filter(a: Any): Boolean =  {
        if(a.asInstanceOf[AstNode].astParent.isCall)
        {
            if(a.asInstanceOf[AstNode].astParent.asInstanceOf[Call].name == "clean_data")
                return true
        }
        return false
    }
    
    def filter_path_for_clean_data(a: Any) = a match {
        case a: Path => a.elements.l.filter(clean_data_filter).size > 0
        case _ => false
    }
    
    def getFlow() = {
        val src = cpg.call.name("get_user_input_str")
        val sink = cpg.call.name("system").argument.order(1)
        sink.reachableByFlows(src).filterNot(filter_path_for_clean_data)
    }
    

    Joern 没有类似 FortifyDataflowCleanseRule 功能,只能对 reachableByFlows 的结果进行过滤, reachableByFlows 返回的是一组 Path 对象,然后我们使用 filterNot 来剔除掉不需要的结果,每个 Path 对象的 elements 属性是这条数据流路径流经的各个代码节点,我们可以遍历这个来查看 Path 中是否存在有调用 clean_data 函数,如果存在就说明返回 true 表示这个结果是不需要的会被剔除掉。

    clean_data 函数调用在 Path 中是以 Identifier ( xx 变量) 存在,所以在规则中需要先将Path里面的每一项强转为 AstNode 类型 ,然后获取它的父节点,最后根据父节点就可以知道是否为 clean_data 的函数调用,这个信息可以通过绘制 ast 图来确定。

    joern> ci.astNode.astParent.astParent.astParent.dotAst.l
    res58: List[String] = List(
      """digraph  {
    "1000532" [label = "(CONTROL_STRUCTURE,if (!clean_data(xx)),if (!clean_data(xx)))" ]
    "1000533" [label = "(<operator>.logicalNot,!clean_data(xx))" ]
    "1000534" [label = "(clean_data,clean_data(xx))" ]
    "1000535" [label = "(IDENTIFIER,xx,clean_data(xx))" ]
    "1000536" [label = "(RETURN,return 1;,return 1;)" ]
    "1000537" [label = "(LITERAL,1,return 1;)" ]
      "1000532" -> "1000533"
      "1000532" -> "1000536"
      "1000533" -> "1000534"
      "1000534" -> "1000535"
      "1000536" -> "1000537"
    }
    """
    )
    

    图中标蓝的就是 Pathclean_data 函数调用的子节点 Identifier .

    image.png

    引用计数漏洞

    本节相关代码

    https://github.com/hac425xxx/sca-workshop/blob/master/joern-example/ref_query/
    

    漏洞代码

    int ref_leak(int *ref, int a, int b)
    {
        ref_get(ref);
        if (a == 2)
        {
            puts("error 2");
            return -1;
        }
        ref_put(ref);
        return 0;
    }
    

    ref_leak 的 漏洞是当 a=2 时会直接返回没有调用 ref_put 对引用计数减一,漏洞模型:在某些存在 return 的条件分支中没有调用 ref_put 释放引用计数。

    首先可以看看代码的 AST 结构

    def getFunction(name: String): Method = {
      return cpg.method.name(name).head.asInstanceOf[Method]
    }
    getFunction("ref_leak").dotAst.l
    

    image.png

    Joern 使用 controlStructure 来表示函数中的控制结构,后续可以使用这个对象来访问函数的控制结构。

    下面具体分析下如何编写规则匹配到这种漏洞,首先获取所有调用 ref_get 的地方

    def search() = {
        var ref_get_callers = cpg.call.name("ref_get")
        ref_get_callers.filter(ref_func_filter).map(e => getEnclosingFunction(e.astNode))
    }
    

    然后对 ref_get_callers 进行过滤,把存在漏洞的函数过滤出来,过滤核心函数位于 ref_func_filter ,关键代码如下

    def ref_func_filter(caller: Call): Boolean =  {
        var node : AstNode = caller.astNode
        var block : Block = null
        var func : Method = null
        var ret : Boolean = false
    
        loop.breakable {
            while(true) {
                if(node.isBlock) {
                    block = node.asInstanceOf[Block]
                }
    
                if(node.isMethod) {
                    func = node.asInstanceOf[Method]
                    loop.break;
                }
                node = node.astParent
            }
        }
    
        var true_block = func.controlStructure.whenTrue.l
        var false_block = func.controlStructure.whenFalse.l
        var ref_count = 1
    
        if(true_block.size != 0) {
            .................
        }
    
        return ret
    }
    

    函数大部分都是使用的 Scala 的语法,其实 Joern 的规则开发在一些情况下就是使用 Scala 语法来搜索代码结构

    由于我们这里过滤的是 ref_get_callers ,所以入参的类型是 Call ,然后通过循环地向上遍历 ast ,获取到该 Call 所在的函数和 Block

    然后根据 func 来获取代码中的控制结构,然后获取到控制结构为 True 或者 False 时的代码块,然后对代码块遍历,搜集到 Return 之前的引用计数是否有问题。

      var true_block = func.controlStructure.whenTrue.l
      var ref_count : Int = 1
    
      if (true_block.size != 0) {
        var block = true_block(0)
        for (elem <- block.astChildren.l) {
            if (elem.isCall && elem.asInstanceOf[Call].name == "ref_put") {
                ref_count -= 1
            }
    
            if (elem.isCall && elem.asInstanceOf[Call].name == "ref_get") {
                ref_count += 1
            }
            
            if (elem.isReturn) {
                println("func_name: " + func.name + ", detect return expr, current ref_count: " + ref_count)
                if (ref_count != 0) {
                    ret = true
                }
                ref_count = refcount_bak
            }
        }
      }
    

    代码解释如下

    1. 首先根据 func.controlStructure.whenTrue 获取控制结构为 True 时执行的代码块,然后遍历它的 astChildren .
    2. 如果是 ref_put 就把 ref_count - 1,如果在 Block 里面有 Return 语句就打印 ref_count ,如果引用计数不为0说明就有问题.

    执行结果如下:

    image.png

    总结

    Joern 本身的功能还是不错的,此外用户还可以使用 Scala 语言来完成框架不支持的事情,也是非常灵活的一个工具,不过有时候需要遍历语法树、控制流图等结构,需要对编译原理有一定的了解。

    参考链接

    https://docs.joern.io/home

    https://docs.joern.io/c-syntaxtree#basic-ast-traversals

  • 相关阅读:
    lower版购物车模拟
    字典的增删改查和操作
    生成四位验证码
    列表的增删改查和操作
    根据输入字符串,分别计算大写,小写,数字,标点的个数
    检查一个数是不是质数
    非诚勿扰,选心动女生(小游戏)
    汉诺塔的实现
    一个纯虚函数导致的问题
    Hello World 之 CGAL
  • 原文地址:https://www.cnblogs.com/hac425/p/14556115.html
Copyright © 2020-2023  润新知