编译源代码会生成二进制文件。在编译期间,您可以为编译器提供标志,以启用或禁用二进制文件上的某些属性。其中一些属性与安全性相关。
Checksec 是一个漂亮的小工具(和一个 shell 脚本),除其他功能外,它还可以识别二进制文件在编译时内置的安全属性。编译器可能会默认启用其中一些属性,您可能需要提供特定的标志来启用其他属性。
本文解释了如何使用 checksec 识别二进制文件上的安全属性,包括:
- checksec 用于查找安全属性信息的底层命令
- 在使用 GNU 编译器集合 (GCC) 编译示例二进制文件时,如何启用安全属性
安装 checksec
要在 Fedora 和其他基于 RPM 的系统上安装 checksec,请使用
$ sudo dnf install checksec
对于基于 Debian 的发行版,请使用等效的 apt
命令。
shell 脚本
Checksec 是一个单文件 shell 脚本,尽管它相当大。一个优点是您可以快速阅读脚本,并理解所有用于查找有关二进制文件或可执行文件的信息的系统命令
$ file /usr/bin/checksec
/usr/bin/checksec: Bourne-Again shell script, ASCII text executable, with very long lines
$ wc -l /usr/bin/checksec
2111 /usr/bin/checksec
使用您可能每天运行的二进制文件来试用 checksec:无处不在的 ls
命令。该命令的格式为 checksec --file=
,后跟 ls
二进制文件的绝对路径
$ checksec --file=/usr/bin/ls
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 5 17 /usr/bin/ls
当您在终端中运行此命令时,您会看到颜色编码,指示哪些是好的,哪些可能不好。我说“可能”是因为即使某些东西是红色的,也并不一定意味着情况很糟糕——这可能只是意味着发行版供应商在编译二进制文件时做出了一些权衡。
第一行提供了通常可用于二进制文件的各种安全属性,例如 RELRO
、STACK CANARY
、NX
等(我在下面详细解释)。第二行显示给定二进制文件(在本例中为 ls
)的这些属性的状态。例如,NX enabled
表示此二进制文件已启用某些属性。
示例二进制文件
在本教程中,我将使用以下“hello world”程序作为示例二进制文件。
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
请注意,我在编译期间没有为 gcc
提供任何额外的标志
$ gcc hello.c -o hello
$ file hello
hello: 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
$ ./hello
Hello World
通过 checksec 运行二进制文件。某些属性与上面的 ls
命令不同(在您的屏幕上,这些属性可能会显示为红色)
$ checksec --file=./hello
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 85) Symbols No 0 0./hello
$
更改输出格式
Checksec 允许各种输出格式,您可以使用 --output
指定。我将选择 JSON 格式并将输出管道传输到 jq
实用程序以进行漂亮的打印。
为了方便学习,请确保您已安装 jq
,因为本教程使用此输出格式从输出中快速 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"
}
}
浏览安全属性
上面的二进制文件包含多个安全属性。我将把该二进制文件与上面的 ls
二进制文件进行比较,以检查启用了哪些属性,并解释 checksec 如何找到这些信息。
1. 符号 (Symbols)
我将从简单的开始。在编译期间,某些符号包含在二进制文件中,主要用于调试。当您开发软件并需要多个周期进行调试和修复时,这些符号是必需的。
在最终二进制文件发布供通用使用之前,通常会从其中剥离(删除)这些符号。这不会以任何方式影响二进制文件的执行;它将像使用符号一样运行。剥离通常是为了节省空间,因为一旦剥离符号,二进制文件会稍微轻一些。在闭源或专有软件中,通常会删除符号,因为在二进制文件中包含这些符号会使其相对容易推断软件的内部工作原理。
根据 checksec,此二进制文件中存在符号,但在 ls
二进制文件中不存在。您还可以通过在程序上运行 file
命令来找到此信息——您会在末尾的输出中看到 not stripped
$ checksec --file=/bin/ls --output=json | jq | grep symbols
"symbols": "no",
$ checksec --file=./hello --output=json | jq | grep symbols
"symbols": "yes",
$ file hello
hello: 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 是如何找到此信息的?它提供了一个方便的 --debug
选项来显示运行了哪些函数。因此,运行以下命令应显示 shell 脚本中运行了哪些函数
$ checksec --debug --file=./hello
在本教程中,我正在寻找用于查找此信息的底层命令。由于它是一个 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
标志,该标志列出二进制文件中的所有符号。然后,它 grep 一个特殊值 .symtab
,该值提供它找到的条目(符号)的计数。您可以尝试对您上面编译的测试二进制文件执行以下命令
$ readelf -W --symbols ./hello
$ readelf -W --symbols ./hello | grep -i symtab
如何剥离符号
您可以在编译后或编译期间剥离符号。
- 编译后: 编译后,您可以使用二进制文件上的
strip
实用程序删除符号。使用file
命令确认它是否有效,该命令现在将输出显示为stripped
$ gcc hello.c -o hello $ $ file hello hello: 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 hello hello: 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 -s hello.c -o hello
$
$ file hello
hello: 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 后,您可以看到 symbols
显示为 no
$ checksec --file=./hello --output=json | jq | grep symbols
"symbols": "no",
$
2. 金丝雀 (Canary)
金丝雀是在栈上的缓冲区和控制数据之间放置的已知值,用于监视缓冲区溢出。当应用程序执行时,会为其分配两种类型的内存。其中一种是栈,它只是一个具有两个操作的数据结构:push
,将数据放入栈中;pop
,以相反的顺序从栈中删除数据。恶意输入可能会使用精心制作的输入溢出或破坏栈,并导致程序崩溃
$ checksec --file=/bin/ls --output=json | jq | grep canary
"canary": "yes",
$
$ checksec --file=./hello --output=json | jq | grep canary
"canary": "no",
$
checksec 如何找出二进制文件是否启用了金丝雀?使用上述方法,您可以通过在 shell 脚本中运行以下命令来缩小范围
$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'
启用金丝雀
为了防止这些情况,编译器提供了 -stack-protector-all
标志,该标志向二进制文件添加额外的代码,以检查此类缓冲区溢出
$ gcc -fstack-protector-all hello.c -o hello
$ checksec --file=./hello --output=json | jq | grep canary
"canary": "yes",
Checksec 显示该属性现在已启用。您还可以使用以下命令进行验证:
$ 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
PIE 代表位置无关可执行文件 (position-independent executable)。顾名思义,它是放置在内存中某处执行的代码,而与其绝对地址无关
$ checksec --file=/bin/ls --output=json | jq | grep pie
"pie": "yes",
$ checksec --file=./hello --output=json | jq | grep pie
"pie": "no",
通常,PIE 仅对库启用,而不对独立的命令行程序启用。在下面的输出中,hello
显示为 LSB executable
,而 libc
标准库 (.so
) 文件标记为 LSB shared object
$ file hello
hello: 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 尝试使用以下命令查找此信息
$ readelf -W -h ./hello | grep EXEC
Type: EXEC (Executable file)
如果您在共享库而不是 EXEC
上尝试相同的命令,您将看到 DYN
$ readelf -W -h /lib64/libc-2.32.so | grep DYN
Type: DYN (Shared object file)
启用 PIE
要在测试程序上启用 PIE,请将以下参数发送给编译器
$ gcc -pie -fpie hello.c -o hello
您可以使用 checksec 验证 PIE 是否已启用
$ checksec --file=./hello --output=json | jq | grep pie
"pie": "yes",
$
它应显示为 PIE 可执行文件,类型从 EXEC
更改为 DYN
$ file hello
hello: 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 使用以下命令确定此信息。末尾的 RW
表示栈是可读写的;由于没有 E
,因此它是不可执行的
$ readelf -W -l ./hello | grep GNU_STACK
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
出于演示目的禁用 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
RELRO 代表重定位只读 (Relocation Read-Only)。可执行链接格式 (ELF) 二进制文件使用全局偏移表 (GOT) 动态解析函数。启用后,此安全属性使二进制文件中的 GOT 变为只读,从而防止某些形式的重定位攻击
$ checksec --file=/bin/ls --output=json | jq | grep relro
"relro": "full",
$ checksec --file=./hello --output=json | jq | grep relro
"relro": "partial",
Checksec 使用以下命令查找此信息。在此处,启用了 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
$ 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
启用它作为供您解决的练习。
$ 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 --dir=/usr/bin
进程
除了二进制文件外,checksec 也适用于执行期间的程序。以下命令查找系统上所有正在运行的程序的安全属性。如果您想检查所有正在运行的进程,可以使用 --proc-all
,或者您可以使用进程名称选择特定进程
$ checksec --proc-all
$ checksec --proc=bash
内核属性
除了本文中描述的 checksec 的用户空间应用程序外,您还可以使用它来检查内置到系统中的内核属性
$ checksec --kernel
试一试
Checksec 是了解启用了哪些用户空间和内核属性的好方法。详细了解每个安全属性,并尝试理解启用每个功能的原因以及它可以防止的攻击类型。
评论已关闭。