C语言实现为终端程序--webshell基石

    技术2022-07-10  126

    之前对ssh一直很困惑它是如何实现的,网上也没有相关代码实例,所以自己花了一段时间研究了一下。本篇博客主要写了两个程序:服务端和客户端,通过客户端可以远程登录服务端,执行shell命令。代码实现的比较糙,但是基本原理一看就明白。

    一、主要核心思想:

    1)创建pty虚拟终端,即open("/dev/ptmx", O_RDWR | O_NOCTTY),关于pty的介绍网上有很多,这里简单说明一下pty类似我们管道,但是pty是全双工的。pty有master、slave,两者之间可以进行通信。当我open的时候返回的是master文件描述,那么slave如何打开呢?调用ptsname(master-fd),返回文件路径,然后在open即可。注意:ptsname入参一定是master文件描述

    2)服务端需要fork一个子进程,然后用exec家族函数进行替换/bin/sh。在替换之前需要通过dup2系统调用,重定向标准输入、标注输出、标准错误输出,这里需要注意,这里文件描述符一定slave fd,例如:

    /* Duplicate pty slave to be child's stdin, stdout, and stderr */ dup2(slave, STDIN_FILENO); dup2(slave, STDOUT_FILENO); dup2(slave, STDERR_FILENO);

    3)当然在exec家族函数执行之前我们需要可以进行一些属性设置,例如:关闭回显、设置窗口大小等详细可以参考代码

    二、编译并运行

    [root@localhost epoll-pipe]# gcc Server.c -o ShellServer -g -lpthread [root@localhost epoll-pipe]# [root@localhost epoll-pipe]# ./ShellServer [root@localhost epoll-pipe]# gcc -g -o client Client.c -lpthread [root@localhost epoll-pipe]# [root@localhost epoll-pipe]# ./client 127.0.0.1 sh-4.2# ifconfig enp0s31f6: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 ether d4:81:d7:c6:4a:d9 txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 device interrupt 16 memory 0xef200000-ef220000

    三、代码

    服务端代码:

    #define _XOPEN_SOURCE #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <strings.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #include <termios.h> #include <sys/ioctl.h> #include <fcntl.h> #include <pthread.h> #define SERVPORT 9527 #define MAXBUF 10240 #define MAXFDS 5000 #define EVENTSIZE 100 int setnonblocking(int fd) { int opts; if( (opts = fcntl(fd, F_GETFL, 0)) == -1) { perror("fcntl"); return -1; } opts = opts | O_NONBLOCK; if( (opts = fcntl(fd, F_SETFL, opts)) == -1) { perror("fcntl"); return -1; } return 0; } int setcloexec(int fd) { int opts; if( (opts = fcntl(fd, F_GETFL, 0)) == -1) { perror("fcntl"); return -1; } opts = opts | FD_CLOEXEC; if( (opts = fcntl(fd, F_SETFL, opts)) == -1) { perror("fcntl"); return -1; } return 0; } void* handle_child_output(void* data) { char buf[MAXBUF] = {0}; int sockfd = (*(uint64_t*)data) & 0xFFFFFFF; int master = (int)((*(uint64_t*)data) >> 32); while(1) { int nread = read(master, buf, MAXBUF); send(sockfd, buf, nread, 0); } } int create_pty(char *slavename, size_t length) { int master; char *p; /* Open pty master */ master = open("/dev/ptmx", O_RDWR | O_NOCTTY); if (master == -1) return -1; /* Grant access to slave pty */ if (grantpt(master) == -1) { close(master); return -1; } /* Unlock slave pty */ if (unlockpt(master) == -1) { close(master); return -1; } /* Get slave pty name. */ p = ptsname(master); if (p == NULL) { close(master); return -1; } if (strlen(p) < length) { strncpy(slavename, p, length); } else {/* Return an error if buffer too small */ close(master); return -1; } return master; } void child_routine(int sockfd, const char* slavename) { /* Start a new session */ if (setsid() == -1) { exit(0); } /* Becomes controlling tty */ int slave = open(slavename, O_RDWR); if (slave == -1) { exit(0); } /* disable ECHO attribute */ struct termios termios; if (tcgetattr(slave, &termios) == -1) exit(0); termios.c_lflag &= ~(ECHO); if (tcsetattr(slave, TCSANOW, &termios) == -1) { exit(0); } #if 0 struct winsize old_wins; if (ioctl(slave, TIOCSWINSZ, &old_wins) == -1) { exit(0); } #endif //根据客户窗口代码进行设置,以便输出结果能够优雅 struct winsize size; recv(sockfd, &size, sizeof(struct winsize), 0); ioctl(slave, TIOCSWINSZ, &size); close(sockfd); /* Duplicate pty slave to be child's stdin, stdout, and stderr */ dup2(slave, STDIN_FILENO); dup2(slave, STDOUT_FILENO); dup2(slave, STDERR_FILENO); if (slave > STDERR_FILENO) /* Safety check */ close(slave); /* No longer need this fd */ char *args[] = {"/bin/sh", "-i", NULL}; execv("/bin/sh", args); return; } void* service_routine(void* data) { int sockfd = *(int*)data; char slavename[256] = {0}; int pty_master = create_pty(slavename, 256);//创建pty虚拟终端 int pid = vfork(); if (pid < 0) { exit(1); } else if (pid == 0) { // pty master fd is not useful in child-process. close(pty_master); child_routine(sockfd, slavename); } else { char buf[MAXBUF] = {0}; pthread_t thread_id; uint64_t data = pty_master; data = data << 32 | sockfd; pthread_create(&thread_id, NULL, handle_child_output, &data); while (1) { int bytes = recv(sockfd, buf, MAXBUF, 0); printf("Recv from client: bytes = %d, errno=%d\n", bytes, errno); if (bytes > 0) { write(pty_master, buf, bytes); } } } } int main(void) { char buf[MAXBUF]; int len, n; struct sockaddr_in servaddr; int sockfd, listenfd, epollfd, nfds; struct epoll_event ev; struct epoll_event events[EVENTSIZE]; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERVPORT); if( (epollfd = epoll_create(MAXFDS)) == -1) { perror("epoll"); exit(1); } if(setcloexec(epollfd) == -1){ perror("setcloexec"); exit(1); } if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } if(setcloexec(listenfd) == -1){ perror("setcloexec"); exit(1); } if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { perror("bind"); exit(1); } if(listen(listenfd, 10) == -1) { perror("listen"); exit(1); } // listen fd只注册EPOLLIN事件, EPOLLOUT不需要注册 ev.events = EPOLLIN | EPOLLET; ev.data.fd = listenfd; if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) { perror("epoll_ctl"); exit(1); } for( ; ; ) { if( (nfds = epoll_wait(epollfd, events, EVENTSIZE, -1)) == -1) { perror("epoll_wait"); exit(1); } for(n = 0; n < nfds; n++) { if(events[n].data.fd == listenfd) { while( (sockfd = accept(listenfd, (struct sockaddr *)NULL, NULL)) > 0) { //创建服务线程 pthread_t thread_id; pthread_create(&thread_id, NULL, service_routine, &sockfd); } continue; } printf("Events = 0x%x\n", events[n].events); if (events[n].events & (EPOLLIN | EPOLLOUT) == (EPOLLIN | EPOLLOUT)) { printf(">> EPOLLIN And EPOLLOUT event, socketfd = %d\n", events[n].data.fd); } else if (events[n].events & EPOLLIN) { printf(">> Only EPOLLIN event, socketfd = %d\n", events[n].data.fd); } else if (events[n].events & EPOLLOUT) { printf(">> Only EPOLLOUT, socketfd = %d\n", events[n].data.fd); } } } }

    客户端代码:

    #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <strings.h> #include <errno.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/socket.h> #include <termios.h> #include <sys/ioctl.h> #include <fcntl.h> #include <pthread.h> #define SERVPORT 9527 #define MAXBUF 10240 int setnonblocking(int fd) { int opts; if( (opts = fcntl(fd, F_GETFL, 0)) == -1) { perror("fcntl"); return -1; } opts = opts | O_NONBLOCK; if( (opts = fcntl(fd, F_SETFL, opts)) == -1) { perror("fcntl"); return -1; } return 0; } void* handle_input(void* d) { int fd = *(int*)d; char buf[MAXBUF]; while(1) { //read cmd from TERMIAL and send it int nbytes = read(STDIN_FILENO, buf, MAXBUF); buf[nbytes] = '\0'; //send send(fd, buf, nbytes, 0); buf[0] = '\0'; } return NULL; } int main(int argc, char* argv[]) { char buf[MAXBUF]; struct sockaddr_in servaddr; int fd; int n, len; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; if (argc > 1) { servaddr.sin_addr.s_addr = inet_addr(argv[1]); } else { servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); } servaddr.sin_port = htons(SERVPORT); if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } if (connect(fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { perror("connect"); exit(1); } pthread_t thread_id; pthread_create(&thread_id, NULL, handle_input, &fd); int bytes = 0; struct winsize size; ioctl(STDIN_FILENO, TIOCGWINSZ, &size); bytes = send(fd, &size, sizeof(struct winsize), 0); while(1) { //resv response bytes = recv(fd, buf, MAXBUF, 0); write(STDOUT_FILENO, buf, bytes); } close(fd);// 会出发server端 EPOLLIN和EPOLLOUT return 0; }

    四、优化

    此代码写的比较糙,需要做一些优化,例如:采用多路复用技术减少线程数,支持退格键、tab键、方向键等。这里启动抛砖引玉,大家一起学习。

    Processed: 0.015, SQL: 9