欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

pwnable.kr input

程序员文章站 2022-05-15 14:33:19
...

题目如下:
pwnable.kr input
运行一下,似乎什么都没发生
pwnable.kr inputcat一下直接看源码

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

大概意思是我们要带参数运行这个程序,然后必须满足一些条件输入才会获得flag
我们将条件拆解一步步分析
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",这样就完成了第一部分。
可以写个验证程序输入一下,在目标的机器上没有pwntools环境,在加上后面的几步的解题方法(进程间通信、socket编程),这边还是选择c来写exp
先在本地创建一个一样的题目和一个exp.c

touch exp.c

写入代码

#include <unistd.h>
void main(int argc, char **argv, char **env){
    char *arg[101]={"/tmp/input2",[1 ... 99]="A", NULL};
    arg['A'] = "\x00";
    arg['B'] = "\x20\x0a\x0d";
    execve(“input”,arg,NULL);
}

这里使用到了execve函数来创建进程,关于这个函数这里有详细介绍

http://man7.org/linux/man-pages/man2/execve.2.html

编译.c命令
(本人还是Linux菜鸟所以想将整个过程记录下来,学习Linux)

gcc -m32 exp.c -o exp

运行通过了第一部分
pwnable.kr input
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");

这段c代码中,还有一个需要注意的是read函数,read函数作用是从文件描述如中读取size大小的元素,并送入buffer中,然后判断对比,这里我们看到文件描述符是0与2,回想一下pwnable第一关fd时,我们便知道这里的0其实是stdin,2是stderr,这里再复习一下

  1. Linux把所有的东西看做文件,包括设备,其中0,1,2比较特殊,分别代表,输入流,输出流和错误输出流。Stdin,stdout,stderr,stdout在输出的时候会进行缓冲,也就是说如果通过输出流输出的时候,一般会等到有一个换行或者程序结束再输出。而stderr就不一样了,它是立即输出的。这样便于错误能够尽快显示出来。
    第一个输入流还好说,我们是可以控制的,第二个输入流不是我们可以直接控制的,网上看了各路大神的分析,都是使用管道(pipe)进行I/O重定向

管道,可以抽象的理解为一根管子,一端输入进去,另一端输出出来。一端write进去,另一端read出来。而且还可以把管道的read重定向到我们的某一个流中。两个进程之间就可以通过这种方式进行通信了。就像这一步中的输入流和错误输出流。那么这个题目的思路是通过子进程将内容写入管道,父进程把管道的read重定向到输入输出流中。其实关键在与如何向错误输出流中准确写入东西。用pipe管道可能是唯一的好办法。

这里有更详细的介绍:

pipe是为了在两个进程之间通信设置的,单方向的通信,一方面读,一方面写。以下是pipe的定义 http://man7.org/linux/man-pages/man2/pipe.2.html

pipe通道是为两个进程通信服务的,但此时只有一个进程,因此我们要fork一个子进程,实现子进程和父进程的通信,以下是fork的原理
https://blog.csdn.net/jason314/article/details/5640969

I/O重定向指的是将已创建的文件描述符指向其他文件,以下是具体原理
http://www.cnblogs.com/weidagang2046/p/io-redirection.html

以下是一个很好的利用pipe通道实现I/O重定向的说明
http://unixwiz.net/techtips/remap-pipe-fds.html

这里有个有注释的例子

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int main(){

	int pipe_arr[2]={117,117}//创建一个整型的管道数组,第一个为读入端,
	第二个为写入端
	pipe(pipe_arr) //疏通管道
	pid_t child;
	if((child=fork())==0)//if语句是子进程运行的代码
	{
		write(pipe_arr[1],"hello",sizeof("hello"));//子进程向管道一端输入数
		据
		close(pipe_arr[0]);//关闭管道
		close(pipe_arr[1]);//关闭管道
		exit(0);//退出子进程
	}else//else语句是父进程运行的代码
{
		char buf[30];
		read(pipe_arr[0],buf,sizeof(buf));//打开pipe_arr[0],也就是把管道的的
		另一端数据读出来到buf缓冲区
		printf("%s\n",buf);
		close(pipe_arr[0]);//关闭管道
		close(pipe_arr[1]);
}
return 0;
}

参考网上大神的代码此时的程序应该为

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

