0x1:system() glibc api
system是linux系统提供的函数调用之一,glibc也提供了对应的封装api。
system函数的原型为:
#include <stdlib.h> int system (const char *string);
它的作用是,运行以字符串参数的形式传递给它的命令并等待该命令的完成。命令的执行情况就如同在shell中执行命令:sh -c string。
如果无法启动shell来运行这个命令,system函数返回错误代码127;如果是其他错误,则返回-1。否则,system函数将返回该命令的退出码。
#include <stdlib.h> #include <stdio.h> int main() { printf("Running ps with system\n"); system("ps au");// 1 printf("ps Done\n"); exit(0); } # gcc -o new_ps_system new_ps_system.c
netlink监控到的进程链信息如下:
0x2:exec系列 glibc api
exec系列函数由一组相关的函数组成,它们在进程的启动方式和程序参数的声明上各有不同。但是exec系列函数都有一个共同的工作方式,就是把当前进程替换为一个新进程,也就是说我们可以使用exec函数将程序的执行从一个程序切换到另一个程序,在新的程序启动后,原来的程序就不再执行了。
新进程由path或file参数指定。
#include <unistd.h> char **environ; int execl (const char *path, const char *arg0, ..., (char*)0); int execlp(const char *file, const char *arg0, ..., (char*)0); int execle(const char *path, const char *arg0, ..., (char*)0, char *const envp[]); int execv (const char *path, char *const argv[]); int execvp(cosnt char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);
如果想用exec系统函数来启动ps进程,则这6个不同的函数的调用语句为:
char *const ps_envp[] = {"PATH=/bin:usr/bin", "TERM=console", 0}; char *const ps_argv[] = {"ps", "au", 0}; execl("/bin/ps", "ps", "au", 0); execlp("ps", "ps", "au", 0); execle("/bin/ps", "ps", "au", 0, ps_envp); execv("/bin/ps", ps_argv); execvp("ps", ps_argv); execve("/bin/ps", ps_argv, ps_envp);
完整的例子如下,
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { printf("Running ps with execlp\n"); execlp("ps", "ps", "au", (char*)0); printf("ps Done"); exit(0); } # gcc -o new_ps_exec new_ps_exec.c -lm
从netlink监控到的进程启动消息可以看到,通过exec方式启动的新进程,直接将当前进程替换为了新的指令进程。
需要注意的是,一般情况下,exec函数是不会返回的,除非发生错误返回-1,由exec启动的新进程继承了原进程的内存空间和句柄,在原进程中已打开的文件描述符在新进程中仍将保持打开,但任何在原进程中已打开的目录流都将在新进程中被关闭。
所以我们可以发现,最后的ps Done并没有输出,因为程序并没有再一次返回到程序new_ps_exec.exe上。
因为调用execlp函数时,new_ps_exec.exe进程被替换为ps进程,当ps进程结束后,整个程序就结束了,并没有回到原来的new_ps_exec.exe进程上,原本的进程new_ps_exec.exe不会再执行,所以语句printf("ps Done");根本没有机会执行。
0x3:fork() glibc api
# multiprocessing.py import os print 'Process (%s) start...' % os.getpid() pid = os.fork() if pid==0: print 'I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()) else: print 'I (%s) just created a child process (%s).' % (os.getpid(), pid)
netlink监控到的进程链信息如下:
Relevant Link:
https://blog.csdn.net/ljianhui/article/details/10089345 https://www.ibm.com/developerworks/cn/linux/l-connector/index.html
2. 通过syscall系统调用执行指令
除了通过glibc调用fork/execv之外,还可以绕过glibc,直接通过汇编触发“int80中断”,从而直接使用操作系统提供的系统调用能力。
0x1:system syscall
0x2:execve syscall
asm_execve.s
.section .data file_to_run: .ascii "/bin/sh" .section .text .globl main main: pushl %ebp movl %esp, %ebp subl $0x8, %esp # array of two pointers. array[0] = file_to_run array[1] = 0 movl file_to_run, %edi movl %edi, -0x4(%ebp) movl $0, -0x8(%ebp) movl $11, %eax # sys_execve movl file_to_run, %ebx # file to execute leal -4(%ebp), %ecx # command line parameters movl $0, %edx # environment block int $0x80 leave ret
Makefile
NAME = asm_execve
$(NAME) : $(NAME).s
gcc -o $(NAME) $(NAME).s
编译并执行
gcc -o asm_execve asm_execve.s
上述汇编原理上和下面这段C代码是等价的,
char *data[2]; data[0] = "/bin/sh"; data[1] = NULL; execve(data[0], data, NULL);
x64版本的汇编如下:
.section .text .globl main main: xor %rdx, %rdx push %rdx sub $0x16, %rsp movb $0x2f, 7(%rsp) movl $0x2f6e6962, 8(%rsp) movl $0x746163, 12(%rsp) leaq 7(%rsp), %rdi pushq %rdx push %rdi mov %rsp, %rsi movb $0x3b, %al syscall
Relevant Link:
https://stackoverflow.com/questions/9342410/sys-execve-system-call-from-assembly https://stackoverflow.com/questions/47897025/assembly-execve-bin-bash-x64 https://www.exploit-db.com/exploits/35205 https://reverseengineering.stackexchange.com/questions/21634/how-to-pass-param-to-execve-to-execute-cat-a-file-in-x64-asm
3. 通过Bash执行指令
0x1:Bash内置指令执行
所谓 Shell 内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件。
例如,用于进入或者切换目录的 cd 命令,该命令并不是某个外部文件,只要在 Shell 中就直接可以运行这个命令,Bash 会完成指令的解析与执行并返回结果。内建指令不会启动新进程。
可以用type指令来查看某个指令是否是Bash内建指令,
root@iZbp1i02dgbk14bmjk8m9vZ:~# type cd cd is a shell builtin root@iZbp1i02dgbk14bmjk8m9vZ:~# type ifconfig ifconfig is /sbin/ifconfig
0x2:通过Bash启动第三方新进程(指令)
本质上Bash通过调用execve() glibc api来实现的,以执行whoami指令为例,
同时,netlink会收到一个进程启动事件消息,
4. Python启动新进程
0x1:os.execv()
#!/usr/bin/python #coding=utf-8 import os def main(): print "Running ps with execlp" os.execlp("who", 'who') print "Done." main() # strace python new_proc.py
strace跟踪如下:
可以看到,python底层还是调用了glibc库的execv api来实现进程启动的。
netlink监控消息如下,
0x2:python Multiprocessing类
- Unix/Linux下,multiprocessing模块封装了fork()调用,使我们不需要关注fork()的细节
- Windows没有fork调用,因此,multiprocessing需要“模拟”出fork的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去
创建出多进程后,每个新进程都拷贝了一份原主进程的完整py代码,新的多进程可以继续执行py代码中指定的callback函数。
同时,进程间通信是通过Queue、Pipes等实现的,方便multiprocess进行多进程管理。
1. Process(用于创建进程模块)
因为python使用全局解释器锁(GIL),他会将进程中的线程序列化,也就是多核cpu实际上并不能达到并行提高速度的目的,而使用多进程则是不受限的,所以实际应用中都是推荐多进程的。
# -*- coding: utf-8 -*- from multiprocessing import Process import time import random def test(): for i in range(1,5): print("---%d---"%i) time.sleep(60) p = Process(target=test) p.start() #让这个进程开始执行test函数里面的代码 p.join() #等进程p结束之后,才会继续向下走 print("---main----")
可以看到,通过python Process类启动的新进程,只会监控到fork事件,从netlink角度来看,就是重复启动了一次原脚本文件。
2. Pool(用于创建管理进程池)
# -*- coding: utf-8 -*- from multiprocessing import Pool import os import time def worker(num): for i in range(2): print("===pid=%d===num=%d"%(os.getpid(), num)) time.sleep(1) pool = Pool(3) #定义一个进程池,最大进程数3 for i in range(5): print("---%d---"%i) pool.apply_async(worker, [i,]) #使用非阻塞方式调用func(并行执行,堵塞方式必须 #等待上一个进程退出才能执行下一个进程) print("---start----") pool.close() #关闭进程池,关闭后pool不能再添加新的请求 pool.join() #等待pool中所有子进程执行完成,必须放在close语句之后 print("---end----")
可以看到,通过python Pool类启动的新进程,底层还是调用的fork。
Relevant Link:
https://thief.one/2016/11/23/Python-multiprocessing/ https://blog.csdn.net/Duke10/article/details/79861201 https://www.liaoxuefeng.com/wiki/897692888725344/923056295693632
0x3:执行中间态缓存文件
Python的程序中,是把原始程序代码放在.py文件里,而Python会在执行.py文件的时候。将.py形式的程序编译成中间式文件(byte-compiled)的.pyc文件,这么做的目的就是为了加快下次执行文件的速度。
同样,攻击者可以提前先编译好pyc文件,之后投递到目标机器上直接执行,从而躲避目标机器上IDS的文本内容审查。
python -m py_compile ./poc.py
值得注意的是,pyc文件对后缀是没有强制要求的,任意后缀都可以被执行。
5. 无文件进程启动方式
进程启动的本质是将一段汇编指令(shellcode)从某种媒介上加载到计算机的内存(RAM)中,并触发操作系统的cpu调度,从某个执行的内存地址中开始按照逻辑顺序执行。
操作系统真正需要的是内存中的shellcode代码以及制定入口点虚拟内存空间,至于这个shellcode从哪里来并不重要,可以是从物理磁盘,也可以是网络IO流,或者是虚拟内存设备。
在Linux系统中实现无文件执行ELF是渗透测试中一种非常有用的技术。这种方法较为隐蔽,可以绕过各种类型的反病毒保护机制、系统完整性保护机制以及基于硬盘监控的防护系统。通过这种方法,我们能够以最小的动静访问目标。
0x1:基于linux内存镜像文件
我们可以利用linux文件系统中的共享内存分区来存储进程文件,例如
- /dev/shm
- /run/shm
这些目录实际上是挂载到文件系统上已分配的内存空间,写入到这些目录下的文件不会实际落到物理磁盘上。
但是如果我们使用ls命令,就可以像查看其他目录一样查看这些目录,会发现目录下的进程文件。
此外,已挂载的这些目录设置了noexec标志,因此只有超级用户才能执行这些目录中的程序。
0x2:基于memfd_create创建内存镜像文件
1. memfd_create基本介绍
#define _GNU_SOURCE /* See feature_test_macros(7) */ #include <sys/mman.h> int memfd_create(const char *name, unsigned int flags); name参数代表文件名,在/proc/self/fd/目录中我们可以看到该文件名为符号链接的目的文件。显示在/proc/self/fd/目录中的文件名始终带有memfd:前缀,并且只用于调试目的。 名称并不会影响文件描述符的行为,因此多个文件可以拥有相同的名称,不会有任何影响。 flags标志位,进行tmpfs无文件执行的时候需要MFD_CLOEXEC标志(类似于O_CLOEXEC),以便当我们执行ELF二进制文件时,我们得到的文件描述符将被自动关闭
memfd_create这个系统调用。该系统调用与malloc比较类似,但并不会返回指向已分配内存的一个指针,而是返回指向某个匿名文件的文件描述符。该匿名文件以链接(link)形式存放在/proc/pid/fd/文件系统中。
#include <stdio.h> #include <stdlib.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main() { 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", "/bin/date", NULL); perror("execlp date"); exit(EXIT_FAILURE); } 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("child said: '%s'n", buf); exit(EXIT_SUCCESS); }