靜態(tài)代碼分析之C語言篇
一、序言
從本篇起,筆者將開啟c語言代碼安全分析篇章,為大家詳細(xì)剖析c語言靜態(tài)代碼分析的各種技術(shù)細(xì)節(jié)。
二、依賴分析
依賴分析是c語言靜態(tài)代碼分析中一個(gè)非常重要的環(huán)節(jié),它的分析準(zhǔn)確與否,關(guān)系到了后續(xù)的漏洞分析的準(zhǔn)確性。
什么是依賴分析
依賴圖是源代碼文件與其依賴庫之間的依賴關(guān)系的一種圖形表示。我們知道,在c語言中,項(xiàng)目真正用到的一些組件庫一般只有在編譯的時(shí)候才能夠確定,它不像java項(xiàng)目,一份代碼,到處運(yùn)行,而是一份代碼,多次編譯,因?yàn)楹芏鄷r(shí)候,我們?yōu)榱丝缙脚_(tái)的需要,需要給同一個(gè)組件準(zhǔn)備不同的適配方案,這也是c語言項(xiàng)目被一些開發(fā)人員長(zhǎng)期詬病的地方。在c/c++項(xiàng)目的分析工作中,我們首要解決的就是程序文件之間的依賴關(guān)系,因?yàn)檫@直接影響到了后續(xù)我們靜態(tài)代碼分析的準(zhǔn)確性。
依賴圖的作用
根據(jù)生成的文件依賴圖,我們可以清晰而準(zhǔn)確的知道整體項(xiàng)目的組織脈絡(luò)。在后續(xù)的符號(hào)識(shí)別和函數(shù)調(diào)用鏈生成階段,需要依靠依賴圖來找到目標(biāo)庫文件,從而保證符號(hào)識(shí)別和函數(shù)調(diào)用鏈構(gòu)建的準(zhǔn)確性。
如何進(jìn)行依賴分析
針對(duì)c/c++項(xiàng)目文件依賴圖的生成,業(yè)內(nèi)的主流辦法是通過解析compile_commands.json文件中記錄的編譯命令來進(jìn)行生成。詳情可參考pvs-stdio這款商業(yè)sca掃描器的技術(shù)說明文檔(pvs-studio.com)。而compile_commands.json文件可以通過編譯工具(make、ninja等)生成,具體的辦法步驟是:
1.收集編譯過程的輸出信息。
2.將輸出信息重定向到解析器,通過正則匹配的方式,生成compile_commands.json文件。
當(dāng)然,cmake等變異工具本身也支持生產(chǎn)compile_commands.json文件。具體是在構(gòu)建方案的時(shí)候,指定參數(shù):-DCMAKE_EXPORT_COMPILE_COMMANDS=1。
那么,有了compile_commands.json文件之后要如何進(jìn)行依賴分析呢?
我們先看下compile_commands.json文件的數(shù)據(jù)結(jié)構(gòu):
[
{
"directory": "/Users/pony/work/sourcehub/cmake-examples/01-basic/B-hello-headers/build",
"command": "/Library/Developer/CommandLineTools/usr/bin/cc -I/Users/pony/work/sourcehub/cmake-examples/01-basic/B-hello-headers/include -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk -o CMakeFiles/hello_headers.dir/src/Hello.c.o -c /Users/pony/work/sourcehub/cmake-examples/01-basic/B-hello-headers/src/Hello.c",
"file": "/Users/pony/work/sourcehub/cmake-examples/01-basic/B-hello-headers/src/Hello.c"
},
{
"directory": "/Users/pony/work/sourcehub/cmake-examples/01-basic/B-hello-headers/build",
"command": "/Library/Developer/CommandLineTools/usr/bin/cc -I/Users/pony/work/sourcehub/cmake-examples/01-basic/B-hello-headers/include -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk -o CMakeFiles/hello_headers.dir/src/main.c.o -c /Users/pony/work/sourcehub/cmake-examples/01-basic/B-hello-headers/src/main.c",
"file": "/Users/pony/work/sourcehub/cmake-examples/01-basic/B-hello-headers/src/main.c"
}
]
其中directory表示當(dāng)前編譯目錄,command表示當(dāng)前執(zhí)行的編譯命令,file表示待編譯的源碼文件。其中,在command命令中,參數(shù)I后面跟的是當(dāng)前源碼所依賴的頭文件目錄的路徑,編譯器在編譯的時(shí)候會(huì)在給定的這個(gè)目錄下搜索相關(guān)的頭文件。
但是,顯然只找到頭文件是不夠的,我們還需要找到函數(shù)的定義位置,這樣我們才能夠真正建立起函數(shù)調(diào)用所在文件和函數(shù)定義所在文件之間的關(guān)聯(lián)關(guān)系。下面我們?yōu)榱烁玫恼f明aurora代碼安全分析引擎是如何對(duì)c項(xiàng)目進(jìn)行依賴分析的,下面,我們以一個(gè)c代碼項(xiàng)目為示例進(jìn)行闡述。
項(xiàng)目目錄:
.
├── CMakeLists.txt
├── README.adoc
├── include
│ └── Hello.h
└── src
├── Hello.c
└── main.c
2 directories, 5 files
CMakeLists.txt
# Set the minimum version of CMake that can be used
# To find the cmake version run
# $ cmake --version
cmake_minimum_required(VERSION 3.5)
# Set the project name
project (hello_headers)
# Create a sources variable with a link to all c files to compile
set(SOURCES
src/Hello.c
src/main.c
)
# Add an executable with the above sources
add_executable(hello_headers ${SOURCES})
# Set the directories that should be included in the build command for this target
# when running g++ these will be included as -I/directory/path/
target_include_directories(hello_headers
PRIVATE
${PROJECT_SOURCE_DIR}/include
)
include/Hello.h
#ifndef __HELLO_H__
#define __HELLO_H__
void print();
#endif
src/Hello.c
#include "Hello.h"
void print()
{
printf("hello world.");
}
src/main.c
#include "Hello.h"
int main(int argc, char *argv[])
{
print();
return 0;
}
這個(gè)項(xiàng)目主要由一個(gè)主程序和一個(gè)組件構(gòu)成,主程序中通過頭文件Hello.h對(duì)組件中定義的print函數(shù)進(jìn)行調(diào)用。那么,首先明確,我們期望建立起來的依賴關(guān)系是main.c和Hello.c之間的依賴關(guān)系。通過對(duì)compile_commands文件的分析,我們已經(jīng)可以知道m(xù)ain.c和Hello.c都和Hello.h是有依賴關(guān)系的,只不過一個(gè)是API調(diào)用,一個(gè)是API定義。那么我們是否可以直接說明這兩個(gè)文件是有依賴的呢?那顯然是不行的,因?yàn)槲覀儫o法確定Hello.c中是否是定義了main.c中調(diào)用的print函數(shù)。只有同時(shí)滿足在main.c中調(diào)用了Hello.h中聲明的print函數(shù),且在Hello.c中定義了Hello.h中聲明的print函數(shù),那么我們才能夠認(rèn)為,main.c和Hello.c之間是存在依賴關(guān)系的,這樣,我們后續(xù)才能夠據(jù)此建立起函數(shù)之間的調(diào)用關(guān)系,即函數(shù)調(diào)用鏈(這對(duì)于全局?jǐn)?shù)據(jù)流構(gòu)建分析很重要)。
我們可以用下面這個(gè)示意圖來表示這個(gè)推導(dǎo)過程。
其中,依賴確定模塊一,通過對(duì)compile_commands.json文件及相關(guān)源碼的AST的分析,確定兩個(gè)源碼文件引入了同一個(gè)頭文件,依賴確定模塊二,通過分析是否同時(shí)導(dǎo)入相同頭文件且一方為函數(shù)調(diào)用,另一方為函數(shù)定義,確定main.c文件到Hello.c之間的依賴關(guān)系,而調(diào)用鏈確定模塊,通過分析main函數(shù)中是否包含對(duì)函數(shù)print的調(diào)用,進(jìn)而確定main.c中的main函數(shù)和src/Hello.c中的print函數(shù)的調(diào)用關(guān)系。
三、代碼數(shù)據(jù)庫構(gòu)建
代碼數(shù)據(jù)庫,顧名思義,存儲(chǔ)的代碼相關(guān)的一些數(shù)據(jù)。這里的代碼數(shù)據(jù)庫可以認(rèn)為是第一層代碼屬性圖,我們一般會(huì)在其中存儲(chǔ)源代碼的AST表示形式。至于源代碼的AST表示形式,我們可以通過一些AST提取工具來獲取,比如eclipse提供的cdt工具,亦或者一些開源的前端解析工具,如antlr,均可以完成這部分工作。當(dāng)然,如果我們利用這些工具提取ast,那么獲得的僅是一個(gè)ast unit class的集合,而為了后續(xù)展示方便,同時(shí)也為了能夠通過分布式架構(gòu)進(jìn)行高效的分析,我們還需要對(duì)這些ast unit類進(jìn)行必要的初步解析工作,然后將其轉(zhuǎn)為json格式進(jìn)行持久化存儲(chǔ),這部分的處理流程可用下圖表示:
四、函數(shù)調(diào)用圖構(gòu)建
什么是函數(shù)調(diào)用圖
函數(shù)調(diào)用圖是函數(shù)之間的調(diào)用關(guān)系的一種圖形表示形式。在進(jìn)行過程間數(shù)據(jù)流分析的時(shí)候,我們需要依賴函數(shù)調(diào)用圖求解函數(shù)調(diào)用鏈,以便于沿著這個(gè)調(diào)用鏈進(jìn)行過程間數(shù)據(jù)流分析。
如何進(jìn)行函數(shù)調(diào)用圖構(gòu)建
函數(shù)調(diào)用鏈構(gòu)建需要建立在依賴分析的基礎(chǔ)之上。在依賴分析中其實(shí)已經(jīng)對(duì)這部分進(jìn)行了形象的闡述,這里就不做過多的贅述。總的來說,依托依賴分析的結(jié)果,我們可以獲得兩個(gè)源碼文件之間的依賴關(guān)系,然后基于此,再對(duì)這些源碼中的函數(shù)定義信息進(jìn)行遍歷分析,判斷下函數(shù)調(diào)用語句的函數(shù)簽名和函數(shù)定義的函數(shù)簽名是否一致,即可構(gòu)建我們后續(xù)分析所需的函數(shù)調(diào)用鏈。
五、控制流及數(shù)據(jù)流分析
什么是控制流及數(shù)據(jù)流
程序控制流表示的是程序的各個(gè)語句結(jié)構(gòu)之間的控制關(guān)系,總的來說有兩種形式,一種是基本塊控制關(guān)系圖,另一種是表達(dá)式控制關(guān)系圖。前者注重在基本塊之間的控制關(guān)系,后者注重在表達(dá)式之間的控制關(guān)系。而數(shù)據(jù)流表示的是程序中數(shù)據(jù)的流動(dòng)關(guān)系,其中,我們會(huì)比較關(guān)注變量的定義和引用關(guān)系,即def-use鏈,以及一些賦值語句引起的數(shù)據(jù)流動(dòng)關(guān)系。
為什么要進(jìn)行控制流和數(shù)據(jù)流分析
控制流分析和數(shù)據(jù)流分析是程序靜態(tài)分析中比較核心的分析環(huán)節(jié),我們?cè)谶M(jìn)行代碼安全審計(jì)的時(shí)候,我們認(rèn)定某一處存在漏洞,如sql注入漏洞,一般需要有比較完整的污點(diǎn)傳播路徑,才能夠比較有充分的理由其漏洞進(jìn)行判定,而污點(diǎn)傳播路徑依賴于數(shù)據(jù)流分析,而數(shù)據(jù)流分析也依賴于控制流分析。總的來說,java和c的數(shù)據(jù)流分析的方法大同小異,針對(duì)java方面的控制流及數(shù)據(jù)流分析方法,我已在前一篇文章中進(jìn)行了詳細(xì)的描述,這里就不做過多的贅述了,詳情見:《DevSecOps建設(shè)之白盒續(xù)篇 - FreeBuf網(wǎng)絡(luò)安全行業(yè)門戶》。
六、代碼安全分析
棧溢出漏洞
那么,我們?nèi)绾螌㈧o態(tài)代碼分析技術(shù)應(yīng)用在代碼安全分析上呢?我們以pwnable.kr上的一個(gè)棧溢出靶場(chǎng)bof為例,詳細(xì)闡述aurora白盒引擎是如何進(jìn)行c代碼安全分析的。漏洞代碼如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}
簡(jiǎn)要說明:這是一段比較典型的棧溢出漏洞代碼,在函數(shù)func中,我們聲明并定義了一個(gè)字符數(shù)組overflowme,并給其分配了32個(gè)字節(jié)的內(nèi)存空間。程序中調(diào)用gets函數(shù),獲取命令行輸入流,如果字符流長(zhǎng)度在overflowme這個(gè)變量定義的合法區(qū)間內(nèi),那么這個(gè)程序?qū)凑照5倪壿嬜撸绻^了overlfowme定義的合法區(qū)間,那么,在沒有對(duì)這個(gè)輸入流長(zhǎng)度進(jìn)行合法性校驗(yàn)的前提下,將會(huì)造成棧溢出漏洞,覆蓋變量key的內(nèi)存空間,影響預(yù)期的分支流程走勢(shì),即走到if的then邏輯部分,執(zhí)行system函數(shù),反彈一個(gè)bash窗口。這個(gè)調(diào)用過程發(fā)生時(shí),內(nèi)存的棧幀分布示意圖如下:
其中,下層為func棧幀,上層為main棧幀,棧幀的排布由高地址向低地址排布,先調(diào)用的函數(shù)占據(jù)高地址,后調(diào)用的函數(shù)占據(jù)低地址,而數(shù)據(jù)寫入規(guī)則則是由低地址到高地址。我們?cè)趯?shí)際利用的時(shí)候,如果要改變if分支中的邏輯,可通過輸入以下payload實(shí)現(xiàn)攻擊:
payload="a"*52+ chr(0xbe) + chr(0xba) + chr(0xfe) +chr(0xca);
其中,chr(0xbe) + chr(0xba) + chr(0xfe) +chr(0xca)即為0xdeadbeef,而52表示key和overflow之間的距離,這些數(shù)據(jù)可以通過gdb調(diào)試獲得。通過給定overflowme超出其合法值范圍的payload,我們即可實(shí)現(xiàn)對(duì)原有邏輯的篡改。
那么,針對(duì)這種漏洞,我們要怎樣通過靜態(tài)分析方法進(jìn)行檢測(cè)呢?
我們可以從棧溢出的原理出發(fā),來思考我們的防護(hù)策略。我們知道,棧溢發(fā)生的本質(zhì)原因是在對(duì)變量進(jìn)行傳值的時(shí)候,未對(duì)輸入數(shù)據(jù)的字節(jié)長(zhǎng)度進(jìn)行合法性校驗(yàn)。那么,我們?cè)跈z測(cè)的時(shí)候可以枚舉源代碼中的內(nèi)存寫入操作的函數(shù)調(diào)用表達(dá)式(如gets、memcpy等),通過分析其在進(jìn)行內(nèi)存寫入的時(shí)候,寫入數(shù)據(jù)的值的大小是否比聲明的區(qū)間大來判定其是否存在漏洞。那么,問題就集中在了對(duì)兩者值的大小分析(區(qū)間分析)上了。針對(duì)緩沖區(qū)大小,我們可以根據(jù)變量對(duì)應(yīng)的def-use鏈找到變量定義的位置,然后結(jié)合其變量定義表達(dá)式中和變量區(qū)間定義相關(guān)的ast數(shù)據(jù),進(jìn)行綜合分析判定。針對(duì)輸入的數(shù)據(jù)的區(qū)間大小,我們也可以用類似方式進(jìn)行分析。當(dāng)然,區(qū)間的值可能并不一定是常量,如果要追求分析的準(zhǔn)確性,那么就要對(duì)區(qū)間范圍進(jìn)行進(jìn)一步的約束求解。
七、區(qū)間分析
什么是區(qū)間分析
區(qū)間分析是指通過約束求解算法,對(duì)變量和表達(dá)式的取值范圍進(jìn)行跟蹤,為進(jìn)一步的程序分析提供精確的數(shù)據(jù)支持。
為什么要做區(qū)間分析
通過上文可知,我們?cè)谧鲆恍┐a安全分析的時(shí)候,比如棧溢出分析,如果我們不能夠比較準(zhǔn)確的分析緩沖區(qū)和輸入數(shù)據(jù)的區(qū)間大小,那么我們是無法準(zhǔn)確地判斷是否存在棧溢出漏洞的。
如何做區(qū)間分析
學(xué)術(shù)上,針對(duì)區(qū)間分析早有很多相關(guān)的方法分析方法,如王雅文、宮云戰(zhàn)等在第五屆中國(guó)測(cè)試學(xué)術(shù)會(huì)議上發(fā)表的論文《區(qū)間運(yùn)算在軟件缺陷檢測(cè)中的應(yīng)用》中就提到了一種區(qū)間分析方法。論文中提出,可針對(duì)不同的變量類型設(shè)置不同的初始區(qū)間值,然后通過表達(dá)式區(qū)間分析、條件區(qū)間分析、控制流區(qū)間分析三個(gè)不同緯度對(duì)變量的取值范圍進(jìn)行約束求解。
1. 表達(dá)式區(qū)間分析
如表達(dá)式3*(++i),如果i的取值范圍是[1,1],那么執(zhí)行完3*(++i)之后,其取將范圍將變成:[6,6]。
2. 條件區(qū)間分析
例如:
if(x>2){
}
else{
}
區(qū)間分析在代碼質(zhì)量領(lǐng)域的應(yīng)用
區(qū)間分析,可以應(yīng)用于程序中的不可達(dá)代碼塊檢測(cè)、代碼覆蓋率分析等。例如:
void func(){
int i=5;
if(i<0){
i++;
}
}
初始控制流圖:
轉(zhuǎn)為帶區(qū)間信息的控制流圖:
從帶區(qū)間的控制流圖中可以發(fā)現(xiàn),在執(zhí)行到stmt_3語句時(shí),因?yàn)閕<0這個(gè)條件的取值范圍是[-∞,-1]n[5,5]=?,所以i<0分支下的所有表達(dá)式語句上i的區(qū)間范圍會(huì)被設(shè)置為null,表示不可達(dá)到。統(tǒng)計(jì)出所有執(zhí)行的語句數(shù)量及總語句數(shù)量即可計(jì)算得語句覆蓋率,計(jì)算公式為:可執(zhí)行語句數(shù)量/語句總數(shù)。統(tǒng)計(jì)所有可達(dá)分支數(shù)量及總的分支數(shù)量即可計(jì)算得分支覆蓋率,計(jì)算公式為:可達(dá)分支/分支總數(shù)。
區(qū)間分析在代碼安全領(lǐng)域的應(yīng)用
比如上面的那個(gè)溢出漏洞案例。我們可以得到其初始控制流圖為:
轉(zhuǎn)化為區(qū)間為帶區(qū)間信息的控制流圖:
其中在stmt_3(即語句gets(overflowme))中,overflowme的區(qū)間范圍易求得為[0,32],而用戶輸入的數(shù)據(jù)的范圍是[0,+∞],這里用1000作為缺省值,當(dāng)然為了以防萬一,也可以設(shè)置大一點(diǎn),顯而易見這里用戶輸入的數(shù)據(jù)的大小的區(qū)間范圍遠(yuǎn)大于overflowme最大的取值32,那么這里是肯定存在溢出漏洞的。當(dāng)然,要比較精確的判定溢出漏洞,我們還可以結(jié)合compile_commands.json中的編譯命令(如一些編譯時(shí)設(shè)定的堆棧溢出的防護(hù)策略)進(jìn)行綜合判定。
八、CI流程自動(dòng)化
一款靜態(tài)代碼安全分析工具,要好用,不僅要引擎能力足夠硬核,在流程方面也應(yīng)該自動(dòng)化,讓使用者能夠很方便的配置,很容易地集成到一些主流的CI piplines中。下面我們以gitlab ci,詳細(xì)闡述我們是如何實(shí)現(xiàn)c代碼自動(dòng)化安全、代碼質(zhì)量檢測(cè)。
以下是示例ci例子
# This file is a template, and might need editing before it works on your project.
# This is a sample GitLab CI/CD configuration file that should run without any modifications.
# It demonstrates a basic 3 stage CI/CD pipeline. Instead of real tests or scripts,
# it uses echo commands to simulate the pipeline execution.
#
# A pipeline is composed of independent jobs that run scripts, grouped into stages.
# Stages run in sequential order, but jobs within stages run in parallel.
#
# For more information, see: https://docs.gitlab.com/ee/ci/yaml/README.html#stages
stages: # List of stages for jobs, and their order of execution
- build
- test
- deploy
build-job: # This job runs in the build stage, which runs first.
image: ci_vtsmap:latest
stage: build
script:
- echo "Compiling the code..."
- mkdir build && cd build
- cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 ..
- curl -F "file=@compile_commands.json" -F "user=${GITLAB_USER_NAME}" -F "projectname=${CI_PROJECT_NAME}" -F "language=c" -F "email=xxx@gmail.com" -F "type=git"-F "branch=${CI_COMMIT_BRANCH}" -F "commitid=${CI_COMMIT_SHA}" -F "gitaddr=${CI_PROJECT_URL}" -F “token=xxx” -x POST http://[domain]/pushjob/
- make .
- echo "Compile complete."
deploy-job: # This job runs in the deploy stage.
stage: deploy # It only runs when *both* jobs in the test stage complete successfully.
script:
- echo "Deploying application..."
- echo "Application successfully deployed."
其中,我們會(huì)在原有的ci腳本中加入兩行命令,一行時(shí)在原有的cmake命令上加上參數(shù)-DCMAKE_EXPORT_COMPILE_COMMANDS=1 來生成compile_commands.json文件,以便于后端的靜態(tài)分析引擎進(jìn)行依賴分析。以下命令用于提交任務(wù)信息:
curl -F "file=@compile_commands.json" -F "user=${GITLAB_USER_NAME}" -F "projectname=${CI_PROJECT_NAME}" -F "language=c" -F "email=xxx@gmail.com" -F "type=git" -F "branch=${CI_COMMIT_BRANCH}" -F "commitid=${CI_COMMIT_SHA}" -F "gitaddr=${CI_PROJECT_URL}" -F “token=xxx” -x POST http://[domain]/pushjob/
九、總結(jié)
綜合來看,c代碼靜態(tài)分析目前最主要的難點(diǎn)在于區(qū)間分析,這部分分析的準(zhǔn)確性對(duì)后續(xù)的漏洞分析的準(zhǔn)確性有很大的影響。所以在c代碼靜態(tài)分析方面,區(qū)間分析方面需要花比較大的功夫去鉆研,不僅要保證分析的分析的準(zhǔn)確性,同時(shí)也要考慮到分析的效率,因?yàn)楹芏郼代碼項(xiàng)目,如linux內(nèi)核等,代碼量非常龐大,如果沒有一個(gè)比較合理的算法,加快代碼分析速度,那么接入到企業(yè)內(nèi)部使用,其體驗(yàn)感也是非常差的。當(dāng)然,精度和速度兩者一般情況下是一種此消彼長(zhǎng)的關(guān)系的,如何從中達(dá)到一個(gè)平衡,還需要不斷的進(jìn)行測(cè)試和實(shí)踐。
王雅文,宮云戰(zhàn),楊朝紅,肖慶,區(qū)間運(yùn)算在軟件缺陷檢測(cè)中的應(yīng)用,第五屆中國(guó)測(cè)試學(xué)術(shù)會(huì)議論文集,2008,51-52。