在做这道题的时候,一位大佬的题解给了重要参考,链接放在最后吧。
但是好多地方基础知识还是需要恶补,因此边查边写,有了自己这篇文章
Stage1:
第一题还可以用脚本来做,只要把文件分隔符设置为一个其他符号就可以了,这里我们设为“-”但是第二题就不那么简单了。
IFS='-'//设置分隔符为“-”
./input `python-c 'print"aaa-"*64+"\x00-"+"\x20\x0a\x0d-"+"aaa-"*33'`
但是到后面其实会发现,就不是脚本那么简单了,而是需要写一个C代码来破这个程序。
所以又回来再把stage1的C代码搞出来。
char*argv[101]={"/home/shelldon/Desktop/input"};
for(inti=1;i<100;i++)argv[i]="A";
argv[100]=NULL;
argv['A']="\x00";
argv['B']="\x20\x0a\x0d";
intret=0;
ret=execve("/home/shelldon/Desktop/input",argv,NULL);
if(ret)printf("createfailed : %d \n",ret);
这里提一下这个execve函数。
int execve(constchar * filename,char * const argv[ ],char * const envp[ ]);
关于她的介绍网上有很多,我就不再赘述,简单来说,功能就是创建一个新的进程,可执行文件的路径是第一个参数,第二个参数是类似于main函数里的第二个参数,就是给这个可执行文件的参数。第三个参数和main函数的第三个函数类似,好像和什么环境变量有关,一直也没搞懂,也没用过,直接设为null就好了。
运行出来就是直接stage1被破掉了。
Stage2:
charbuf[4];
read(0,buf, 4);
if(memcmp(buf,"\x00\x0a\x00\xff", 4)) {printf("test%d ispassed\n",1);return 0;};
read(2,buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) {printf("test%d ispassed\n",2);return 0;};
printf("Stage2 clear!\n");
我为了便于调试,在源码的基础上加了两句输出,因为一开始的思路有点混乱,所以输出语句有点语义错误,不要介意.
这里需要补充一下.关于Linux文件描述符的内容了.read函数其实第一个参数就是指文件描述符。记得在做pwnable的fd的时候简单了解过。
Linux把所有的东西看做文件,包括设备,其中0,1,2比较特殊,分别代表,输入流,输出流和错误输出流。Stdin,stdout,stderr。好吧我承认,本菜鸡从来没有听过什么stderr。所以查了一下,既然都是输出流,和stdout有什么区别。Stderr和stdout都是向终端输出。但是有几点不同:
1. stdout在输出的时候会进行缓冲,也就是说如果通过输出流输出的时候,一般会等到有一个换行或者程序结束再输出。而stderr就不一样了,它是立即输出的。这样便于错误能够尽快显示出来。Printf的时候用的是stdout,至于它为什么能够不在结束的时候输出来,可能是里面加了什么东西,就不细究了。
2. 当程序重定向输出到某一文件的时候,stderr还是会出现在屏幕上。同样的,是为了错误能够尽快显示出来,因为文件总是存在着错误的可能。
3. 具体想了解更多,请自行查资料。
那么问题就来了,第一个read是输入流,还好说,毕竟输入是我们可以控制的。
从输出结果上来看,通过脚本还是可以过第一个read的。但是后面read(2,buf,4)是在输出流里读那么这就不是我们能通过脚本就能控制的了。这有点超出我们的范围了。所以新学了点东西,叫做管道,使用来保证进程间通信的。
管道,可以抽象的理解为一根管子,一端输入进去,另一端输出出来。一端write进去,另一端read出来。而且还可以把管道的read重定向到我们的某一个流中。两个进程之间就可以通过这种方式进行通信了。就像题目中的输入流和错误输出流。那么这个题目的思路是通过子进程将内容写入管道,父进程把管道的read重定向到输入输出流中。其实关键在与如何向错误输出流中准确写入东西。用pipe管道可能是唯一的好办法。
下面是知识点复习:
这里再复习一下关于创建fork函数的内容,fork函数是用来创造一个和父进程一样的进程的函数,创建完成以后,返回两次,在父进程中返回创建子进程的pid,在子进程中返回0.我们也可以用这一点来控制在父进程和子进程中分别做的事情。
然后是pipe函数,pipe的作用是创建一个pipe管道,参数是一个整形数组,第一个用来表示管道读入端,第二个用来表示管道写入端。而且输入的值好像并不是特别严格,经过测试了几组值发现,无论初始化的什么值或者干脆不初始化,结果是始终正确的。
测试代码如下:
#include <stdio.h>
#include <string.h>
#include<unistd.h>
int main()
{
intfields[2]={-1312,123123};//这里更换了几次值,输出结果都没变。
if(pipe(fields))
{
printf("Createfailed!\n");
return0;
}
pid_tchild;
child=fork();
if(child==0)
{
write(fields[1],"hello",sizeof("hello"));
close(fields[0]);
close(fields[1]);
}
else
{
charbuf[30];
read(fields[0],buf,sizeof(buf));
printf("%s\n",buf);
close(fields[0]);
close(fields[1]);
}
return 0;
}
下面是运行结果。
这里只是针对管道来说,后面还需要重定向到流中的时候我们再利用dup2函数。
int dup2(int oldfd,int newfd);dup2函数,功能是复制文件描述符,我们说过Linux把设备也看作文件,输入输出流其实又叫输入输出设备,所以我们可以把通过管道的输入输出端复制到输入输出流上面去,然后就能够直接向输出流里写东西了!
具体代码如下:
intpipe2stdin[2] = {-1,-1};
intpipe2stderr[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 ){
/* 子进程*/
close(pipe2stdin[0]);close(pipe2stderr[0]); // 关闭读,防止写入被意外插入
write(pipe2stdin[1],"\x00\x0a\x00\xff",4);
write(pipe2stderr[1],"\x00\x0a\x02\xff",4);
}
else {
//sleep(0.5);//让进程睡眠一会,一般不会出现问题,加一句,是为了应付偶尔的因为子进程意外延缓执行所造成的失败导致不成功现象。
/*父进程*/
close(pipe2stdin[1]);close(pipe2stderr[1]); // 关闭写,防止读入过程出现混乱
dup2(pipe2stdin[0],0);dup2(pipe2stderr[0],2); // Map to stdin and stderr
close(pipe2stdin[0]);close(pipe2stderr[1]); // 关闭输入流的读和输出流的写,防止出现中间意外错误
execve("/home/shelldon/Desktop/input",argv,NULL);//Execute the program
}
不过经过尝试发现总是出现一个segmentation fault,指针错误,不知道是哪里又指错了。打开gdb看一下这个核心转储文件。注意了,Linux在程序崩溃的时候,一般会产生一个核心转储文件core作为调试的参考。崩溃以后,可以采用如下命令格式查看:
gdb –c core [可执行文件名]
如果提示没生成core,可以用ulimit -cunlimited设置一下。
结果如下图:
问题出在strcmp函数上。
可是前面的stage1和stage2应该没什么问题,那问题应该是出现在后面的strcmp上。
应该是出在stage3这里,我们在创建进程的时候,envp那里的参数设置的是NULL。这也是本人在之前的一贯做法。看来这次要好好查一查到底是怎么回事了。
Stage3:
首先看一下函数,关键是这个getenv(constchar *name);
这个的意思是获取名字是name的环境变量。那么环境变量到底是个什么东西?
在Windows下的时候,最早接触环境变量,是因为下载了nc工具,但是每次都要把nc的绝对路径输进去,很不方便。所以找到办法说把nc的路径作为环境变量添加进系统,就可以了。果然添加以后,可以每次直接输入就好了。
就像这样。
后来是在学习Java的时候,要在命令行下编译运行Java代码需要先配置环境变量。猜测应该就是让系统能够找到所需要执行的文件在哪。
那么在Linux下是不是这样的呢?程序自身的环境变量又是什么呢?
经过简单查资料:
Linux下的环境变量和Windows类似,就是告诉你什么东西在哪的变量。而main函数的char*envp一般是运行时,系统直接赋值给他系统的环境变量,所以通过打印envp的值能打出系统内的所有环境变量。而且其最后一个值一定是NULL,用来表示结束,和argv数组一个道理,最后一个必须是NULL,而且不计入参数个数。
从这里的题目来看,系统中应该不会存在叫作"\xde\xad\xbe\xef"的环境变量。唯一的好办法是我们自己搞一个环境变量的数组传进去。
char* env[2]={“\xde\xad\xbe\xef=\xca\xfe\xba\xbe”,NULL};
execve("/home/shelldon/Desktop/input",argv,env);
至此放一波代码。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include<sys/socket.h>
#include<netinet/in.h>
int main (){
//stage1
char*argv[101]={"/home/shelldon/Desktop/input"};
for(inti=1;i<100;i++)argv[i]="A";
argv[100]=NULL;
argv['A']="\x00";
argv['B']="\x20\x0a\x0d";
//stage2
int pipe2stdin[2] = {-1,-1};
int pipe2stderr[2] = {-1,-1};
pid_t childpid;
if ( pipe(pipe2stdin) < 0 ||pipe(pipe2stderr) < 0){
printf("Cannot create thepipe\n");
return 0;
}
if ( ( childpid = fork() ) < 0){
printf("Cannot fork\n");
return 0;
}
if ( childpid == 0 ){
/* Child process*/
close(pipe2stdin[0]);close(pipe2stderr[0]); write(pipe2stdin[1],"\x00\x0a\x00\xff",4);
write(pipe2stderr[1],"\x00\x0a\x02\xff",4);
}
else {
/* Parent process */
//sleep(0.1);
close(pipe2stdin[1]); close(pipe2stderr[1]);
dup2(pipe2stdin[0],0);dup2(pipe2stderr[0],2);
close(pipe2stdin[0]);close(pipe2stderr[1]);
//stage3
char* env[2]={"\xde\xad\xbe\xef=\xca\xfe\xba\xbe",NULL};
execve("/home/shelldon/Desktop/input",argv,env);// Execute theprogram
}
return 0;
}
没想到的是stage4意外成功了!不论如何,还是要进入看一看滴。
Stage4:
看源代码,题目意思就是打开这个”\x0a”的文件,并且读取四个字节,然后如果这四个字节是四个0x00的话,就成功。
至于前面我们碰巧成功的原因估计是,这个文件前几位是0x00吧。其实我不是很明白,这个文件到底是如何打开的,我并不知道我电脑上,还有这么个文件。。。而且我自己开的另外一个c文件也能打开这个什么“\x0a”。之前也是一直疑惑在这里,看了题解真是让人无言以对,可能有什么道理在里面我还不懂,日后查清楚了再补充吧!
不管了,既然能打开,那么我们如果要保证它前几位是0x00,直接自己打开文件写进去就好了嘛!
FILE* fp =fopen("\x0a","w");
fwrite("\x00\x00\x00\x00",4,1,fp);
fclose(fp);
还是可以过的。
Stage5:
这个题目牵涉到网络连接的事情,也就是Linux下的socket编程。网上关于这个的介绍很多。不再细讲,只是讲一下大体流程。Socket编程分为服务端和客户端。
服务端流程:
Socket函数建立socket—>bind函数绑定某一个端口—>listen监听端口,等待连接-->
发现请求-->然后accept返回连接后的socket-->然后receive函数接受内容—>直到最后断开连接。
客户端流程:
建立socketàconnect函数建立连接à向网络中写内容à直到断开连接。
这里的题目源码是把这个input作为一个服务端,绑定的端口值是argv[‘C’]里面存的数值,当然代码用了一下atoi转化字符串为数字,比如存的是char 0,那么绑定的端口就是0号端口。然后验证的是传进来的某连接发送的内容是"\xde\xad\xbe\xef".
所以我们自己写pwninput的时候,可以给input指定一个端口,然后我们的pwninput再连接这个端口,发送"\xde\xad\xbe\xef"就好了。
代码如下:
intsockfd;
structsockaddr_in server;
sockfd= socket(AF_INET,SOCK_STREAM,0);
if (sockfd < 0){
perror("Cannot create thesocket");
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);
}
charbuf[4] = "\xde\xad\xbe\xef";
write(sockfd,buf,4);
close(sockfd);
好的,下面是激动人心的时刻
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main (){
//stage1
char *argv[101]={"/home/shelldon/Desktop/input"};
for(int i=1;i<100;i++)argv[i]="A";
argv[100]=NULL;
argv['A']="\x00";
argv['B']="\x20\x0a\x0d";
argv['C']="55555";
//stage2
int pipe2stdin[2] = {-1,-1};
int pipe2stderr[2] = {-1,-1};
pid_t childpid;
if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0){
printf("Cannot create the pipe\n");
return 0;
}
if ( ( childpid = fork() ) < 0 ){
printf("Cannot fork\n");
return 0;
}
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 */
//sleep(0.1);
close(pipe2stdin[1]); close(pipe2stderr[1]); // Close pipes for writing
dup2(pipe2stdin[0],0); dup2(pipe2stderr[0],2); // Map to stdin and stderr
close(pipe2stdin[0]); close(pipe2stderr[1]); // Close write end (the fd has been copied before)
//stage3
char* env[2]={"\xde\xad\xbe\xef=\xca\xfe\xba\xbe",NULL};
//stage4
FILE* fp = fopen("\x0a","w");
fwrite("\x00\x00\x00\x00",4,1,fp);
fclose(fp);
execve("/home/shelldon/Desktop/input",argv,env);// Execute the program
}
//stage5
sleep(5);
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);
}
char buf[4] = "\xde\xad\xbe\xef";
write(sockfd,buf,4);
close(sockfd);
return 0;
}
现在我们只是在本地达到了目标,但是怎么让我们的程序跑在服务器上呢?
题解上给的办法是,直接在tmp文件夹下上传我们的文件,然后编译执行我们的文件,然后加上一条硬链接
ln /home/input2/flagflag从而在我们的程序找不到flag的时候自动去查找home下的flag。可是我们在加这句硬链接的时候出现了问题,说我们没有权限。
然后我也是在尝试的时候,发现不知道哪位大神早已经在里面建立好了一个asdf文件夹,一看就是随手按得。我好奇地把我的c文件传到这里面去,然后编译执行,也没有加入硬连接竟然成功了。后来才看到大神已经把flag悄悄复制到这里面去了。所以直接查看的时候,可以查处来。想来这也是一种办法。
不过从这里来看,我们的pwnable.kr可能并不是想让我们通过这种办法来做题,所以,才不让加入硬连接,没有硬连接是读不出flag来的。
总算是站在巨人的肩膀上成功了!
最后附上参考的一个不错的题解:https://werewblog.wordpress.com/2016/01/11/pwnable-kr-input/