反彈shell-逃逸基於execve的命令監控(上)

  • 2019 年 12 月 26 日
  • 筆記

微信公眾號:七夜安全博客 關注信息安全技術、關注 系統底層原理。問題或建議,請公眾號留言。

一.前言

本篇聊一聊 新的主題:《反彈shell-逃逸基於execve的命令監控》,打算寫一個專題,預估可以寫三篇,內容確實有點多,也是最近研究了一些有意思的東西,想給大家分享一下。喜歡的話,請大家一定點在看,並分享出去,算是對我原創最大的支持了。

二.Shell命令監控

在linux中,大家用的比較多的是shell命令,同樣在滲透到linux服務器,植入木馬後,探測信息,執行惡意操作,維持權限,橫向移動,shell命令也是必不可少的。既然在攻擊側,shell命令如此重要,那在安全防禦方面,shell命令的監控也是非常關鍵的檢測維度。各大廠商,一般怎麼監控shell命令的調用呢?

2.1 基於execve的shell命令監控

系統命令,其實就是一個個程序,執行起來也就是一個個進程。命令執行的監控,也就是對外部進程創建的監控。在linux中,啟動外部進程,是通過execve系統調用進行創建的,我們使用strace 打印一下在bash中啟動ls的系統調用,第一句就是通過execve啟動ls。

但是我們在開發linux程序的時候,執行系統命令,並沒有直接使用execve系統調用,這是因為libc/glibc庫對execve系統調用封裝成了函數,方便我們調用。

因此基於execve的系統命令監控方式,分成了用戶態和內核態。用戶態通過劫持libc/glibc的exec相關函數來實現,內核態則通過系統自身組件或者劫持execve syscall 來實現。

1.用戶態

在libc/glibc中,對execve syscall 進行了一系列的封裝,簡稱exec族函數。exec系列函數調用時,啟動新進程,替換掉當前進程。即程序不會再返回到原進程,具體內容如下:

int execl(constchar*path,constchar*arg0,...,(char*)0);  int execlp(constchar*file,constchar*arg0,...,(char*)0);  int execle(constchar*path,constchar*arg0,...,(char*)0,char*const envp[]);    int execv(cosnt char*path,char*const argv[]);  int execvp(cosnt char*file,char*const argv[]);  int execve(cosnt char*path,char*const argv[],char*const envp[]);

怎麼劫持libc/glibc中的函數,我就不擴展了,大家google一下 so preload 劫持

2.內核態

在內核態監控其實是最準確,而且是最難繞過的。在內核態,一般是通過三種辦法來監控:

  1. Netlink Connector
  2. Audit
  3. Hook execve syscall

(1) Netlink Connector

在介紹 Netlink Connector 之前,首先了解一下 Netlink 是什麼,Netlink 是一個套接字家族(socket family),它被用於內核與用戶態進程以及用戶態進程之間的 IPC 通信,ss命令就是通過 Netlink 與內核通信獲取的信息。

Netlink Connector 是一種 Netlink ,它的 Netlink 協議號是NETLINK_CONNECTOR,其代碼位於:

https://github.com/torvalds/linux/tree/master/drivers/connector

其中 connectors.c 和 cnqueue.c 是 Netlink Connector 的實現代碼,而 cnproc.c 是一個應用實例名為進程事件連接器,我們可以通過該連接器來實現對進程創建的監控。

實現方案:

