Linux 进程IO杂项

Linux 进程IO杂项

本文结合一个 pwn 例题,在分析例题的过程中穿插介绍相关知识。
例题来源:PWNABLE.KR 网站,Toddler's Bottle 小节,习题 input
例题内容:

Mom? How can I pass my input to a computer program?
ssh input2@pwnable.kr -p 2222 (pw: guest).

连接服务器#

对于这类提供了 SSH 连接地址的题,我们通常要连接到服务器并在服务器上按提示信息完成 pwn。登录成功后,看到如下信息:

Copy
____ __ __ ____ ____ ____ _ ___ __ _ ____ | \| |__| || \ / || \ | | / _] | |/ ]| \ | 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 敬上!

Copy
$ 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

好吧……出题人辛苦了,在文件/目录的所属、权限上可谓下足了功夫!在主目录下,只有三个文件对我们来说是有用的:flaginputinput.c,盲猜也知道需要运行 input 可执行文件,得到 flag 中存放的信息。这里 flag 文件只有 input2_pwn 用户有权限读取,可是当前登录用户是 input2 用户,怎么办呢?

注意到 input 可执行文件的权限字符串是 -r-sr-x---,等等!这个 s 是个什么情况?事实上,这里的 s字符为“强制位”,它的存在将使可执行文件在执行时临时获取文件所有者/所属组的身份。再联系 inputflag 文件相同的所有者,看来这个 flag 是非得用 input 读取不可了!另外,我们还能注意到,/tmp 目录的权限位中也有一个 t 权限,这又是什么鬼?

强制位经 chmod 设置后,会显示在原可执行权限的位置,这时如果 s 显示为小写,则表明已有 x 权限;若 S 大写,则表示该位无 x 权限。若没有 x 权限,即使已经设置了强制位,也无法获得临时身份。
相似的,对于目录来说,也有第十个权限位 t,即粘滞位。它的存在允许用户在具有 w 权限的前提下,在该目录中随意创建文件和目录,但只能删除其中自己创建的文件或目录。这在单独使用 w 权限的情形下是实现不了的。

到这里我们就明白了,我们可以任意在 /tmp 目录下执行我们的操作,但偏偏这个目录没有给读权限,也就是说只能 cd 进去凭感觉执行文件……行吧,学 pwn 的男人无所畏惧!既然已经了解了出题人的基本意图,咱们还是撸起袖子加油干吧!

源文件分析#

服务器上给出了 input 文件的源码,主要分为三个部分:Stage 2Stage 3Stage 4Stage 5,分别考察了参数列表、标准I/O、环境变量、文件读写和网络交互六个方面内容。下面我们针对各个部分代码进行分析。先给出完整代码:

Copy
#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 来启动程序:

Copy
int execve(const char * path,char * const argv[],char * const envp[])

exec 函数族包含多个类似的函数,但它们都是基于 execve 的封装,最终都转化为了此系统调用。所谓系统调用,即由操作系统声明,供程序在执行时调用的一类接口。
这里 execve 的作用是“进程替换”,即调用此函数后,当前执行的进程完全转化为指定的另一个进程,其进程号 pid 不变。所以,原进程中此调用后的语句都不会执行——除非调用失败。
execve 而言,其参数列表包含三个指针,第一个参数是一个指向可执行文件的地址字符串;第二个参数传入一个字符型二级指针,代表一个字符串数组,即新进程的参数数组;第三个参数类似地,传入另一个字符串数组,它是新进程的环境变量数组。这两个数组都需要以0指针结尾,以结束参数输入。

根据要求,我们创建一个长为101的字符指针数组,分别指向100个字符串,其中最后一个指针赋值为0。那么我们可以写出以下代码:

Copy
#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#

一看代码,要求从文件描述符 02 的文件中各读取4字节,关键这俩一个是 stdin,一个是 stderr 啊!

