这题流程相对较长,考查Linux编程的基本功(笔者做到这题不禁感叹自己基本功还是欠了不少火候)。
在一开始,尝试写Python脚本去完成验证,但stage 2关于stdio的验证却苦无思路。
这里感谢werew在他的writeup中提供的解决思路,这才豁然开朗。
参考链接:https://werewblog.wordpress.com/2016/01/11/pwnable-kr-input/
关于file descriptor maping的内容我另外做了整理,
详见 http://blog.csdn.net/qq_19550513/article/details/61915756
先放上题目源码:
/* ssh input2@pwnable.kr -p2222 (pw:guest) */
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
第一步比较常规,argv需要传递100个参数,即实际为argv[101](argv[0]指向执行的路径,argv[100]为NULL)。
/* stage 1 */
char *argv[101] = {0};
for(int i = 1; i<100; ++i)
argv[i] = "a";
argv[0] = "/home/input2/input";
argv['A'] = "\x00";
argv['B'] = "\x20\x0a\x0d";
argv[100] = NULL;
Stage 2 stdio
可以根据上面的链接查看我整理的关于file descriptor maping的帖子,个中内容不再赘述。主要原理就是利用fork()创建一个当前进程的复刻,父进程和子进程分开进行两个操作:使用dup2()函数将管道重定向,将所需的内容写入相应的pipe。按题设要求我们需要重定向0-stdin和2-stderr。
然后是关于execve,这里使用该函数创建input进程:
在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。
- 头文件: unistd.h
- 函数定义: int execve(const char *filename, char *const argv[ ], char *const envp[ ]);
- 返回值: 函数执行成功时没有返回值,执行失败时的返回值为-1.
- 函数说明: execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。
/* stage 2 */
int pipe_stdin[2] = {-1, -1};
int pipe_stderr[2] = {-1, -1};
pid_t pid_child;
if ( pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0 )
{
perror("Cannot create the pipe.");
exit(1);
}
#define STDIN_READ pipe_stdin[0]
#define STDIN_WRITE pipe_stdin[1]
#define STDERR_READ pipe_stderr[0]
#define STDERR_WRITE pipe_stderr[1]
if ( ( pid_child = fork() ) < 0 ) // do not forget the ()!
{
perror("Cannot create fork child.");
exit(1);
}
if( pid_child == 0 )
{
/* child proc */
sleep(1); //wait to pipe link 0,2
close(STDIN_READ);
close(STDERR_READ);
write(STDIN_WRITE, "\x00\x0a\x00\xff", 4);
write(STDERR_WRITE, "\x00\x0a\x02\xff", 4);
}
else
{
/* parent proc */
close(STDIN_WRITE);
close(STDERR_WRITE);
dup2(STDIN_READ, 0); //dup to 0-stdin
dup2(STDERR_READ, 2); //dup to 2-stderr
printf("start execve input.\n");
execve("/home/input2/input", argv, envp); //envp see stage 3
perror("Fail to execute the program");
exit(1);
}
printf("pipe link.\n");
Stage 3 envp
getenv(“\xde\xad\xbe\xef”)意为查看名为”\xde\xad\xbe\xef”的环境变量的值,这里我们只需传递一个环境变量,名为”\xde\xad\xbe\xef”,值为”\xca\xfe\xba\xbe”即可。
/* stage 3 */
char *envp[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
Stage 4 file
按要求创建文件写入内容即可。需要注意的是对于文件的操作需要在execve函数被调用之前完成,因为input进程不会等待当前进行对该文件的创建和写入,需要保证在执行验证之前完成文件的构造。
/* stage 4 */
// ! : file open before execve , or the check will fail
FILE *fp = fopen("\x0a", "wb"); // wb,w are similar in linux but differ in win
if(!fp) //see \x0d\x0a in win and \x0a in linux
{
perror("Cannot open file.");
exit(1);
}
printf("open file success.\n");
fwrite("\x00\x00\x00\x00", 4, 1, fp);
fclose(fp);
Stage 5 network
通过socket完成linux下进程间的通信,和在windows下基本类似。
具体可见我关于windows下的socket通信的简单实例。http://blog.csdn.net/qq_19550513/article/details/54965653
唯一区别在于windows下需要额外对winsock信息 WSAData 利用 WSAStartup() 函数进行初始化操作。
/* stage 5 */
sleep(2); // wait the server start
int sockfd;
char buf[10] = {0}; // buf to be sent
int len; // len of avail buf
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9999); // port in argv['C']
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //local
if( (sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
{
perror("socket error.");
exit(1);
}
if ( connect(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0 )
{
perror("connect error.");
exit(1);
}
printf("socket connect.\n");
strcpy(buf, "\xde\xad\xbe\xef");
len = strlen(buf);
send(sockfd, buf, len, 0);
close(sockfd);
Final Stage
整合后,ssh登录后在/tmp目录下我们自己的目录,利用vi写入c文件,再用gcc编译生成可执行文件。
到这里还没有结束,因为只有在当前目录guest才有写的权限,而flag路径为/home/input2/flag。
这里我们需要一个soft link指向flag文件,在当前目录 ln -s /home/input2/flag flag
即可。注意不能创建hard link,因为guest对flag文件本身是没有读写权限的。
c文件内容如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
/* stage 1 */
char *argv[101] = {0};
for(int i = 1; i<100; ++i)
argv[i] = "a";
argv[0] = "/home/input2/input";
argv['A'] = "\x00";
argv['B'] = "\x20\x0a\x0d";
argv['C'] = "9999"; //server port
argv[100] = NULL;
/* stage 3 */
char *envp[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
/* stage 4 */ // ! : file open before execve , or the check will fail
FILE *fp = fopen("\x0a", "wb"); // wb,w are similar in linux but differ in win
if(!fp) //see \x0d\x0a in win and \x0a in linux
{
perror("Cannot open file.");
exit(1);
}
printf("open file success.\n");
fwrite("\x00\x00\x00\x00", 4, 1, fp);
fclose(fp);
/* stage 2 */
int pipe_stdin[2] = {-1, -1};
int pipe_stderr[2] = {-1, -1};
pid_t pid_child;
if ( pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0 )
{
perror("Cannot create the pipe.");
exit(1);
}
#define STDIN_READ pipe_stdin[0]
#define STDIN_WRITE pipe_stdin[1]
#define STDERR_READ pipe_stderr[0]
#define STDERR_WRITE pipe_stderr[1]
if ( ( pid_child = fork() ) < 0 ) // do not forget the ()!
{
perror("Cannot create fork child.");
exit(1);
}
if( pid_child == 0 )
{
/*child proc*/
sleep(1); //wait to pipe link 0,2
close(STDIN_READ);
close(STDERR_READ);
write(STDIN_WRITE, "\x00\x0a\x00\xff", 4);
write(STDERR_WRITE, "\x00\x0a\x02\xff", 4);
}
else
{
/*parent proc*/
close(STDIN_WRITE);
close(STDERR_WRITE);
dup2(STDIN_READ, 0); //dup to 0-stdin
dup2(STDERR_READ, 2); //dup to 2-stderr
printf("start execve input.\n");
execve("/home/input2/input", argv, envp);
perror("Fail to execute the program");
exit(1);
}
printf("pipe link.\n");
/* stage 5 */
sleep(2); // wait the server start
int sockfd;
char buf[10] = {0}; // buf to be sent
int len; // len of avail buf
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9999); // port in argv['C']
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //local
if( (sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
{
perror("socket error.");
exit(1);
}
if ( connect(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0 )
{
perror("connect error.");
exit(1);
}
printf("socket connect.\n");
strcpy(buf, "\xde\xad\xbe\xef");
len = strlen(buf);
send(sockfd, buf, len, 0);
close(sockfd);
return 0;
}
最后的运行情况如下:
input2@ubuntu:/tmp/umiade$ ./setinput
open file success.
start execve input.
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
pipe link.
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
socket connect.
Stage 5 clear!
Mommy! I learned how to pass various input in Linux :)