Linux fork()、exec() Hook Risk、Design-Principle In Multi-Threadeed Program

目录

1. Linux exec指令执行监控Hook方案
2. 在"Multi-Threadeed Program"环境中调用fork存在的风险
3. Fork When Multi-Threadeed Program的安全设计原则
4. Fork When Multi-Threadeed Program Deaklock Demo Code

1. Linux exec指令执行监控Hook方案

1. 基于LD_PRELOAD技术的glibc API劫持Hook技术
    1) 优点: 位于Ring3应用层,基于Linux原生提供的LD_PRELOAD共享库符号加载调试技术,具有很好的兼容性和稳定性
    2) 缺点: LD_PRELOAD技术的核心是SO加载劫持注入,只要在安装了Hook模块之后启动的程序才能被Hook捕获到,系统先于Hook模块启动的常驻进程无法被捕获到,这对于在持续运行的业务服务器上部署入侵检测系统是非常不利的
2. 基于ptrace()进程注入调试技术+内核模块提供进程启动事件的通知机制
    1) 优点: 能灵活控制需要Hook的进程,可以实现一套Ring3的Whitelist机制,对不需要Hook的进程予以放行
    2) 缺点: 进程创建的事件通知和Hook模块的处理逻辑之间无法实现串行,当发生瞬发进程执行的时候,有可能发生进程创建事件通知到ptrace注入模块的时候,进程已经exit退出了
3. 基于Kernel Inline Hook对指定系统调用进行审计监控
4. 基于0x80中断劫持system_call->sys_call_table进行系统调用Hook
5. 基于Linux内核机制kprobe机制(kprobes, jprobe和kretprobe)进行系统调用Hook
6. LSM(linux security module)钩子技术(linux原生机制) 

Relevant Link:

http://www.cnblogs.com/LittleHann/p/3854977.html

2. 在"Multi-Threadeed Program"环境中调用fork存在的风险

0x1: Fork()的实现原理

关于fork()系统调用的实现原理和内核源码分析,请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/3853854.html
//搜索:4. sys_execve()函数

在整个过程中,我们关注以下几个重点(完整的详细过程请参阅另一篇文章)

1. 子进程复制了父进程的内存页
    1) 数据段: 静态变量、字符串等数据
    2) 代码段: 程序代码
    3) 堆栈区: 保存调用时可能正处于"中间状态"的变量运算结果
2. 子进程复制了父进程的文件描述符数组
    1) 通过current->struct files_struct *files可以寻址、并操作当前进程打开的文件
3. 在多线程执行的情况下调用fork()函数,仅会将发起调用的线程复制到子进程中
    1) 如果父进程包含多个线程,父进程(主线程)在调用fork的时候,其他线程均在子进程中"立即停止并消失",并且不会为这些线程调用清理函数以及针对线程局部存储变量的析构函数

0x2: 共享区、锁(Critical sections, mutexes)中存在的风险

虽然只将发起fork()调用的线程复制到子进程中,但全局变量的状态以及所有的pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留,这就造成一个危险的问题
关于fork when multi-thread risk问题,我们通过一个code case来逐步学习

#include <pthread.h>

void* doit()
{
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    struct timespec ts = {10, 0};
    nanosleep(&ts, 0); // 10秒sleep

    pthread_mutex_unlock(&mutex);
    return;
}

int main(void)
{
        pthread_t t;

    //启动子进程,并在子进程中试图锁定一个全局锁
        pthread_create(&t, 0, doit, 0);
        if (fork() == 0)
    {
        //子进程
        doit();

              return 0;
        }
    // 等待子线程结束
        pthread_join(t, 0);
}

/*
gcc -Wall -o fork fork.c -lpthread
./fork
*/

分析一下可能产生死锁的原因

以下是说明死锁的理由:

1. 子进程只拷贝了调用fork的主线程(进程)的
2. 在内存区域里,静态变量mutex的内存会被拷贝到子进程里。而且,父进程里即使存在多个线程,但它们也不会被继承到子进程里
3. 父进程产生的线程里的doit()先执行.
    1) doit执行的时候会给互斥体变量mutex加锁.
    2) mutex变量的内容会原样拷贝到fork出来的子进程中(在此之前,mutex变量的内容已经被父进程产生的线程改写成锁定状态)
