Linux 进程IO杂项

Linux 进程IO杂项

本文结合一个 pwn 例题,在分析例题的过程中穿插介绍相关知识。
例题来源:PWNABLE.KR 网站,Toddler's Bottle 小节,习题 input
例题内容:

Mom? How can I pass my input to a computer program?
ssh input2@pwnable.kr -p 2222 (pw: guest).

连接服务器

对于这类提供了 SSH 连接地址的题,我们通常要连接到服务器并在服务器上按提示信息完成 pwn。登录成功后,看到如下信息:

 ____  __    __  ____    ____  ____   _        ___      __  _  ____
|    \|  |__|  ||    \  /    ||    \ | |      /  _]    |  |/ ]|    \
|  o  )  |  |  ||  _  ||  o  ||  o  )| |     /  [_     |  ' / |  D  )
|   _/|  |  |  ||  |  ||     ||     || |___ |    _]    |    \ |    /
|  |  |  `  '  ||  |  ||  _  ||  O  ||     ||   [_  __ |     \|    \
|  |   \      / |  |  ||  |  ||     ||     ||     ||  ||  .  ||  .  \
|__|    \_/\_/  |__|__||__|__||_____||_____||_____||__||__|\_||__|\_|

- Site admin : daehee87.kr@gmail.com
- IRC : irc.netgarage.org:6667 / #pwnable.kr
- Simply type "irssi" command to join IRC now
- files under /tmp can be erased anytime. make your directory under /tmp
- to use peda, issue `source /usr/share/peda/peda.py` in gdb terminal

可以看到上面列出了网站管理员、聊天服务器地址,并给出说明 “files under /tmp can be erased anytime. make your directory under /tmp”,即只允许在 /tmp 目录下执行操作。首先,不管三七二十一,能见度不足,ls 敬上!

$ ls -al
total 44
drwxr-x---   5 root       input2  4096 Oct 23  2016 .
drwxr-xr-x 114 root       root    4096 May 19 15:59 ..
d---------   2 root       root    4096 Jun 30  2014 .bash_history
-r--r-----   1 input2_pwn root      55 Jun 30  2014 flag
-r-sr-x---   1 input2_pwn input2 13250 Jun 30  2014 input
-rw-r--r--   1 root       root    1754 Jun 30  2014 input.c
dr-xr-xr-x   2 root       root    4096 Aug 20  2014 .irssi
drwxr-xr-x   2 root       root    4096 Oct 23  2016 .pwntools-cache

$ ls -ld /tmp
drwxrwx-wt 4833 root root 135168 Oct 24 08:38 /tmp

好吧……出题人辛苦了,在文件/目录的所属、权限上可谓下足了功夫!在主目录下,只有三个文件对我们来说是有用的:flaginputinput.c,盲猜也知道需要运行 input 可执行文件,得到 flag 中存放的信息。这里 flag 文件只有 input2_pwn 用户有权限读取,可是当前登录用户是 input2 用户,怎么办呢?

注意到 input 可执行文件的权限字符串是 -r-sr-x---,等等!这个 s 是个什么情况?事实上,这里的 s字符为“强制位”,它的存在将使可执行文件在执行时临时获取文件所有者/所属组的身份。再联系 inputflag 文件相同的所有者,看来这个 flag 是非得用 input 读取不可了!另外,我们还能注意到,/tmp 目录的权限位中也有一个 t 权限,这又是什么鬼?

强制位经 chmod 设置后,会显示在原可执行权限的位置,这时如果 s 显示为小写,则表明已有 x 权限;若 S 大写,则表示该位无 x 权限。若没有 x 权限,即使已经设置了强制位,也无法获得临时身份。
相似的,对于目录来说,也有第十个权限位 t,即粘滞位。它的存在允许用户在具有 w 权限的前提下,在该目录中随意创建文件和目录,但只能删除其中自己创建的文件或目录。这在单独使用 w 权限的情形下是实现不了的。

到这里我们就明白了,我们可以任意在 /tmp 目录下执行我们的操作,但偏偏这个目录没有给读权限,也就是说只能 cd 进去凭感觉执行文件……行吧,学 pwn 的男人无所畏惧!既然已经了解了出题人的基本意图,咱们还是撸起袖子加油干吧!

源文件分析

服务器上给出了 input 文件的源码,主要分为三个部分:Stage 2Stage 3Stage 4Stage 5,分别考察了参数列表、标准I/O、环境变量、文件读写和网络交互六个方面内容。下面我们针对各个部分代码进行分析。先给出完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc, char* argv[], char* envp[]){
    printf("Welcome to pwnable.kr\n");
    printf("Let's see if you know how to give input to program\n");
    printf("Just give me correct inputs then you will get the flag :)\n");

    // argv
    if(argc != 100) return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n");

    // stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
    read(2, buf, 4);
    if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
    printf("Stage 2 clear!\n");

    // env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");

    // file
    FILE* fp = fopen("\x0a", "r");
    if(!fp) return 0;
    if( fread(buf, 4, 1, fp)!=1 ) return 0;
    if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
    fclose(fp);
    printf("Stage 4 clear!\n");

    // network
    int sd, cd;
    struct sockaddr_in saddr, caddr;
    sd = socket(AF_INET, SOCK_STREAM, 0);
    if(sd == -1){
        printf("socket error, tell admin\n");
        return 0;
    }
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons( atoi(argv['C']) );
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
        printf("bind error, use another port\n");
        return 1;
    }
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
    if(cd < 0){
        printf("accept error, tell admin\n");
        return 0;
    }
    if( recv(cd, buf, 4, 0) != 4 ) return 0;
    if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
    printf("Stage 5 clear!\n");

    // here's your flag
    system("/bin/cat flag");
    return 0;
}

接下来具体分析各部分代码,为节省篇幅,不作任何可靠性检验,不检查任何返回值。

Stage 1: argv

参数列表部分,根据第 73, 74, 75, 108 行的要求,我们需要在启动 input 程序时传入100个参数,其中第 'A', 'B', 'C' 位必须是指定的值(从0开始计数),故决定使用系统调用 execve 来启动程序:

int execve(const char * path,char * const argv[],char * const envp[]);

exec 函数族包含多个类似的函数,但它们都是基于 execve 的封装,最终都转化为了此系统调用。所谓系统调用,即由操作系统声明,供程序在执行时调用的一类接口。
这里 execve 的作用是“进程替换”,即调用此函数后,当前执行的进程完全转化为指定的另一个进程,其进程号 pid 不变。所以,原进程中此调用后的语句都不会执行——除非调用失败。
execve 而言,其参数列表包含三个指针,第一个参数是一个指向可执行文件的地址字符串;第二个参数传入一个字符型二级指针,代表一个字符串数组,即新进程的参数数组;第三个参数类似地,传入另一个字符串数组,它是新进程的环境变量数组。这两个数组都需要以0指针结尾,以结束参数输入。

根据要求,我们创建一个长为101的字符指针数组,分别指向100个字符串,其中最后一个指针赋值为0。那么我们可以写出以下代码:

#include <unistd.h>
int main () {
    // Stage 1
    char * argv [101];
    for (int i=0; i<100; i++) {
        // 因要求 argv['A'] 与 "\x00" 比较结果为0
        // 且使用的是 strcmp 函数:遇 '\0' 停止匹配
        // 故先全部赋值为空串
        argv[i] = "";
    }
    // 最后一位设为0指针
    argv[100] = 0;
    argv['B'] = "\x20\x0a\x0d";
    // 根据 108 行要求,任意设置一个端口号
    char port [] = "12933";
    argv['C'] = port;
    char * envp [] = {0};
    execve ("/home/input2/input", argv, envp);
}

编译运行后,不出所料,得到输出:Stage 1 clear!。下面进入下一环节。

Stage 2: stdio

一看代码,要求从文件描述符 02 的文件中各读取4字节,关键这俩一个是 stdin,一个是 stderr 啊!

注:int 类型的文件描述符是由 Linux 系统调用 openclose 所操作的,只能通过系统调用 writeread 进行读写;而 FILE * 类型的文件指针则由 C 语言定义,并用 C 函数 fwritefreadf 开头的函数进行读写。stdinstdoutstderr 正是三个特殊的文件指针,因为它们与文件描述符 012 一一对应,分别指向了 标准输入设备标准输出设备标准错误设备
对于 C 语言来说,其 printf 函数默认输出到 stdout,但我们可以使用 fprintf 定向输出到标准错误。类似的,C++ 中的对象 cincoutcerr 也分别与这三个设备关联。

这里由于题中要求的8个字节属特殊字符,故不直接往 02 中写数据。我们可以采用新建普通文件或新建管道的方式。我们先来学习一下使用普通文件的方式,涉及操作包括建立并打开文件、删除文件原有数据、写入数据并关闭文件,然后重新打开文件并将之关联到指定描述符。如下:

#include <unistd.h>
#include <fcntl.h>

int main () {
    // 以只写方式打开,创建文件|只读|截断原有内容
    // 最末一个参数为8进制文件权限标识,同 chmod
    int in = open ("stdin", O_CREAT|O_WRONLY|O_TRUNC, 0644);
    char buf [] = {0x00, 0x0a, 0x00, 0xff};
    write (in, buf, 4);
    close (in);
    // 以只读方式打开
    in = open ("stdin", O_RDONLY, 0644);
    // STDIN_FILENO 即 0
    // dup2 函数将 in 文件描述信息复制给 0
    dup2 (in, STDIN_FILENO);
}
int dup(int oldfd);
int dup2(int oldfd, int newfd);

dup2 函数与 dup 函数类似,用于将某个文件描述符复制一份,产生一个新的描述符并返回。与后者不同的是,dup2 可以在第二个参数中指定新的描述符的值,若该值已被使用,则关闭原先使用的文件。这里 in 是被复制的描述符,而 STDIN_FILENO 即0,表示将0所指的标准输入设备关闭,并重新指向 in 所指向的文件。
STDIN_FILENO 是头文件 fcntl.h 中的一个宏定义,其值为0。类似的,STDOUT_FILENOSTDERR_FILENO 值分别为1和2。

新建普通文件的方式比较麻烦且不够优雅,仅供参考。我们还是更愿意采用管道的方式。管道仅适用于从同一进程中通过调用 fork 得到的分支进程之间,其中一方从管道的写端写入数据,而另一方则从管道的读端读出数据。下例:

#include <unistd.h>
int main () {
    // 待传输数据
    char str1 [] = {0x00, 0x0a, 0x00, 0xff};
    char str2 [] = {0x00, 0x0a, 0x02, 0xff};
    // 使用长度为2的整型数组创建管道
    int in [2], err [2];
    pipe (in);
    pipe (err);
    // fork 系统调用,进程克隆
    if (0 == fork ()) {
        // 新进程关闭写端
        close (in[1]);
        close (err[1]);
        // 重新绑定文件描述符
        dup2 (in[0], 0);
        dup2 (err[0], 2);
        // 进程替换
        execve ("/home/input2/input", argv, envp);
    }
    // 原进程关闭读端
    close (in[0]);
    close (err[0]);
    // 向管道写入数据
    write (in[1], str1, 4);
    write (err[1], str2, 4);
    // 关闭写端
    close (in[1]);
    close (err[1]);
}

结合上一小节的代码,至此可以轻松得到输出:Stage 2 clear!

pid_t fork( void);
int pipe(int fd[2]);

PCB进程控制块。进程是操作系统中资源分配的基本单位,早期也是调度的基本单位,每个进程的所有信息保存在各自的进程控制块中。在引入线程的操作系统中,CPU 调度的基本单位是线程。
fork 系统调用,此函数将当前执行的进程进行克隆,得到两个状态完全相同的分支进程。其中原有分支的 PCB 信息不变,新分支得到一个新的 PCB,它们分别称为父进程和子进程。尽管两个进程的所有数据都一致,但 fork 函数的返回值在两个分支中是不同的,其中父进程中的 fork 函数返回子进程的 pid,此值为正整数;而子进程中的 fork 函数返回值为0。如果调用失败,返回值为-1。
所以,一般调用之后,进程需要自省已身,通过其返回值确定自已是父进程还是子进程,以便接下来执行各自的任务。
要在父子进程之间使用管道传输数据,需要先创建一个长度为2的整型数组,并以首地址为参数呼叫系统调用 pipe,此时系统会将两个文件描述符写入到数组中,成功返回0,失败返回-1。其中0端为读端,1端为写端,即向1端写入的数据,可以从0端读取出来。
例中父进程通过 write 系统调用向 in[1] 写入 str1 的前4个字节,子进程即可使用 read 系统调用从 in[0] 读取得到4字节相同的数据。

Stage 3: env

根据源文件第87行的要求,我们需要在执行 input 时传入环境变量 \xde\xad\xbe\xef,其值应为 \xca\xfe\xba\xbe,这与传入参数列表类似,话不多说:

#include <unistd.h>
int main () {
    char * argv [] = {0};
    char * envp [] = {
        "\xde\xad\xbe\xef=\xca\xfe\xba\xbe", 0
    };
    execve ("/home/input2/input", argv, envp);
}

补充之前的代码,即得:Stage 3 clear!

Stage 4: file

第四关考察的是文件读写,既可以使用系统调用 read/write,也可以使用 C 库函数 fread/fwrite。不过既然题设源代码用了库函数,这里也使用库函数,聊表敬意。

#include <stdio.h>
int main () {
    // 只写方式打开文件
    FILE * fp = fopen ("\x0a", "w");
    char buf [] = {0x00, 0x00, 0x00, 0x00};
    // 向文件中写入数据
    fwrite (buf, 4, 1, fp);
    // 关闭文件
    fclose (fp);
}

事实上,如果知道文件名 \x0a 代表的是什么,我们甚至不需要写这一段,直接将对应文件放到目录下就行。总之,这关不难:Stage 4 clear!

Stage 5: network

这一部分源代码采用了 C 语言中 IPv4 协议的流式套接字传输数据的方式,要求我们在主函数参数中传入端口号,然后向指定的端口请求连接并发送数据。要建立网络连接,首先创建地址结构和套接字并设置地址信息,之后使用 connect 函数请求连接(为确保服务端接收到请求,可以使用 sleep 函数手动等待)。连接建立成功后,调用 send 函数发送数据。也有不建立连接发送数据的方法,参见 recvfromsendto

#include <sys/socket.h>
#include <arpa/inet.h>
int main () {
    // 地址结构
    struct sockaddr_in caddr;
    // 建立套接字,参数指定INET地址族、字节流类型,0表示自动选择协议
    // 按照惯例,返回值是一个描述字,如出错则返回错误代码
    int cd = socket (AF_INET, SOCK_STREAM, 0);
    // 设置地址信息:类型、目标和端口
    caddr.sin_family = AF_INET;
    caddr.sin_addr.s_addr = inet_addr ("127.0.0.1");
    caddr.sin_port = htons (atoi (port));
    // 请求连接
    // 注:struct sockaddr 与 struct sockaddr_in 是并列结构
    connect (cd, (struct sockaddr *) &caddr, sizeof (struct sockaddr_in));
    // 发送数据
    char buff [] = {0xde, 0xad, 0xbe, 0xef};
    send (cd, buff, 4, 0);
}

到这里,我们终于可以通关了:Stage 5 clear!

其他问题

  • 注意到远程服务器用户主目录并没有 w 权限,而可执行程序又要在 flag 文件的同级目录下创建 $\n 文件,这怎么实现呢?其实答案并不难,因为对于 /tmp 目录我们是有写权限的,为什么不在 /tmp 下建立一个指向 flag 文件的链接呢?

  • 如果你真的去服务器上尝试了,那么你还会发现——/tmp 目录下有一个已经存在的 flag 目录,并且对于它我们并没有权限操作!不过这倒是不难,直接新建一个临时目录,然后在目录下建立 flag 的链接并运行我们的程序即可。

  • 远程服务器上因各种权限问题,加上频繁的 /tmp 目录清理,编码环境极其恶劣(>_<),故建议先在本地调试,待成功通关后再用 scp 命令将编译好的可执行文件传送到服务器的 /tmp 目录下,然后再连接服务器进行操作。

附上解题完整代码,供各路英雄好汉参考。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main (int argus, char ** argul) {
    // Stage 1
    char * argv [101];
    for (int i=0; i<100; i++) {
        argv[i] = "";
    }
    argv[100] = 0;
    argv['B'] = "\x20\x0a\x0d";
    char port [] = "12933";
    argv['C'] = port;

    // Stage 2
    char str1 [] = {0x00, 0x0a, 0x00, 0xff};
    char str2 [] = {0x00, 0x0a, 0x02, 0xff};
    int in [2], err [2];
    pipe (in);
    pipe (err);

    // Stage 3
    char * envp [2];
    envp[0] = "\xde\xad\xbe\xef=\xca\xfe\xba\xbe";
    envp[1] = 0;

    // Stage 4
    FILE * fp = fopen ("\x0a", "w");
    char buf [] = {0x00, 0x00, 0x00, 0x00};
    fwrite (buf, 4, 1, fp);
    fclose (fp);

    if (0 == fork ()) {
        // New process
        close (in[1]);
        close (err[1]);
        dup2 (in[0], 0);
        dup2 (err[0], 2);
        // Execute
        execve ("/home/input2/input", argv, envp);
    }
    // Parent process
    close (in[0]);
    close (err[0]);
    write (in[1], str1, 4);
    write (err[1], str2, 4);
    close (in[1]);
    close (err[1]);

    // Stage 5
    sleep (1);
    struct sockaddr_in caddr;
    int cd = socket (AF_INET, SOCK_STREAM, 0);
    caddr.sin_family = AF_INET;
    caddr.sin_addr.s_addr = inet_addr ("127.0.0.1");
    caddr.sin_port = htons (atoi (port));
    connect (cd, (struct sockaddr *) &caddr, sizeof (struct sockaddr_in));
    char buff [] = {0xde, 0xad, 0xbe, 0xef};
    send (cd, buff, 4, 0);

    wait ();
    return 0;
}

如有错漏,欢迎指正!

posted @ 2019-10-25 17:15  王牌饼干  阅读(123)  评论(0编辑  收藏  举报