void main(int argc, char **argv, char **env){
    char *arg[101]={"/tmp/input2",[1 ... 99]="A", NULL};
    arg['A'] = "\x00";
    arg['B'] = "\x20\x0a\x0d";
    
int pipe2stdin[2] = {-1,-1};
int pipe2stderr[2] = {-1,-1};
pid_t childpid;

if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0){
    perror("Cannot create the pipe");
    exit(1);
}

if ( ( childpid = fork() ) < 0 ){
    perror("Cannot fork");
    exit(1);
}

if ( childpid == 0 ){
    /* Child process */
    close(pipe2stdin[0]); close(pipe2stderr[0]); // Close pipes for reading
    write(pipe2stdin[1],"\x00\x0a\x00\xff",4);   //写入管道的一端
    write(pipe2stderr[1],"\x00\x0a\x02\xff",4);
}
else {
            /* Parent process */
    close(pipe2stdin[1]); close(pipe2stderr[1]);   // Close pipes for writing
    dup2(pipe2stdin[0],0); dup2(pipe2stderr[0],2); // 重定向到 stdin 和 stderr
    close(pipe2stdin[0]); close(pipe2stderr[1]);   // Close write end (the fd has been copied before)
    execve("input",arg,NULL);  // 执行新的进程
}
}

编译了一下运行,通过第二步
pwnable.kr input

这步关键就一行代码:

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

而且看起来关键函数在于getenv,我们先来看下getenv的介绍

从环境中取字符串,获取环境变量的值,getenv()用来取得参数envvar环境变量的内容。参数envvar为环境变量的名称,如果该变量存在则会返回指向该内容的指针。环境变量的格式为envvar=value。getenv函数的返回值存储在一个全局二维数组里,当你再次使用getenv函数时不用担心会覆盖上次的调用结果。https://baike.baidu.com/item/getenv/935515?fr=aladdin

Linux下的环境变量和Windows类似,就是告诉你什么东西在哪的变量。而main函数的char*envp一般是运行时,系统直接赋值给他系统的环境变量,所以通过打印envp的值能打出系统内的所有环境变量。而且其最后一个值一定是NULL,用来表示结束,和argv数组一个道理,最后一个必须是NULL,而且不计入参数个数。
再回过头来看看题目,代码中是getenv("\xde\xad\xbe\xef"),但是我们的环境变量中一般是不会有键名为\xde\xad\xbe\xef的键值对,所以这里就需要我们手动向里面写入环境变量
这里就利用到了execve函数的第三个参数,向里面传入我们自己建立的环境变量

这里要还注意的一点就是env数组的指针都是以null结尾的,所以我们必须要这么写

char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
execve("input",arg,env);  

此时的代码为

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

void main(int argc, char **argv, char **env){
    char *arg[101]={"/tmp/input2",[1 ... 99]="A", NULL};
    arg['A'] = "\x00";
    arg['B'] = "\x20\x0a\x0d";
    
int pipe2stdin[2] = {-1,-1};
int pipe2stderr[2] = {-1,-1};
pid_t childpid;

if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0){
    perror("Cannot create the pipe");
    exit(1);
}

if ( ( childpid = fork() ) < 0 ){
    perror("Cannot fork");
    exit(1);
}

if ( childpid == 0 ){
    /* Child process */
    close(pipe2stdin[0]); close(pipe2stderr[0]); // Close pipes for reading
    write(pipe2stdin[1],"\x00\x0a\x00\xff",4);   //写入管道的一端
    write(pipe2stderr[1],"\x00\x0a\x02\xff",4);
}
else {
            /* Parent process */
    close(pipe2stdin[1]); close(pipe2stderr[1]);   // Close pipes for writing
    dup2(pipe2stdin[0],0); dup2(pipe2stderr[0],2); // 重定向到 stdin 和 stderr
    close(pipe2stdin[0]); close(pipe2stderr[1]);   // Close write end (the fd has been copied before)
char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
execve("input",arg,env);    // 执行新的进程
}
}

重新编译执行,到达第三步
pwnable.kr input
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");	

代码大概意思是打开一个路径为"\x0a"的文件读取4个字节到buf,然后如果是"\x00\x00\x00\x00"的话就成功了
这里的一个比较疑惑的地方是\x0a这个文件,目录下并不存在但是还是可以正常打开(这里网上搜了一下别人对这个的解释也是含糊不清,留作日后补充把,或者有 知道的大牛也教一下我)
我们直接打开写入

    FILE* fp = fopen("\x0a","w");
    fwrite("\x00\x00\x00\x00",4,1,fp);
    fclose(fp);