4. 父进程fork出的子进程再次调用doit的时候,在锁定互斥体mutex的时候会发现它已经被加锁,所以就只能一直等待,直到拥有该互斥体的进程释放它(但是能够解锁这个mutex的线程并没有通过fork复制到子进程中,sleep 10s模拟了这个情况).
5. 父进程的线程的doit执行完成之前会把自己的mutex释放,但是因为copy on write的关系(父子进程在这个互锁的期间有很高概率对内存进行了写草走),所以这个时候父进程的mutex和子进程里的mutex已经是两份内存。所以即使父进程释放了mutex锁也不会对子进程里的mutex造成什么影响,子进程就处于无限等待状态中

0x3: 文件操作中存在的风险

由于子进程会将父进程的大多数数据拷贝一份,这样在文件操作中就意味着子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于dup()函数调用,因此父、子进程中对应的文件描述符均指向相同的打开的文件句柄,而且打开的文件句柄包含着当前文件的偏移量以及文件状态标志,所以在父子进程中处理文件时很有可能发生对同一个文件的同时操作,导致文件内容出现混乱或者别的问题

0x4: 数据一致性运算中存在的风险

全局变量的状态也可能处于不一致的状态,因为对其更新的操作只做到了一半对应的线程就消失了,或者父子进程同时对同一个数据进行了操作,通常情况下这和锁风险是同时发生的

0x5: 子进程中引用指针引发的错误

因为并未执行清理函数和针对线程局部存储数据的析构函数,所以多线程情况下可能会导致子进程的内存泄露。另外,子进程中的线程可能无法访问(父进程中)由其他线程所创建的线程局部存储变量,因为子进程没有任何相应的引用指针

Relevant Link:

http://www.linuxprogrammingblog.com/threads-and-fork-think-twice-before-using-them
http://mail-index.netbsd.org/tech-userlevel/2014/06/23/msg008609.html
https://sourceware.org/bugzilla/show_bug.cgi?id=4737
https://bugzilla.redhat.com/show_bug.cgi?id=241665

3. Fork When Multi-Threadeed Program的安全设计原则

0x1: 在调用fork()系统调用之后需要避免使用的函数

在多线程里因为fork而引起问题的函数,我们把它叫做"fork-unsafe函数"。反之,不能引起问题的函数叫做"fork-safe函数",典型的"fork-unsafe函数"例如

1. malloc:
malloc函数就是一个维持自身固有mutex的典型例子,malloc()在访问全局状态时会加锁,因此通常情况下它是fork-unsafe的。依赖于malloc函数的函数有很多,例如
    1) printf()
    2) dlsym()
它们也是变成fork-unsafe

2. stdio functions like printf() - this is required by the standard.
    1) 因为其他线程可能恰好持有stdout/stderr的锁

3. syslog()

4. 任何可能分配或释放内存的函数,包括
    1) new
    2) map::insert()
    3) snprintf()

5. 任何pthreads函数
    1) 不能用pthread_cond_signal()去通知父进程,只能通过读写pipe(2)来同步 

6. 除了man 7 signal中明确列出的"signal安全"函数"之外"的任何函数

0x2: 在fork之后调用"异步信号安全函数"

fork()函数被调用之后,子进程就相当于处于signal handler之中,此时就不能调用线程安全的函数(用锁机制实现安全的函数),因为线程安全函数中的锁操作很可能会导致死锁(deadlock),除非函数是可重入的,而只能调用异步信号安全(async-signal-safe)的函数,调用异步信号安全函数是规格标准
对于那些必须执行fork(),而其后又无exec()紧随其后的程序来说(例如入侵检测Hook程序),pthreads API提供了一种机制:利用函数pthread_atfork()来创建fork()处理函数 pthread_atfork()声明如下

/*
Upon successful completion, pthread_atfork() shall return a value of zero; otherwise, an error number shall be returned to indicate the error.
1. prepare: 新子进程产生之前被调用
2. parent: 新子进程产生之后在父进程被调用
3. child: 新子进程产生之后,在子进程被调用

一些典型的应用场景
1. when in the prepare handler mutexes are locked, in the parent handler unlocked and in the child handler reinitialized
2. 在子进程产生之后,父进程关闭不需要操作的文件句柄
*/
int pthread_atfork (void (*prepare) (void), void (*parent) (void), void (*child) (void));

