Pwnable.kr—input
解题思路
接触这个题目的时候,刚刚做完一个和返回地址重写有关的题目,就想这个题目是不是也可以直接跳过中间的一大片,直接system(/bin/ cat flag)。哈哈开个玩笑
题目代码很明显,要求我们一共通过五个关卡,才能执行到system()函数,cat flag;那就只能一个一个来过了,分解目标。
这里使用C来编写脚本文件,调用input。
stage1
// 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");
将argv[]在main.c中赋值,然后执行execve()函数调用可执行文件;代码如下:
cahr *argv[101]={"/home/input2/input",[1...99]="A",NULL};
argv['A']="\x00";
argv['B']="\x20\x0a\x0d";
execve("/home/input2/input",argv,NULL);
stage2
// stdio
char buf[4];
read(0, buf, 4);
puts(buf);
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");
这里要明白read()函数是从哪里读取的。read(0,,)是从stdin读取,read(2,,)是从stderr读取。这部分也是要借助脚本文件,由于需要从stdin和stderr读取,那么使用管道通信,在脚本文件中分别利用两个管道写入”\x00\x0a\x00\xff”和”\x00\x0a\x02\xff”,然后将这两个管道的读端口重定向到stdin和stderr,然后执行input,此时input就可以read(0,,)和read(2,,)。
代码如下:
int pipe2stdin[2]={-1,-1};
int pipe2stderr[2]={-1,-1};
pid_t childpid;
if(pipe(pipe2stdin)<0||pipe(pipe2stderr)<0){
perror("cannot create pipe");
exit(1);
}
if((childpid=fork())<0){
perror("cannot fork()");
exit(1);
}
else if(childpid==0){
close(pipe2stdin[0]);close(pipe2stderr[0]);
write(pipe2stdin[1],"\x00\x0a\x00\xff");
write(pipe2stderr[1],"\x00\x0a\x02\xff");
}
else{
close(pipe2stdin[1]);close(pipe2stderr[1]);
dup2(pipe2stdin[0],0);close(pipe2stdin[0]);
dup2(pipe2stderr[0],2);close(pipe2stderr[0]);
execve("/home/input2/input",argv,NULL);
}
stage3
// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");
这里就到envp[]上场了,只需要初始化一个envp[],然后赋予相应的值,然后作为exceve()的第三个参数传入就可以了。代码如下:
char *envp[2]={"\xde\xad\xbe\xef=\xca\xfe\xba\xbe",NULL};
然后修改exceve():
execve("/home/input2/input",argv,envp);
stage4
// 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”的文件中读取内容到buf,并和指定值进行比较,相等即可。那么只需要向该文件按照相同的格式(数据项字节数、数据项数)写入数据即可。代码如下:
FILE *fp=fopen("\x0a","w");
if(!fp)return 0;
if(fwrite("\x00\x00\x00\x00",4,1,fp)!=1)return 0;
fclose(fp);
stage5
// 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通信的服务器端代码,其中涉及到绑定(bind)、监听(listen)、接收连接(accept)。这一系列之后就是从cd(客户端的socket)接收数据,并与已给的字符串进行比较,相等即过关。那么我们要做的就是在脚本程序中写入socket通信的客户端代码,并向server发送指定数据即可。代码如下:
int socket=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(55555);
servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
if(connect(sock_cli,(struct sockaddr*)&servaddr,sizeof(servaddr))<0){
perror("connect");
exit(1);
}
send(sock_cli,"\xde\xad\xbe\xef",4,0);
close(sock_cli);
stage Final
虽然每一步写完了,但是整个是一个脚本文件,所以还需要整合到一起。这里就需要注意一下脚本文件的执行和input执行的顺序问题,特别注意的就是最后的net部分,需要保证server端已经在侦听并可以accept之后,client端才可以connet,于是要在stage5之前Sleep().
由于input中需要打开flag文件,可是我们并没有读取/home/input2/flag文件的权限;此外,我们的脚本文件也无法写入到/home/input2的目录下。我们需要这样做(我不是很明白为什么):
a.先回到root目录下,cd .. cd ..,然后找到/tmp,在该目录下mkdir自己的文件夹
b.在自己的文件夹下编辑脚本文件,并编译
c.将/home/input2/flag软连接到/tmp/自己文件夹(即当前目录)
ln -s /home/input2/flag newname
d.执行编译好的脚本文件即可
答案
还是给链接吧,我的大部分都是借鉴别人的
遇到的问题
Q1
Q: 我尝试通过终端输入一共100个参数(包括文件路径),其中令第65(‘A’)和第66分别为“\x00”,”\x20\x0a\x0d”,但是输入的时候出现了问题,并不是想象的样子。于是我写了一个简单的测试函数想弄明白这个strcmp()函数的第二个参数到底代表的是什么?(不是字符串”\x00”)
void main(char argv[])
{
printf("%s\n",argv);
int mark=strcmp(argv,"\x01");
if(mark)
printf("no\n");
else
printf("yes\n");
printf("mark=%d\n",mark);
}
我发现不管怎么输入都无法得到yes或者得到mark=-1,也就是无法输入一个ascii值小于“\x01”的字符串。
A:目前我接受的解释是,“\x01”代表的是十六进制为1的不可见字符,那么我需要输入的就是“\x01”或“\x00”才可以使mark<=0。这两个都是不可见字符,目前我无法通过终端输入。终端输入的“\x01”是字符串\x01与“\x01”是不一样的………………
于是就可以利用脚本输入,这里应该是因为标准输入和程序里边的数据流是不一样的吧………………
Q2
Q:在做到stage3的时候,我认为envp代表的就是一个函数所在的所有的环境变量的键值对。于是我的计划是利用setEnvironVariable()函数向原来envp[]中添加一组环境变量。没有尝试……
看到的WriteUp是像argv[]一样,自定义了envp[],没有理会实际的环境变量…..
Q3
Q:客户端socket程序编写过程中对端口号和IP地址的取值不是很清楚?
A:如果是在localhost上,一般选择1024-65535中的端口号。因为前1024已有专用。由于是和本机通信,所以IP选择127.0.0.1 。这里注意并没有和pwnable的服务器进行通信,这里只有两个本地进程的通信。
收获
if(zero)返回false;if(non_zero)返回true,这里non_zero包含positive和negative。
python中print(r”\x01”)输出“\x01”,r的作用反转义。可以在\x01再加一个\反转义。
execve(参数1,参数2,参数3)的使用:参数1是命令所在的路径;参数2是命令集合;参数3是传递给执行文件的环境变量集。
read(FILE *fd,buf,size),从文件描述符fd所指的文件中读取size字节到buf。其中比较特殊的fd有:1–stdin标准输入流;2–stdout标准输出流;3–stderr标准错误流
dup&dup2:这里主要使用了dup2,其中利用了它数据流的重定向功能。创建了两个管道A、B,管道A的写端口被子进程写入,管道A的读端口在父进程中被重定向为stdin;管道B的写端口被子进程写入,管道B的读端口在父进程中被重定向为stderr。于是当在父进程执行exceve()函数时就继承了这些standard streams。
often,the descriptors in the child are duplicates onto standard input or output. this child can then exec another program, which inherits the standard streams.
fopen()返回一个文件描述符;文件描述符是文件的唯一标识;函数的第一个参数是文件的pathname;第二个参数是mode,即打开方式,或称流形态,通常有”r”,”w”,”r+”,”w+”等等。
- size_t fread(void *buffer, size_t size, size_t count, FILE *stream)。第一个参数–接收数据的内存地址;第二个参数–要读的每个数据项的字节数;第三个参数–要读的数据项的个数;第四个参数–输入流。
- fwrite()同上,只是第一个参数表示获取数据的地址,其余含义相同。
- main(int argc, char* argv[], char* envp[])其中的envp指的是环境变量,其存储形式是以类似于键值对的形式。
- 软连接ln -s filename filename。文件用户数据中存放的内容是另一文件路径的指向。当前文件有自己的inode号以及数据块。有自己的文件属性和权限。可对不存在的文件软连接,可交叉文件系统,for dir, for file。i_nlink不会增加。delete软连接不影响被指向文件;若被指向的文件被delete,则链接变成死链接,dangling link;若被指向的文件被重新创建,恢复软连接。
- 硬链接 ln filename filename; link filename filename。相同的inode指向不同的文件名,但是有相同的data block。只能对已经存在的文件创建。不能交叉文件系统。not for dir, for file。delete不影响其他具有相同inode的文件。
仍存在的疑惑
- 为什么使用system(/bin/ cat flag)就可以得到答案?但是我并没有看到目录/bin下边有flag文件的存在啊?
- 最后socket部分的前边Sleep()的原因?如果不sleep的话服务器那边还没有准备好,就无法建立连接了。所以要保证在客户端发起connect请求之前服务器端就已经准备好Accept/listen了,那么就需要在socket之前sleep()???还需要认真思考这个问题?
- 最后clear了5个stages之后,并没有cat到flag。原因在于,我是直接将脚本文件放在/tmp目录下,然后进行编译,同时建立/home/input2/flag文件到当前目录的软连接,结果并得不到flag。然而当我在/tmp目录下mkdir自己的文件夹,并在这个文件夹里边操作的话,就可以cat到flag?
- 这个flag的运行机理是什么?为什么system(/bin/ cat flag)可以打开?这个系统调用函数打开的到底是哪个文件?在这个题目中,当我执行脚本文件的时候,脚本文件调用input可执行文件,可执行文件中调用了system()函数,那么system()执行的到底是tmp下的flag,还是/input2下边的flag?如果不建立软连接可以吗?有待测试………….
- 字符串数组的初始化?
- 字符串数组的定义?
- 脚本文件调用input文件时,两个可执行文件在两个进程中还是一个进程,脚本文件sleep()的时候,input继续执行?
- 可以创建软连接,不能创建硬链接,权限问题?
- 为什么软连接之前没有权限读取flag?软连接之后就有了?