Linux IPC 学习 起因 专门去学习Linux IPC的缘由是CNSS 2020 Recruit的一道招新题,题目直接给了一个shell,但是获取了shell连上服务器之后,由于flag权限不足,只能通过readflag程序来读取flag,而readflag这个程序需要进行简单的程序交互,通过题目给的shell是无法完成这一操作的,我们需要编写一个交互程序,来自动完成与readflag的交互让其输出flag。
这个题的预期解是通过运行一个perl程序来完成交互,服务器上perl环境已经安装好了。但是这个预期解的局限性也就在于必须得有perl环境才能正常操作,深入思考了预期解的payload,会发现其实整个程序的本质就是两个linux进程之间的通信过程,进程间通信我们也可以通过c或者go等语言,编译成binary直接丢上服务器运行,这样就可以不依赖任何特定的语言环境了。
进程/线程 首先简单讲讲进程和线程相关的概念。
进程与线程的概念 进程:操作系统进行资源分配的基本单位。
线程:操作系统进行调度和执行的基本单位。
进程的描述 操作系统中主要使用进程控制块(即PCB)来描述进程。Linux中的PCB是task_struct
类型结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 #include <linux/sched.h> struct task_struct { volatile long state; unsigned long flags; int sigpending; mm_segment_t addr_limit; struct exec_domain *exec_domain ; volatile long need_resched; unsigned long ptrace; int lock_depth; long counter; long nice; unsigned long policy; struct mm_struct *mm ; int processor; unsigned long cpus_runnable, cpus_allowed; struct list_head run_list ; unsigned long sleep_time; struct task_struct *next_task , *prev_task ; struct mm_struct *active_mm ; struct list_head local_pages ; unsigned int allocation_order, nr_local_pages; struct linux_binfmt *binfmt ; int exit_code, exit_signal; int pdeath_signal; unsigned long personality; int did_exec:1 ; pid_t pid; pid_t pgrp; pid_t tty_old_pgrp; pid_t session; pid_t tgid; int leader; struct task_struct *p_opptr , *p_pptr , *p_cptr , *p_ysptr , *p_osptr ; struct list_head thread_group ; struct task_struct *pidhash_next ; struct task_struct **pidhash_pprev ; wait_queue_head_t wait_chldexit; struct completion *vfork_done ; unsigned long rt_priority; unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer ; struct tms times ; unsigned long start_time; long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1 ; uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; int ngroups; gid_t groups[NGROUPS]; kernel_cap_t cap_effective, cap_inheritable, cap_permitted; int keep_capabilities:1 ; struct user_struct *user ; struct rlimit rlim [RLIM_NLIMITS ]; unsigned short used_math; char comm[16 ]; int link_count, total_link_count; struct tty_struct *tty ; unsigned int locks; struct sem_undo *semundo ; struct sem_queue *semsleeping ; struct thread_struct thread ; struct fs_struct *fs ; struct files_struct *files ; spinlock_t sigmask_lock; struct signal_struct *sig ; sigset_t blocked; struct sigpending pending ; unsigned long sas_ss_sp; size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; u32 parent_exec_id; u32 self_exec_id; spinlock_t alloc_lock; void *journal_info; };
进程状态 R, TASK_RUNNING:就绪态或者运行态,进程就绪可以运行,但是不一定正在占有CPU S, TASK_INTERRUPTIBLE:浅度睡眠,等待资源,可以响应信号,一般是进程主动sleep进入的状态 D, TASK_UNINTERRUPTIBLE:深度睡眠,等待资源,不响应信号,典型场景是进程获取信号量阻塞 Z, TASK_ZOMBIE:僵尸态,进程已退出或者结束,但是父进程还不知道,没有回收时的状态 T, TASK_STOPED:停止,调试状态,收到SIGSTOP信号进程挂起
进程的创建与消亡 (1) system() 通过调用shell启动一个新进程
(2) exec() 以替换当前进程映像的方式启动一个新进程
(3) fork() 以复制当前进程映像的方式启动一个新进程,子进程中fork()返回0,父进程fork()返回为子进程ID。
(4) wait() 父进程挂起,等待子进程结束。
(5) 孤儿进程与僵尸进程 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程不会浪费资源。 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。僵尸进程会浪费系统资源。 僵尸进程产生原因:
子进程结束后向父进程发出SIGCHLD信号,父进程默认忽略了它。
父进程没有调用wait()或waitpid()函数来等待子进程的结束。
避免僵尸进程的方法:
父进程调用wait()或者waitpid()等待子进程结束,这样处理父进程一般会阻塞在wait处而不能处理其他事情。
捕捉SIGCHLD信号,并在信号处理函数里面调用wait函数,这样处理可避免1中描述的问题。
fork两次,父进程创建儿子进程,儿子进程再创建一个孙子进程,然后儿子进程自杀,孙子进程成为孤儿进程被init进程收养。
进程相关基础操作(c语言) fork()函数 fork()函数用于创建一个进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <unistd.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> void print_process_message () { pid_t myprocess_id = getpid(); uid_t uid = getuid(); gid_t ugid = getgid(); printf ("getpid = %d getuid= %d getgid= %d \n" , myprocess_id, uid, ugid); } int main (int argc, char const *argv[]) { int n = 0 ; printf ("before fork: n = %d\n" ,n); pid_t fpid = fork(); if ( fpid < 0 ) { perror("fork error" ); exit (EXIT_FAILURE); } else if (fpid == 0 ) { n++; printf ("child_proc(%d, ppid=%d): n= %d\n" , getpid(), getppid(), n); } else { n--; printf ("parent_proc(%d): n= %d\n" ,getpid(),n); } print_process_message(); printf ("quit_proc(%d) ...\n" ,getpid()); return 0 ; }
注意fork()函数执行一次返回两次,一次是父进程返回,返回大于0的数(子进程ID),一次是子进程返回,若执行成功返回0。
fork() 和 vfork() fork创建子进程,把父进程数据空间、堆和栈复制一份; vfork创建子进程,与父进程内存数据共享; 但是后来的fork也进行了改变,不是一开始调用fork就复制数据,而是只有在子进程要修改数据的时候,才进行复制,即copy-on-write(COW写时拷贝技术)
wait和waitpid函数 pid_t wait(int \*status)
等待任意子进程退出,并捕获退出状态pid_t waitpid(pid_t pid, int \*status, int options)
等待子进程退出,并捕获退出状态
两个函数返回的都是退出的子进程的id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <wait.h> int main (int argc, char const *argv[],char *envp[]) { pid_t fpid = fork(), pid; if ( fpid < 0 ) { perror("fork error" ); exit (EXIT_FAILURE); } else if ( fpid == 0 ) { sleep(5 ); exit (5 ); } else { int stat; while (1 ) { pid = waitpid(fpid, &stat, WNOHANG); if ( pid > 0 ) { break ; } else { printf ("wait child proc ... \n" ); sleep(1 ); } } if (WIFEXITED(stat)) { printf ("child_proc(%d): exit_code :%d\n" , pid, WEXITSTATUS(stat)); } } return 0 ; }
程序启动了一个子进程,休眠5秒后结束,父进程每1秒轮询一次子进程的状态,若子进程已退出,pid为子进程ID大于0,则退出循环,否则继续等待。
可以使用两种方式来处理子进程的退出,一种是通过signal()函数处理SIGCHLD信号,例如忽略该信号以防止出现僵尸进程,另一种方式就是通过wait()和waitpid()函数来回收子进程以防止出现僵尸进程。
exec系列函数 exec()是一个系统调用,它将以新的进程空间替换现在的进程空间执行程序,进程pid不变。调用exec后,系统会申请一块新的进程空间来存放被调用的程序,然后当前进程会携带pid跳转到新的进程空间,并从main函数开始执行,旧的进程空间被回收。
1 2 3 4 5 6 7 8 9 #include <stdio.h> #include <unistd.h> int main (int argc ,const char **argv) { char *argv_ch[]={"ls" ,"-al" ,"/usr/include/linux" ,NULL }; char *envp_ch[]={0 ,NULL }; execve("/bin/ls" ,argv_ch,envp_ch); }
程序通过execve()函数执行/bin/ls
程序,并传入参数和环境变量。程序将用新的子程序进程空间替换当前程序的进程空间来执行程序,进程pid不变。
守护进程 Linux Daemon进程是运行在后台的一种特殊进程。 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程; 守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理; 守护进程的名称通常以d结尾,比如sshd,xinetd,crond等
函数原型:
1 2 #include <unistd.h> int daemon (int nochdir, int noclose) ;
system()和popen()函数 system函数执行一个shell命令,system()函数调用/bin/sh来执行参数指定的命令,/bin/sh一般是一个软连接,指向某个具体的shell,比如bash。
system()函数的调用执行了三步操作:
fork一个子进程
在子进程中调用exec函数去执行command
在父进程中调用wait去等待子进程结束;
注意:system()并不能获取命令执行的输出结果,只能得到执行的返回值;
popen函数启动另外一个进程去执行一个shell命令行,称调用popen的进程为父进程,由popen启动的进程称为子进程。
popen函数还创建一个管道用于父子进程间通信,父进程要么从管道读信息,要么向管道写信息,至于是读还是写取决于父进程调用popen时传递的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main (int argc, char *argv[]) { if (argc < 2 ){ fprintf (stderr , "usage: %s <cmd>\n" , argv[0 ]); exit (EXIT_FAILURE); } char output[1024 +1 ]; FILE *pp = popen(argv[1 ], "r" ); if (pp == NULL ){ perror("popen error" ); exit (EXIT_FAILURE); } int nread = fread(output, 1 , 1024 , pp); int status = pclose(pp); if (status < 0 ){ perror("pclose error" ); exit (EXIT_FAILURE); } output[nread] = '\0' ; if (WIFEXITED(status)){ printf ("status: %d\n%s" , WEXITSTATUS(status), output); } return 0 ; }
程序通过popen()函数创建子进程执行指定的命令,并创建一个只读管道,用于从中读子进程命令执行的输出。
signal信号 信号(signal)是一种软中断,信号机制是进程间通信的一种方式,采用异步通信方式。
几个主要的信号:
信号
含义
SIGINT(2)
中断(即CTRL + C)
SIGKILL(9)
kill信号(强杀,进程不能阻止)
SIGPIPE(13)
管道破损,没有读端的管道写数据,就是那个brokenpipe。默认杀进程,所以网络编程中要处理这个信号。 (当服务器close一个连接时,根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。)
SIGTERM(15)
终止信号,这个不是强制的,它可以被捕获和解释(或忽略)的过程。类似于和这个进程商量一下,让它退出。
SIGCHLD(17)
子进程退出,默认忽略
SIGSTOP(19)
进程停止,不能被忽略、处理和阻塞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <unistd.h> void handle_signal (int signum) { printf ("received signal: %d\n" , signum); exit (0 ); } int main (void ) { signal(SIGINT, handle_signal); for (;;){ printf ("running ... \n" ); sleep(1 ); } return 0 ; }
程序启动后持续监听,直到有任何信号发送给程序,输出信号的值后退出。
进程间通信 Linux的所有进程间通信方式
管道:在创建时分配一个page大小的内存,缓存区大小比较有限;
消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信;
信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;
这里主要详细介绍管道通信和unix域套接字通信。
管道 管道是一个进程连接数据流到另一个进程的通道,它通常是用作把一个进程的输出通过管道连接到另一个进程的输入。例如我们在shell中输入命令:ls -l | grep string
,我们知道ls命令会把当前目录中的文件都列出来,但是它不会直接输出,而是把本来要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入,然后这个进程对输入的信息进行筛选,把存在string的信息的字符串(以行为单位)打印在屏幕上。
popen(), pclose()函数 1 2 3 #include <stdio.h> FILE* popen (const char *command, const char *open_mode) ;int pclose (FILE *stream_to_close) ;
popen()函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command是要运行的程序名和相应的参数。open_mode只能是”r”和”w”的其中之一。注意,popen()函数的返回值是一个FILE类型的指针,也就是说我们可以使用stdio I/O库中的文件处理函数来对其进行操作。
如果open_mode是”r”,主调用程序就可以使用被调用程序的输出,通过函数返回的FILE指针使用stdio函数(如fread)来读取程序的输出;如果open_mode是”w”,主调用程序就可以向被调用程序发送数据,即通过stdio函数(如fwrite)向被调用程序写数据,而被调用程序就可以在自己的标准输入中读取这些数据。
pclose()函数用于关闭由popen创建出的关联文件流。pclose()只在popen启动的进程结束后才返回,如果调用pclose()时被调用进程仍在运行,pclose()调用将等待该进程结束。它返回关闭的文件流所在进程的退出码。
管道默认是阻塞模式的,通过fcntl(fd, F_SETFL, flags | O_NONBLOCK)
可以设置非阻塞的管道
我们将ls -l | grep a
命令使用程序实现一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main () { FILE *read_fp = NULL ; FILE *write_fp = NULL ; char buffer[BUFSIZ + 1 ]; int chars_read = 0 ; memset (buffer, '\0' , sizeof (buffer)); read_fp = popen("ls -l" , "r" ); write_fp = popen("grep a" , "w" ); if (read_fp && write_fp) { chars_read = fread(buffer, sizeof (char ), BUFSIZ, read_fp); while (chars_read > 0 ) { buffer[chars_read] = '\0' ; fwrite(buffer, sizeof (char ), chars_read, write_fp); chars_read = fread(buffer, sizeof (char ), BUFSIZ, read_fp); } pclose(read_fp); pclose(write_fp); exit (EXIT_SUCCESS); } exit (EXIT_FAILURE); }
popen()的实现方式和优缺点: 当请求popen()调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它。
这样就带来了一个优点和一个缺点。优点是:在Linux中所有的参数扩展都是由shell来完成的。所以在启动程序(command中的命令程序)之前先启动shell来分析命令字符串,也就可以使各种shell扩展(如通配符)在程序启动之前就全部完成,这样我们就可以通过popen()启动非常复杂的shell命令。
而它的缺点就是:对于每个popen()调用,不仅要启动一个被请求的程序,还要启动一个shell,即每一个popen()调用将启动两个进程,从效率和资源的角度看,popen()函数的调用比正常方式要慢一些。
另外popen()函数还有一个缺点,每次打开的一个进程只能创建一个读或者写的管道,也就是说我们不能实现与子进程的完全全双工交互,实现这个功能我们就需要使用pipe()函数。
pipe()函数 pipe()函数是一个底层的调用,与popen()函数不同的是,它在两个进程之间传递数据不需要启动一个shell来解释请求命令,同时它还提供对读写数据的更多的控制。
pipe()函数的原型如下:
1 2 #include <unistd.h> int pipe (int file_descriptor[2 ]) ;
可以看到pipe()函数的定义非常特别,该函数在数组中包含两个新的文件描述符后返回0,如果返回返回-1,并设置errno()来说明失败原因。数组中的两个文件描述符以一种特殊的方式连接起来,数据基于先进先出的原则,写到file_descriptor[1]的所有数据都可以从file_descriptor[0]读回来。由于数据基于先进先出的原则,所以读取的数据和写入的数据是一致的。
我们可以使用pipe()函数实现匿名管道通信。
匿名管道 一个简单的匿名管道实现父子进程通信的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> int main (int argc, char *argv[]) { if (argc < 3 ) { fprintf (stderr , "usage: %s parent_sendmsg child_sendmsg\n" , argv[0 ]); exit (EXIT_FAILURE); } int pipes[2 ]; if (pipe(pipes) < 0 ) { perror("pipe" ); exit (EXIT_FAILURE); } pid_t pid = fork(); if ( pid < 0 ) { perror("fork" ); exit (EXIT_FAILURE); } else if ( pid > 0 ) { char buf[BUFSIZ + 1 ]; int nbuf; strcpy (buf, argv[1 ]); write(pipes[1 ], buf, strlen (buf)); sleep(1 ); nbuf = read(pipes[0 ], buf, BUFSIZ); buf[nbuf] = 0 ; printf ("parent_proc(%d) recv_from_child: %s\n" , getpid(), buf); close(pipes[0 ]); close(pipes[1 ]); } else if ( pid == 0 ) { char buf[BUFSIZ + 1 ]; int nbuf = read(pipes[0 ], buf, BUFSIZ); buf[nbuf] = 0 ; printf ("child_proc(%d) recv_from_parent: %s\n" , getpid(), buf); strcpy (buf, argv[2 ]); write(pipes[1 ], buf, strlen (buf)); close(pipes[0 ]); close(pipes[1 ]); } return 0 ; }
实际中为了实现双向通信,应该准备两根管道,一根负责从父进程往子进程写数据(同时子进程从这里读取数据),一根负责从子进程往父进程写数据(父进程也从这里读数据)。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #define READ 0 #define WRITE 1 int doubleInteract (const char * cmdstring) { const char *sendStr = "y\n" ; int pipeR[2 ]; int pipeW[2 ]; pid_t pid; char buf[1024 ]; int len = sizeof (buf); memset (buf, 0 , len); pipe(pipeR); pipe(pipeW); if ((pid = fork()) < 0 ) return -1 ; if (pid > 0 ) { close(pipeW[READ]); close(pipeR[WRITE]); read(pipeR[READ], buf, len); printf ("child:%s" , buf); write(pipeW[WRITE], sendStr, strlen (sendStr)+1 ); printf ("parent:%s" , sendStr); memset (buf,0 ,len); read(pipeR[READ], buf, len); printf ("child:%s" , buf); close(pipeW[WRITE]); close(pipeR[READ]); waitpid(pid, NULL , 0 ); } else { close(pipeW[WRITE]); close(pipeR[READ]); if (pipeW[READ] != STDOUT_FILENO) { if (dup2(pipeW[READ], STDIN_FILENO) != STDIN_FILENO) { return -1 ; } close(pipeW[READ]); } if (pipeR[WRITE] != STDOUT_FILENO) { if (dup2(pipeR[WRITE], STDOUT_FILENO) != STDOUT_FILENO) { return -1 ; } close(pipeR[WRITE]); } execl("/bin/sh" , "sh" , "-c" , cmdstring, (char *)0 ); exit (127 ); } return 0 ; } int main () { doubleInteract("/readflag" ); return 0 ; }
一个简化版的readflag,需要向程序中输入y才能读取到flag,这里可以采用匿名管道的方式,创建两个管道,一个管道读,一个管道写,实现父子进程的双向交互,从而成功获取flag。
命名管道 命名管道的定义 命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。
由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用。
创建命名管道 我们可以使用两下函数之一来创建一个命名管道,他们的原型如下:
1 2 3 4 5 6 #include <sys/types.h> #include <sys/stat.h> int mkfifo (const char *filename, mode_t mode) ;int mknod (const char *filename, mode_t mode | S_IFIFO, (dev_t )0 ) ;
这两个函数都能创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,filename指定了文件名,而mode则指定了文件的读写权限。
注:mknod是比较老的函数,而使用mkfifo函数更加简单和规范,所以在可能的情况下,尽量使用mkfifo而不是mknod。
举个例子,通过命名管道完成文件数据的发送和收取。
write_data.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <limits.h> #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <string.h> int main () { const char *fifo_name = "/tmp/tmp_fifo" ; int pipe_fd = -1 ; int data_fd = -1 ; int res = 0 ; const int open_mode = O_WRONLY; int bytes_sent = 0 ; char buffer[PIPE_BUF + 1 ]; if (access(fifo_name, F_OK) == -1 ) { res = mkfifo(fifo_name, 0777 ); if (res != 0 ) { fprintf (stderr , "Could not create fifo %s\n" , fifo_name); exit (EXIT_FAILURE); } } printf ("Process %d opening FIFO O_WRONLY\n" , getpid()); pipe_fd = open(fifo_name, open_mode); data_fd = open("/a.txt" , O_RDONLY); printf ("Process %d result %d\n" , getpid(), pipe_fd); if (pipe_fd != -1 ) { int bytes_read = 0 ; bytes_read = read(data_fd, buffer, PIPE_BUF); buffer[bytes_read] = '\0' ; while (bytes_read > 0 ) { res = write(pipe_fd, buffer, bytes_read); if (res == -1 ) { fprintf (stderr , "Write error on pipe\n" ); exit (EXIT_FAILURE); } bytes_sent += res; bytes_read = read(data_fd, buffer, PIPE_BUF); buffer[bytes_read] = '\0' ; } close(pipe_fd); close(data_fd); } else { exit (EXIT_FAILURE); } printf ("Process %d finished\n" , getpid()); exit (EXIT_SUCCESS); }
read_data.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <limits.h> #include <string.h> int main () { const char *fifo_name = "/tmp/my_fifo" ; int pipe_fd = -1 ; int data_fd = -1 ; int res = 0 ; int open_mode = O_RDONLY; char buffer[PIPE_BUF + 1 ]; int bytes_read = 0 ; int bytes_write = 0 ; memset (buffer, '\0' , sizeof (buffer)); printf ("Process %d opening FIFO O_RDONLY\n" , getpid()); pipe_fd = open(fifo_name, open_mode); data_fd = open("/recvfromfifo.txt" , O_WRONLY | O_CREAT, 0644 ); printf ("Process %d result %d\n" , getpid(), pipe_fd); if (pipe_fd != -1 ) { do { res = read(pipe_fd, buffer, PIPE_BUF); bytes_write = write(data_fd, buffer, res); bytes_read += res; } while (res > 0 ); close(pipe_fd); close(data_fd); } else { exit (EXIT_FAILURE); } printf ("Process %d finished, %d bytes read\n" , getpid(), bytes_read); exit (EXIT_SUCCESS); }
命名管道相比于匿名管道的优点是可以实现两个完全不相干的进程之间的通信,只需要同时去访问一个管道即可,无需是父子进程关系。
Unix Domain Socket socket原本是为了网络通讯设计的,但是后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。 虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:
不需要经过网络协议栈;
不需要打包拆包;
不需要计算校验和;
不需要维护序号和应答;
这是因为IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。
UNIX Domain Socket也提供面向流和面向数据报两种API接口,类似TCP和UDP,但是面向数据报的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。
使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_STREAM或SOCK_DGRAM,protocol参数仍然指定为0即可。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un
表示。
网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已经存在,则bind()错误返回。
server.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <ctype.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <netdb.h> #include <fcntl.h> #include <sys/ioctl.h> #include <signal.h> #include <sys/wait.h> #define SOCK_PATH "/run/echo.sock" #define BUF_SIZE 1024 int listenfd;void handle_signal (int signo) ;int main (void ) { signal(SIGINT, handle_signal); signal(SIGHUP, handle_signal); signal(SIGTERM, handle_signal); if ((listenfd = socket(AF_UNIX, SOCK_STREAM, 0 )) < 0 ){ perror("socket" ); exit (EXIT_FAILURE); } struct sockaddr_un servaddr ; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sun_family = AF_UNIX; strcpy (servaddr.sun_path, SOCK_PATH); unlink(SOCK_PATH); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof (servaddr)) < 0 ){ perror("bind" ); exit (EXIT_FAILURE); } chmod(SOCK_PATH, 00640 ); if (listen(listenfd, SOMAXCONN) < 0 ){ perror("listen" ); exit (EXIT_FAILURE); } int connfd, nbuf; char buf[BUF_SIZE + 1 ]; for (;;){ if ((connfd = accept(listenfd, NULL , NULL )) < 0 ){ perror("accept" ); continue ; } nbuf = recv(connfd, buf, BUF_SIZE, 0 ); buf[nbuf] = 0 ; printf ("new msg: \"%s\"\n" , buf); send(connfd, buf, nbuf, 0 ); close(connfd); } return 0 ; } void handle_signal (int signo) { if (signo == SIGINT){ fprintf (stderr , "received signal: SIGINT(%d)\n" , signo); }else if (signo == SIGHUP){ fprintf (stderr , "received signal: SIGHUP(%d)\n" , signo); }else if (signo == SIGTERM){ fprintf (stderr , "received signal: SIGTERM(%d)\n" , signo); } close(listenfd); unlink(SOCK_PATH); exit (EXIT_SUCCESS); }
client.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <ctype.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <netdb.h> #include <fcntl.h> #include <sys/ioctl.h> #include <signal.h> #include <sys/wait.h> #define SOCK_PATH "/run/echo.sock" #define BUF_SIZE 1024 int main (int argc, char *argv[]) { if (argc < 2 ){ fprintf (stderr , "usage: %s msg\n" , argv[0 ]); exit (EXIT_FAILURE); } int sockfd; if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0 )) < 0 ){ perror("socket" ); exit (EXIT_FAILURE); } struct sockaddr_un servaddr ; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sun_family = AF_UNIX; strcpy (servaddr.sun_path, SOCK_PATH); if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof (servaddr)) < 0 ){ perror("connect" ); exit (EXIT_FAILURE); } char buf[BUF_SIZE + 1 ]; int nbuf; nbuf = strlen (argv[1 ]); send(sockfd, argv[1 ], nbuf, 0 ); nbuf = recv(sockfd, buf, BUF_SIZE, 0 ); buf[nbuf] = 0 ; printf ("echo msg: \"%s\"\n" , buf); close(sockfd); return 0 ; }
上述程序实现了通过unix domain socket的client-server 数据传输,就像是通过/var/echo.sock这个文件传输数据。有点类似于命名管道,但是Unix Domain Socket的使用更加广泛。例如wsl2与windows主机的通信,nginx与django进程的通信都是采用unix domain socket来完成。
原题目采用IPC的思路完成最终payload 由于c语言实现两个进程的多次连续交互存在一些复杂性,这里采用封装更完善,效率更高且更加安全的go语言完成payload编写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainimport ( "fmt" "os/exec" "strconv" ) func main () { cmd_getflag := exec.Command("/readflag" ) getflag_stdin_pipe, getflag_stdin_pipe_error := cmd_getflag.StdinPipe() getflag_stdout_pipe, getflag_stdout_pipe_error := cmd_getflag.StdoutPipe() if getflag_stdin_pipe_error != nil { fmt.Println("Open getflag stdin pipe error:" , getflag_stdin_pipe_error) return } if getflag_stdout_pipe_error != nil { fmt.Println("Open getflag stdout pipe error:" , getflag_stdout_pipe_error) return } if getflag_start_error := cmd_getflag.Start(); getflag_start_error != nil { fmt.Println("Run getflag error:" , getflag_start_error) return } buf := make ([]byte , 300 ) n, _ := getflag_stdout_pipe.Read(buf) fmt.Println(string (buf[:n])) getflag_stdin_pipe.Write([]byte ("y\n" )) n, _ = getflag_stdout_pipe.Read(buf) fmt.Println(string (buf[:n])) getflag_stdin_pipe.Write([]byte ("2\n" )) n, _ = getflag_stdout_pipe.Read(buf) fmt.Println(string (buf[n-16 : n])) buf1 := string (buf[n-16 : n-9 ]) buf2 := string (buf[n-8 : n-1 ]) a1, _ := strconv.Atoi(buf1) a2, _ := strconv.Atoi(buf2) res := strconv.Itoa(a1 + a2) getflag_stdin_pipe.Write([]byte (res + "\n" )) n, _ = getflag_stdout_pipe.Read(buf) fmt.Println(string (buf[:n])) return }
这样我们就可以不依赖于特定语言环境完成题目要求。