此时代码为

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

void main(int argc, char **argv, char **env){
    char *arg[101]={"/tmp/input2",[1 ... 99]="A", NULL};
    arg['A'] = "\x00";
    arg['B'] = "\x20\x0a\x0d";
    
int pipe2stdin[2] = {-1,-1};
int pipe2stderr[2] = {-1,-1};
pid_t childpid;

if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0){
    perror("Cannot create the pipe");
    exit(1);
}

if ( ( childpid = fork() ) < 0 ){
    perror("Cannot fork");
    exit(1);
}

if ( childpid == 0 ){
    /* Child process */
    close(pipe2stdin[0]); close(pipe2stderr[0]); // Close pipes for reading
    write(pipe2stdin[1],"\x00\x0a\x00\xff",4);   //写入管道的一端
    write(pipe2stderr[1],"\x00\x0a\x02\xff",4);
}
else {
            /* Parent process */
    close(pipe2stdin[1]); close(pipe2stderr[1]);   // Close pipes for writing
    dup2(pipe2stdin[0],0); dup2(pipe2stderr[0],2); // 重定向到 stdin 和 stderr
    close(pipe2stdin[0]); close(pipe2stderr[1]);   // Close write end (the fd has been copied before)
	char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
    FILE* fp = fopen("\x0a","w");
    fwrite("\x00\x00\x00\x00",4,1,fp);
    fclose(fp);
execve("input",arg,env);    // 执行新的进程
}
}

重新编译运行,到达第四步
pwnable.kr input

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

这段代码的大概意思是把这个input作为一个服务端,绑定的端口值是argv[‘C’]里面存的数值,当然代码用了一下atoi转化字符串为数字,比如存的是char 0,那么绑定的端口就是0号端口。然后验证的是传进来的某连接发送的内容是"\xde\xad\xbe\xef"
这道题需要用到linux下socket编程的知识,不明白socket编程可以去补一下
低位的一些端口都被系统或者别的程序占用了,我们写个大点的

         int sockfd;
        struct sockaddr_in server;
        sockfd = socket(AF_INET,SOCK_STREAM,0);
        if ( sockfd < 0){
            perror("Cannot create the socket");
            exit(1);
        }
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr("127.0.0.1");
        server.sin_port = htons(55555);
        if ( connect(sockfd, (struct sockaddr*) &server, sizeof(server)) < 0 ){
            perror("Problem connecting");
            exit(1);
        }
        printf("Connected\n");
        char buf[4] = "\xde\xad\xbe\xef";
        write(sockfd,buf,4);
        close(sockfd);

也就是以这个可执行文件作为客户端,主动连接127.0.0.1:55555,并发送字符串”\xde\xad\xbe\xef”,所以这个代码我们写在创建进程之后,也可以创建完成后sleep 等一下 以免连接不到
目前代码如下

#include<stdio.h>//fopen perror
#include <stdlib.h>
#include <unistd.h>//pipe execve
#include <string.h>//strcmp  
#include <sys/types.h>//bind
#include <sys/socket.h>// linux socket
#include <netinet/in.h>   
#include <netdb.h>   
#include <arpa/inet.h>  

void main(int argc, char **argv, char **env){
    char *arg[101]={"/tmp/input2",[1 ... 99]="A", NULL};
    arg['A'] = "\x00";
    arg['B'] = "\x20\x0a\x0d";
    arg['C'] = "55555";
int pipe2stdin[2] = {-1,-1};
int pipe2stderr[2] = {-1,-1};
pid_t childpid;

if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0){
    perror("Cannot create the pipe");
    exit(1);
}

if ( ( childpid = fork() ) < 0 ){
    perror("Cannot fork");
    exit(1);
}

