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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM