Linux 进程IO杂项
连接服务器
对于这类提供了 SSH 连接地址的题,我们通常要连接到服务器并在服务器上按提示信息完成 pwn。登录成功后,看到如下信息:
____ __ __ ____ ____ ____ _ ___ __ _ ____
| \| |__| || \ / || \ | | / _] | |/ ]| \
| o ) | | || _ || o || o )| | / [_ | ' / | D )
| _/| | | || | || || || |___ | _] | \ | /
| | | ` ' || | || _ || O || || [_ __ | \| \
| | \ / | | || | || || || || || . || . \
|__| \_/\_/ |__|__||__|__||_____||_____||_____||__||__|\_||__|\_|
- Site admin : daehee87.kr@gmail.com
- IRC : irc.netgarage.org:6667 / #pwnable.kr
- Simply type "irssi" command to join IRC now
- files under /tmp can be erased anytime. make your directory under /tmp
- to use peda, issue `source /usr/share/peda/peda.py` in gdb terminal
可以看到上面列出了网站管理员、聊天服务器地址,并给出说明 “files under /tmp can be erased anytime. make your directory under /tmp”,即只允许在 /tmp
目录下执行操作。首先,不管三七二十一,能见度不足,ls
敬上!
$ ls -al
total 44
drwxr-x--- 5 root input2 4096 Oct 23 2016 .
drwxr-xr-x 114 root root 4096 May 19 15:59 ..
d--------- 2 root root 4096 Jun 30 2014 .bash_history
-r--r----- 1 input2_pwn root 55 Jun 30 2014 flag
-r-sr-x--- 1 input2_pwn input2 13250 Jun 30 2014 input
-rw-r--r-- 1 root root 1754 Jun 30 2014 input.c
dr-xr-xr-x 2 root root 4096 Aug 20 2014 .irssi
drwxr-xr-x 2 root root 4096 Oct 23 2016 .pwntools-cache
$ ls -ld /tmp
drwxrwx-wt 4833 root root 135168 Oct 24 08:38 /tmp
好吧……出题人辛苦了,在文件/目录的所属、权限上可谓下足了功夫!在主目录下,只有三个文件对我们来说是有用的:flag
,input
, input.c
,盲猜也知道需要运行 input
可执行文件,得到 flag
中存放的信息。这里 flag
文件只有 input2_pwn
用户有权限读取,可是当前登录用户是 input2
用户,怎么办呢?
注意到 input
可执行文件的权限字符串是 -r-sr-x---
,等等!这个 s
是个什么情况?事实上,这里的 s
字符为“强制位”,它的存在将使可执行文件在执行时临时获取文件所有者/所属组的身份。再联系 input
与 flag
文件相同的所有者,看来这个 flag
是非得用 input
读取不可了!另外,我们还能注意到,/tmp
目录的权限位中也有一个 t
权限,这又是什么鬼?
到这里我们就明白了,我们可以任意在 /tmp
目录下执行我们的操作,但偏偏这个目录没有给读权限,也就是说只能 cd
进去凭感觉执行文件……行吧,学 pwn 的男人无所畏惧!既然已经了解了出题人的基本意图,咱们还是撸起袖子加油干吧!
源文件分析
服务器上给出了 input
文件的源码,主要分为三个部分:Stage 2,Stage 3,Stage 4 和 Stage 5,分别考察了主函数参数、标准I/O、环境变量、文件读写和网络交互六个方面内容。下面我们针对各个部分代码进行分析。先给出完整代码:
#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;
}
接下来具体分析各部分代码,为节省篇幅,不作任何可靠性检验,不检查任何返回值。
Stage 1: argv
主函数参数部分,根据第 73, 74, 75, 108 行的要求,我们需要在启动 input
程序时传入100个参数,其中第 'A', 'B', 'C' 位必须是指定的值(从0开始计数),故决定使用系统调用 execve
来启动程序:
int execve(const char * path,char * const argv[],char * const envp[]);
根据要求,我们创建一个长为101的字符指针数组,分别指向100个字符串,其中最后一个指针赋值为0。那么我们可以写出以下代码:
#include <unistd.h>
int main () {
// Stage 1
char * argv [101];
for (int i=0; i<100; i++) {
// 因要求 argv['A'] 与 "\x00" 比较结果为0
// 且使用的是 strcmp 函数:遇 '\0' 停止匹配
// 故先全部赋值为空串
argv[i] = "";
}
// 最后一位设为0指针
argv[100] = 0;
argv['B'] = "\x20\x0a\x0d";
// 根据 108 行要求,任意设置一个端口号
char port [] = "12933";
argv['C'] = port;
char * envp [] = {0};
execve ("/home/input2/input", argv, envp);
}
编译运行后,不出所料,得到输出:Stage 1 clear!。下面进入下一环节。
Stage 2: stdio
一看代码,要求从文件描述符 0
和 2
的文件中各读取4字节,关键这俩一个是 stdin
,一个是 stderr
啊!
这里由于题中要求的8个字节属特殊字符,故不直接往 0
和 2
中写数据。我们可以采用新建普通文件或新建管道的方式。我们先来学习一下使用普通文件的方式,涉及操作包括建立并打开文件、删除文件原有数据、写入数据并关闭文件,然后重新打开文件并将之关联到指定描述符。如下:
#include <unistd.h>
#include <fcntl.h>
int main () {
// 以只写方式打开,创建文件|只读|截断原有内容
// 最末一个参数为8进制文件权限标识,同 chmod
int in = open ("stdin", O_CREAT|O_WRONLY|O_TRUNC, 0644);
char buf [] = {0x00, 0x0a, 0x00, 0xff};
write (in, buf, 4);
close (in);
// 以只读方式打开
in = open ("stdin", O_RDONLY, 0644);
// STDIN_FILENO 即 0
// dup2 函数将 in 文件描述信息复制给 0
dup2 (in, STDIN_FILENO);
}
新建普通文件的方式比较麻烦且不够优雅,仅供参考。我们还是更愿意采用管道的方式。管道仅适用于从同一进程中通过调用 fork
得到的分支进程之间,其中一方从管道的写端写入数据,而另一方则从管道的读端读出数据。下例:
#include <unistd.h>
int main () {
// 待传输数据
char str1 [] = {0x00, 0x0a, 0x00, 0xff};
char str2 [] = {0x00, 0x0a, 0x02, 0xff};
// 使用长度为2的整型数组创建管道
int in [2], err [2];
pipe (in);
pipe (err);
// fork 系统调用,进程克隆
if (0 == fork ()) {
// 新进程关闭写端
close (in[1]);
close (err[1]);
// 重新绑定文件描述符
dup2 (in[0], 0);
dup2 (err[0], 2);
// 进程替换
execve ("/home/input2/input", argv, envp);
}
// 原进程关闭读端
close (in[0]);
close (err[0]);
// 向管道写入数据
write (in[1], str1, 4);
write (err[1], str2, 4);
// 关闭写端
close (in[1]);
close (err[1]);
}
结合上一小节的代码,至此可以轻松得到输出:Stage 2 clear!。
Stage 3: env
根据源文件第87行的要求,我们需要在执行 input
时传入环境变量 \xde\xad\xbe\xef
,其值应为 \xca\xfe\xba\xbe
,这与传入参数列表类似,话不多说:
#include <unistd.h>
int main () {
char * argv [] = {0};
char * envp [] = {
"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", 0
};
execve ("/home/input2/input", argv, envp);
}
补充之前的代码,即得:Stage 3 clear!。
Stage 4: file
第四关考察的是文件读写,既可以使用系统调用 read
/write
,也可以使用 C 库函数 fread
/fwrite
。不过既然题设源代码用了库函数,这里也使用库函数,聊表敬意。
#include <stdio.h>
int main () {
// 只写方式打开文件
FILE * fp = fopen ("\x0a", "w");
char buf [] = {0x00, 0x00, 0x00, 0x00};
// 向文件中写入数据
fwrite (buf, 4, 1, fp);
// 关闭文件
fclose (fp);
}
事实上,如果知道文件名 \x0a
代表的是什么,我们甚至不需要写这一段,直接将对应文件放到目录下就行。总之,这关不难:Stage 4 clear!。
Stage 5: network
这一部分源代码采用了 C 语言中 IPv4 协议的流式套接字传输数据的方式,要求我们在主函数参数中传入端口号,然后向指定的端口请求连接并发送数据。要建立网络连接,首先创建地址结构和套接字并设置地址信息,之后使用 connect
函数请求连接(为确保服务端接收到请求,可以使用 sleep
函数手动等待)。连接建立成功后,调用 send
函数发送数据。也有不建立连接发送数据的方法,参见 recvfrom
和 sendto
。
#include <sys/socket.h>
#include <arpa/inet.h>
int main () {
// 地址结构
struct sockaddr_in caddr;
// 建立套接字,参数指定INET地址族、字节流类型,0表示自动选择协议
// 按照惯例,返回值是一个描述字,如出错则返回错误代码
int cd = socket (AF_INET, SOCK_STREAM, 0);
// 设置地址信息:类型、目标和端口
caddr.sin_family = AF_INET;
caddr.sin_addr.s_addr = inet_addr ("127.0.0.1");
caddr.sin_port = htons (atoi (port));
// 请求连接
// 注:struct sockaddr 与 struct sockaddr_in 是并列结构
connect (cd, (struct sockaddr *) &caddr, sizeof (struct sockaddr_in));
// 发送数据
char buff [] = {0xde, 0xad, 0xbe, 0xef};
send (cd, buff, 4, 0);
}
到这里,我们终于可以通关了:Stage 5 clear!。
其他问题
注意到远程服务器用户主目录并没有
w
权限,而可执行程序又要在flag
文件的同级目录下创建$\n
文件,这怎么实现呢?其实答案并不难,因为对于/tmp
目录我们是有写权限的,为什么不在/tmp
下建立一个指向flag
文件的链接呢?如果你真的去服务器上尝试了,那么你还会发现——
/tmp
目录下有一个已经存在的flag
目录,并且对于它我们并没有权限操作!不过这倒是不难,直接新建一个临时目录,然后在目录下建立flag
的链接并运行我们的程序即可。远程服务器上因各种权限问题,加上频繁的
/tmp
目录清理,编码环境极其恶劣(>_<),故建议先在本地调试,待成功通关后再用scp
命令将编译好的可执行文件传送到服务器的/tmp
目录下,然后再连接服务器进行操作。
附
附上解题完整代码,供各路英雄好汉参考。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main (int argus, char ** argul) {
// Stage 1
char * argv [101];
for (int i=0; i<100; i++) {
argv[i] = "";
}
argv[100] = 0;
argv['B'] = "\x20\x0a\x0d";
char port [] = "12933";
argv['C'] = port;
// Stage 2
char str1 [] = {0x00, 0x0a, 0x00, 0xff};
char str2 [] = {0x00, 0x0a, 0x02, 0xff};
int in [2], err [2];
pipe (in);
pipe (err);
// Stage 3
char * envp [2];
envp[0] = "\xde\xad\xbe\xef=\xca\xfe\xba\xbe";
envp[1] = 0;
// Stage 4
FILE * fp = fopen ("\x0a", "w");
char buf [] = {0x00, 0x00, 0x00, 0x00};
fwrite (buf, 4, 1, fp);
fclose (fp);
if (0 == fork ()) {
// New process
close (in[1]);
close (err[1]);
dup2 (in[0], 0);
dup2 (err[0], 2);
// Execute
execve ("/home/input2/input", argv, envp);
}
// Parent process
close (in[0]);
close (err[0]);
write (in[1], str1, 4);
write (err[1], str2, 4);
close (in[1]);
close (err[1]);
// Stage 5
sleep (1);
struct sockaddr_in caddr;
int cd = socket (AF_INET, SOCK_STREAM, 0);
caddr.sin_family = AF_INET;
caddr.sin_addr.s_addr = inet_addr ("127.0.0.1");
caddr.sin_port = htons (atoi (port));
connect (cd, (struct sockaddr *) &caddr, sizeof (struct sockaddr_in));
char buff [] = {0xde, 0xad, 0xbe, 0xef};
send (cd, buff, 4, 0);
wait ();
return 0;
}
如有错漏,欢迎指正!