if ( childpid == 0 ){
    /* Child process */
    close(pipe2stdin[0]); close(pipe2stderr[0]); // Close pipes for reading
    write(pipe2stdin[1],"\x00\x0a\x00\xff",4);   //写入管道的一端
    write(pipe2stderr[1],"\x00\x0a\x02\xff",4);
}
else {
            /* Parent process */
    close(pipe2stdin[1]); close(pipe2stderr[1]);   // Close pipes for writing
    dup2(pipe2stdin[0],0); dup2(pipe2stderr[0],2); // 重定向到 stdin 和 stderr
    close(pipe2stdin[0]); close(pipe2stderr[1]);   // Close write end (the fd has been copied before)
	char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
    FILE* fp = fopen("\x0a","w");
    fwrite("\x00\x00\x00\x00",4,1,fp);
    fclose(fp);
execve("input",arg,env);    // 执行新的进程
}
	
    	sleep(1);
         int sockfd;
        struct sockaddr_in server;
        sockfd = socket(AF_INET,SOCK_STREAM,0);
        if ( sockfd < 0){
            perror("Cannot create the socket");
            exit(1);
        }
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr("127.0.0.1");
        server.sin_port = htons(55555);
        if ( connect(sockfd, (struct sockaddr*) &server, sizeof(server)) < 0 ){
            perror("Problem connecting");
            exit(1);
        }
        printf("Connected\n");
        char buf[4] = "\xde\xad\xbe\xef";
        write(sockfd,buf,4);
        close(sockfd);
}

编译执行成功到达第五步
pwnable.kr input
不过目前我们都是在本地执行,要想获得flag就要在目标机上执行
这时候我们把执行input的目录修改一下
最终代码为

#include<stdio.h>//fopen perror
#include <stdlib.h>
#include <unistd.h>//pipe execve
#include <string.h>//strcmp  
#include <sys/types.h>//bind
#include <sys/socket.h>// linux socket
#include <netinet/in.h>   
#include <netdb.h>   
#include <arpa/inet.h>  

void main(int argc, char **argv, char **env){
    char *arg[101]={"/home/input2/input",[1 ... 99]="A", NULL};
    arg['A'] = "\x00";
    arg['B'] = "\x20\x0a\x0d";
    arg['C'] = "55555";
int pipe2stdin[2] = {-1,-1};
int pipe2stderr[2] = {-1,-1};
pid_t childpid;

if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0){
    perror("Cannot create the pipe");
    exit(1);
}

if ( ( childpid = fork() ) < 0 ){
    perror("Cannot fork");
    exit(1);
}

if ( childpid == 0 ){
    /* Child process */
    close(pipe2stdin[0]); close(pipe2stderr[0]); // Close pipes for reading
    write(pipe2stdin[1],"\x00\x0a\x00\xff",4);   //写入管道的一端
    write(pipe2stderr[1],"\x00\x0a\x02\xff",4);
}
else {
            /* Parent process */
    close(pipe2stdin[1]); close(pipe2stderr[1]);   // Close pipes for writing
    dup2(pipe2stdin[0],0); dup2(pipe2stderr[0],2); // 重定向到 stdin 和 stderr
    close(pipe2stdin[0]); close(pipe2stderr[1]);   // Close write end (the fd has been copied before)
	char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
    FILE* fp = fopen("\x0a","w");
    fwrite("\x00\x00\x00\x00",4,1,fp);
    fclose(fp);
execve("/home/input2/input",arg,env);    // 执行新的进程
}
	
    	sleep(1);
         int sockfd;
        struct sockaddr_in server;
        sockfd = socket(AF_INET,SOCK_STREAM,0);
        if ( sockfd < 0){
            perror("Cannot create the socket");
            exit(1);
        }
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr("127.0.0.1");
        server.sin_port = htons(55555);
        if ( connect(sockfd, (struct sockaddr*) &server, sizeof(server)) < 0 ){
            perror("Problem connecting");
            exit(1);
        }
        printf("Connected\n");
        char buf[4] = "\xde\xad\xbe\xef";
        write(sockfd,buf,4);
        close(sockfd);
}

可以去根目录 ls -al 看一下操作权限,不过连接一开始就告诉了我们可以操作tmp
pwnable.kr input
所以我这里没有使用上传的方法,而是cd到tmp 目录后新建一个inputexp文件夹,里面再新建一个exp.c使用

aaa@qq.com:~$ cd /tmp
aaa@qq.com:/tmp$ mkdir inuptexp
aaa@qq.com:/tmp/inuptexp$ touch exp.c
aaa@qq.com:/tmp/inuptexp$ vim exp.c

来写入代码,最后使用gcc命令编译
执行后发现目录下还是没有flag,我们也没有权限去移动flag文件,网上参考了大神的方法,需要软连接到flag文件,具体过程如下
pwnable.kr input
终于获得flag

最后:
vim 不会用真蛋疼。。。
pwnable.kr input