反彈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.內核態
在內核態監控其實是最準確,而且是最難繞過的。在內核態,一般是通過三種辦法來監控:
- Netlink Connector
- Audit
- 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 內核中用來進行審計的組件,可監控系統調用和文件訪問,具體架構如下:

架構說明:
- 用戶通過用戶態的管理進程配置規則(例如圖中的 go-audit ,也可替換為常用的 auditd ),並通過 Netlink 套接字通知給內核。
- 內核中的 kauditd 通過 Netlink 獲取到規則並加載。
- 應用程序在調用系統調用和系統調用返回時都會經過 kauditd ,kauditd 會將這些事件記錄下來並通過 Netlink 回傳給用戶態進程。
- 用戶態進程解析事件日誌並輸出。
優點
- 組件完善,使用 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舉例:
- 通過 -c 參數輸入命令
- 通過stdin輸入命令。
在這兩個地方將寫文件的代碼嵌入進去即可。
三.已知對抗Shell命令監控方法
以上講解了現有Shell命令監控方法,下面一一進行擊破。對抗命令監控一般是在三個方面動手腳:
- 繞過Shell命令監控方法,使之收集不到命令執行的日誌。
- 無法繞過命令監控,但是能篡改命令執行的進程和參數,使之收集到假的日誌
- 無法繞過監控,也無法篡改內容, 猜測命令告警的策略並繞過(例如通過混淆繞過命令靜態檢測)
在上述的三個方法中,第一種和第二種方法算是比較根本的方法,沒有真實的數據,策略模型就無法命中目標並告警,第三種方法需要較多的經驗,但是通過混淆命令繞過靜態檢測策略,也是比較常見的。
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