Linux 进程IO杂项

连接服务器

对于这类提供了 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 权限,这又是什么鬼?

到这里我们就明白了,我们可以任意在 /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[]);

根据要求,我们创建一个长为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 啊!

这里由于题中要求的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);
}

新建普通文件的方式比较麻烦且不够优雅,仅供参考。我们还是更愿意采用管道的方式。管道仅适用于从同一进程中通过调用 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!

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;
}

如有错漏,欢迎指正!

01-05 10:53
查看更多