checksec原理分析与C语言重写2

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逻辑为例。

  1. 首先要找到配置文件,不同发行版的配置文件不同,如果默认编译选项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。

  1. 接下来在配置文件或空文件中匹配字符串,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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值