Linux HIDS agent 概要和用戶態 HOOK(一)

做者:u2400@知道創宇404實驗室
時間:2019年12月19日html

原文:https://paper.seebug.org/1102/linux

前言:最近在實現linux的HIDS agent, 搜索資料時發現雖然資料很多, 可是每一篇文章都各自有側重點, 少有按部就班, 講的比較全面的中文文章, 在一步步學習中踩了很多坑, 在這裏將以進程信息收集做爲切入點就如何實現一個HIDS的agent作詳細說明, 但願對各位師傅有所幫助.git

1. 什麼是HIDS?

主機***檢測, 一般分爲agent和server兩個部分
github

其中agent負責收集信息, 並將相關信息整理後發送給server.
web

server一般做爲信息中心, 部署由安全人員編寫的規則(目前HIDS的規則尚未一個編寫的規範),收集從各類安全組件獲取的數據(這些數據也可能來自waf, NIDS等), 進行分析, 根據規則判斷主機行爲是否異常, 並對主機的異常行爲進行告警和提示.
shell

HIDS存在的目的在於在管理員管理海量IDC時不會被安全事件弄的手忙腳亂, 能夠經過信息中心對每一臺主機的健康狀態進行監視.
安全

相關的開源項目有OSSEC, OSquery等, OSSEC是一個已經構建完善的HIDS, 有agent端和server端, 有自帶的規則, 基礎的rootkit檢測, 敏感文件修改提醒等功能, 而且被包含到了一個叫作wazuh的開源項目, OSquery是一個facebook研發的開源項目, 能夠做爲一個agent端對主機相關數據進行收集, 可是server和規則須要本身實現.
網絡

每個公司的HIDS agent都會根據自身須要定製, 或多或少的增長一些個性化的功能, 一個基礎的HIDS agent通常須要實現的有:session

  • 收集進程信息併發

  • 收集網絡信息

  • 週期性的收集開放端口

  • 監控敏感文件修改

下文將從實現一個agent入手, 圍繞agent討論如何實現一個HIDS agent的進程信息收集模塊

2. agent進程監控模塊提要

2.1進程監控的目的

在Linxu操做系統中幾乎全部的運維操做和***行爲都會體現到執行的命令中, 而命令執行的本質就是啓動進程, 因此對進程的監控就是對命令執行的監控, 這對運維操做升級和***行爲分析都有極大的幫助

2.2 進程監控模塊應當獲取的數據

既然要獲取信息那就先要明確須要什麼, 若是不知道須要什麼信息, 那實現便無從談起, 即使硬着頭皮先實現一個能獲取pid等基礎信息的HIDS, 後期也會由於缺乏規劃而頻繁改動接口, 白白耗費人力, 這裏參考《互聯網企業安全高級指南》給出一個獲取信息的基礎列表, 在後面會補全這張表的的獲取方式

數據名稱 含義
path 可執行文件的路徑
ppath 父進程可執行文件路徑
ENV 環境變量
cmdline 進程啓動命令
pcmdline 父進程啓動命令
pid 進程id
ppid 父進程id
pgid 進程組id
sid 進程會話id
uid 啓動進程用戶的uid
euid 啓動進程用戶的euid
gid 啓動進程用戶的用戶組id
egid 啓動進程用戶的egid
mode 可執行文件的權限
owner_uid 文件全部者的uid
owner_gid 文件全部者的gid
create_time 文件建立時間
modify_time 最近的文件修改時間
pstart_time 進程開始運行的時間
prun_time 父進程已經運行的時間
sys_time 當前系統時間
fd 文件描述符

2.3 進程監控的方式

進程監控, 一般使用hook技術, 而這些hook大概分爲兩類:

