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
--proc={process name}
--proc-all
--proc-libs={process ID}
--fortify-proc={process ID}
--extended
进程检测的逻辑在 src/functions/proccheck.sh,检测进程的文件的同时额外检测seccomp。
Seccomp (SECure COMPuting) 是 Linux 内核 2.6.12 版本引入的安全模块,主要是用来限制某一进程可用的系统调用 (system call),Seccomp本身并不是一个沙盒,它只是一种减少 Linux 内核暴露的机制,是构建一个安全的沙盒的重要组成部分。
在Linux系统下,一个进程如果由可执行文件生成,那么该可执行文件的链接保存在 /proc/{pid}/ 目录下,链接名称为exe。例如,下面checksec.sh对进程RELRO的检测,只是将文件名改成/proc/{pid}/exe,执行错误消息改成权限不足(not root)。
proccheck() {
# check for RELRO support
if ${readelf} -l "${1}/exe" 2> /dev/null | grep -q 'Program Headers'; then
if ${readelf} -l "${1}/exe" 2> /dev/null | grep -q 'GNU_RELRO'; then
if ${readelf} -d "${1}/exe" 2> /dev/null | grep -q 'BIND_NOW' || ! ${readelf} -l "${1}/exe" 2> /dev/null | grep -q '\.got\.plt'; then
echo_message '\033[32mFull RELRO \033[m ' 'Full RELRO,' ' relro="full"' '"relro":"full",'
else
echo_message '\033[33mPartial RELRO\033[m ' 'Partial RELRO,' ' relro="partial"' '"relro":"partial",'
fi
else
echo_message '\033[31mNo RELRO \033[m ' 'No RELRO,' ' relro="no"' '"relro":"no",'
fi
else
echo -n -e '\033[31mPermission denied (please run as root)\033[m\n'
exit 1
fi
...
}
seccomp是进程额外的检测,这一特性的指定保存在/proc/{pid}/status空文件中。这里空文件特指保存在内核空间内存中的数据,内核以文件作为接口将其暴露出来,使得用户空间可以读写这块内存区域。
# check for Seccomp mode
seccomp=$(grep 'Seccomp:' "${1}/status" 2> /dev/null | cut -b10)
if [[ "${seccomp}" == "1" ]]; then
echo_message '\033[32mSeccomp strict\033[m ' 'Seccomp strict,' ' seccomp="strict"' '"seccomp":"strict",'
elif [[ "${seccomp}" == "2" ]]; then
echo_message '\033[32mSeccomp-bpf \033[m ' 'Seccomp-bpf,' ' seccomp="bpf"' '"seccomp":"bpf",'
else
echo_message '\033[31mNo Seccomp \033[m ' 'No Seccomp,' ' seccomp="no"' '"seccomp":"no",'
fi
checksecc进程检测
上文解释了进程检测=文件检测+seccomp检测,逻辑很简单,我们重点关注checksecc对细节的处理,代码逻辑在 srcs/chk_proc.c :
比如我们要检测进程的PID=1,首先,需要拼接好exe的路径 /proc/1/exe,使用readlink函数读取进程可执行文件的实际路径,保存在字符数组中。
void chk_proc(char *option,chk_proc_option cpo){
DIR *dir=NULL;
...
char *proc=str_append("/proc/",option);
dir=opendir(proc);
if(dir == NULL) CHK_ERROR2(proc,"pid is not exist or not unprivileged(not root)");
char *link=str_append(proc,"/exe");
// max len 64
char exe[64];
int len=readlink(link,exe,64);
if(len < 0) CHK_ERROR2(option,"Permission denied. Requested process ID belongs to a kernel thread");
chk_linux_proc(proc,option,exe);
break;
...
...
}
这里需要注意的是:从1号进程往后的若干PID中,并非都是由可执行文件生成的,而是由0号进程创建的内核线程。如果我们使用ps命令查看,可以看到这些内核线程对应的内核函数,被[ ]框住。
root@debian:~/ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 164524 10808 ? Ss 08:52 0:02 /sbin/init
root 2 0.0 0.0 0 0 ? S 08:52 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? I< 08:52 0:00 [rcu_gp]
root 4 0.0 0.0 0 0 ? I< 08:52 0:00 [rcu_par_gp]
root 6 0.0 0.0 0 0 ? I< 08:52 0:00 [kworker/0:0H-events_highpri]
root 8 0.0 0.0 0 0 ? I< 08:52 0:00 [mm_percpu_wq]
root 9 0.0 0.0 0 0 ? S 08:52 0:00 [rcu_tasks_rude_]
root 10 0.0 0.0 0 0 ? S 08:52 0:00 [rcu_tasks_trace]
root 11 0.0 0.0 0 0 ? S 08:52 0:00 [ksoftirqd/0]
root 12 0.0 0.0 0 0 ? I 08:52 0:05 [rcu_sched]
...
回到cheksecc对seccomp的检测,chk_linux_proc_seccomp函数对空文件/proc/1/status进行读取,并且在末尾加入\0以便对字符串Seccomp匹配。Seccomp有三个模式:No、Strict、BPF。
// check Seccomp mode
char *chk_linux_proc_seccomp(char *path){
char *status=str_append(path,"/status");
FILE *fp;
// status is empty file, read 4096 bytes
fp=fopen(status,"r");
if(fp == NULL) CHK_ERROR4("open /proc/pid/status failed");
char *seccomp=MALLOC(MAXBUF+1,char);
if(fread(seccomp,sizeof(char),MAXBUF,fp) < 0) CHK_ERROR4("read /proc/pid/status failed");
// need to add '\0'
seccomp[MAXBUF]='\0';
char *location=strstr(seccomp,"Seccomp:");
if(location == NULL) CHK_ERROR4("Seccomp flag not found");
// Seccomp: x , flag=x
unsigned int offset=9;
char flag=*(location+offset);
// collect resource
fclose(fp);
free(seccomp);
if(flag == '0') return "\033[31mNo Seccomp\033[m";
else if(flag == '1') return "\033[32mSeccomp strict\033[m";
else if(flag =='2') return "\033[32mSeccomp-bpf\033[m";
else return "Unknown Seccomp LEVEL";
}
内核检测
内核检测的原理很简单,checksec.sh和checksecc都是读取内核运行的配置文件或空文件,从中作字符串匹配,以checksecc逻辑为例。
- 首先要找到配置文件,不同发行版的配置文件不同,如果默认编译选项CONFIG_IKCONFIG=y,则配置文件会以/proc/config.gz的形式保存。gz格式的文件需要包含#include<zlib.h>文件头调用gz族的函数处理。
char *chk_kernel_config(char *option,char **kconfig){
...
// set CONFIG_IKCONFIG
gzFile gzfp;
config="/proc/config.gz";
gzfp=gzopen(config,"rb");
if(gzfp != NULL) {
size=gzFILE_SIZE(gzfp);
if(size == 0) CHK_ERROR4("/proc/config.gz empty file");
kernelinfo=MALLOC(size+1,char);
size_t result=gzread(gzfp,kernelinfo,size);
gzclose(gzfp);
if(result < 0){
free(kernelinfo);
CHK_PRINT1("read /proc/config.gz failed");
}
else goto kernelinfo_ok;
...
}
如果没有设置CONFIG_IKCONFIG,配置文件会在/boot/config-{release}或/usr/src/linux-headers-{release}/.config。
- 接下来在配置文件或空文件中匹配字符串,checksecc调用以下九个检测函数。
void chk_kernel(char *kernelinfo,char *option){
// kconfig path
char *kconfig=NULL;
if(kernelinfo == NULL)
kernelinfo=chk_kernel_config(option,&kconfig);
// chk kernel feature
char *(*chk_kernel_func[CHK_KERN_NUM])(char *)={
chk_user_aslr,
chk_kernel_aslr,
chk_kernel_nx,
chk_kernel_stack_canary,
chk_kernel_stack_poison,
chk_kernel_slab_freelist_hardened,
chk_kernel_slab_freelist_random,
chk_kernel_smap,
chk_kernel_pti
};
char *chk_kernel_array[CHK_KERN_NUM]={
"User ASLR",
"Kernel ASLR",
"Kernel NX",
"Kernel Stack Canary",
"Kernel Stack Poison",
"Slab Freelist Hardened",
"Slab Freelist Random",
"SMAP",
"PTI"
};
...
}
上述九个安全特性是x86体系下常用的安全特性,这些特性以内核编译选项的形式提供配置,官方的描述在 https://www.kernelconfig.io/ 都可以找到。
- 用户态ASLR不必多说,要启用内核态ASLR则需要在编译内核时选项CONFIG_RANDOMIZE_BASE=y,官方解释在https://www.kernelconfig.io/config_randomize_base,大意是随机化内核映像解压的物理地址和映射内核映像的虚拟地址,即:加载内核时要随机化物理地址、页表中内核的虚拟地址也要随机化处理。
- 内核态NX,我们都知道NX的实现基于两点:1.CPU MMU支持NX功能 2.虚拟内存页设置不可执行权限,内核态NX和用户态NX没什么区别,在编译内核时选项CONFIG_STRICT_KERNEL_RWX=y,官方解释在https://www.kernelconfig.io/config_strict_kernel_rwx,映射内核时配置好只读数据权限,数据区、代码区权限即可。
- 内核栈保护,checksecc检测了两项特性:Kernel Stack Canary和Kernel Stack Poison,分别对应编译选项CONFIG_STACKPROTECTOR=y和CONFIG_GCC_PLUGIN_STACKLEAK=y。
- Slab链表保护,checksecc检测了两项特性:Slab Freelist Hardened和Slab Freelist Random,分别对应编译选项CONFIG_SLAB_FREELIST_HARDENED=y和CONFIG_SLAB_FREELIST_RANDOM=y。
- SMAP是基于硬件的安全特性,最早在x86 CPU中引入,默认开启选项CONFIG_X86_SMAP=y,官方解释https://www.kernelconfig.io/config_x86_smap,意在阻止ret2usr攻击。
- PTI,在没有页表隔离之前,进程不论在用户态还是内核态都使用同一个页表,这意味着当在用户态运行时,TLB中可能缓存内核态的地址映射,进而Meltdown漏洞可以通过CPU的预测执行来访问内核态地址。PTI通过让内核态和用户态使用独立的页表来规避Meltdown漏洞攻击,这也使得性能开销剧增。默认开启选项CONFIG_PAGE_TABLE_ISOLATION=y,官方解释https://www.kernelconfig.io/config_page_table_isolation。