linux環境下無文件執行elf
https://blog.spoock.com/2019/08/27/elf-in-memory-execution/
說明
有關linux無文件滲透執行elf的文章晚上已經有非常多了,比如In-Memory-Only ELF Execution (Without tmpfs)和ELF in-memory execution以及這兩篇文章對應的中文版本Linux無文件滲透執行ELF,Linux系統內存執行ELF的多種方式,還存在部分工具fireELF(介紹:fireELF:無文件Linux惡意代碼框架).所有的無文件滲透最關鍵的方法就是memfd_create()這個方法.
MEMFD_CREATE
關於MEMFD_CREATE,在其介紹上面的說明如下:MEMFD_CREATE
int memfd_create(const char *name, unsigned int flags);
memfd_create() creates an anonymous file and returns a file descriptor that refers to it. The file behaves like a regular file,and so can be modified, truncated, memory-mapped, and so on.However, unlike a regular file, it lives in RAM and has a volatile backing storage. Once all references to the file are dropped, it is automatically released. Anonymous memory is used for all backing pages of the file. Therefore, files created by memfd_create() have the same semantics as other anonymous memory allocations such as those allocated using mmap with the MAP_ANONYMOUS flag.
The initial size of the file is set to 0. Following the call, the file size should be set using ftruncate(2). (Alternatively, the file may be populated by calls to write(2) or similar.)
The name supplied in name is used as a filename and will be displayed as the target of the corresponding symbolic link in the directory /proc/self/fd/. The displayed name is always prefixed with memfd: and serves only for debugging purposes. Names do not affect the behavior of the file descriptor, and as such multiple files can have the same name without any side effects.
翻譯為中文就是:
memfd_create()會創建一個匿名文件並返回一個指向這個文件的文件描述符.這個文件就像是一個普通文件一樣,所以能夠被修改,截斷,內存映射等等.不同於一般文件,此文件是保存在RAM中.一旦所有指向這個文件的連接丟失,那么這個文件就會自動被釋放.匿名內存用於此文件的所有的后備存儲.所以通過memfd_create()創建的匿名文件和通過mmap以MAP_ANONYMOUS的flag創建的匿名文件具有相同的語義.
這個文件的初始化大小是0,之后可以通過ftruncate或者write的方式設置文件大小.
memfd_create()函數提供的文件名,將會在/proc/self/fd所指向的連接上展現出來,但是文件名通常會包含有memfd的前綴.這個文件名僅僅只是用來debug,對這個匿名文件的使用沒有任何的影響,同時多個文件也能夠有一個相同的文件名.
在介紹完了memfd_create()之后,我們將以幾個實際的例子來說明情況.
ptrace
ptrace是由奇安信推出的一個開源的工具,其介紹是 Linux低權限模糊化執行的程序名和參數,避開基於execve系統調用監控的命令日志.其示例代碼如下:
1 |
|
對以上的代碼進行分析
lseek
lseek的函數原型是:
1 |
|
其中whence的取值有三個,分別是SEEK_SET,SEEK_CUR,SEEK_END三個值,取值不同對offset的解釋也不同,具體參考LSEEK(2)
而本例中的 filesize = lseek(fd, SEEK_SET, SEEK_END); 等價於 filesize = lseek(fd, 0, SEEK_END); 表示獲取整個文件的大小
1 |
fd = open(path, O_RDONLY); |
所以上面的代碼含義就是:讀取path文件,通過lseek獲取path文件的大小,並通過write函數將path文件的內容寫入到elfbuf中.
memfd_create
按照我們前面對memfd_create的討論,直接通過memfd_creat(“elf”, MFD_CLOEXEC);這樣理論上就可以得到一個匿名文件的fd 和上面代碼中的 syscall(__NR_memfd_create, “elf”, MFD_CLOEXEC);是完全等價的
關於這一點我非常的納悶,后來看了In-Memory-Only ELF Execution 才知道這篇文章中使用的perl語言,考慮到在perl語言中沒有libc庫,所以無法直接調用memfd_create()函數.所以需要借助與syscall的方式調用memfd_create()方法.那么通過syscall()調用需要知道memfd_create()的系統調用碼.
1 |
$ uname -a |
memfd_create的函數調用碼是319,MFD_CLOEXEC對應的值是1U.綜合以下的三種方式都是等價的:
- memfd_create(“elf”,MFD_CLOSEXEC)
- syscall(__NR_memfd_create, “elf”, MFD_CLOEXEC);
- syscall(319,”elf”,1);
除此之外,還要說明下MFD_CLOEXEC這個設置的含義.MFD_CLOEXEC等同於close-on-exec.顧名思義,就是在運行完畢之后關閉這個文件句柄.在復雜系統中,有時我們fork子進程時已經不知道打開了多少個文件描述符(包括socket句柄等),這此時進行逐一清理確實有很大難度。我們期望的是能在fork子進程前打開某個文件句柄時就指定好:“這個句柄我在fork子進程后執行exec時就關閉”。其實時有這樣的方法的:即所謂的 close-on-exec。
execve
執行的關鍵代碼是:
1 |
sprintf(cmdline, "/proc/self/fd/%d", fdm); |
將所得到的匿名文件句柄賦值給當前進程的文件描述符,返回給cmdline,所以cmdline就是當前進程的文件描述符(其內容就是anonyexec函數所傳遞過來的path的內容)
所以execve(argv[0],argv,NULL),在本例中就等同於execve(“/binuname”,”-a”,NULL);
通過auditd監控,我們得到的結果如下:
1 |
type=EXECVE msg=audit(1566354435.549:153): argc=2 a0="/proc/self/fd/4" a1="-a" |
捕獲到的代碼執行的語句是 type=EXECVE msg=audit(1566354435.549:153): argc=2 a0=”/proc/self/fd/4” a1=”-a” 根本就沒有出現uname,而是/proc/self/fd/4,躲避了利用execve進行命令監控的檢測.
通過監控proc,得到對應的信息是: {“pid”:”8360”,”ppid”:”22571”,”uid”:”1000”,”cmdline”:”/proc/self/fd/4 -a “,”exe”:”/memfd:elf (deleted)”,”cwd”:”/home/spoock/Desktop/test”} 與auditd監控到的數據是吻合的.
至於memfd_create()函數提供的文件名,在exe上面體現出來了,即/memfd:elf (deleted),以memfd:開頭緊接着是文件名.
ELF in-memory execution
再來看看在ELF in-memory execution中的示例程序,與ptrace的程序還是存在區別.
1 |
|
與ptrace不同的是,上述的代碼使用了fork()來實現無文件滲透的目的.前面的fd = syscall(SYS_memfd_create, “foofile”, 0);和ptrace的含義一樣,這里就不做說明了.
fork
1 |
child = fork(); |
- child=fork(),fork得到一個子進程;
- child == 0 判斷當前的進程是否為子進程,如果是子進程,就進行后面的操作;
- dup2(fd, 1);close(fd); 將子進程的1文件描述符(標准輸出)指向fd
- execlp(“/bin/date”, “/bin/date”, NULL); execlp()和execve()的作用一樣,都是執行程序.在這里就是執行/bin/date代碼;
由於子進程已經將標准輸出指向了fd,那么通過execlp(“/bin/date”, “/bin/date”, NULL);執行的結果就會寫入到fd中.
read
關於fork,我們需要明確的是,執行fork()時,子進程會獲得父進程所以文件描述符的副本.這些副本的創建方式類似於dup(),這也意味着父,子進程中對應的描述符均指向相同的打開文件句柄.所以在子進程對fd修改了之后,在父進程中也是能夠看到對fd修改的.
分析下面的代碼:
1 |
lseek(fd, 0, SEEK_SET); |
- lseek(fd, 0, SEEK_SET); 將文件fd的偏移量重置為文件開頭
- br = read(fd, buf, BUFSIZ); 讀取fd的大小至buf中,並返回讀取文件的長度br
- buf[br] = 0; 將最后一個字符設置為0
最終就是通過printf(“child said: ‘%s’\n”, buf); 打印fd的結果,其實就是/bin/date的執行結果.
我們分析通過audit和proc下面來觀察執行過程.
audit的查看結果如下:
1 |
type=SYSCALL msg=audit(1566374961.124:5777): arch=c000003e syscall=59 success=yes exit=0 a0=55d8b6c9ac1a a1=7ffdd40de700 a2=7ffdd40e08a8 a3=0 items=2 ppid=22918 pid=22919 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts1 ses=2 comm="date" exe="/bin/date" key="rule01_exec_command" |
在proc中查看的信息:
1 |
$ ls -al /proc/22918/fd |
這個特征還是很明顯的,文件描述符3和ptrace的特征是一樣的,都是以memfd開頭,后面跟着是通過memfd_create()創建的匿名文件的名字.
fireELF
fireELF也是一款無文件的滲透測試工具,其介紹如下:
fireELF is a opensource fileless linux malware framework thats crossplatform and allows users to easily create and manage payloads. By default is comes with ‘memfd_create’ which is a new way to run linux elf executables completely from memory, without having the binary touch the harddrive.
根據其介紹,說明其也是通過memfd_create()的方式來創建一個位於內存中的匿名文件進行無文件滲透實驗的.分析其核心代碼:simple.py
1 |
import base64 |
其實關鍵代碼還是:
1 |
libc = ctypes.CDLL(None) |
本質上還是調用memfd_create()創建了一個匿名文件,通過os.write(fd, content)注入payload,最后利用fexecve(fd, argv, argv)執行.和前面的兩種做法本質上還是一樣的.
總結
無文件執行elf本質上其實就是利用了memfd_create()創建了一個位於內存中的匿名文件,某種程度上給檢測還是帶來一些挑戰.雖然如此,通過memfd_create()的方式執行elf還是有一些特征的.
參考
Linux無文件滲透執行ELF
Linux系統內存執行ELF的多種方式
================ End