Linux ELF无文件内存执行学习小记 2021-12-16 04:52:34 Steven Xeldax > 无文件攻击有很多中,在java中有内存马,在windows中有PE反射加载,这些内存加载技术很大一定程度上能够帮助我们规避蓝队防护软件的检测。但是在linux中很少有人提及到ELF内存马的技术,其实主要原因在于ELF内存载入很难解决延迟绑定和动态依赖库加载的问题,大部分elf加载需要依靠memfd和shm这种linux特性的办法来做,而这两种办法需要linux系统版本有一定的限制。 [TOC] ## 前言 一个elf内存马必须这些条件: 1. elf文件不落地 2. elf动态加载 关于elf文件其实有许多之前人提出过的不错的思路 - http://www.hick.org/code/skape/papers/remote-library-injection.pdf - https://grugq.github.io/docs/ul_exec.txt - http://z0mbie.daemonlab.org/infelf.html - http://phrack.org/issues/63/11.html - https://github.com/mak/pyself - https://blog.gdssecurity.com/labs/2017/9/5/linux-based-inter-process-code-injection-without-ptrace2.html 这些研究者的所提出的内存加载更多使用的是汇编角度进行而我们再本文主要还是偏向于linux一些特性的探究。 ## memfd 无文件方法 > memfd_create:允许我们在内存中创建一个文件,但是它在内存中的存储并不会被映射到文件系统中 这是linux系统的底层调用函数memfd_create(2),它在内核3.17中引入,会创建一个匿名文件并返回一个文件描述符指向它,该文件表现和常规文件类同, 可以进行修改,截断,内存映射等等,但不同的是,它存在于RAM当中。这就是可以被攻击者所利用的,如果有办法将需要执行elf通过memfd_create(2)写入内存中进行执行的话就可以达到我们的目的。 运行的elf从proc文件系统是可以看到memfd的特征的 ![](/download/eb3e95c2-929f-4e9f-8e65-3d127656901a.png) 使用memfd很简单,只要使用特定的系统调用,然后使用write调用将我们的elf写入到memfd句柄就行。 ``` int anoyexec(const char *path, char argv[]){ int fd, fdm, filesize; void *elfbuf; char cmdline[256]; fd = open(path, O_RDONLY); filesize = lseek(fd, SEEK_SET, SEEK_END); lseek(fd,SEEK_SET,SEEK_SET); elfbuf = malloc(filesize); read(fd, elfbuf, filesize); close(fd); fdm = syscall(__NR_memfd_create, "elf", MFD_CLOEXEC); ftruncate(fdm,filesize); write(fdm, elfbuf, filesize); free(elfbuf); sprintf(cmdline, "/proc/self/fd/%d", fdm); // argv[0] = cmdline; execve(cmdline, argv, NULL); printf("%s",argv[0]); // free(elfbuf); return -1; } ``` 然后这个句柄本身就可以在bash下直接执行,当然也可以在c中运行execve执行 ![](/download/5de285d6-bc1d-4702-8a3f-a8256bd9cc5a.png) 使用memfd实现fork ``` int elf_mem_exe_fork(){ int fd; pid_t child; char buf[BUFSIZ] = ""; ssize_t br; fd = syscall(SYS_memfd_create, "foofile",0); if(fd == -1){ perror("memfd_create"); exit(EXIT_FAILURE); } child = fork(); if(child == 0){ dup2(fd,1); close(fd); execlp("/bin/date","",NULL); perror("execlp date"); } else if(child == -1){ perror("fork"); exit(EXIT_FAILURE); } waitpid(child, NULL,0); lseek(fd,0,SEEK_SET); br = read(fd, buf, BUFSIZ); if(br == -1){ perror("read"); exit(EXIT_FAILURE); } buf[br] = 0; printf("pid: %d\n",getpid()); printf("child sad: %s \n",buf); pause(); exit(EXIT_SUCCESS); } ``` 在其他语言一样如此,甚至使用起来还要更加简单一点,比如说python ``` import ctypes import os binary = open('/bin/date','rb').read() fd = ctypes.CDLL(None).syscall(319,"",1) final_fd = open('/proc/self/fd/'+str(fd),'wb') final_fd.write(binary) final_fd.close() os.execl('/proc/self/fd/'+str(fd),"") ``` ## shm 无文件方法 这个方法最早发现于glibc 中fexecve这个方法所应用。 fexecve同样的功能很强大,它能使我们执行一个程序(同execve),但是传递给这个函数的是文件描述符,而不是文件的绝对路径。 memfd_create 是在kernel3.17才被引进来,fexecve是glibc的一个函数,是在版本2.3.2之后才有的 ![](/download/ddd833a8-a999-4c85-be2c-16096f099b93.png) 关于fexecve用法如下: ``` #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> static char *args[] = { "hic et nunc", "-l", "/dev/shm", NULL }; extern char **environ; int main(void) { struct stat st; void *p; int fd, shm_fd, rc; shm_fd = shm_open("wurstverschwendung", O_RDWR | O_CREAT, 0777); if (shm_fd == -1) { perror("shm_open"); exit(1); } rc = stat("/bin/ls", &st); if (rc == -1) { perror("stat"); exit(1); } rc = ftruncate(shm_fd, st.st_size); if (rc == -1) { perror("ftruncate"); exit(1); } p = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); if (p == MAP_FAILED) { perror("mmap"); exit(1); } fd = open("/bin/ls", O_RDONLY, 0); if (fd == -1) { perror("openls"); exit(1); } rc = read(fd, p, st.st_size); if (rc == -1) { perror("read"); exit(1); } if (rc != st.st_size) { fputs("Strange situation!\n", stderr); exit(1); } munmap(p, st.st_size); close(shm_fd); shm_fd = shm_open("wurstverschwendung", O_RDONLY, 0); fexecve(shm_fd, args, environ); perror("fexecve"); return 0; } ``` 编译的时候使用gcc -lrt不然会报错,提示找不到shm_open的符号 ![](/download/67e634c1-78e7-4134-9faa-a03199b06ece.png) 运行以后可以看到执行了ls,当然我们去/dev/shm下也能够看到这个文件,和memfd一模一样我们能够在bash中执行这个二进制 ![](/download/d72a397c-4486-439f-97ed-3fe3f8d8a4cc.png) ## 一种golang elf内存马的实现 这是在run embeded elf 上修改的项目,使用asm内联汇编修改满足golang elf rt0_linux的堆栈空间迁移,然后jmp到rt0_linux入口点从而实现的内存加载方式。 项目地址如下: ``` https://github.com/merlinxcy/memory_execute_golang_elf ``` ## 推荐工具 ### run-embedded-elf-from-memory https://github.com/AXDOOMER/run-embedded-elf-from-memory ![](/download/14889af0-3f21-4434-abe7-ec05b08a1dcb.png) ### fileless-xec https://github.com/ariary/fileless-xec ![](/download/1697c092-5cef-4e74-afc9-df024e74c41c.png) ### fireELF https://github.com/rek7/fireELF ![](/download/fc954aa8-38f4-41f0-acfb-b516b2f95de6.png) ### go-memfd https://github.com/justincormack/go-memfd ![](/download/964c679e-bcda-4208-80f4-8f677423f9a9.png) ### memfd-exmaple https://github.com/a-darwish/memfd-examples ![](/download/ea111ef3-62f3-47c1-96b6-a53e6982db78.png) ### memrun https://github.com/guitmz/memrun ![](/download/dd5674bf-ebb4-4d67-a220-4ca53a99c799.png) ## 参考资料 参考资料 http://www.suphp.cn/anquanke/4/168204.html https://b0ldfrev.gitbook.io/note/linux_operating_system/linux-wu-wen-jian-zhi-hang-elf https://blog.spoock.com/2019/08/27/elf-in-memory-execution/ https://blog.csdn.net/whatday/article/details/99430757?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link https://kevien.github.io/2018/02/20/linux%E4%B8%80%E7%A7%8D%E6%97%A0%E6%96%87%E4%BB%B6%E5%90%8E%E9%97%A8%E6%8A%80%E5%B7%A7(%E8%AF%91%E6%96%87)/ https://cloud.tencent.com/developer/article/1590998 https://www.anquanke.com/post/id/168791#h2-0 https://www.anquanke.com/post/id/168204#h2-1