Linux下可信环境绕过思路

本文提到的可信是指操作系统验证执行程序是否合法的安全机制。

一般的可信执行实现方式是hook execve函数,在加载时判断程序是否带有签名信息并验证签名,只有签名正确才继续执行。

以下几点是常规绕过思路:

系统自带程序替换

系统自带程序(如ls,ps等)可能是基于直接判断文件名和路径,可以尝试用同名程序替换绕过。

程序安装包伪造

发行版Linux(如ubuntu/CentOS等)都有自带的包安装机制。针对这种安装包做签名验证很不方便,有可能做成一个特殊的流程来处理。

绕过思路就是自己构造deb或者rpm包,通过命令行安装。甚至伪造apt/yum源,通过apt/yum安装,绕过可信机制。

ld-linux.so动态链接器绕过

ld动态库是一个很特殊的存在,可以通过ldd查看系统/程序使用的动态链接器。

┌──(root㉿kali)-[~]
└─# ldd /bin/ls 
        linux-vdso.so.1 (0x00007ffec15f8000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f8937a64000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f893788b000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f89377ef000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f89377e9000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f8937ad2000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f89377c8000)

可以看到动态链接器是/lib64/ld-linux-x86-64.so.2

PS:libdl.so和ld-linux.so有点像,但是这个动态库的作用是让程序在执行过程中可以动态的把so加载到进程空间。 支持的函数有dlopen(),dlsym()等

ld-linux.so这个动态库很有意思,这个动态库是可以直接执行的,也可以用它执行其他程序。

常规的动态库没有程序入口地址(入口地址为0,执行就报段错误)

┌──(root㉿kali)-[~]
└─# readelf -h /root/hook_read.so         
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          13664 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         28
  Section header string table index: 27

而dl-linux.so不一样,有正确的入口地址,可以直接执行

┌──(root㉿kali)-[~]
└─# readelf -h /lib64/ld-linux-x86-64.so.2
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1050
  Start of program headers:          64 (bytes into file)
  Start of section headers:          201048 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         23
  Section header string table index: 22

也可以通过ld-linux.so 执行其他程序,如下:

┌──(root㉿kali)-[~]
└─# /lib64/ld-linux-x86-64.so.2 /usr/bin/pwd
/root

一般程序开启另外一个进程的操作是fork + execve。 但是通过ld-linux.so执行程序的过程是没有调用fork和execve函数的,ld-linux.so启动后把程序加载到自己的进程内存空间并执行。通过这种方式可以绕过execve函数,进而绕过可信执行校验。

动态库劫持

一般程序编译的时候,如果加上’--static’选项,则编译过程中程序依赖的函数会一起编译进ELF文件中,缺点是文件较大,优点是不依赖环境中的库。而默认是编译成依赖动态库的ELF文件,优点是文件小,so复用内存消耗小。缺点是如果跨OS可能会出现动态库版本不一致的兼容性问题。

以cat程序为例:

┌──(root㉿kali)-[~]
└─# ldd /usr/bin/cat
        linux-vdso.so.1 (0x00007fffcabaa000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007febd90cd000)
        /lib64/ld-linux-x86-64.so.2 (0x00007febd92cc000)

cat程序依赖libc.so.6,在Linux上基本上所有程序都依赖libc.so.6,也即是俗称的c库。c库是对系统调用的封装。 编程时include , include 这些头文件定义的函数,基本都是c库实现的。

比如Linux上有open系统调用,在c库上实现了fopen的函数,是对open的封装,这有一个好处是代码可以跨平台编译。 如果直接用系统调用,无法跨Linux/Windows编译。

执行cat的时候会打开文件,最终会调用open函数,这个open函数是在c库中实现的。

Linux下动态库加载顺序

另外要注意的是LD_PRELOAD环境变量,这个变量不在动态库搜索顺序中讨论,是因为无论程序需不需要,LD_PRELOAD指定的动态库都会优先加载到进程内存空间中。

使用LD_PRELOAD劫持有一个好处,比如劫持c库,如果是通过上述加载顺序进行劫持,那么我们提供的c库就得是完整的c库,程序依赖的函数都得实现。

而LD_PRELOAD可以劫持函数,而不关心所有的库函数。如下所示,劫持close函数来实现绕过可信环境检测。

┌──(root㉿kali)-[~]
└─# cat hook_close.c    
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>

typedef int (*selfdef_fun)(int);
int close(int i)
{
    printf("hock close function \n");

    void *handle = dlopen("/lib/x86_64-linux-gnu/libc.so.6", RTLD_LAZY);
    selfdef_fun libc_close = dlsym(handle, "close");
    return libc_close(i);
}
                                                                                                                                    
┌──(root㉿kali)-[~]
└─# gcc hook_close.c -fPIC -Wall --shared -ldl -o hook_close.so
                                                                                                                                    
┌──(root㉿kali)-[~]
└─# export LD_PRELOAD="/root/hook_close.so"                    
                                                                                                                                    
┌──(root㉿kali)-[~]
└─# cat hook_close.c                                           
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>

typedef int (*selfdef_fun)(int);
int close(int i)
{
    printf("hock close function \n");

    void *handle = dlopen("/lib/x86_64-linux-gnu/libc.so.6", RTLD_LAZY);
    selfdef_fun libc_close = dlsym(handle, "close");
    return libc_close(i);
}
hock close function 
                                                                                                                                    
┌──(root㉿kali)-[~]
└─# 
                      

参考文献

[1] https://en.wikipedia.org/wiki/Rpath

Table of Contents