首页 黑客接单正文

如何增强Linux内核中的访问控制安全

背景

不久前,我们的项目组正在帮助客户解决操作系统安全领域的一些问题windows,Linux,macOS三个操作系统平台。无论什么操作系统,本质上都是软件,任何软件在设计开始时都不能***为了满足人们的需求,操作系统也是如此。为了尽可能满足人们的需求,人们必须提供一些定制操作系统的机制。当然,除了一些官方机制外,还有一些黑魔法,不推荐使用,但有时面对特定的业务场景,可以作为参考。

Linux常见的拦截过滤

本文重点介绍Linux平台上常见的拦截:

                   
  • 拦截用户动态库。
  •                
  • 内核系统调用拦截。
  •                
  • 堆栈文件系统拦截。
  •                
  • inline hook拦截。
  •                
  • L *** (Linux Security Modules)

动态库劫持

Linux动态库劫持主要基于LD_PRELOAD该环境变量的主要功能是改变动态库的加载顺序,让用户有选择地将相同的函数载入不同的动态库。然而,使用不当会导致严重的安全问题。我们可以在主程序和动态连接库中加载其他动态函数,这为我们向他人的程序注入恶意代码提供了机会。

假设有以下用户名密码验证函数:

  • #include<stdio.h>
  • #include<string.h>
  • #include<stdlib.h>
  • intmain(intargc,char**argv)
  • {
  • charpasswd[]="password";
  • if(argc<2){
  • printf("Invalidargc!\n");
  • return;
  • }
  • if(!strcmp(passwd,argv[1])){
  • printf("CorrectPassword!\n");
  • return;
  • }
  • printf("InvalidPassword!\n");
  • }
  • 再写一段hookStrcmp让这个比较永远正确的程序。

  • #include<stdio.h>
  • intstrcmp(constchar*s1,constchar*s2)
  • {
  • return0;
  • }
  • 如果我们依次执行将使我们hook先执行程序。

  • gcc-Wall-fPIC-shared-ohookStrcmp.sohookStrcmp.c
  • exportLD_PRELOAD=”./hookStrcmp.so”
  • 结果会发现我们自己写的strcmp函数优先被调用了。这是一个最简单的劫持 ,但是如果劫持了类似于geteuid/getuid/getgid,让它返回0相当于暴露root权限。因此,为了安全起见,一般会LD_PRELOAD禁止使用环境变量。

    Linux系统调用劫持

    最近发现在4.4.0的内核中有513多个系统调用(很多都没用过),系统调用劫持的目的是改变系统中原有的系统调用,用我们自己的程序替换原有的系统调用。Linux内核中所有的系统调用都放在一个叫做sys_call_table在核数组中,数组的值表示系统调用服务程序的入口地址。整个系统调用过程如下:

    当用户态启动系统调用时,它将通过80软中断进入syscall hander,然后进入系统调用表的全局sys_call_table去查找具体的系统调用,那么如果我们将这个数组中的地址改成我们自己的程序地址,就可以实现系统调用劫持。但是内核为了安全,对这种操作做了一些限制:

                     
    • sys_call_table未导出的符号不能直接获得。
    •                
    • sys_call_table内存页面只读属性,不能直接修改。

    以上两个问题的解决方案如下(不止一种 *** ):

                     
    • 获取sys_call_table的地址 :
    •                
    • grepsys_call_table/boot/System.map-uname-r
    •                
    • 控制页面的只读属性CR0寄存器的WP位置控制,只要清除此位置,就可以修改只读页面。
  • intmake_rw(unsignedlongaddress)
  • {
  • unsignedintlevel;
  • pte_t*pte=lookup_address(address,&level);////找到虚拟地址所在的页面地址
  • pte->pte|=_PAGE_RW;///设置页面读写属性
  • return0;
  • }
  • intmake_ro(unsignedlongaddress)
  • {
  • unsignedintlevel;
  • pte_t*pte=lookup_address(address,&level);
  • pte->pte&=~_PAGE_RW;//设置只读属性
  • return0;
  • }
  • 1. 开始更换系统调用

    本文实现了对 ls该命令对应系统调用,系统调用号为__NR_getdents。

  • staticintsyscall_init_module(void)
  • {
  • orig_getdents=sys_call_table[__NR_getdents];
  • make_rw((unsignedlong)sys_call_table);///修改页面属性
  • sys_call_table[__NR_getdents]=(unsignedlong*)hacked_getdents;///设置新的系统调用地址
  • make_ro((unsignedlong)sys_call_table);
  • return0;
  • }
  • 2. 恢复原状

  • staticvoidsyscall_cleanup_module(void)
  • {
  • printk(KERN_ALERT"Modulesyscallunloaded.\n");
  • make_rw((unsignedlong)sys_call_table);
  • sys_call_table[__NR_getdents]=(unsignedlong*)orig_getdents;
  • make_ro((unsignedlong)sys_call_table);
  • }
  • 使用Makefile编译,in *** od插入内核模块后,执行ls时,就会进入到我们的系统调用,我们可以在hook删除代码中的某些文件,ls这些文件不会显示,但仍然存在。

    堆栈文件系统

    Linux通过vfs虚拟文件系统从上到下统一抽象具体磁盘文件系统IO栈形成了堆栈式。通过对核源码的分析,以一次读操作为例,从上到下执行的流程如下:

    内核中有很多用途c面向对象的语言形式,即函数指针的形式,如read是vfs提供用户界面,具体调用是ext2的read操作。我们只需要实现VFS堆栈文件系统可以通过提供的各种接口实现。Linux一些堆栈文件系统已经集成在核中,比如Ubuntu提醒您是否需要加密home目录实际上是一个堆栈式加密文件系统(eCryptfs),原理如下:

    堆栈文件系统的实现相当于所有的读写操作都会进入我们的文件系统,可以获得所有的数据,可以进行一些拦截和过滤。

    以下是我实现的最简单的堆栈文件系统,实现了最简单的打开、阅读和写作文件。麻雀虽小,但五脏俱全。

    https://github.com/wangzhangjun/wzjfs

    inline hook

    我们知道内核中的函数不可能在这个函数中实现所有功能。它必须调用其下层函数。如果这个下层函数可以得到我们想要的过滤信息内容,我们可以在上层函数中使用下层函数offset用新函数替换offset,这样,当上层函数调用下层函数时,它就会跳到新函数中,在新函数中过滤和劫持内容。因此,原则上,inline hook可以想hook哪里就hook哪里。

    inline hook 有两个重要问题:

                     
    • 如何定位hook点。
    •                
    • 如何注入hook函数入口。

    1. 对于***个问题:

    内核源码需要一点经验,比如read操作,源代码如下:

    在此发起read系统调用后,将进入sys_read,在sys_read中会调用vfs_read函数,在vfs_read我们需要过滤的信息恰好在参数中,所以我们可以把它们过滤掉vfs_read当做一个hook点。

    2. 第二个问题:

    如何Hook?这里有两种方式:

                     
    • *** *** :直接替换二进制,将call替换指令的操作数hook函数地址。
    •                

                     
    • 第二种 *** :Linux内核提供的kprobes机制。

    其原理是在hook点注入int 3(x86)机器代码,让cpu运行到这里的时候会触发sig_trap然后定制用户的信号hook函数注入到sig_trap在回调函数中,触发hook函数的目的。这实际上是调试器的原理。

    L ***

    L *** 是Linux Secrity Module的简称,即linux安全模块Linux安全框架,具有效率高,简单易用等特点。原理如下:

    L *** 内核工作如下:

                     
    • 将安全域添加到特定的内核数据结构中。
    •                
    • 将安全钩函数插入内核源代码中的不同关键点。
    •                
    • 加入通用安全系统调用。
    •                
    • 允许内核模块注册为安全模块或注销的函数。
    •                
    • 将capabilities大多数逻辑移植为具有可扩展性的可选安全模块。

    适用场景

    以上几种Hook有不同的应用场景。

                     
    • 动态库劫持不完很完整,劫持信息可能不能满足我们的需求,也可能有人在你之前劫持,一旦禁止LD_PRELOAD就失效了。
    •                
    • 系统调用劫持,劫持信息可能无法满足我们的需求,例如无法获得struct file结构体取文件的取文件的绝对路径等。
    •                
    • 依靠堆栈文件系统Mount,可能需要重启系统。
    •                
    • inline hook,灵活性高,随意Hook,一旦某些函数发生变化,即时生效不需要重启,但在不同内核版本之间的通用性较差,Hook失效。
    •                
    • L *** ,在早期内核中,只能允许一个L *** 例如,内核模块加载SELinux,其他的不能加载L *** 模块,在***这个问题在核在于核版本中。

    总结

    篇幅有限,本文仅介绍Linux上的拦截技术,后续有机会可以一起探讨windows和macOS拦截技术。事实上,类似的审计HOOK在任何系统中都是刚需,不仅仅是kernel,我们可以看到越来越多的人vm和runtime甚至包括很多web提供更灵活的组件和前端应用hook这是透明度和实时性两大安全趋势下最常见的解决方案。

    【本文是51CTO专栏作者“ThoughtWorks”微信微信官方账号原稿:思特沃克,请联系原作者转载

    戳这里,看作者更好的文章

       
    版权声明

    本文仅代表作者观点,不代表本站立场。
    本文系作者授权发表,未经许可,不得转载。