應用級(工做在r3, 常見的就是劫持libc庫, 一般簡單可是可能被繞過 - 內核級(工做在r0或者r1, 內核級hook一般和系統調用VFS有關, 較爲複雜, 且在不一樣的發行版, 不一樣的內核版本間都可能產生兼容性問題, hook出現嚴重的錯誤時可能致使kenrel panic, 相對的沒法從原理上被繞過

首先從簡單的應用級hook提及

3. HIDS 應用級hook

3.1 劫持libc庫

庫用於打包函數, 被打包事後的函數能夠直接使用, 其中linux分爲靜態庫和動態庫, 其中動態庫是在加載應用程序時才被加載, 而程序對於動態庫有加載順序, 能夠經過修改 /etc/ld.so.preload 來手動優先加載一個動態連接庫, 在這個動態連接庫中能夠在程序調用原函數以前就把原來的函數先換掉, 而後在本身的函數中執行了本身的邏輯以後再去調用原來的函數返回原來的函數應當返回的結果.

想要詳細瞭解的同窗, 參考這篇文章

劫持libc庫有如下幾個步驟:

3.1.1 編譯一個動態連接庫

一個簡單的hook execve的動態連接庫以下.
邏輯很是簡單

  1. 自定義一個函數命名爲execve, 接受參數的類型要和原來的execve相同

  2. 執行本身的邏輯

#define _GNU_SOURCE
#include <unistd.h>
#include <dlfcn.h>
typedef ssize_t (*execve_func_t)(const char* filename, char* const argv[], char* const envp[]);
static execve_func_t old_execve = NULL;
int execve(const char* filename, char* const argv[], char* const envp[]) {
        //從這裏開始是本身的邏輯, 即進程調用execve函數時你要作什麼
    printf("Running hook\n");
    //下面是尋找和調用本來的execve函數, 並返回調用結果
    old_execve = dlsym(RTLD_NEXT, "execve");
    return old_execve(filename, argv, envp);
}

經過gcc編譯爲so文件.

gcc -shared -fPIC -o libmodule.so module.c

3.1.2 修改ld.so.preload

ld.so.preload是LD_PRELOAD環境變量的配置文件, 經過修改該文件的內容爲指定的動態連接庫文件路徑,

注意只有root才能夠修改ld.so.preload, 除非默認的權限被改動了

自定義一個execve函數以下:

extern char **environ;
int execve(const char* filename, char* const argv[], char* const envp[]) {
    for (int i = 0; *(environ + i) ; i++)
    {
        printf("%s\n", *(environ + i));
    }
    printf("PID:%d\n", getpid());
    old_execve = dlsym(RTLD_NEXT, "execve");
    return old_execve(filename, argv, envp);
}

能夠輸出當前進程的Pid和全部的環境變量, 編譯後修改ld.so.preload, 重啓shell, 運行ls命令結果以下
33.png

3.1.3 libc hook的優缺點

優勢: 性能較好, 比較穩定, 相對於LKM更加簡單, 適配性也很高, 一般對抗web層面的***.

缺點: 對於靜態編譯的程序一籌莫展, 存在必定被繞過的風險.

3.1.4 hook與信息獲取

設立hook, 是爲了創建監控點, 獲取進程的相關信息, 可是若是hook的部分寫的過大過多, 會致使影響正常的業務的運行效率, 這是業務所不能接受的, 在一般的HIDS中會將能夠不在hook處獲取的信息放在agent中獲取, 這樣信息獲取和業務邏輯併發執行, 下降對業務的影響.

4 信息補全與獲取

若是對信息的準確性要求不是很高, 同時但願盡一切可能的不影響部署在HIDS主機上的正常業務那麼能夠選擇hook只獲取PID和環境變量等必要的數據, 而後將這些東西交給agent, 由agent繼續獲取進程的其餘相關信息, 也就是說獲取進程其餘信息的同時, 進程就已經繼續運行了, 而不須要等待agent獲取完整的信息表.

/proc/[pid]/stat

/proc是內核向用戶態提供的一組fifo接口, 經過僞文件目錄的形式調用接口

每個進程相關的信息, 會被放到以pid命名的文件夾當中, ps等命令也是經過遍歷/proc目錄來獲取進程的相關信息的.

一個stat文件內容以下所示, 下面self是/proc目錄提供的一個快捷的查看本身進程信息的接口, 每個進程訪問/self時看到都是本身的信息.

#cat /proc/self/stat
3119 (cat) R 29973 3119 19885 34821 3119 4194304 107 0 0 0 0 0 0 0 20 0 1 0 5794695 5562368 176 18446744073709551615 94309027168256 94309027193225 140731267701520 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 94309027212368 94309027213920 94309053399040 140731267704821 140731267704841 140731267704841 140731267706859 0

會發現這些數據雜亂無章, 使用空格做爲每個數據的邊界, 沒有地方說明這些數據各自表達什麼意思.

通常折騰找到了一篇文章裏面給出了一個列表, 這個表裏面說明了每個數據的數據類型和其表達的含義, 見文章附錄1

最後整理出一個有52個數據項每一個數據項類型各不相同的結構體, 獲取起來仍是有點麻煩, 網上沒有找到輪子, 因此本身寫了一個

具體的結構體定義:

struct proc_stat {
    int pid; //process ID.    char* comm; //可執行文件名稱, 會用()包圍    char state; //進程狀態    int ppid;   //父進程pid    int pgid;
    int session;    //sid    int tty_nr;     
    int tpgid;
    unsigned int flags;
    long unsigned int minflt;
    long unsigned int cminflt;
    long unsigned int majflt;
    long unsigned int cmajflt;
    long unsigned int utime;
    long unsigned int stime;
    long int cutime;
    long int cstime;
    long int priority;
    long int nice;
    long int num_threads;
    long int itrealvalue;
    long long unsigned int starttime;
    long unsigned int vsize;
    long int rss;
    long unsigned int rsslim;
    long unsigned int startcode;
    long unsigned int endcode;
    long unsigned int startstack;
    long unsigned int kstkesp;
    long unsigned int kstkeip;
    long unsigned int signal;   //The bitmap of pending signals    long unsigned int blocked;
    long unsigned int sigignore;
    long unsigned int sigcatch;
    long unsigned int wchan;
    long unsigned int nswap;
    long unsigned int cnswap;
    int exit_signal;
    int processor;
    unsigned int rt_priority;
    unsigned int policy;
    long long unsigned int delayacct_blkio_ticks;
    long unsigned int guest_time;
    long int cguest_time;
    long unsigned int start_data;   
    long unsigned int end_data;
    long unsigned int start_brk;    
    long unsigned int arg_start;    //參數起始地址    long unsigned int arg_end;      //參數結束地址    long unsigned int env_start;    //環境變量在內存中的起始地址    long unsigned int env_end;      //環境變量的結束地址    int exit_code; //退出狀態碼};

從文件中讀入並格式化爲結構體:

struct proc_stat get_proc_stat(int Pid) {
    FILE *f = NULL;
    struct proc_stat stat = {0};
    char tmp[100] = "0";
    stat.comm = tmp;
    char stat_path[20];
    char* pstat_path = stat_path;

    if (Pid != -1) {
        sprintf(stat_path, "/proc/%d/stat", Pid);
    } else {
        pstat_path = "/proc/self/stat";
    }

    if ((f = fopen(pstat_path, "r")) == NULL) {
        printf("open file error");
        return stat;
    }

    fscanf(f, "%d ", &stat.pid);
    fscanf(f, "(%100s ", stat.comm);
    tmp[strlen(tmp)-1] = '\0';
    fscanf(f, "%c ", &stat.state);
    fscanf(f, "%d ", &stat.ppid);
    fscanf(f, "%d ", &stat.pgid);

    fscanf (
            f,
            "%d %d %d %u %lu %lu %lu %lu %lu %lu %ld %ld %ld %ld %ld %ld %llu %lu %ld %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %d %d %u %u %llu %lu %ld %lu %lu %lu %lu %lu %lu %lu %d",
            &stat.session, &stat.tty_nr, &stat.tpgid, &stat.flags, &stat.minflt,
            &stat.cminflt, &stat.majflt, &stat.cmajflt, &stat.utime, &stat.stime,
            &stat.cutime, &stat.cstime, &stat.priority, &stat.nice, &stat.num_threads,
            &stat.itrealvalue, &stat.starttime, &stat.vsize, &stat.rss, &stat.rsslim,
            &stat.startcode, &stat.endcode, &stat.startstack, &stat.kstkesp, &stat.kstkeip,
            &stat.signal, &stat.blocked, &stat.sigignore, &stat.sigcatch, &stat.wchan,
            &stat.nswap, &stat.cnswap, &stat.exit_signal, &stat.processor, &stat.rt_priority,
            &stat.policy, &stat.delayacct_blkio_ticks, &stat.guest_time, &stat.cguest_time, &stat.start_data,
            &stat.end_data, &stat.start_brk, &stat.arg_start, &stat.arg_end, &stat.env_start,
            &stat.env_end, &stat.exit_code    );
    fclose(f);
    return stat;}

和咱們須要獲取的數據作了一下對比, 能夠獲取如下數據

ppid 父進程id
pgid 進程組id
sid 進程會話id
start_time 父進程開始運行的時間
run_time 父進程已經運行的時間

/proc/[pid]/exe

經過/proc/[pid]/exe獲取可執行文件的路徑, 這裏/proc/[pid]/exe是指向可執行文件的軟連接, 因此這裏經過readlink函數獲取軟連接指向的地址.

這裏在讀取時須要注意若是readlink讀取的文件已經被刪除, 讀取的文件名後會多一個 (deleted), 可是agent也不能盲目刪除文件結尾時的對應字符串, 因此在寫server規則時須要注意這種狀況

char *get_proc_path(int Pid) {
    char stat_path[20];
    char* pstat_path = stat_path;
    char dir[PATH_MAX] = {0};
    char* pdir = dir;
    if (Pid != -1) {
        sprintf(stat_path, "/proc/%d/exe", Pid);
    } else {
        pstat_path = "/proc/self/exe";
    }

    readlink(pstat_path, dir, PATH_MAX);
    return pdir;}

/proc/[pid]/cmdline

獲取進程啓動的是啓動命令, 能夠經過獲取/proc/[pid]/cmdline的內容來得到, 這個獲取裏面有兩個坑點

  1. 因爲啓動命令長度不定, 爲了不溢出, 須要先獲取長度, 在用malloc申請堆空間, 而後再將數據讀取進變量.

  2. /proc/self/cmdline文件裏面全部的空格和回車都會變成 '\0'也不知道爲啥, 因此須要手動換源回來, 並且若干個相連的空格也只會變成一個'\0'.

這裏獲取長度的辦法比較蠢, 可是用fseek直接將文件指針移到文件末尾的辦法每次返回的都是0, 也不知道咋辦了, 只能先這樣

long get_file_length(FILE* f) {
    fseek(f,0L,SEEK_SET);
    char ch;
    ch = (char)getc(f);
    long i;
    for (i = 0;ch != EOF; i++ ) {
        ch = (char)getc(f);
    }
    i++;
    fseek(f,0L,SEEK_SET);
    return i;}

獲取cmdline的內容

char* get_proc_cmdline(int Pid) {
    FILE* f;
    char stat_path[100] = {0};
    char* pstat_path = stat_path;

    if (Pid != -1) {
        sprintf(stat_path, "/proc/%d/cmdline", Pid);
    } else {
        pstat_path = "/proc/self/cmdline";
    }

    if ((f = fopen(pstat_path, "r")) == NULL) {
        printf("open file error");
        return "";
    }
    char* pcmdline = (char *)malloc((size_t)get_file_length(f));
    char ch;
    ch = (char)getc(f);
    for (int i = 0;ch != EOF; i++ ) {
        *(pcmdline + i) = ch;
        ch = (char)getc(f);
        if ((int)ch == 0) {
            ch = ' ';
        }
    }
    return pcmdline;}

小結

這裏寫的只是實現的一種最多見最簡單的應用級hook的方法具體實現和代碼已經放在了github上, 同時github上的代碼會保持更新, 下次的文章會分享如何使用LKM修改sys_call_table來hook系統調用的方式來實現HIDS的hook.

參考文章

https://www.freebuf.com/articles/system/54263.html
http://abcdefghijklmnopqrst.xyz/2018/07/30/Linux_INT80/
https://cloud.tencent.com/developer/news/337625
https://github.com/g0dA/linuxStack/blob/master/%E8%BF%9B%E7%A8%8B%E9%9A%90%E8%97%8F%E6%8A%80%E6%9C%AF%E7%9A%84%E6%94%BB%E4%B8%8E%E9%98%B2-%E6%94%BB%E7%AF%87.md

附錄1

這裏完整的說明了/proc目錄下每個文件具體的意義是什麼.
http://man7.org/linux/man-pages/man5/proc.5.html

相關文章
相關標籤/搜索