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

Linux进程启动/指令执行方式研究 

netlink监控到的进程链信息如下:

Linux进程启动/指令执行方式研究 

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

Linux进程启动/指令执行方式研究

从netlink监控到的进程启动消息可以看到,通过exec方式启动的新进程,直接将当前进程替换为了新的指令进程。

Linux进程启动/指令执行方式研究

需要注意的是,一般情况下,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监控到的进程链信息如下:

Linux进程启动/指令执行方式研究

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指令为例,

Linux进程启动/指令执行方式研究

同时,netlink会收到一个进程启动事件消息,

Linux进程启动/指令执行方式研究

 

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跟踪如下:

Linux进程启动/指令执行方式研究 

Linux进程启动/指令执行方式研究

可以看到,python底层还是调用了glibc库的execv api来实现进程启动的。 

netlink监控消息如下,

Linux进程启动/指令执行方式研究

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----")

Linux进程启动/指令执行方式研究

可以看到,通过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----")

Linux进程启动/指令执行方式研究

可以看到,通过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

Linux进程启动/指令执行方式研究

Linux进程启动/指令执行方式研究

值得注意的是,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);
}
View Code

相关文章:

  • 2021-04-26
  • 2022-12-23
  • 2022-02-12
  • 2022-12-23
  • 2021-05-30
  • 2022-01-02
猜你喜欢
  • 2022-12-23
  • 2021-08-20
  • 2021-11-07
  • 2021-06-13
  • 2021-10-01
  • 2022-12-23
  • 2022-12-23
相关资源
相似解决方案