该函数的作用就是往进程中注册三个函数,以便在不同的阶段调用,有了这三个参数,我们就可以在对应的函数中加入对应的处理功能。同时需要注意的是

1. 每次调用pthread_atfork()函数会将prepare添加到一个函数列表中,创建子进程之前会(按与注册次序相反的顺序)自动执行该函数列表中函数
2. parent与child也会被添加到一个函数列表中,在fork()返回前,分别在父子进程中自动执行(按注册的顺序) 

0x3: 在多线程应用中不使用fork

在程序中fork()与多线程的协作性很差,这是POSIX系列操作系统的历史包袱。因为长期以来程序都是单线程的,fork()运转正常。当20世纪90年代初期引入线程之后,fork()的适用范围就大为缩小了
在多线程应用中用pthread_create来代替fork,这是一个比较好的安全实践

0x4: 在fork之后立刻调用exec

子进程在创建后,是写时复制的,也就是子进程刚创建时,与父进程一样的副本,当exce后,那么老的地址空间被丢弃,而被新的exec的命令的内存的印像覆盖了进程的内存空间(一切和进程相关的资源都会被重置),所以锁的状态无关紧要了。
请注意这里使用的"马上"这个词.即使exec前仅仅只是调用一回printf("I’m child process")

Relevant Link:

http://blog.csdn.net/swgsunhj/article/details/8871758
http://blog.csdn.net/cywosp/article/details/27316803
http://blog.chinaunix.net/uid-26885237-id-3210394.html

4. Fork When Multi-Threadeed Program Deaklock Demo Code

如果在并发多线程(多进程)的场景下采用LD_PRELOAD技术对glibc的API调用进行Hook,造成的直接结果如下

1. 系统中可能存在这中代码
while(1000 times)
{
    if(pid = fork() = 0)
    {
        //do some calculate
        execve(new programe);
    }
}

2. 使用LD_PRELOAD技术对execve进行Hook之后,相当于产生了"fork when multi-thread programe"的场景,Hook模块的代码延长了fork和execve之间的代码执行时间
    1) Hook模块中基本都有对内存的写入操作,也就是说在这种情况下,几乎100%会发生copy on write事件,进程创建的内存复制开销大大增加了
    2) 子进程调用Hooked execve函数中,调用了dlsym,dlsym中包含malloc的调用,这有可能导致deadlock
    3) hook延迟了子进程fork和execve之间的代码执行时间,同时有更大的可能性出现父子进程共同操作同一个fs、锁等资源

0x1: Code Example

test.c

#include <stdio.h>

int main(int argc, char* argv[])
{
        printf("hello world\n");

        return 0;
}

//gcc test.c -o test

fork.c

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* doit()
{
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    struct timespec ts = {10, 0};
    nanosleep(&ts, 0); // 10秒sleep

    pthread_mutex_unlock(&mutex);
}

int main(int argc, char *argv[])
{
    int times;
    pthread_t t;
    char *newargv[] = { NULL, "hello", "world", NULL };
    char *newenviron[] = { NULL };

    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s <file-to-exec>\n", argv[0]);
        return 0;
    }
    newargv[0] = argv[1];  

    //启动子进程,并在子进程中锁定一个全局锁
    pthread_create(&t, 0, doit, 0);
    for (times = 0; times < 5000; times++)
    {
        //fork a duplicate process
        pid_t child_pid = fork(); 

        //child
        if (child_pid == 0)
        {
            doit();
            execve(argv[1], newargv, newenviron);
        }
    }

    return 0;
}

/*
gcc -Wall -o fork fork.c -lpthread
./fork test
*/

Copyright (c) 2014 LittleHann All rights reserved

时间: 2024-10-10 10:18:57

Linux fork()、exec() Hook Risk、Design-Principle In Multi-Threadeed Program的相关文章

PHP Datatype Conversion Safety Risk、Floating Point Precision、Operator Security Risk、Safety Coding Principle

catalog 0. 引言 1. PHP operator introduction 2. 算术运算符 3. 赋值运算符 4. 位运算符 5. 执行运算符 6. 递增/递减运算符 7. 数组运算符 8. 类型运算符 9. PHP自动类型转换 10. 浮点数运算中的精度损失 11. 比较运算符 0. 引言 本文试图讨论PHP中因为运算符导致的各种安全问题/风险/漏洞,其他很多本质上并不能算PHP本身的问题,而更多时候在于PHP程序员对语言本身的理解以及对安全编码规范的践行,我们逐个讨论PHP中的运

