
編譯源代碼會生成一個二進制文件(LCTT 譯注:即 ??.o?
?? 文件)。在編譯期間,你可以向 ??gcc?
? 編譯器提供 標志flags,以啟用或禁用二進制文件的某些屬性,這些屬性與安全性相關。
Checksec 是一個漂亮的小工具,同時它也是一個 shell 腳本。Checksec 可以識別編譯時構建到二進制文件中的安全屬性。編譯器可能會默認啟用一些安全屬性,你也可以提供特定的標志,來啟用其他的安全屬性。
本文將介紹如何使用 Checksec ,來識別二進制文件的安全屬性,包括:
- Checksec 在查找有關安全屬性的信息時,使用了什么底層的命令
- 在將源代碼編譯成二進制文件時,如何使用GNU 編譯器套件GNU Compiler Collection(即 GCC)來啟用安全屬性。
安裝 checksec
要在 Fedora 和其他基于 RPM 的 Linux 系統上,安裝 Checksec,請使用以下命令:
$ sudo dnf install checksec
對于基于 Debian 的 Linux 發行版,使用對應的 ??apt?
? 命令,來安裝 Checksec。
$ sudo apt install checksec
shell 腳本
在安裝完 Checksec 后,能夠發現 Checksec 是一個單文件的 shell 腳本,它位于 ??/usr/bin/checksec?
?,并且這個文件挺大的。Checksec 的一個優點是你可以通過快速通讀這個 shell 腳本,從而了解 Checksec 的執行原理、明白所有能查找有關二進制文件或可執行文件的安全屬性的系統命令:
$ file /usr/bin/checksec/usr/bin/checksec: Bourne-Again shell script, ASCII text executable, with very long lines$ wc -l /usr/bin/checksec2111 /usr/bin/checksec
以下的命令展示了如何對你每天都會使用的:??ls?
? 命令的二進制文件運行 Checksec。Checksec 命令的格式是:??checksec --file=?
?,后面再跟上二進制文件的絕對路徑:
$ checksec --file=/usr/bin/lsRELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILEFull RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 5 17 /usr/bin/ls
當你在終端中對某個二進制文件運行 Checksec 時,你會看到安全屬性有顏色上的區分,顯示什么是好的安全屬性(綠色),什么可能不是好的安全屬性(紅色)。我在這里說 “可能” 是因為即使有些安全屬性是紅色的,也不一定意味著這個二進制文件很糟糕,它可能只是表明發行版供應商在編譯二進制文件時做了一些權衡,從而舍棄了部分安全屬性。
Checksec 輸出的第一行提供了二進制文件的各種安全屬性,例如 ??RELRO?
?、??STACK CANARY?
?、??NX?
? 等(我將在后文進行詳細解釋)。第二行打印出給定二進制文件(本例中為 ??ls?
?)在這些安全屬性的狀態(例如,??NX enabled?
? 表示為堆棧中的數據沒有執行權限)。
示例二進制文件
在本文中,我將使用以下的 “hello world” 程序作為示例二進制文件。
#include <stdio.h>int main(){ printf("Hello World\n"); return 0;}
請注意,在編譯源文件 ??hello.c?
? 的時候,我沒有給 ??gcc?
? 提供任何額外的標志:
$ gcc hello.c -o hello $ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped$ ./helloHello World
使用 Checksec 運行二進制文件 ??hello?
?,打印的某些安全屬性的狀態,與上面的 ??ls?
? 二進制文件的結果不同(在你的屏幕上,某些屬性可能顯示為紅色):
$ checksec --file=./helloRELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILEPartial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 85) Symbols No 0 0./hello$
(LCTT 譯注:在我的 Ubuntu 22.04 虛擬機,使用 11.3.0 版本的 ??gcc?
?,結果與上述不太相同,利用默認參數進行編譯,會得到 RELRO、PIE、NX 保護是全開的情況。)
更改 Checksec 的輸出格式
Checksec 允許自定義各種輸出格式,你可以使用 ??--output?
? 來自定義輸出格式。我將選擇的輸出格式是 JSON 格式,并將輸出結果通過管道傳輸到 ??jq?
? 實用程序,來得到漂亮的打印。
接下來,確保你已安裝好了 ??jq??,因為本教程會使用 ??jq?
? 從 Checksec 的輸出結果中,用 ??grep?
? 來快速得到某一特定的安全屬性狀態,并報告該安全屬性是否啟動(啟動為 ??yes?
?,未啟動為 ??no?
?):
$ checksec --file=./hello --output=json | jq{ "hello": { "relro": "partial", "canary": "no", "nx": "yes", "pie": "no", "rpath": "no", "runpath": "no", "symbols": "yes", "fortify_source": "no", "fortified": "0", "fortify-able": "0" }}
看一看所有的安全屬性
上面的二進制文件 ??hello?
? 包括幾個安全屬性。我將該二進制文件與 ??ls?
? 的二進制文件進行比較,以檢查啟用的安全屬性有何不同,并解釋 Checksec 是如何找到此信息。
1、符號(Symbol)
我先從簡單的講起。在編譯期間,某些 符號symbols包含在二進制文件中,這些符號主要用作于調試。開發軟件時,需要用到這些符號,來調試和修復錯誤。
這些符號通常會從供用戶普遍使用的最終二進制文件中刪除。刪除這些符號不會影響到二進制文件的執行。刪除符號通常是為了節省空間,因為一旦符號被刪除了,二進制文件就會稍微小一些。在閉源或專有軟件中,符號通常都會被刪除,因為把這些符號放在二進制文件中,可以很容易地推斷出軟件的內部工作原理。
根據 Checksec 的結果,在二進制文件 ??hello?
? 中有符號,但在 ??ls?
? 的二進制文件中不會有符號。同樣地,你還可以用 ??file?
? 命令,來找到符號的信息,在二進制文件 ??hello?
? 的輸出結果的最后,看到 ??not stripped?
?,表明二進制文件 ??hello?
? 有符號:
$ checksec --file=/bin/ls --output=json | jq | grep symbols "symbols": "no",$ checksec --file=./hello --output=json | jq | grep symbols "symbols": "yes",$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped
Checksec 是如何找到符號的信息呢?Checksec 提供了一個方便的 ??--debug?
? 選項,來顯示運行了哪些函數。因此,運行以下的命令,會顯示在 shell 腳本中運行了哪些函數:
$ checksec --debug --file=./hello
在本教程中,我試圖尋找 Checksec 查找安全屬性信息時,使用了什么底層命令。由于 Checksec 是一個 shell 腳本,因此你始終可以使用 Bash 功能。以下的命令將輸出從 shell 腳本中運行的每個命令:
$ bash -x /usr/bin/checksec --file=./hello
如果你滾動瀏覽上述的輸出結果的話,你會看到 ??echo_message?
? 后面有各個安全屬性的類別。以下顯示了 Checksec 檢測二進制文件是否包含符號時,運行的底層命令:
+ readelf -W --symbols ./hello+ grep -q '\\.symtab'+ echo_message '\033[31m96) Symbols\t\033[m ' Symbols, ' symbols="yes"' '"symbols":"yes",'
上面的輸出顯示,Checksec 利用 ??readelf?
?,來讀取二進制文件,并提供一個特殊 ??--symbols?
? 標志,來列出二進制文件中的所有符號。然后它會查找一個特殊值:??.symtab?
?,它提供了所能找到的條目的計數(即符號的個數)。你可以在上面編譯的測試二進制文件 ??hello?
? 上,嘗試以下命令,得到與 Checksec 查看二進制文件類似的符號信息:
$ readelf -W --symbols ./hello$ readelf -W --symbols ./hello | grep -i symtab
(LCTT 譯注:也可以通過直接查看 ??/usr/bin/checksec?
? 下的 Checksec 源文件。)
如何刪除符號
你可以在編譯后或編譯時刪除符號。
- 編譯后: 在編譯后,你可以使用
strip
,手動地來刪除二進制文件的符號。刪除后,使用 file
命令,來檢驗是否還有符號,現在顯示 stripped
,表明二進制文件 hello
無符號了:
$ gcc hello.c -o hello$$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, not stripped$$ strip hello$$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, stripped$
- 編譯時: 你也可以在編譯時,用
-s
參數讓 gcc 編譯器幫你自動地刪除符號:
$ gcc -s hello.c -o hello$$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=247de82a8ad84e7d8f20751ce79ea9e0cf4bd263, for GNU/Linux 3.2.0, stripped$
重新運行 Checksec,你可以看到現在二進制文件 ??hello?
? 的 ??symbols?
? 這一屬性的值是??no?
?:
$ checksec --file=./hello --output=json | jq | grep symbols "symbols": "no",$
2、Canary(堆棧溢出哨兵)
Canary 是放置在緩沖區和 棧stack 上的控制數據之間的已知值,它用于監視緩沖區是否溢出。當應用程序執行時,會為其分配兩種內存,其中之一就是 棧。棧是一個具有兩個操作的數據結構:第一個操作 ??push?
??,將數據壓入堆棧;第二個操作 ??pop?
?,以后進先出的順序從棧中彈出數據。惡意的輸入可能會導致棧溢出,或使用特制的輸入破壞棧,并導致程序崩潰:
$ checksec --file=/bin/ls --output=json | jq | grep canary "canary": "yes",$$ checksec --file=./hello --output=json | jq | grep canary "canary": "no",$
Checksec 是如何確定二進制文件是否啟用了 Canary 的呢?使用上述同樣的方法,得到 Checksec 在檢測二進制文件是否啟用 Canary 時,運行的底層命令:
$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'
啟用 Canary
為了防止棧溢出等情況,編譯器提供了 ??-stack-protector-all?
? 標志,它向二進制文件添加了額外的代碼,來檢查緩沖區是否溢出:
$ gcc -fstack-protector-all hello.c -o hello$ checksec --file=./hello --output=json | jq | grep canary "canary": "yes",
Checksec 顯示 Canary 屬性現已啟用。你還可以通過以下方式,來驗證這一點:
$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie' 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3) 83: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@@GLIBC_2.4$
3、位置無關可執行文件(PIE)
位置無關可執行文件Position-Independent Executable(PIE),顧名思義,它指的是放置在內存中某處執行的代碼,不管其絕對地址的位置,即代碼段、數據段地址隨機化(ASLR):
$ checksec --file=/bin/ls --output=json | jq | grep pie "pie": "yes",$ checksec --file=./hello --output=json | jq | grep pie "pie": "no",
通常,PIE 僅對 庫libraries啟用,并不對獨立命令行程序啟用 PIE。在下面的輸出中,??hello?
?? 顯示為 ??LSB executable?
??,而 ??libc?
?? 標準庫(??.so?
??) 文件被標記為 ??LSB shared object?
?:
$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped$ file /lib64/libc-2.32.so/lib64/libc-2.32.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4a7fb374097fb927fb93d35ef98ba89262d0c4a4, for GNU/Linux 3.2.0, not stripped
Checksec 查找是否啟用 PIE 的底層命令如下:
$ readelf -W -h ./hello | grep EXEC Type: EXEC (Executable file)
如果你在共享庫上嘗試相同的命令,你將看到 ??DYN?
??,而不是 ??EXEC?
?:
$ readelf -W -h /lib64/libc-2.32.so | grep DYN Type: DYN (Shared object file)
啟用 PIE
要在測試程序 ??hello.c?
? 上啟用 PIE,請在編譯時,使用以下命令:
$ gcc -pie -fpie hello.c -o hello`
你可以使用 Checksec,來驗證 PIE 是否已啟用:
$ checksec --file=./hello --output=json | jq | grep pie "pie": "yes",$
現在,應該會顯示為 “PIE 可執行pie executable”,其類型從 ??EXEC?
?? 更改為 ??DYN?
?:
$ file hellohello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bb039adf2530d97e02f534a94f0f668cd540f940, for GNU/Linux 3.2.0, not stripped$ readelf -W -h ./hello | grep DYN Type: DYN (Shared object file)
4、NX(堆棧禁止執行)
NX 代表 不可執行non-executable。它通常在 CPU 層面上啟用,因此啟用 NX 的操作系統可以將某些內存區域標記為不可執行。通常,緩沖區溢出漏洞將惡意代碼放在堆棧上,然后嘗試執行它。但是,讓堆棧這些可寫區域變得不可執行,可以防止這種攻擊。在使用 ??gcc?
? 對源程序進行編譯時,默認啟用此安全屬性:
$ checksec --file=/bin/ls --output=json | jq | grep nx "nx": "yes",$ checksec --file=./hello --output=json | jq | grep nx "nx": "yes",
Checksec 使用以下底層命令,來確定是否啟用了 NX。在尾部的 ??RW?
?? 表示堆棧是可讀可寫的;因為沒有 ??E?
?,所以堆棧是不可執行的:
$ readelf -W -l ./hello | grep GNU_STACK GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
演示如何禁用 NX
我們不建議禁用 NX,但你可以在編譯程序時,使用 ??-z execstack?
? 參數,來禁用 NX:
$ gcc -z execstack hello.c -o hello$ checksec --file=./hello --output=json | jq | grep nx "nx": "no",
編譯后,堆棧會變為可讀可寫可執行(??RWE?
?),允許在堆棧上的惡意代碼執行:
$ readelf -W -l ./hello | grep GNU_STACK GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10
5、RELRO(GOT 寫保護)
RELRO 代表 “重定位只讀Relocation Read-Only”。可執行鏈接格式(ELF)二進制文件使用全局偏移表(GOT)來動態地解析函數。啟用 RELRO 后,會設置二進制文件中的 GOT 表為只讀,從而防止重定位攻擊:
$ checksec --file=/bin/ls --output=json | jq | grep relro "relro": "full",$ checksec --file=./hello --output=json | jq | grep relro "relro": "partial",
Checksec 使用以下底層命令,來查找是否啟用 RELRO。在二進制文件 ??hello?
?? 僅啟用了 RELRO 屬性中的一個屬性,因此,在 Checksec 驗證時,顯示 ??partial?
?:
$ readelf -W -l ./hello | grep GNU_RELRO GNU_RELRO 0x002e10 0x0000000000403e10 0x0000000000403e10 0x0001f0 0x0001f0 R 0x1$ readelf -W -d ./hello | grep BIND_NOW
啟用全 RELRO
要啟用全 RELRO,請在 ??gcc?
? 編譯時,使用以下命令行參數:
$ gcc -Wl,-z,relro,-z,now hello.c -o hello$ checksec --file=./hello --output=json | jq | grep relro "relro": "full",
現在, RELRO 中的第二個屬性也被啟用,使程序變成全 RELRO:
$ readelf -W -l ./hello | grep GNU_RELRO GNU_RELRO 0x002dd0 0x0000000000403dd0 0x0000000000403dd0 0x000230 0x000230 R 0x1$ readelf -W -d ./hello | grep BIND_NOW 0x0000000000000018 (BIND_NOW)
6、Fortify
Fortify 是另一個安全屬性,但它超出了本文的范圍。Checksec 是如何在二進制文件中驗證 Fortify,以及如何在 ??gcc?
? 編譯時啟用 Fortify,作為你需要解決的課后練習。
$ checksec --file=/bin/ls --output=json | jq | grep -i forti "fortify_source": "yes", "fortified": "5", "fortify-able": "17"$ checksec --file=./hello --output=json | jq | grep -i forti "fortify_source": "no", "fortified": "0", "fortify-able": "0"
其他的 Checksec 功能
關于安全性的話題是永無止境的,不可能在本文涵蓋所有關于安全性的內容,但我還想提一下 Checksec 命令的一些其他功能,這些功能也很好用。
對多個二進制文件運行 Checksec
你不必對每個二進制文件都進行一次 Checksec。相反,你可以提供多個二進制文件所在的目錄路徑,Checksec 將一次性為你驗證所有文件:
對進程運行 Checksec
Checksec 除了能檢查二進制文件的安全屬性,Checksec 還能對程序起作用。以下的命令用于查找你系統上所有正在運行的程序的安全屬性。如果你希望 Checksec 檢查所有正在運行的進程,可以使用 ??--proc-all?
?,或者你也可以使用進程名稱,選擇特定的進程進行檢查:
$ checksec --proc-all$ checksec --proc=bash
對內核運行 Checksec
除了本文介紹的用 Checksec 檢查用戶態應用程序的安全屬性之外,你還可以使用它來檢查系統內置的 內核屬性kernel properties:
快來試一試 Checksec 吧
Checksec 是一個能了解哪些用戶空間和內核的安全屬性被啟用的好方法。現在,你就可以開始使用 Checksec,來了解每個安全屬性是什么,并明白啟用每個安全屬性的原因,以及它能阻止的攻擊類型。