【Writeup】Pwnable.kr 0x07 input

0x07 input

题目描述

Mom? how can I pass my input to a computer program?

ssh input2@pwnable.kr -p2222 (pw:guest)

题目代码

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

解题分析

这道题是一道很综合的题,主要是考察linux下,argv,env,stdio,file,socket等的运用,查询了大量资料,也学习到了很多知识,主要参考了http://blog.csdn.net/SmalOSnail/article/details/53048109的writeup,在这里,我自己主要是分析一下其中的脚本思路吧,再详细得记录每条知识点,当作学习,同时要感谢分享的writeup。
在这里,先附上python的解题脚本:

import os
import socket
import time
import subprocess

stdinr, stdinw = os.pipe()
stderrr, stderrw = os.pipe()

args = list("A"*99)
args[ord('A') - 1] = ""
args[ord('B') - 1] = "\x20\x0a\x0d"
args[ord("C") - 1] = "8888"

os.write(stdinw, "\x00\x0a\x00\xff")
os.write(stderrw, "\x00\x0a\x02\xff")

environ = {"\xde\xad\xbe\xef" : "\xca\xfe\xba\xbe"}

f = open("\x0a" , "wb")
f.write("\x00"*4)
f.close()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

pro = subprocess.Popen(["/home/input2/input"]+args, stdin=stdinr,stderr=stderrr,env=environ)

time.sleep(2)
s.connect(("127.0.0.1", 8888))
s.send("\xde\xad\xbe\xef")
s.close()

这里,我们分步详解:

part 1

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

这里呢,首先是要求参数要等于100个,并且argv['A'],argv['B']分别要符合要求,那么,这里的A和B其实分别是其ascii码作为索引值,即参数第64个要为"\x00"而参数第65个要为"\x20\x0a\x0d",这样就完成了第一部分。对于第一部分,在python脚本中是先用99个A初始化,为什么用99个A而不是100个呢,可以看到,在最后的Popen()函数中,其实是将第100个参数加上的,至于为什么是/home/input2/input呢?这是因为,对于命令行参数,argc[0]通常是脚本名的。

part 2

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

对于第二部分的解题思路,writeup的作者主要采用os的pipe()函数,所以我们需要了解一下os的pipe函数:os.pipe()用于创建一个管道,返回一对文件描述符(r,w)分别为读和写。这段c代码中,还有一个需要注意的是read函数,read函数作用是从文件描述如中读取size大小的元素,并送入buffer中,这里我们看到文件描述符是0与2,回想一下pwnable第一关fd时,我们便知道这里的0其实是stdin,2是stderr,所以在python脚本的最初,我们声明了两个pipe管道,stdinr,stdinw和stderrr,stderrw,其作用就是向stdin和stderr写入题目需要的字符串,而如果直接用stdin和stderr是无法直接写入的,所以利用管道的双向读写功能来完成这一部分。

part 3

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

这里呢,重点是getenv()这个函数,这个函数的功能是:返回一给定的环境变量值,环境变量名可大写或小写。如果指定的变量在环境中未定义,则返回一空串。这里呢,是要让"\xde\xad\xbe\xef"的环境变量等于"\xca\xfe\xba\xbe",那么在python脚本中就可以通过定义一个字典,然后让"\xde\xad\xbe\xef"对应"\xca\xfe\xba\xbe",在Popen函数的时候,将这个字典当作env的参数就好了。

part 4

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

接下来是第四部分,重点解读一下fread函数,该函数原型 size_t fread(void *buffer, size_t size, size_t count FILE *stream),
就是读取stream流中count个元素,每个元素size大小,读取到buffer中,在这里的c代码中,是从fp中读取一个元素,4字节大小到buf中,因此,只要在python脚本中写入4个\x00就好了。

part 5

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

最后一部分,第五部分,前面都是建立socket连接,只要成功建立socket连接就不会报错,后面recv(cd, buf, 4, 0)这里,cd是之前accept返回的套接字描述符,这段socket代码是一个服务端的代码,accept返回的这个cd可以想象成服务端与客户端建立连接的一个钥匙,那么这个recv其实就很好理解了,就是服务端接受的4字节的数据并且放到buf中,第四个参数0是flag一个标志位,一般为0,这里可以不用管他。然后是一个memcmp函数,这个函数就是比较buf与"\xde\xad\xbe\xef"的前4字节的内容是否相等,相等则返回0,因此这部分就是向服务端发送"\xde\xad\xbe\xef"这个字符串即可通过。

part 6

总共有5部分,那么我为什么要写part 6呢?这是因为这道题有几个坑,首先/home/input这个目录下没有写的权限,所以需要把脚本放在/tmp去执行,而且part 4创建文件的操作也需要在tmp下进行,但是读取flag的system("/bin/cat flag")中的/bin/cat flag是使用相对路径的,是无法读取到flag的,所以这里使用了软连接的方法,那么在这里我们可以学习一下linux下的ln链接命令:

ln src dest
这里需要注意的是,第一,ln命令会保持每一处链接文件的同步性,也就是说,不论你改动了哪一处,其它的文件都会发生相同的变化;第二,ln的链接又软链接 和硬链接两种,软链接就是ln -s str dest,它只会在你选定的位置上生成一个文件的镜像,不会占用磁盘空间,硬链接ln src dest,没有参数-s, 它会在你选定的位置上生成一个和源文件大小相同的文件,无论是软链接还是硬链接,文件都保持同步变化。
如果我们用ls察看一个目录时,会发现有的文件后面有一个@的符号,那就是一个用ln命令生成的文件,用ls -l命令去察看,就可以看到显示的link的路径了。 

所以这里就将flag通过软链接的方式在/tmp目录下创建一个副本,这样相对路径的问题就可以解决了。
但是解决了相对路径的问题后运行还是不能弹出flag,发现是因为/tmp目录下没有读的权限,但是有写的权限,就有建了一个input目录,最后就可以弹出flag了。


推荐阅读更多精彩内容