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

目錄html

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方案linux

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來逐步學習async

#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>
#include <malloc.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 };
    int* p;

    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);
    
    //父進程調用malloc
    p = (int*)malloc(sizeof(int)*128);
    if(NULL == (int*)p)
    {
        perror("error...");
        return 0;
    } 

    for (times = 0; times < 50000; times++)
    {
        //fork a duplicate process
        pid_t child_pid = fork(); 

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

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

 

Copyright (c) 2014 LittleHann All rights reserved

相關文章
相關標籤/搜索