checksec项目开源 https://github.com/slimm609/checksec.sh
checksec是一个用于检测文件、进程、内核安全特性的开源项目,开源在使用shell脚本编写,主要应用在Linux平台。
根据项目主页描述,checksec项目最早由漏洞挖掘专家Tobias Klein编写,目前该项目主要由slimm609、teoberi等人维护。C语言重写项目开源 https://github.com/fuxxcss/checksecc建议下载main版本
根据项目主页描述,checksecc是checksec的C语言重写版,并给出了详细的安装和使用教程。项目全部由C语言和Makefile编写,适合C语言的初学者学习。
本章节将简要分析checksec检测文件的原理,重点分析使用C语言重写该项目的逻辑。下文以checksec.sh和checksecc来区分二者。
checksec.sh文件检测
checksec.sh文件检测有以下五个用法。
## Checksec Options
--file={file}
--dir={directory}
--listfile={text file with one file per line}
--fortify-file={executable-file}
--extended
file、dir、listfile选项的逻辑都是检测文件、文件列表的基础安全特性,最新版本支持以下11种。
- RELRO:got表只读保护,有No、Partial和Full三种情况,针对延迟绑定特性进行保护。
- STACK CANARY:栈中敏感数据防覆写保护。
- NX:代码不可写,数据不可执行。
- PIE:位置无关代码,配合ASLR阻止ROP攻击。
- RPATH、RUNPATH:文件是否使用了这种危险的环境变量。
- Symbols:符号信息。
- FORTIFY、Fortified、Fortifiable:危险函数是否被加固。
- FILE:被检测文件名。
fortify-file选项是对危险函数加固情况的具体说明,为了性能考虑,编译时默认不启用加固。该选项包含以下信息。
* FORTIFY_SOURCE support available (libc) # libc是否支持加固
* Binary compiled with FORTIFY_SOURCE support # 文件是否启用加固
# 对应函数
------ EXECUTABLE-FILE ------- . -------- LIBC --------
FORTIFY-able library functions | Checked function names
-------------------------------------------------------
fdelt_chk | __fdelt_chk
read | __read_chk
syslog_chk | __syslog_chk
fprintf_chk | __fprintf_chk
vsnprintf_chk | __vsnprintf_chk
fgets | __fgets_chk
strncpy | __strncpy_chk
snprintf_chk | __snprintf_chk
memset | __memset_chk
strncat_chk | __strncat_chk
memcpy | __memcpy_chk
fread | __fread_chk
sprintf_chk | __sprintf_chk
# 数量的总结
SUMMARY:
* Number of checked functions in libc
* Total number of library functions in the executable
* Number of FORTIFY-able functions in the executable
* Number of checked functions in the executable
* Number of unchecked functions in the executable
extended扩展选项在基础特性上增加了3个特性。
- SELFRANDO:Tor网络开发的,用于内存地址随机化。
- Clang CFI:Clang编译器插桩选项,用于控制流完整性检测。
- SafeStack:更强大的栈保护策略。
checksec.sh文件检测原理
checksec.sh检测文件的核心函数在源码 src/functions/filecheck.sh 中。基本逻辑是使用readelf工具对文件的程序头表、动态节、节头表、函数名进行字符匹配,来判断该文件是否开启了某一安全特性。
以检测RELRO为例,使用readelf检测文件的程序头表,是否有GNU_RELRO。( GNU_RELRO仅在加载文件时进行解析,而不映射在段中 )如果编译器配置了GNU_RELRO,则说明启用了RELRO保护策略,需要进一步判断是Partial还是Full。接着通过匹配动态节的输出信息,如果有BIND_NOW标志,则说明文件需要在加载时绑定,而非延迟绑定,即RELRO Full策略。
filecheck() {
# check for RELRO support
if [[ $(${readelf} -l "${1}" 2> /dev/null) =~ "no program headers" ]]; then
echo_message '\033[32mN/A \033[m ' 'N/A,' '<file relro="n/a"' " \"${1}\": { \"relro\":\"n/a\","
elif ${readelf} -l "${1}" 2> /dev/null | grep -q 'GNU_RELRO'; then
if ${readelf} -d "${1}" 2> /dev/null | grep -q 'BIND_NOW' || ! ${readelf} -l "${1}" 2> /dev/null | grep -q '\.got\.plt'; then
echo_message '\033[32mFull RELRO \033[m ' 'Full RELRO,' '<file relro="full"' " \"${1}\": { \"relro\":\"full\","
else
echo_message '\033[33mPartial RELRO\033[m ' 'Partial RELRO,' '<file relro="partial"' " \"${1}\": { \"relro\":\"partial\","
fi
else
echo_message '\033[31mNo RELRO \033[m ' 'No RELRO,' '<file relro="no"' " \"${1}\": { \"relro\":\"no\","
fi
...
}
checksecc文件检测
checksecc是checksec.sh项目的C重写,将fortify特性合并到了extended选项中。并且考虑到GCC、Clang一些安全特性都是通过插桩实现的,将CFI、SafeStack等统一合并到Sanitized,同时添加了Asan、Tsan等检测。
项目主页的例子,使用clang -fsanitize=address test.c -o test编译出test,检测test可得到如下输出。
> checkc --file=./test --extended
File ./test
RELRO Partial RELRO
STACK CANARY Canary found
NX NX enabled
PIE PIE enabled
RPATH NO RPATH
RUNPATH NO RUNPATH
Stripped Not Stripped
Sanitized asan Yes
Sanitized tsan NO
Sanitized msan NO
Sanitized lsan Yes
Sanitized ubsan Yes
Sanitized dfsan NO
Sanitized safestack NO
Sanitized cet-ibt NO
Sanitized cet-shadow-stack NO
Fortified FORTIFY SOURCE support available (/lib/x86_64-linux-gnu/libc.so.6) : Yes
Fortified Binary compiled with FORTIFY SOURCE support (./test) : Yes
Fortified __sprintf_chk Fortified
Fortified __longjmp_chk Fortified
Fortified __fprintf_chk Fortified
Fortified __vsprintf_chk Fortified
Fortified __snprintf_chk Fortified
Fortified __vsnprintf_chk Fortified
checksecc文件检测原理
使用C语言重写checksec.sh的重点在于,如何编程替代readelf工具。一种方案是使用原生库libbfd,libbfd是GNU Binutils的原生库,广泛应用于GCC、GDB,包括readelf等涉及二进制文件解析的工具中。
而checksecc通过编写一个适用于检测安全特性的最简加载器,来替代readelf/libbfd完成工作。加载器的核心实现在 srcs/loader.c 。每个文件通过loader解析到结构体Binary中,Binary中保存着解析时的映射地址、文件类型、架构、名称、大小、入口点。还有符号链表头、节链表头,最后是各种表头。
// include/loader.h
typedef struct Binary{
/* mmap address */
void *mem;
/* file format */
bin_type bin_type;
/* arch */
bin_arch bin_arch;
/* binary name */
const char *bin_name;
/* binary size */
uint64_t bin_size;
/* entry point */
uint64_t entry;
/* binary symbols */
Symbol *sym;
/* binary sections */
Section *sect;
/* binary headers */
Header *hd;
}Binary;
还是以检测RELRO为例,上文提到RELRO的检测需要程序头表和动态节,所以我们重点关注loader解析程序头表、节的实现。
loader解析程序头表、节
loader对程序头表的解析实现在函数load_elf_programhs中,参数传递Binary指针和程序头信息。该函数根据elf是32位还是64位,分别生成链表保存程序头表的信息。
// srcs/loader.c
void load_elf_programhs(Binary *elf,uint64_t *ph_info){
void *mem=elf->mem;
/* program headers addr */
uintptr_t ph_addr=(uintptr_t)mem+ph_info[0];
/* head */
Programh *ph=MALLOC(1,Programh);
elf->hd->Pxheader.ph=ph;
switch (elf->bin_type){
case BIN_TYPE_ELF32:
for(uint16_t ph_num=0;ph_num < ph_info[2];ph_num++){
uintptr_t ph32_addr=ph_addr+ph_num*ph_info[1];
E32_ph *ph32=(E32_ph*)ph32_addr;
Programh *new=MALLOC(1,Programh);
new->sgm_type=load_elf_programh_types(ph32->p_type);
new->sgm_vma=ph32->p_vaddr;
new->sgm_flag=ph32->p_flags;
ph->ph_next=new;
ph=new;
}
break;
case BIN_TYPE_ELF64:
for(uint16_t ph_num=0;ph_num < ph_info[2];ph_num++){
uintptr_t ph64_addr=ph_addr+ph_num*ph_info[1];
E64_ph *ph64=(E64_ph*)ph64_addr;
Programh *new=MALLOC(1,Programh);
new->sgm_type=load_elf_programh_types(ph64->p_type);
new->sgm_vma=ph64->p_vaddr;
new->sgm_flag=ph64->p_flags;
ph->ph_next=new;
ph=new;
}
}
/* tail */
ph->ph_next=NULL;
}
查看调用load_elf_programhs函数的load_elf函数,ph_info数组是根据elf文件头解析出来的三个信息:程序头表文件偏移e_phoff、每个程序头的大小e_phentsize和数量e_phnum。
void load_elf(Binary *elf,void *mem){
...
switch (elf->bin_type)
{
case BIN_TYPE_ELF32:
...
/* program header information */
ph_info[0]=elf32_fh->e_phoff;
ph_info[1]=elf32_fh->e_phentsize;
ph_info[2]=elf32_fh->e_phnum;
break;
case BIN_TYPE_ELF64:
...
/* program header information */
ph_info[0]=elf64_fh->e_phoff;
ph_info[1]=elf64_fh->e_phentsize;
ph_info[2]=elf64_fh->e_phnum;
break;
}
/* load headers */
load_elf_programhs(elf,mem,ph_info);
...
}
elf文件头有很多信息,参考图如下(图源 《Practical Binary Analysis》)。
loader解析节的逻辑类似,但是有两点注意。
- 需要先调用load_elf_section_shstrtab解析保存节名称字符串表的节shstrtab,获取每个节的名称。
/* we need .shstrtab first */
uintptr_t shstrtab_addr=load_elf_section_shstrtab(elf,sh_info);
- bss节是没有数据,但是有大小的,所以在后续读取节内容的时候要跳过bss节。
/* load section contents */
/* do not load .bss ,IT IS NOBITS */
if(strcmp(name,".bss") != 0){
/* plus one for '\0' */
uint8_t *bytes=MALLOC(size+1,uint8_t);
new->sect_bytes=bytes;
for(uint64_t offset=0;offset < size;offset++)
bytes[offset]=*(uint8_t*)(sc_addr+offset);
bytes[size]='\0';
}
checksecc检测RELRO
loader完成解析任务后,交给chk_file执行检测任务。checksecc检测文件的核心逻辑在 srcs/chk_file.c 。
检测一个elf文件的逻辑在函数chk_file_one_elf中,这里使用函数指针的形式管理所有检测函数,在没有指定extended选项的情况下,chk_file_one_elf会检测以下的安全特性。
chk_info *chk_file_one_elf(Binary *elf){
/* We have 8 basic check functions */
char *(*chk_basic_func[CHK_BAS_NUM])(Binary*)={
chk_elf_name,
chk_elf_relro,
chk_elf_stack_canary,
chk_elf_nx,
chk_elf_pie,
chk_elf_rpath,
chk_elf_runpath,
chk_elf_stripped,
};
char *chk_basic_array[CHK_BAS_NUM]={
"File",
"RELRO",
"STACK CANARY",
"NX",
"PIE",
"RPATH",
"RUNPATH",
"Stripped",
};
/* current */
chk_info *elf_info=MALLOC(1,chk_info);\
/* head */
chk_info *head=elf_info;
for(int num=0;num < CHK_BAS_NUM;num++){
chk_info *new=MALLOC(1,chk_info);
new->chk_type=chk_basic_array[num];
char *result=chk_basic_func[num](elf);
/* null handler */
if(!result) new->chk_result="NULL";
else new->chk_result=result;
elf_info->chk_next=new;
elf_info=new;
}
...
}
回到检测RELRO上,chk_elf_relro函数的执行过程可以分为三步。这里逻辑和checksec.sh相同,都是先寻找GNU_RELRO,再寻找BIND_NOW,不过C语言实现需要找到具体的字段来匹配。
- 在程序头表中寻找类型为PH_GNU_RELRO的程序头。
/* check relro */
char *chk_elf_relro(Binary *elf){
bool relro=false;
bool full=false;
/* search program header */
Programh *ph=elf->hd->Pxheader.ph->ph_next;
while(ph){
/* segment type == GNU_RELRO*/
if(ph->sgm_type == PH_GNU_RELRO){
relro=true;
break;
}
ph=ph->ph_next;
}
...
}
- 在动态节中寻找BIND_NOW标志,根据oracle对动态节字段的解释。(https://docs.oracle.com/cd/E26926_01/html/E25910/chapter6-42444.html)
DT_BIND_NOW被DF_BIND_NOW标志所取代。所以需要dyn->d_un.d_val == DF_BIND_NOW来匹配。
DT_BIND_NOW
表示在将控制权返回给程序之前,必须处理此目标文件的所有重定位项。通过环境或 dlopen(3C) 指定时,提供的此项优先于使用延迟绑定的指令。此元素的用途已被 DF_BIND_NOW 标志取代。请参见执行重定位的时间。
/* search dynamic section */
Section *dynamic=NULL;
Section *sect=elf->sect->sect_next;
while(sect){
if(strcmp(sect->sect_name,".dynamic")==0){
dynamic=sect;
break;
}
sect=sect->sect_next;
}
if(!dynamic) CHK_ERROR4("dynamic section not found.");
/* search BIND_NOW falg */
switch (elf->bin_type){
case BIN_TYPE_ELF32:
uint16_t dyn32_num=dynamic->sect_size/sizeof(E32_dyn);
for(uint16_t num=0;num < dyn32_num;num++){
uintptr_t dyn32_addr=(uintptr_t)sect->sect_bytes+num*sizeof(E32_dyn);
E32_dyn *dyn32=(E32_dyn*)dyn32_addr;
/* d_tag == DT_FLAGS */
if(dyn32->d_tag == DT_FLAGS)
/* d_val == DF_BIND_NOW */
if(dyn32->d_un.d_val == DF_BIND_NOW)
full=true;
}
break;
case BIN_TYPE_ELF64:
uint16_t dyn64_num=dynamic->sect_size/sizeof(E64_dyn);
for(uint16_t num=0;num < dyn64_num;num++){
uintptr_t dyn64_addr=(uintptr_t)sect->sect_bytes+num*sizeof(E64_dyn);
E64_dyn *dyn64=(E64_dyn*)dyn64_addr;
/* d_tag == DT_FLAGS */
if(dyn64->d_tag == DT_FLAGS)
/* d_val == DF_BIND_NOW */
if(dyn64->d_un.d_val == DF_BIND_NOW)
full=true;
}
}
- 最后一步,根据上面两步的结果返回字符串。
if(relro){
if(full) return "\033[32mFull RELRO\033[m";
else return "\033[33mPartial RELRO\033[m";
}
else return "\033[31mNo RELRO\033[m";