注:int 类型的文件描述符是由 Linux 系统调用 openclose 所操作的,只能通过系统调用 writeread 进行读写;而 FILE * 类型的文件指针则由 C 语言定义,并用 C 函数 fwritefreadf 开头的函数进行读写。stdinstdoutstderr 正是三个特殊的文件指针,因为它们与文件描述符 012 一一对应,分别指向了 标准输入设备标准输出设备标准错误设备
对于 C 语言来说,其 printf 函数默认输出到 stdout,但我们可以使用 fprintf 定向输出到标准错误。类似的,C++ 中的对象 cincoutcerr 也分别与这三个设备关联。

这里由于题中要求的8个字节属特殊字符,故不直接往 02 中写数据。我们可以采用新建普通文件或新建管道的方式。我们先来学习一下使用普通文件的方式,涉及操作包括建立并打开文件、删除文件原有数据、写入数据并关闭文件,然后重新打开文件并将之关联到指定描述符。如下:

Copy
#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); }
Copy
int dup(int oldfd); int dup2(int oldfd, int newfd);

dup2 函数与 dup 函数类似,用于将某个文件描述符复制一份,产生一个新的描述符并返回。与后者不同的是,dup2 可以在第二个参数中指定新的描述符的值,若该值已被使用,则关闭原先使用的文件。这里 in 是被复制的描述符,而 STDIN_FILENO 即0,表示将0所指的标准输入设备关闭,并重新指向 in 所指向的文件。
STDIN_FILENO 是头文件 fcntl.h 中的一个宏定义,其值为0。类似的,STDOUT_FILENOSTDERR_FILENO 值分别为1和2。

新建普通文件的方式比较麻烦且不够优雅,仅供参考。我们还是更愿意采用管道的方式。管道仅适用于从同一进程中通过调用 fork 得到的分支进程之间,其中一方从管道的写端写入数据,而另一方则从管道的读端读出数据。下例:

Copy
#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!

Copy
pid_t fork( void); int pipe(int fd[2]);

PCB进程控制块。进程是操作系统中资源分配的基本单位,早期也是调度的基本单位,每个进程的所有信息保存在各自的进程控制块中。在引入线程的操作系统中,CPU 调度的基本单位是线程。
fork 系统调用,此函数将当前执行的进程进行克隆,得到两个状态完全相同的分支进程。其中原有分支的 PCB 信息不变,新分支得到一个新的 PCB,它们分别称为父进程和子进程。尽管两个进程的所有数据都一致,但 fork 函数的返回值在两个分支中是不同的,其中父进程中的 fork 函数返回子进程的 pid,此值为正整数;而子进程中的 fork 函数返回值为0。如果调用失败,返回值为-1。
所以,一般调用之后,进程需要自省已身,通过其返回值确定自已是父进程还是子进程,以便接下来执行各自的任务。
要在父子进程之间使用管道传输数据,需要先创建一个长度为2的整型数组,并以首地址为参数呼叫系统调用 pipe,此时系统会将两个文件描述符写入到数组中,成功返回0,失败返回-1。其中0端为读端,1端为写端,即向1端写入的数据,可以从0端读取出来。
例中父进程通过 write 系统调用向 in[1] 写入 str1 的前4个字节,子进程即可使用 read 系统调用从 in[0] 读取得到4字节相同的数据。

Stage 3: env#

根据源文件第87行的要求,我们需要在执行 input 时传入环境变量 \xde\xad\xbe\xef,其值应为 \xca\xfe\xba\xbe,这与传入参数列表类似,话不多说:

Copy
#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。不过既然题设源代码用了库函数,这里也使用库函数,聊表敬意。

Copy
#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 函数发送数据。也有不建立连接发送数据的方法,参见 recvfromsendto

Copy
#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 目录下,然后再连接服务器进行操作。

#

附上解题完整代码,供各路英雄好汉参考。

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

如有错漏,欢迎指正!

posted @   王牌饼干  阅读(136)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示
CONTENTS