Linux下Fork与Exec使用

http://www.cnblogs.com/hicjiajia/archive/2011/01/20/1940154.html Linux下Fork与Exec使用 一.引言 对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一:它执行一次却返回两个值.fork函数是Unix系统最杰出的成就之一,它是七十年代UNIX早期的开发者经过长期在理论和实践上的艰苦探索后取得的成果,一方面,它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简洁明了的多进程

system()、exec()、fork()三个与进程有关的函数的比较

启动新进程(system函数) system()函数可以启动一个新的进程. int system (const char *string ) 这个函数的效果就相当于执行sh –c string. 一般来说,使用system函数远非启动其他进程的理想手段,因为它必须用一个shell来启动需要的程序.这样对shell的安装情况,以及shell的版本依赖性很大. system函数的特点: 建立独立进程,拥有独立的代码空间,内存空间 等待新的进程执行完毕,system才返回.(阻塞) 替换进程映像(ex

Linux多任务编程之一:任务、进程、线程(转)

来源:CSDN  作者:王文松  转自:Linux公社 Linux下多任务介绍 首先,先简单的介绍一下什么叫多任务系统?任务.进程.线程分别是什么?它们之间的区别是什么?,从而可以宏观的了解一下这三者,然后再针对每一个仔细的讲解. 什么叫多任务系统?多任务系统指可以同一时间内运行多个应用程序,每个应用程序被称作一个任务. 任务定义:任务是一个逻辑概念,指由一个软件完成的任务,或者是一系列共同达到某一目的的操作. 进程定义:进程是指一个具有独立功能的程序在某个数据集上的一次动态执行过程,它是系统进

Linux fork exec等

http://www.cnblogs.com/leoo2sk/archive/2009/12/11/talk-about-fork-in-linux.html http://www.cnblogs.com/jimwind/archive/2012/12/26/2834147.html http://www.cnblogs.com/hicjiajia/archive/2011/01/20/1940154.html Linux下一个进程在内存里有三部分的数据,就是"代码段"."堆

Linux进程状态(ps stat)之R、S、D、T、Z、X 转:http://blog.csdn.net/huzia/article/details/18946491

Linux是一个多用户,多任务的系统,可以同时运行多个用户的多个程序,就必然会产生很多的进程,而每个进程会有不同的状态. Linux进程状态:R (TASK_RUNNING),可执行状态. 只有在该状态的进程才可能在CPU上运行.而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中).进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行. 很多操作系统教科

Docker容器学习梳理--容器登陆方法梳理(attach、exec、nsenter)

对于运行在后台的Docker容器,我们运维人员时常是有登陆进去的需求.登陆Docker容器的方式: 1)使用ssh登陆容器.这种方法需要在容器中启动sshd,存在开销和攻击面增大的问题.同时也违反了Docker所倡导的一个容器一个进程的原则. 参考Docker容器学习梳理--SSH方式登陆容器 2)使用自带命令docker attach登陆容器.命令格式:docker attach container_id.不过docker attach存在的问题是:当多个窗口同时attach到同一个容器时,所

第6章 进程控制(3)_wait、exec和system函数

5. 等待函数 (1)wait和waitpid 头文件 #include <sys/types.h> #include <sys/wait.h> 函数 pid_t wait(int* status); pid_t waitpid(pid_t pid, int* status, int options); 返回值 成功返回子进程ID,出错返回-1 功能 等待子进程退出并回收,防止僵尸进程的产生 参数 (1)status参数: ①为空时,代表任意状态结束的子进程: ②不为空时,则等待指

Linux自学第二天(权限管理命令、文件搜索命令、帮助命令)

添加用户两步走:1.useradd username   2.passwd ps  权限管理命令 命令名称:chmod  ->>change the permissions mode of a file 命令所在路径:/bin/chmod 执行权限:所有用户 语法:chmod [{u g o} {+-=}{rwx}][文件或目录] [mode = 421] [文件或目录] +:增加权限 -:减少权限 =:直接赋予权限 掌握的重点是用数字的方式进行权限控制,r=4 w=2 x=1. rwxr-x