(引用來源:https://4hou.win/wordpress/?p=29586)

說明

  • linux內核提供連接器模塊與進程事件收集機制,無需任何改動,只需要在linux>2.6.14開啟即可。通過 cat/boot/config-$(uname-r)|egrep'CONFIGCONNECTOR|CONFIGPROC_EVENTS'就可以查看。
  • 在用戶態實現輕量級ncp(netlink connector process)應用程序接收netlink進程事件消息

優點

輕量級,在用戶態即可獲得內核提供的信息。

缺點

僅能獲取到 pid,詳細信息需要查/proc/pid/,這就存在時間差,可能有數據丟失。

(2) Audit

Audit 是 Linux 內核中用來進行審計的組件,可監控系統調用和文件訪問,具體架構如下:

架構說明

  1. 用戶通過用戶態的管理進程配置規則(例如圖中的 go-audit ,也可替換為常用的 auditd ),並通過 Netlink 套接字通知給內核。
  2. 內核中的 kauditd 通過 Netlink 獲取到規則並加載。
  3. 應用程序在調用系統調用和系統調用返回時都會經過 kauditd ,kauditd 會將這些事件記錄下來並通過 Netlink 回傳給用戶態進程。
  4. 用戶態進程解析事件日誌並輸出。

優點

  • 組件完善,使用 auditd 軟件包中的工具即可滿足大部分需求,無需額外開發代碼。
  • 相比於 Netlink Connector ,獲取的信息更為全面,不僅僅是 pid 。

缺點

性能消耗隨着進程數量提升有所上升。

(3) Hook execve syscall

除了Netlink Connector 和 Audit 這兩種Linux 本身提供的監控系統調用方式,如果想擁有更大程度的可定製化,就需要通過安裝內核模塊來對系統調用進行 hook。

目前常用的 hook 方法是通過修改syscall table(Linux 系統調用表)來實現,原理是系統在執行系統調用時是通過系統調用號在syscalltable中找到相應的函數進行調用,所以只要將syscalltable中execve對應的地址改為我們安裝的內核模塊中的函數地址即可.

具體細節請參考:馭龍 HIDS實現進程監控,裏面已經介紹的非常詳細了,不再贅述。

優點

高定製化,從系統調用層面獲取完整信息。

缺點

  • 開發難度大,非常考驗開發人員的技術功底。
  • 兼容性差,需針對不同發行版和內核版本進行定製和測試。

2.2 基於 Patch Shell解釋器的命令監控

基於 Patch Shell解釋器的命令監控是基於execve的系統命令監控的補充方案,因為通過監控execve系統調用的方式,理論上可以完全覆蓋系統命令的調用,那為什麼還要 Patch Shell解釋器呢?

大家別忘了,shell不僅可以調用外部系統命令,自身還有很多內置命令。內置命令是shell解釋器中的一部分,可以理解為是shell解釋器中的一個函數,並不會額外創建進程。因此監控execve系統調用是無法監控這部分的,當然能用作惡意行為的內置命令並不多,算是一個補充。

如何判斷是否是內置命令呢?通過type指令,示例如下:

[root@localhost ~]# type cd  cd is a Shell builtin  [root@localhost ~]# type ifconfig  ifconfig is/sbin/ifconfig

完整的內置命令列表,請參考 shell內置命令[http://c.biancheng.net/view/1136.html]。

如何Patch Shell解釋器 ? 原理很簡單,對shell解釋器的輸入進行修改,將輸入寫入到文件中,進行分析即可。shell解釋器很多,以bash舉例:

  1. 通過 -c 參數輸入命令
  2. 通過stdin輸入命令。

在這兩個地方將寫文件的代碼嵌入進去即可。

三.已知對抗Shell命令監控方法

以上講解了現有Shell命令監控方法,下面一一進行擊破。對抗命令監控一般是在三個方面動手腳:

  1. 繞過Shell命令監控方法,使之收集不到命令執行的日誌。
  2. 無法繞過命令監控,但是能篡改命令執行的進程和參數,使之收集到假的日誌
  3. 無法繞過監控,也無法篡改內容, 猜測命令告警的策略並繞過(例如通過混淆繞過命令靜態檢測)

在上述的三個方法中,第一種和第二種方法算是比較根本的方法,沒有真實的數據,策略模型就無法命中目標並告警,第三種方法需要較多的經驗,但是通過混淆命令繞過靜態檢測策略,也是比較常見的。

3.1 無日誌-繞過Shell命令監控

已知的繞過命令監控的方案:用戶態glibc/libc exec劫持,Patch Shell解釋器,內核態的execve監控,均可被繞過。

1. 繞過glibc/libc exec劫持

方法1:glibc/libc是linux中常用的動態鏈接庫,也就是說在動態鏈接程序的時候才會用到它,那麼我們只需要將木馬後門進行靜態編譯即可,不依賴系統中的glibc/libc執行,就不會被劫持。

方法2: glibc/libc是對linux系統調用(syscall)的封裝,我們使用它是為了簡化對系統調用的使用,其實我們可以不用它,直接使用彙編 sysenter/int 0x80指令調用execve系統調用,下面是使用int 0x80調用execve syscall的簡寫代碼:

mov     byte       al, 0x0b # 好了,現在把execve()的系統調用號11號放入eax的最下位的al中  mov                ebx, esi # 現在是第一個參數,字符串的位置放入ebx  lea                ecx, [esi+8] # 第二個參數,要點是這個參數類型是char **, 如果/bin/sh有其它參數的話,整個程序寫法就又不一樣了  lea                edx, [esi+12] #  最後是null的地址,注意,是null的地址,不是null,因為寫這是為了shellcode做準備,shellcode中不可以有null  int0x80

當然還有其他方法,比如重寫LD_PRELOAD環境變量,這樣的動作太大,就不講了。

2. 繞過Patch Shell解釋器

方法1:不使用shell解釋器執行命令,直接使用execve 方法2:不使用被Patch的shell解釋器,例如大家常用的bash被patch,那你可以使用linux另一個 tcsh解釋器來執行命令。

[root@VM_0_13_centos ~]# tcsh -c "echo hello"  hello

3.繞過內核態execve syscall

只要你使用了execve執行了命令,就絕對逃不過內核態execve syscall的監控,太底層了,除非你把防禦方的內核驅動給卸載了。既然如此,那怎麼繞過呢?

方法很簡單,就是不使用execve系統調用。(不是廢話)

大家想想為什麼會有反彈shell? 為什麼要彈shell?

其實是我們想借用linux中自帶的系統命令來達到我們的目的,尤其是在linux中以系統命令操作為主。

ls 命令為例子,功能是查看目錄中有哪些文件,假如我們不想使用ls命令,那我們有什麼辦法呢?

那就自己寫一個類似功能程序的代碼,然後執行就可以了。既然不想使用execve啟動進程來執行,那直接在木馬中執行shellcode就ok了。

我以python shellcode為例子(你也可以寫 彙編 shellcode):

ls_shellcode = '''  import os    dst_path = '{dst_path}'    dirs = os.listdir(dst_path)    for file in dirs:      print(file)    '''  exec(ls_shellcode.format(dst_path = "C:/"))

輸出:

$Recycle.Bin  DocumentsandSettings  Intel  pagefile.sys  PerfLogs  ProgramFiles  ProgramFiles(x86)  ......

這樣根本不會出現execve系統調用,你要把shellcode通過網絡傳輸過來即可。

隱秘還是挺隱秘的,缺點就是費事,尤其是寫彙編shellcode的時候,linux中使用的命令還是挺多的,而且自己寫的shellcode,也沒有原始linux命令使用的親切感。

3.2 假日誌 – 混淆進程名與進程參數

1.混淆進程名

在linux中有個syscall,名字叫做memfd_create (http://man7.org/linux/man-pages/man2/memfd_create.2.html)。

memfd_create()會創建一個匿名文件並返回一個指向這個文件的文件描述符.這個文件就像是一個普通文件一樣,所以能夠被修改,截斷,內存映射等等.不同於一般文件,此文件是保存在RAM中.一旦所有指向這個文件的連接丟失,那麼這個文件就會自動被釋放

這就是說memfd_create創建的文件是存在與RAM中,那這個的文件名類似 /proc/self/fd/%d,也就是說假如我們把 ls命令bin文件使用memfd_create寫到內存中,然後在內存中使用execve執行,那看到的不是 ls,而是執行的 /proc/self/fd/%d ,從而實現了進程名稱混淆 和無文件。

具體看這篇文章(http://www.polaris-lab.com/index.php/archives/666/),非常詳細,還有例子說明。

2.混淆進程參數

使用的是linux中另一個syscall: ptrace。ptrace是用來調試程序用的,使用execve啟動進程,相對於自身來說是啟動子進程,ptrace 的使用流程一般是這樣的:

父進程 fork() 出子進程,子進程中執行我們所想要 trace 的程序,在子進程調用 exec() 之前,子進程需要先調用一次 ptrace,以 PTRACETRACEME 為參數。這個調用是為了告訴內核,當前進程已經正在被 traced,當子進程執行 execve() 之後,子進程會進入暫停狀態,把控制權轉給它的父進程(SIGCHLD信號), 而父進程在fork()之後,就調用 wait() 等子進程停下來,當 wait() 返回後,父進程就可以去查看子進程的寄存器或者對子進程做其它的事情了

假如我們想執行 ls-alh,在上文中 ls 已經可以被混淆了。接下來使用ptrace 對 -alh進行混淆。大體的操作流程如下:

  • 第一步:首先我們fork出來一個子進程,然後在子進程中先調用ptrace,接着執行execve("ls xxxxxx"),這個時候基於execve監控到的就是一個假參數
  • 第二步:既然傳入的是假參數,那肯定是是無法執行到想要的結果,這個時候父進程等待子進程execve後停下來,然後修改傳入參數的寄存器,將其修改為 -alh,最後接着讓子進程繼續運行即可。

具體請看這篇文章:http://www.polaris-lab.com/index.php/archives/667/,不再贅述。

四.新方法-無"命令"反彈shell

在已知的繞過方法中,通過shellcode方式繞過內核態的execve監控,算是相對優雅的方式了,我比較喜歡這種,但是這種方式又太麻煩,linux的命令我都要重寫成shellcode, 而且顯示方式肯定沒有原來這麼可愛

主要是懶。。。。

其實我的需求很簡單:

我既想要linux命令原有的功能,又不想用execve syscall的方式啟動

想了想,怎麼辦呢?

暫時不寫了,刪了一次,有點敏感,還望見諒

最後

這篇文章寫了三個星期,主要是工作挺忙了,每天寫一點,後台也有朋友經常催更的,很抱歉了。

最近工作方面也取得一些短暫的進展,運氣還是會傾向於努力的人

繼續戰鬥,敬請期待。

參考文獻:

http://www.polaris-lab.com/index.php/archives/667/;

https://segmentfault.com/a/1190000019828080