SRCINV-原始碼審計系統的簡要介紹
Contents
- 1 - 簡介
- 1.1 - 框架
- 1.2 - 依賴
- 1.3 - 針對目標
- 2 - 框架的簡單使用
- 3 - 資訊收集
- 4 - 資訊解析
- 4.1 - 資訊調整
- 4.2 - 基礎資訊獲取
- 4.3 - 詳細資訊獲取
- 4.4 - 交叉引用資訊整理
- 4.5 - 間接呼叫1
- 4.6 - 間接呼叫2
- 5 - 資訊使用
- 6 - 後續
- 7 - 引用
1 - 簡介
該文件將介紹SRCINV框架(beta)的使用, 以及設計思路, 和主要針對的目標.
1.1 - 框架
SRCINV是source code investigation的縮寫, 是根據經驗, 對原始碼審計工作的程式碼體現.
原始碼(或二進位制程式碼)是提供給研究人員檢視的, 也是提供給編譯器(或直譯器或執行單元)的,
該框架從編譯器編譯原始碼檔案的中間資訊, 提取解析該專案的所有原始碼資訊提供給研究人員使用.
由於通常的gcc外掛, 看到的是當前編譯單元的資訊, 無法看到整個專案. 所以我們將整個專案的 每個編譯單元的資訊提取出來再進行整合, 便於從全域性角度去觀察整個專案.
該框架旨在對開源專案進行高度自動化的程式碼審計, 以幫助開發測試/安全研究人員, 發現專案 中可能存在的程式碼問題, 並提供一定程度的驗證樣本生成和程式碼補丁生成等功能.
對於每個專案, 使用一個struct src結構記錄所有生成的索引資訊, 包含
sibuf(用於表示編譯檔案的索引資訊)連結串列,
resfile(用於表示提取的結果檔案)連結串列,
sinodes(用於表示非區域性變數/函式/型別, 分兩大種類:名稱索引, 位置索引. 對於檔案 變數(static)/檔案函式/有位置資訊的型別, 使用位置索引; 對於全域性變數/全域性函式/沒有位 置資訊但是有名稱的型別, 使用名稱索引; 對於沒有位置也沒有名稱的型別, 記錄在sibuf中)
目錄結構如下:
si_core.c主程式 collect/編譯器外掛, 資訊收集 plugins/資訊解析及使用等各種功能plugin output/索引資料等檔案
1.2 - 依賴
該框架只能運行於64位GNU/Linux系統, 且需要存在personality
系統呼叫以關閉進行aslr.
終端顏色顯示, 不同的發行版可能會存在一些相容問題.
其他依賴的庫:
clib (https://github.com/snorez/clib) ncurses readline libcapstone
需要的標頭檔案:
gcc plugin 庫檔案
1.3 - 針對目標
該框架預期為儘可能多種類的開源專案進行測試. 當前該框架只針對gcc編譯器編譯的C專案.
2 - 框架的簡單使用
框架集成了一些基礎命令以供使用(可檢視 –[ 5 - 資訊使用 節的演示視訊):
si_core:主程式 help:使用幫助, 檢視當前可使用的命令 exit/quit:退出程式 load_plugin:載入plugin unload_plugin:解除安裝plugin reload_plugin:重新載入plugin list_plugin:顯示當前所有可供使用的plugin do_make:按照核心編譯模式編譯指定專案 do_sh:執行終端命令 set_plugin_dir: 設定plugin目錄, 預設為plugins/ showlog:顯示日誌資訊, 日誌檔案位於plugins/log.txt load_srcfile:載入索引資訊檔案 set_srcfile:設定索引資訊路徑 getinfo:獲取收集的專案資訊的索引資訊 staticchk:靜態檢測 itersn:輸出所有的索引資訊 SRCINV> help ========= USAGE INFO ========= help: Show this help message exit: Exit this main process quit: Exit this main process load_plugin: (plugin_path) [plugin_args...] unload_plugin: (id|path)Unload specific plugin reload_plugin: (id|path) [plugin_args]Reload target plugin list_plugin: Show current loaded plugins do_make: (c|cpp|...) (sodir) (projectdir) (outfile) [extras] Build target project, make sure Makefile has EXTRA_CFLAGS do_sh: Execute bash command set_plugin_dir: (plugin_dir)Set the plugin directory, the original still count showlog: Show current log messages load_srcfile: [srcfile] set_srcfile: (srcfile_name) getinfo: (res_path) (is_builtin) (linux_kernel?) (step) Get information of resfile, for step 0 Get all information 1 Get base information 2 Get detail information 3 Get xrefs information 4 Get indirect call information 5 Check if all GIMPLE_CALL are set staticchk: Run registered static check methods itersn: [output_path]Traversal all sinodes to stderr/file sn_load: [id]most for test cases ========= USAGE END ========= SRCINV> list_plugin 00loadedfuzz >>>> /home/zerons/workspace/todo/srcinv/plugins/fuzz.so 10loadedsibuf >>>> /home/zerons/workspace/todo/srcinv/plugins/sibuf.so 21loadedstaticchk >>>> /home/zerons/workspace/todo/srcinv/plugins/staticchk.so 30unload(null) >>>> /home/zerons/workspace/todo/srcinv/plugins/test.so 41loadedsinode >>>> /home/zerons/workspace/todo/srcinv/plugins/sinode.so 57loadedgetinfo >>>> /home/zerons/workspace/todo/srcinv/plugins/getinfo.so 61loadeddebuild >>>> /home/zerons/workspace/todo/srcinv/plugins/debuild.so 70loadedsn_load >>>> /home/zerons/workspace/todo/srcinv/plugins/sn_load.so 80loadeduninit >>>> /home/zerons/workspace/todo/srcinv/plugins/uninit.so 91loadedresfile >>>> /home/zerons/workspace/todo/srcinv/plugins/resfile.so 104loadedsrc >>>> /home/zerons/workspace/todo/srcinv/plugins/src.so 111loadedgen_sample >>>> /home/zerons/workspace/todo/srcinv/plugins/gensample.so 120loadedc >>>> /home/zerons/workspace/todo/srcinv/plugins/c.so 130loadeditersn >>>> /home/zerons/workspace/todo/srcinv/plugins/itersn.so 141loadedutils >>>> /home/zerons/workspace/todo/srcinv/plugins/utils.so list_plugin顯示當前可供使用的plugin. 顯示的資訊包含: 序號引用計數是否載入plugin名稱plugin路徑
更多的指令詳細用法, 請參考專案倉庫doc/commands.md檔案.
框架的使用分為三個階段, 資訊收集, 資訊解析, 資訊使用. 下面將依次介紹.
3 - 資訊收集
注意: 資訊收集之前, 需要刪除之前的結果檔案.
對單個檔案, 可以使用gcc的-fplugin, -fplugin-arg引數編譯, 對使用make進行編譯的專案,
確保Makefile提供了類似EXTRA_CFLAGS的編譯引數, 此時可使用make EXTRA_CFLAGS+='-fplugin=/.../x.so -fplugin-arg-x-output=/.../...'
編譯
資訊收集, 是使用編譯器提供的介面, 提取原始碼編譯時的中間資訊, 進而對整個專案的資訊 進行彙總. 此功能實現在collect目錄下, 獨立於主程式, 需要在專案編譯時使用. 本質為 編譯器外掛.
關於GCC外掛的使用, 該框架處理的是low-level GIMPLE形式的語句, 它的形成過程是:
前端語言分析原始碼檔案, 生成前端語言的AST表示 將前端語言的AST表示轉換成GIMPLE中間表示
然後GCC對GIMPLE中間表示進行處理, 這些過程叫pass. 包括從高階GIMPLE轉換成低階GIMPLE(框 架使用的), IPA處理, GIMPLE優化, 最終由GIMPLE轉換成RTL等.
Pass根據處理的物件及功能的不同, 分為四大類: GIMPLE_PASS, RTL_PASS, SIMPLE_IPA_PASS, IPA_PASS. 其中GIMPLE_PASS以GIMPLE中間表示為處理物件, RTL_PASS的處理物件為RTL中間表示, SIMPLE_IPA_PASS和IPA_PASS處理物件也是GIMPLE中間表示, 但功能主要是過程間分析(IPA, Inter-Procedural Analysis). 例如如下幾個PASS(新版本GCC有些pass可能不存在了):
all_lowering_passes |--->useless[GIMPLE_PASS] |--->mudflap1[GIMPLE_PASS] |--->omplower[GIMPLE_PASS] |--->lower[GIMPLE_PASS] |--->ehopt[GIMPLE_PASS] |--->eh[GIMPLE_PASS] |--->cfg[GIMPLE_PASS] ... all_ipa_passes |--->visibility[SIMPLE_IPA_PASS] ...
對原始碼中每個定義的函式, 會先執行一遍all_lowering_passes中的處理過程, 也就是說, 在處理 到cfg pass的時候, 有些函式並未進行lower處理.
更多的GCC外掛資訊, 可以參考refs[1] refs[3], 或者查詢GCC原始碼.
當前實現的針對c原始碼檔案的資訊提取, 是gcc外掛, 在載入時會檢測當前編譯的檔名稱, 匹 配檔案路徑(主要針對核心原始碼結構進行的檢測), 註冊回撥函式, 在cfg pass執行之前呼叫回 調函式. 在PLUGIN_ALL_IPA_PASSES_START執行時將資料寫入檔案. 檔案大小使用PAGE_SIZE對齊.
這裡我們不能使用PLUGIN_PRE_GENERICIZE來處理每個tree_function_decl, 原因在於, gcc在 這個處理階段, 是流式的, 可能一個函式呼叫的函式還並未定義. 比如
static void test_func0(void); static void test_func1(void) { test_func0(); } static void test_func0(void) { /* test_func0 body */ }
這種情況, 處理test_func1時, 會跟入test_func0, 而此時的test_func0的tree_function_decl 結構體並未進行完整的初始化.
當gcc完成整個原始碼檔案的掃描工作, 所有的資料會被串起來(TREE_CHAIN), 我們需要儘可能的 避免資料的重複, 並保證資料的完整性.
當呼叫該pass的處理程式時, 處理物件為struct function, 其成員decl指向包含這個物件的 tree_function_decl. 這個過程會將tree_function_decl->saved_tree成員(為函式體)保留的 資訊轉換成GIMPLE語句儲存到tree_function_decl->f->gimple_body中. 此時, 由於所有的函式 已被串起來, 我們在跟蹤tree_function_decl的時候, 需要判斷這個結構是否已經完成AST-> GIMPLE的轉換, 如果還存在saved_tree, 不處理這個函式, 後續轉換這個函式時會完成該函式 的資訊收集.
對於tree_var_decl(變數), 我們著重需要記錄的是非區域性變數的資訊. 在gcc的實現中, 函式 is_global_var的實現(gcc/gcc/tree.h)如下:
static inline bool is_global_var (const_tree t) { return (TREE_STATIC(t) || DECL_EXTERNAL(t)); }
TREE_STATIC對於變數來說, 表示這個函式是否使用靜態儲存區, 如果使用, 表明這個變數is global variable. DECL_EXTERNAL表示該變數是否是外部引用. 當前實現的c.cc在此基礎上添 加了一個檢測:
if (is_global_var(node) && ((!DECL_CONTEXT(node)) || (TREE_CODE(DECL_CONTEXT(node)) == TRANSLATION_UNIT_DECL))) { objs[start].is_global_var = 1; }
DECL_CONTEXT為空或者為TRANSLATION_UNIT_DECL時, 表示該變數為函式外變數.
另外, 需要記錄每個物件的指標, 每個位置資訊等.
該collect/c.cc在linux kernel 4.14.x上的測試顯示, make vmlinux -j9耗時大致為20分鐘, 提取出的資訊檔案為19.4G.
4 - 資訊解析
解析是框架需要實現的主要功能之一, 也是最複雜的. 我們需要完成提取的專案資訊的整理和 分類, 構建索引資訊, 方便後續的使用.
該實現主要在plugin/getinfo.c中, 其對每個編譯檔案的資訊進行檢測, 檢視該檔案編譯類 型(enum si_lang_type), 查詢是否有對應的註冊函式(struct lang_ops), 並依次呼叫.
針對大型專案的解析, 為了有更好的體驗, 框架使用ncurses來提供進度條提示(框架編譯時使 用make ver=release).
資訊解析過程, 需要將提取資訊檔案(稱為resfile)載入到記憶體. 而在linux kernel 4.14.x中 的測試, 生成的resfile達到19G之多, 無法一次載入到記憶體中, 且後續的資訊使用亦需要這些 資訊, 解決方案如下:
自定義程序的記憶體佈局. 在RESFILE_BUF_START位置開始載入resfile檔案, 對每個編 譯檔案生成一個sibuf結構體, 記錄檔案的載入位置, 載入大小, 以及載入的資料在 檔案中的偏移等資訊. 當載入到記憶體的檔案大小超過RESFILE_BUF_SIZE時, 呼叫munmap取消之前的載入, 繼 續載入後續的資料. 當需要讀取之前已經munmap的資料時, 只需要呼叫resfile__resfile_load函式, 將 sibuf對應的檔案資料載入到適當的記憶體位置, 即可直接進行讀寫. 記憶體佈局如下圖: 0x0--- 0x400000NULL pages 0x400000--- 0x403000si_core指令 0x602000--- 0x603000si_core資料 0x603000--- 0x605000si_core資料 0x605000--- 0x647000heap SRC_BUF_START--- RESFILE_BUF_START索引資訊區域 RESFILE_BUF_START--- 0x????????resfile載入區域 0x700000000000--- 0x7fffffffffff執行緒 動態庫 plugins 程序棧 當SRC_BUF_START為0x100000000, RESFILE_BUF_START為0x1000000000時, 索引資訊最 高可達到64G, resfile可處理高達1024G(末端到達0x100 0000 0000)的資料檔案.
4.1 - 資訊調整
資訊收集階段生成的檔案, 因為包含了很多指標, si_core程序並不能直接讀寫其中的資訊, 需 要先完成適當的指標轉換. 此過程需要與collect/x.cc對應. 名為PHASE1前半部分.
讀取函式資訊, 對其中包含的指標資料修改之後, 依次對每個物件進行訪問並調整指標資料. 同
時, 對location結構, 由於location_t只有4位元組大小, 設定其為sibuf->payload的偏移位置,
於是*(expanded_location *)(sibuf->payload + loc)
即可獲取需要的位置資訊.
當所有物件調整完畢, 即完成PHASE1, 設定完檔案狀態資訊, 返回.
4.2 - 基礎資訊獲取
PHASE1後半部分, 提取每個檔案的基礎資訊: 有哪些定義了的函式, 非區域性變數, 型別.
函式分TYPE_FUNC_GLOBAL和TYPE_FUNC_STATIC,
非區域性變數分TYPE_VAR_GLOBAL和TYPE_VAR_STATIC,
型別分TYPE_TYPE_LOC和TYPE_TYPE_NAME.
如果型別為TYPE_FUNC_GLOBAL/TYPE_VAR_GLOBAL/TYPE_TYPE_NAME, 為名稱索引
如果型別為TYPE_FUNC_STATIC/TYPE_VAR_STATIC/TYPE_TYPE_LOC, 為位置索引
如果型別為TYPE_NONE, 該節點為tree_type_non_common, 放入sibuf->type_nodes中.
首先查詢當前sinodes中, 是否有重複的節點, 如果存在, 為位置索引時則進行下次迴圈, 為名 稱索引時需要檢測名稱衝突, 比如對於TYPE_*_GLOBAL, 存在weak symbol.
生成新的sinode, 根據當前得到的位置 名稱等資訊, 完成初始化.
4.3 - 詳細資訊獲取
PHASE2, 獲取每個sinode節點的詳細資訊.
對於型別, 需要獲取該型別指向的型別, 或者型別的大小, 型別的成員等資訊.
對於變數, 當前只獲取了變數是什麼型別.
對於函式, 首先獲取返回值型別, 然後處理引數列表(以var_node_list表示), 然後獲取函式體( 以code_path表示).
關於函式體的表示, 以label為分隔點, label之後的語句為一個code_path, 直到另一個label或者到GIMPLE_SWITCH/GIMPLE_GOTO/GIMPLE_COND/含nl的GIMPLE_ASM語句. 同時會檢測該函式中是否有不可到達的語句, 比如:
static int test_func(void) { int err = 0; if (err) return 1; else return 0; return 0;/* not reachable */ }
4.4 - 交叉引用資訊整理
PHASE3, 處理非區域性變數的初始化值, 設定每個變數的可能值(possible_value_list).
對每個函式(除了直接呼叫)和變數的使用位置(use_at_list)進行標記. 並獲取直接呼叫資訊.
每個函式的呼叫均由GIMPLE_CALL語句表示, 其第一個運算元為返回值, 第二個運算元可能為函 數地址, 也可能為VAR_DECL/PARM_DECL等, 後面的運算元為函式的實際引數. 直接呼叫即為第二 個運算元為函式地址的情形. 記錄的函式使用位置是除第二個運算元外的所有引用位置.
PHASE3暫時還未通過linux kernel 4.14.x的vmlinux的resfile檢測, 存在一些未考慮到的情形.
4.5 - 間接呼叫1
PHASE4, 處理被標記了的函式, 即表示該函式存在除直接呼叫之外的引用情形.
如果引用語句為GIMPLE_ASSIGN(賦值語句), 獲取左值的var_node, 新增possible_value.
4.6 - 間接呼叫2
PHASE5, 處理GIMPLE_CALL語句的第二個引數為VAR_DECL/PARM_DECL的情形.
如果為VAR_DECL情形, 獲取變數的var_node, 檢視possible_value_list, 新增呼叫關係;
然後檢視該變數的use_at_list, 檢測對其的賦值, 然後遞迴跟蹤.
例如:
static void test_func0(void); static void test_func1(void) { void (*testf)(void); testf = test_func0; testf(); }
或:
static void test_func0(void); struct test_a { int a; void (*b)(void); }; static struct test_a static_a = { .a = 1, .b = test_func0, }; static void test_func1(void) { static_a.b(); }
可以得到test_func1呼叫了test_func0的結果.
暫時未提供處理PARM_DECL呼叫情形的功能.
5 - 資訊使用
對索引資訊的使用, 可以有多種可能. 由於我們收集的資訊是lower-level形式的GIMPLE語句, 所以編寫的框架plugin處理的物件也是lower GIMPLE語句.
這裡以未初始化變數引用的某種情形進行簡單說明
例如如下程式碼:
static void test_func(int flag) { int need_free; char *buf; if (flag) { buf = (char *)malloc(0x10); need_free = 1; } /* do something here */ if (need_free) free(buf); }
plugins/uninit.cc顯示瞭如何檢測這種形式的問題.
該plugin會對所有的函式依次進行檢測, 首先, 呼叫utils__gen_code_path獲取該函式所有可能的執行流, 然後:
獲取該函式使用的一個區域性變數(不包含函式內的static變數) 遍歷所有執行流, 檢視該區域性變數的第一個引用位置 如果第一個引用位置的操作是讀取該變數, 則存在未初始化變數引用的情形.
檢視檢測ofollow,noindex" target="_blank">視訊
6 - 後續
- 對linux kernel生成的resfile的完整支援
- 完全通過所有解析步驟
- .s/.S檔案中包含的符號提取, 完善呼叫鏈.
- 對添加了核心防護的版本的辨識
- 變數等資料的跨函式的追蹤
- 標記資料引用位置的操作, 比如讀還是寫, 或者取地址等
- 對使用者層應用的解析的完善, 比如很多符號都是外部庫的
- 跨函式的所有執行流的生成
- 執行流之間的依賴關係, 比如sys_read函式的呼叫需要sys_open的輸出
- 驗證樣本的自動生成
- 某些形式的程式碼問題的補丁自動生成
- 其他程式語言的支援
歡迎各種建議以及新的思路, 當前, 提交請求(Push Request)也是極好的.
7 - 引用
[1]GNU Compiler Collection Internals
[3]深入分析GCC