标题: 远程SHELL中进程因TCP连接中断而失去控制的预防及救急方案
创建: 2019-02-15 17:50 更新: 2023-04-12 15:15 链接: https://scz.617.cn/unix/201902151750.txt
目录:
☆ 原始问题
1) 模拟场景
2) vi本身的swap机制
☆ 预防措施
1) screen
2) tmux
3) screen vs tmux
☆ 救急方案
1) cryopid
2) screenify(可用)
3) retty
4) injcode
5) neercs
6) reptyr(推荐)
6.1) reptyr(1)
6.2) reptyr原理简介
6.3) "reptyr -T"原理简介
☆ 深入理解伪终端机制
1) pty(7)/tty(4)
2) termios(3)
3) stty(1)
4) ioctl(2)/ioctl_tty(2)
5) Sessions and Process Groups
6) Job control
7) 信号与任务控制示例
8) setsid(1)
9) jobs/disown/nohup
☆ 将nc得到的简易shell升级成全功能交互式TTY
1) Python pty module
2) socat(full)
3) nc+python+stty(full)
4) script
5) reptyr
6) Windows的挣扎
☆ 结束语
☆ 原始问题
在一个SSH会话里执行vi,后因TCP连接中断而失去控制。重新登录后发现原SSH会话 对应的伪终端还在,其中的vi进程也在。有什么办法重新获取对vi的控制?
这种情况一般是单向TCP故障所致,即服务端没有收到FIN或RST,客户端单方面中止 了TCP连接,现实中并不罕见。
1) 模拟场景
设计一个实验确保精确复现这种情况。
服务端是位于Guest中的Linux,客户端是Host中的SecureCRT。多登录几个SSH会话, 其中一个SSH会话中执行"vi some.txt"。在VMware中断开虚拟网卡,在Host中用 Tcpview切断vi进程所在SSH会话对应的TCP连接,由于虚拟网卡已断开,Guest中的 SSH会话不会收到RST或FIN,而Host中的SecureCRT会收到。在其他SSH会话中用 netstat、pstree、ps等工具确认目标SSH会话及vi进程仍在。
$ netstat -ntp | grep :22 tcp 0 52 x.x.x.x:22 y.y.y.y:1999 ESTABLISHED 1185/sshd: root@pts tcp 0 0 x.x.x.x:22 y.y.y.y:2069 ESTABLISHED 2244/sshd: scz [pri tcp 0 0 x.x.x.x:22 y.y.y.y:2070 ESTABLISHED 2342/sshd: scz [pri
$ pstree -npu -al 2244
sshd,2244
-sshd,2263,scz
-bash,2264
`-vi,2341 some.txt
$ pstree -H pidof -s vi
-npu
systemd(1)-+-systemd-journal(223)
...
|-sshd(651)-+-sshd(1185)-+-bash(1204)
| | -bash(2271)---pstree(2401)
| |-sshd(2244)---sshd(2263,scz)---bash(2264)---vi(2341)
|
-sshd(2342)---sshd(2349,scz)---bash(2350)
...
[scz@ /tmp]> echo $$ 2350 [scz@ /tmp]> ps -f -o pid,user,args PID USER COMMAND 2350 scz -bash 2474 scz _ ps -f -o pid,user,args 2264 scz -bash 2341 scz _ vi some.txt
2) vi本身的swap机制
[scz@ /tmp]> ls -l .some.txt.swp -rw------- 1 scz scz 12288 Feb 14 11:50 .some.txt.swp
如果启动vi时没有指定-n,缺省有swap文件用于crash后的恢复。
[scz@ /tmp]> vi -r some.txt
它会自动从.some.txt.swp中恢复内容到some.txt,之后可以删除swap文件。
[scz@ /tmp]> rm .some.txt.swp
此处不考虑vi本身的这种恢复机制,考虑更普遍情形。
☆ 预防措施
1) screen
以前跑oclHashcat-plus时我就碰上过客户端单方面中止TCP连接的事,当 时周大给我推荐了screen。
$ aptitude install screen
简单演示一下screen:
[scz@ /tmp]> tty /dev/pts/4 [scz@ /tmp]> screen -S screen.scz.pts_4 [scz@ /tmp]> tty /dev/pts/5
[scz@ /tmp]> vi pts_4.txt
Ctrl-A D (具体操作是,按完Ctrl-A,保持Ctrl,松开A,再按D)
[detached from 2647.screen.scz.pts_4]
在另一个伪终端里恢复对vi的控制:
[scz@ /tmp]> tty /dev/pts/6 [scz@ /tmp]> screen -r screen.scz.pts_4
可以简单地"screen -r"、"screen -x"。看到vi界面,退出vi后检查当前伪终端:
[scz@ /tmp]> tty /dev/pts/5
退出screen状态,可以exit,也可Ctrl-D。
[screen is terminating]
2) tmux
$ aptitude install tmux
简单演示一下tmux:
[scz@ /tmp]> tty /dev/pts/4 [scz@ /tmp]> tmux [scz@ /tmp]> tty /dev/pts/7
[scz@ /tmp]> vi pts_4.txt
在另一个伪终端里夺取对vi的控制:
[scz@ /tmp]> tty /dev/pts/6 [scz@ /tmp]> tmux ls 0: 1 windows (created Thu Feb 14 13:47:38 2019) [132x57] (attached) [scz@ /tmp]> tmux attach -t 0
看到vi界面,退出vi后检查当前伪终端:
[scz@ /tmp]> tty /dev/pts/7
后果与screen类似。
[scz@ /tmp]> tmux detach [detached (from session 0)] [scz@ /tmp]> tty /dev/pts/6
退出tmux状态,可以exit,也可Ctrl-D。假设在tmux状态的vi中,想保持vi的情况下 暂时离开tmux状态,先按Ctrl-B,全松开后按D。
[exited]
3) screen vs tmux
这是二者的比较:
http://www.wikivs.com/wiki/screen_vs_tmux
其他参考
《普通用户无法进入screen状态的排查》 https://scz.617.cn/unix/202204111632.txt
☆ 救急方案
screen、tmux要求提前考虑到风险,它们都是预防措施,非原始问题的答案。原始问 题发生时,显然没有提前进入screen、tmux状态,还有救吗?
1) cryopid
A: Bernard Blackham 2004
https://github.com/maaziz/cryopid https://github.com/maaziz/cryopid.git
CryoPID允许捕捉正在运行中的进程状态并将之保存到文件中,将来利用该文件恢复 进程状态,甚至可以在系统重启后或迁移至另一台主机时生效。简单理解成进程级快 照,不过很怀疑它的实用性,而且没能编译成功。
[scz@ /tmp]> git clone https://github.com/maaziz/cryopid.git [scz@ /tmp/cryopid/src]> make
2) screenify(可用)
A: Timo Lindfors 2004
http://tomaw.net/tmp/screenify
脚本编写年代过早,对于较新版本GDB,需要做点小修改,下面是我改过的:
!/bin/sh
Copyright Timo Lindfors 2004
function usage() { echo usage: $0 pid exit 1 } TCGETS=0x5401 TCSETS=0x5402 SIZEOF_STRUCT_TERMIOS=60 O_RDWR=2 ((FLAGS=O_RDWR))
PID=$1
if [ xwhich gdb
== x ]; then
echo gdb not found in PATH. Please apt-get install gdb
exit
fi
if [ x$PID == x ]; then
usage;
fi
if [ x$2 != x ]; then
usage;
fi
MYPID=$$
MYFD0=readlink /proc/$MYPID/fd/0
MYFD1=readlink /proc/$MYPID/fd/1
MYFD2=readlink /proc/$MYPID/fd/2
EXE=readlink /proc/$PID/exe
if [ x$EXE == x ]; then
echo $0: $PID: no such pid
exit 1
fi
BATCHFILE=mktemp -p /tmp "gdb.$$_${RANDOM}_XXXXXXXXXX"
cat >$BATCHFILE <
cat <<EOF Process $PID should now be talking to this pty. Refresh the screen (e.g. ESC CTRL+L) and have fun! EOF
exec tail -f --pid=$PID /proc/$PID/stat
GDB调试目标进程;用TCGETS取stdin、stdout、stderr的终端属性,这个应该就是 tcgetattr();关闭原来的0/1/2号句柄;分三次打开当前SHELL所在伪终端,正常情 况下它们的句柄依次是0/1/2;用TCSETS重设stdin、stdout、stderr的终端属性,这 个应该就是tcsetattr();至此,目标进程的stdin、stdout、stderr转移到当前 SHELL所在伪终端。
参看ioctl_tty(2)。
脚本中的三次open()是在目标进程空间中执行的,受制于目标进程的权限,如果当前 SHELL与目标进程均属于同一用户,不存在问题。假设目标进程属于scz,当前SHELL 属于root,open()会失败(返回-1),这是个小坑。
这种只依赖GDB的技术方案有一些限制,参看:
《2.12 在GDB中调用被调试进程空间中的函数》 《2.19 在GDB里如何搜索内存》 《2.31 GDB中如何调用指定动态链接库中的导出函数》 《2.62 GDB条件断点中进行字符串比较》
[scz@ /tmp]> tty /dev/pts/3 [scz@ /tmp]> vi some.txt
随便编辑点内容,然后切到另一个伪终端:
[scz@ /tmp]> tty /dev/pts/4 [scz@ /tmp]> ps -f -o pid,user,args PID USER COMMAND 3599 scz -bash 3608 scz _ ps -f -o pid,user,args 3590 scz -bash 3606 scz _ vi some.txt
夺取对vi的控制:
[scz@ /tmp]> ./screenify 3606
按ESC之后再Ctrl-L刷新屏幕,看到vi界面,可以正常操作并保存退出。
此时/dev/pts/3上的bash已经无法工作,但vi正常退出,之前编辑的内容得以保存。
3) retty
A: Petr Baudis, Jan Sembera 2006
retty attach processes running on other terminals http://pasky.or.cz//dev/retty/ http://pasky.or.cz//dev/retty/retty-1.0.tar.bz2
retty本质上同screenify,我未实测。
4) injcode
A: Thomas Habets 2009-03-21
Moving a process to another terminal https://blog.habets.se/2009/03/Moving-a-process-to-another-terminal.html http://github.com/ThomasHabets/injcode git clone git://github.com/ThomasHabets/injcode.git
2009年之后再未更新。
5) neercs
A: Sam Hocevar, Jean-Yves Lamoureux, Pascal Terjan
http://caca.zoy.org/wiki/neercs http://caca.zoy.org/wiki/neercs?format=txt http://caca.zoy.org/wiki/neercs/devel git://git.zoy.org/neercs.git http://caca.zoy.org/git/neercs.git
neercs比较复杂,我未实测。
6) reptyr(推荐)
A: Nelson Elhage 2011
reptyr: Attach a running process to a new terminal - [2011-01-21] https://blog.nelhage.com/2011/01/reptyr-attach-a-running-process-to-a-new-terminal/ http://github.com/nelhage/reptyr https://github.com/nelhage/reptyr.git
reptyr: Changing a process's controlling terminal - [2011-02-08] https://blog.nelhage.com/2011/02/changing-ctty/ (介绍reptyr修改目标进程控制终端的技术原理)
New reptyr feature: TTY-stealing - [2014-08-20] https://blog.nelhage.com/2014/08/new-reptyr-feature-tty-stealing/
Reptyr Move A Running Process From One Terminal To Another Without Closing It - [2017-02-26] https://www.ostechnix.com/reptyr-move-running-process-new-terminal/
对less使用screenify,less仍从旧终端读取输入。对ncurses程序使用screenify, 无法调整窗口大小。对程序使用screenify,新终端上Ctrl-C无效。reptyr解决了这 些问题。
reptry使用ptrace(2)调试目标进程并在目标进程空间中利用一些Hacking技术执行由 reptry注入的代码,高度依赖系统调用的细节(考虑shellcode的情形)。platform子 目录下有freebsd、linux两种OS。如想移植到其他OS,理论上可行,但需要很多底层 知识。
reptyr可以在i386、x86_64、ARM上运行。
常见使用方式:
a) reconnect ssh
b) screen
c) ps -a | grep
Debian有这个包,说明reptyr已成为业界通用工具。万一发行版不带reptyr,就自己 编译源码吧。
$ aptitude install reptyr
6.1) reptyr(1)
NAME
reptyr - 给正在运行中的目标进程更换控制终端(CTTY)
SYNOPSIS
reptyr PID
reptyr -l|-L [COMMAND [ARGS]]
OPTIONS
-T
reptyr不是用ptrace(2)调试目标进程,而是试图找出目标进程对应的terminal
emulator并劫持mater pty。这种模式更可靠,适用性更强。此时可以更改目标
进程所在session的所有进程的CTTY。缺点是,除非以root身份执行reptyr,否
则不能用于sshd(8)的子进程。
-l, -L [COMMAND [ARGS]]
此时没有目标进程。这种模式将创建新的pty对,在新master pty与当前终端之
间进行数据转发,显示新slave pty的名字(/dev/pts/N),新slave pty没有进程
与之关联。假设正用gdb调试某进程,新slave pty可做为"set inferior-tty"的
参数,这比被调试进程直接使用当前终端要好。
(gdb) help set inferior-tty
Set terminal for future runs of program being debugged.
Usage: set inferior-tty [TTY]
If TTY is omitted, the default behavior of using the same terminal as
GDB is restored.
如果指定了COMMAND、ARGS,将做为reptyr的子进程运行,其进程空间环境变量
REPTYR_PTY指向新slave pty。
-L相比-l,前者会将子进程的0、1、2号fd指向新slave pty,子进程会在一个新
session中运行,其CTTY对应新slave pty。
Python有os.openpty()、pty.openpty()、pty.spawn()可用。
-s
缺省情况下,reptyr只会让目标进程中确实与CTTY相关联的fd指向新终端。指定
-s后,reptyr死活将目标进程中的0、1、2号fd指向新终端,即使目标进程本来
没有CTTY。
一般情况下用不着-s,用reptyr时,目标进程很大可能是交互式进程。
-v
显示版本
-h
显示帮助
-V
输出冗余调试信息
NOTES
reptyr使用ptrace(2)调试目标进程。在Ubuntu Maverick及更高版本上,出于安
全考虑缺省禁止这种行为。可以临时解禁:
# echo 0 > /proc/sys/kernel/yama/ptrace_scope
也可以编辑/etc/sysctl.d/10-ptrace.conf永久解禁。
BUGS
如果目标进程的屏幕未能重绘,按Ctrl-L
假设目标进程对stdin使用epoll(),reptyr并未更新epoll()所用数据,epoll()
仍将访问原来的stdin。使用select()、poll()的目标进程存在类似问题。
reptyr并非劫持、抢夺伪终端的完美解决方案,如非应急救援,尽量少用。
6.2) reptyr原理简介
reptyr用ptrace(2)调试目标进程(vi),利用一些Hacking技术在vi进程空间里执行由 reptyr提供的代码,比如打开新的伪终端,利用dup(2)使之变成vi进程的stdout、 stderr。
相比screenify,reptyr更改了vi进程的控制终端(CTTY),于是支持对目标进程 Ctrl-C、Ctrl-Z。
同一session中的所有进程共用同一个CTTY。
参看ioctl_tty(2)
TIOCSCTTY int arg
修改主调进程的CTTY。主调进程必须是session leader,同时不能已经拥有CTTY。
此时这样调用:
ioctl( slave_pty_fd, TIOCSCTTY, 0 )
如果slave_pty_fd已经是某个session的CTTY,ioctl()失败(EPERM),除非主调
进程拥有CAP_SYS_ADMIN权限且arg等于1,此时会抢夺CTTY,原session中所有进
程将失去slave_pty_fd对应的CTTY。此时这样调用:
ioctl( slave_pty_fd, TIOCSCTTY, 1 )
此处我有个疑问。参reptyr.c、attach.c,slave_pty_fd经open("/dev/ptmx")、 unlockpt()、grantpt()、ptsname()、openat(O_RDWR|O_NOCTTY)而得,但最后一步 是ioctl(slave_pty_fd,TIOCSCTTY,1),为什么第三形参不是0?既然新创建了pty对, 不会出现slave_pty_fd已经是某个session的CTTY的情形,不需要root权限抢夺CTTY, 为什么ioctl()第三形参用1?
从bash中启动vi,bash是session leader,vi是process group leader,该进程组只 包含vi进程。为了在vi中调用ioctl(TIOCSCTTY),须设法让vi成为session leader。
参看setsid(2)
EPERM
主调进程PID等于某个PGID,即主调进程是process group leader时,setsid()
失败。
vi现在是process group leader,无法调用setsid(2)。可以fork(),子进程仍在同一 session、同一进程组,但不是process group leader,该子进程可以setsid()。但 fork()后杀掉父进程的做法有潜在风险,谁知道vi有没有依赖PID的行为。看有无其 他办法更改vi的PGID,使得vi不再是process group leader。
参看setpgid(2)
setpgid( pid, pgid );
bash处理管道符时会用setpgid()将指定进程移入指定进程组。这个操作要求
pgid与pid位于同一session(参看setsid(2)、credentials(7))。
需要在vi所在session中找一个进程组,把vi移入该进程组,使得vi可以调用 setsid()。bash似乎是个候选者,但我们采用更直接的办法,创建一个新进程组。在 vi进程空间中fork(2),同时用ptrace(2)调试子进程。让子进程调用setpgid()创建 新进程组,将父进程移入该新进程组,父进程中的vi可以调用setsid()创建新 session,父进程成为session leader,父进程调用ioctl(TIOCSCTTY)指定新的CTTY。
injcode、neercs、reptyr使用同样的技术更改目标进程的CTTY。
6.3) "reptyr -T"原理简介
"reptyr -T"使用了新技术,劫持目标进程关联的master pty。
不使用-T时,reptyr更改单个目标进程的slave pty。使用-T时,reptyr尝试寻找目 标进程对应的terminal emulator,用ptrace(2)调试后者(而不是目标进程),寻找 master pty fd,利用AF_UNIX、SCM_RIGHTS将master pty fd传递到reptyr进程。 reptyr在terminal emulator进程空间中更改master pty fd,使之指向/dev/null, 最后从terminal emulator detach。
接着reptyr扮演terminal emulator的角色,从前述master pty fd读取output并写到 当前终端,从当前终端读取input并写到前述master pty fd。
假设terminal emulator是sshd(8)的子进程,sshd会调用setuid(2)以匹配登录帐号。 Linux禁止ptrace(2)调试这种调用过setuid(2)的进程,除非以root身份执行reptyr。
☆ 深入理解伪终端机制
单说解决原始问题,不需要深入了解伪终端内部细节,如果充满好奇心,可以继续阅 读如下四个链接:
A Brief Introduction to termios - [2009-12-22] https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios/
A Brief Introduction to termios: termios(3) and stty - [2009-12-30] https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios-termios3-and-stty/
A Brief Introduction to termios: Signaling and Job Control - [2010-01-11] https://blog.nelhage.com/2010/01/a-brief-introduction-to-termios-signaling-and-job-control/
The TTY demystified - [2008-07-25] http://www.linusakesson.net/programming/tty/index.php (介绍TTY的历史及架构变迁,比如PTY如何出现)
简译了对我本人有用的部分内容,可能有错误。
1) pty(7)/tty(4)
-----------input-------------->
+--------+ +--------+ +-------+ +--------+ +--------+ |terminal|=| master |=|termios|=| slave |=|shell or| |emulator| | pty | | | | pty | |other(s)| +--------+ +--------+ +-------+ +--------+ +--------+
<----------output--------------
xterm /dev/ptmx /dev/pts/N bash screen tmux gnome-terminal sshd(ssh) telnetd(telnet)
sshd会拥有指向/dev/ptmx的fd,可以用lsof确认:
$ lsof -lnPR +c0 +f g -o1 -p
与sshd配合的客户端ssh、SecureCRT等负责处理屏显相关的控制序列,比如移动光标。
termios有很大一部分在kernel里。termios负责:
Line buffering
行缓冲
Echo
回显
Line editing
退格删除
Newline translation
\n转\r\n
Signal generation
Ctrl-C SIGINT
Ctrl-Z SIGTSTP
SIGTSTP可以被进程捕捉并处理,SIGSTOP则不能
右侧允许多个进程连接同一个slave pty,不一定是bash。
2) termios(3)
emacs、vi之类的程序使用了curses。可以通过struct termios调整termios的行为。
tcflag_t c_iflag; / input modes / tcflag_t c_oflag; / output modes / tcflag_t c_cflag; / control modes / tcflag_t c_lflag; / local modes / cc_t c_cc[NCCS]; / control chars /
local modes (c_lflag)
ICANON
canonical mode就是line editing mode,与之相反的是cbreak mode(raw mode)
ECHO
回显
ISIG
若未设置,Ctrl-C、Ctrl-Z不会产生信号,而是向右侧传递相应ASCII码
TOSTOP
参后
input and output modes ( c_iflag/c_oflag)
IXON in c_iflag
是否允许流控。缺省启用,master pty收到Ctrl-S后,slave pty不再从右
侧接收任何输出,向slave pty的write()操作将阻塞,直到master pty收到
Ctrl-Q,恢复正常。
IUTF8 in c_iflag
IUTF8告诉termios输入流是UTF-8编码过的,处理退格删除时以单个UTF-8字
符为单位进行删除。
OLCUC in c_oflag
Map[s] lowercase characters to uppercase on output
control chars (c_cc[NCCS])
下列c_cc[i]为0时表示禁用
VINTR
c_cc[VINTR] = 0x3;
Ctrl-C产生SIGINT,要求ISIG置位
VSUSP
Ctrl-Z产生SIGTSTP,要求ISIG置位
VERASE
Ctrl-H或Ctrl-?退格删除
VEOF
Ctrl-D causes the next read call by the slave to return EOF.
VSTOP
Ctrl-S,要求IXON置位
VSTART
Ctrl-Q,要求IXON置位
control modes (c_cflag)
现在都是伪终端,很少需要直接面对物理终端,c_cflag很少用到。假设正在模
拟哑终端,就会涉及c_cflag,比如:
term.c_cflag &= ~CBAUD;
if ( !strcmp( devparam.baud, "50" ) )
{
/*
* 设置波特率
*/
term.c_cflag |= B50;
}
...
else
{
/*
* 无匹配时使用9600
*/
term.c_cflag |= B9600;
}
/*
* 设置数据位
*/
term.c_cflag &= ~CSIZE;
if ( !strcmp( devparam.data, "5" ) )
{
term.c_cflag |= CS5;
}
...
else
{
/*
* 无匹配时使用7位数据位
*/
term.c_cflag |= CS7;
}
/*
* 无奇偶校验
*/
if ( !strcmp( devparam.parity, "none" ) )
{
/*
* 禁止奇偶校验
*/
term.c_cflag &= ~PARENB;
}
/*
* 奇校验
*/
else if ( !strcmp( devparam.parity, "odd" ) )
{
term.c_cflag |= PARENB;
term.c_cflag |= PARODD;
}
/*
* 无匹配时做偶校验
*/
else
{
term.c_cflag |= PARENB;
term.c_cflag &= ~PARODD;
}
term.c_cflag |= HUPCL;
第一次接触这些东西是1998年;当时给湖南省移动局开发移动综合业务系统,正赶上 模拟信号手机向数字信号手机转移的大潮,从长沙去益阳驻场开发;其中涉及curses 编程,与西门子某设备进行串口通信。一晃20多年过去了。
如果你是第一次接触这些东西,并且很有好奇心的话,给你留两个小作业:
a) 如何屏蔽Ctrl-C,有几种办法? b) 如何屏蔽Ctrl-D
3) stty(1)
stty封装了对tcgetattr()、tcsetattr()的调用。
stty -a
以人类可读方式显示struct termios
stty -isig
复位ISIG,此时无法Ctrl-C中止进程。
stty intr ^G
c_cc[VINTR] = 0x7;
Ctlr-G产生SIGINT
stty -ixon stop undef
复位IXON、屏蔽Ctrl-S
c_cc[VSTOP] = 0;
stty -a -F /dev/pts/N
查看指定伪终端
bash有自己的termios设置,从bash中启动其他进程时,bash会将控制终端的termios 设置恢复回去。所以在bash中直接stty与从其他终端stty -F看到的不一致。
4) ioctl(2)/ioctl_tty(2)
tcgetattr( fd, p )
ioctl( fd, TCGETS, p )
tcsetattr( fd, p )
ioctl( fd, TCSETS, p )
5) Sessions and Process Groups
session leader (SID==PID) setsid(2) process group leader (PGID==PID) setpgid(2) process process ... process group leader process process ... ...
[scz@ /tmp]> cat /dev/urandom > /dev/null
cat /proc/$(pidof -s cat)/stat
7559 (cat) R 5487 7559 5487 ...
前6项是
pid (name) state ppid pgid sid
pstree -npu -al 5487
bash,5487,scz └─cat,7559 /dev/urandom
ps -o pid,args,state,ppid,pgid,sid $(pidof -s cat)
PID COMMAND S PPID PGID SID 7559 cat /dev/urandom R 5487 7559 5487
每个session有一个控制终端(CTTY)。单个进程可以打开多个终端,但只有CTTY可以 进行任务控制(job control),比如Ctrl-Z。一个终端最多只能成为一个session的 CTTY。某进程调用setsid(2)创建新session的同时,会失去原有的CTTY。若某进程没 有CTTY,当它不带O_NOCTTY标志打开某终端时,该终端自动成为其CTTY。
每个CTTY只有一个前台进程组,同一session中的其他进程属于后台进程组。
CTTY产生的控制信号不是发往单个进程,而是发往前台进程组(中的所有进程)。
前台进程组可以任意读写CTTY,可以对CTTY调用tcsetattr()。
后台进程组中的进程试图读CTTY时,该后台进程组将收到SIGTTIN。后台进程组中的 进程可以写CTTY,除非c_lflag中TOSTOP置位,此时该后台进程组将收到SIGTTOU。后 台进程组中的进程对CTTY调用tcsetattr()时,该后台进程组将收到SIGTTOU。
session中的进程可以调用tcsetpgrp()设置前台进程组,所受限制同前述tcsetattr()。
关于SIGHUP,参看:
《24.3 如何编写daemon程序》
一般来说,有两种典型的与SIGHUP信号相关的情形。
假设某session有控制终端,当session leader终止时,系统会向该session前台进程 组中所有进程及后台进程组中处于"停止"状态的每个进程分发SIGHUP信号。
如果某进程组中有一个进程,其父进程属于同一会话(session)的另一个进程组,则 该进程组不是"孤儿进程组",反之该进程组称为"孤儿进程组"。
APUE 9.10指出,当某进程的终止导致一个新的"孤儿进程组"产生,系统会向这个新 的"孤儿进程组"中处于"停止"状态的每个进程分发SIGHUP信号,然后分发SIGCONT信 号。那些未处于"停止"状态的进程不会收到这两个信号。
6) Job control
任务就是进程组的别称。bash有个内部命令jobs,"help jobs"了解细节。
假设在bash中执行"foo | bar | grep baz",bash会调用setpgid(),把这三个进程 置于同一进程组,接着调用tcsetpgrp()使之成为前台进程组,最后调用waitpid()。 此时Ctrl-C会杀死所有三个进程。按下Ctrl-Z,这三个进程都会被挂起,bash对 waitpid()的调用将返回,bash恢复自己成前台进程组,这三个进程所在进程组变成 后台进程组。
在bash中使用"bg %n"命令时,bash调用killpg(2)向指定后台进程组发送SIGCONT。 后台进程组试图读CTTY时,会收到SIGTTIN,bash的wait*()会监控到后台进程组的状 态变化。
在bash中使用"fg %n"命令时,bash调用tcsetpgrp()将指定进程组变成前台进程组。
7) 信号与任务控制示例
假设你正在用emacs编辑大文件,光标位于屏幕中部某处,此时emacs正对该文件进行 搜索、替换操作。按下Ctrl-Z,emacs所在前台进程组收到SIGTSTP。
emacs的SIGTSTP信号句柄得到执行,通过向CTTY写入相应控制序列移动光标至屏幕最 后一行。接着emacs向自己所在前台进程组发送SIGSTOP。
emacs现在被挂起,session leader收到SIGCHLD,知道emacs状态发生变化。当前台 进程组中所有进程被挂起后,session leader保存当前termios设置,以备将来恢复 用。session leader调用tcsetpgrp()将自身所在进程组设置成前台进程组,输出形 如"[1]+ Stopped"的信息,通知用户有任务被挂起。
ps(1)可以看到emacs处在停止状态(T)。可以用bash内置命令bg使emacs继续执行,也 可以用kill(1)向emacs发送SIGCONT,emacs的SIGCONT信号句柄得到执行,这将试图 重绘emacs的GUI。但是,emacs现在位于后台进程组,写CTTY导致emacs收到SIGTTOU, emacs再次停止运行,session leader再次收到SIGCHLD,再次输出"[1]+ Stopped"。
用bash内置命令fg,bash恢复之前保存的termios设置,调用tcsetpgrp()将emacs所 在进程组设置成前台进程组,向前台进程组发送SIGCONT。emacs的SIGCONT信号句柄 得到执行,重绘emacs的GUI。
8) setsid(1)
这是个外部命令
$ dpkg -S $(which setsid) util-linux: /usr/bin/setsid
NAME
setsid - 在新session中运行指定程序
SYNOPSIS
setsid [options] program [arguments]
DESCRIPTION
setsid在一个新session中执行指定程序。
如果setsid本身已经是process group leader,会调用fork(2)创建子进程,在
子进程中调用setsid(2),否则直接调用setsid(2);最后调用exec*()执行指定
程序。如果使用--fork参数,则总是创建子进程。考虑setsid不从bash启动,
而是由其他进程或脚本启动。
OPTIONS
-c, --ctty
将指定程序的CTTY设置成当前终端
一个终端最多只能成为一个session的CTTY,ioctl(TIOCSCTTY)会抢夺CTTY。
-f, --fork
总是创建新(子)进程
-w, --wait
等待指定程序执行结束,其退出码做为setsid命令的退出码
-V, --version
显示版本
-h, --help
显示帮助
SEE ALSO
setsid(2)
$ strace -f -ff -o /tmp/setsid.log setsid cat /tmp/some.txt scz@nsfocus $ ls -l /tmp/setsid.log* -rw-r--r-- 1 scz scz 5755 Feb 19 11:21 /tmp/setsid.log.8526
$ vi /tmp/setsid.log.8526 ... getpgrp() = 8523 getpid() = 8526 setsid() = 8526 execve("/bin/cat", ["cat", "/tmp/some.txt"], 0xbfa0c2b4 / 20 vars /) = 0 ...
strace的情形下setsid已经是在fork(2)后的子进程中运行,此时setsid不是process group leader,可以直接调用setsid(2)。
$ rm /tmp/setsid.log $ strace -f -ff -o /tmp/setsid.log setsid -f cat /tmp/some.txt $ ls -l /tmp/setsid.log -rw-r--r-- 1 scz scz 2654 Feb 19 11:32 /tmp/setsid.log.8549 -rw-r--r-- 1 scz scz 3274 Feb 19 11:32 /tmp/setsid.log.8550
$ vi /tmp/setsid.log.8549 ... clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb7f0a168) = 8550 ...
$ vi /tmp/setsid.log.8550 setsid() = 8550 execve("/bin/cat", ["cat", "/tmp/some.txt"], 0xbfb9f428 / 20 vars /) = 0 ...
"setsid -f"时死活fork(),现在libc中的fork(3)由clone(2)实现,尽管fork(2)仍 然可用。setsid.log.8549中没有getpgrp()、getpid(),因为-f参数不需要检查 setsid是否是process group leader。
9) jobs/disown/nohup
$ help jobs jobs: jobs [-lnprs] [jobspec ...] or jobs -x command [args] Display status of jobs.
Lists the active jobs. JOBSPEC restricts output to that job.
Without options, the status of all active jobs is displayed.
Options:
-l lists process IDs in addition to the normal information
-n lists only processes that have changed status since the last
notification
-p lists process IDs only
-r restrict output to running jobs
-s restrict output to stopped jobs
If -x is supplied, COMMAND is run after all job specifications that
appear in ARGS have been replaced with the process ID of that job's
process group leader.
Exit Status:
Returns success unless an invalid option is given or an error occurs.
If -x is used, returns the exit status of COMMAND.
$ help disown disown: disown [-h] [-ar] [jobspec ... | pid ...] Remove jobs from current shell.
Removes each JOBSPEC argument from the table of active jobs. Without
any JOBSPECs, the shell uses its notion of the current job.
Options:
-a remove all jobs if JOBSPEC is not supplied
-h mark each JOBSPEC so that SIGHUP is not sent to the job if the
shell receives a SIGHUP
-r remove only running jobs
Exit Status:
Returns success unless an invalid option or JOBSPEC is given.
$ type disown disown is a shell builtin
bash(1)关于SIGHUP的内容不清晰,下面加以补充。
交互式(无论登录、非登录)bash收到SIGHUP会退出,在其结束前会向所有前后台任务 (无论状态)发SIGHUP。假设此时有停止状态的后台任务,交互式bash可能还会向其发 送SIGCONT,但我从未观察到过。
交互式(无论登录、非登录)bash主动exit时,不会向运行状态的后台任务发送任何信 号,会向停止状态的后台任务发送SIGTERM,不是SIGHUP。
$ shopt -s huponexit $ shopt | grep hup huponexit on
假设huponexit被启用(缺省是off),交互式登录bash在退出前(无论是收到SIGHUP还 是主动exit)会向所有前后台任务发送SIGHUP,包括运行状态的后台任务。huponexit 只影响交互式登录bash,不影响交互式非登录bash。比如SSH登录后的bash受 huponexit影响,在登录bash中新启动的bash不受huponexit影响。
这些结论可以用strace确认。
"disown %n"是bash内置命令,其作用是将后台任务从bash的任务列表中移除,之后 无法对被移除的后台任务使用fg、bg命令,同时阻止交互式bash收到SIGHUP之后向被 移除任务发送SIGHUP。
"disown -h"只对付SIGHUP,不从任务列表中移除指定任务,此时可以fg、bg。
disown不会影响PID、PPID、PGID、SID,也不剥离CTTY。
若某停止状态的后台任务事先被disown过,收到SIGHUP的交互式bash确实不会向之发 送SIGHUP,但是kernel会向之发送SIGHUP,因为此时有新的"孤儿进程组"产生。这意 味着disown过的停止状态的后台任务仍将被杀,strace可能看到:
--- stopped by SIGTSTP --- --- SIGHUP {si_signo=SIGHUP, si_code=SI_KERNEL} --- +++ killed by SIGHUP +++
对运行状态的任务(无论前后台)使用disown才稍有意义,disown其实十分鸡肋。
参看:
Difference between nohup, disown and & - [2010-11-09] https://unix.stackexchange.com/questions/3886/difference-between-nohup-disown-and
对比&、nohup、disown的使用,不过有些内容术语混乱、表述不严谨,请自行修正。
nohup启动进程之前重定向stdout、对SIGHUP使用SIG_IGN(被启动进程可能更改这种 设置)。nohup并不影响session、CTTY。
&只是将进程丢入bash后台运行,不影响stdin、stdout、stderr、CTTY。
☆ 将nc得到的简易shell升级成全功能交互式TTY
Upgrading simple shells to fully interactive TTYs - ropnop [2017-07-10] https://blog.ropnop.com/upgrading-simple-shells-to-fully-interactive-ttys/
Reverse Shell Cheat Sheet - Pentest Monkey http://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sheet
7 Linux Shells Using Built-in Tools - [2011-05-27] http://lanmaster53.com/2011/05/7-linux-shells-using-built-in-tools/
与原始问题无关,是伪终端相关的一些奇技淫巧。
1) Python pty module
这次主要目的是搞伪终端,用bindshell简单演示
server端
nc -l -p
client端
nc -n
在nc得到的简易shell中执行su,在server端看到错误提示:
su: must be run from a terminal
直接在简易shell中执行
python -c 'import pty;pty.spawn("/bin/bash")'
再执行su就可以了。这样得到的伪终端shell比简易shell要强一些,但不够强,比如 Ctrl-Z无效、TAB补齐无效、不支持vi。
Ric030指出,在pty.spawn()得到的增强shell中依次输入Ctrl-V/C、回车,可在该增 强shell中达到Ctrl-C的效果,此时可以打断"cat /dev/urandom > /dev/null"、 "cat > /dev/null"。同理,Ctrl-V/D、回车可以达到Ctrl-D的效果,Ctrl-V/Z、回 车可以达到Ctrl-Z的效果。
测试时client/server可在同一主机,都用同一帐号登录时,用如下命令确认细节
ps -f -o user,sid,pgid,ppid,tty,state,pid,args
2) socat(full)
这个办法跟nc无关,要求client/server都用socat。比如:
server端
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp-listen:
client端
socat file:tty
,raw,echo=0 tcp:
得到全功能交互式TTY,支持SIGINT/SIGTSTP、TAB补齐、vi等等。
3) nc+python+stty(full)
这是对第一种方案的改进
server端
nc -l -p
client端
nc -n
在nc得到的简易shell中执行
python -c 'import pty;pty.spawn("/bin/bash")'
Ctrl-Z
这会将client端"nc -n
[1]+ Stopped
在client端执行
$ echo $TERM
vt100
$ stty -a
speed 38400 baud; rows 58; columns 132; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol =
在client端盲输入(已经关闭回显):
fg reset export SHELL=bash export TERM=vt100 stty rows 58 columns 132
得到全功能交互式TTY,支持SIGINT/SIGTSTP、TAB补齐、vi等等。
4) script
假设server端安装有script
$ dpkg -S $(which script) bsdutils: /usr/bin/script $ aptitude install bsdutils
server端
nc -l -p
client端
nc -n
在nc得到的简易shell中执行
script /dev/null
效果同第一种方案。
"script /dev/null"会创建新pty对、执行"bash -i",并将bash的stdin、stdout导 向新slave pty。"script /dev/null"实际相当于:
python -c 'import pty;pty.spawn(("bash","-i"))'
5) reptyr
server端
nc -l -p
client端
nc -n
在nc得到的简易shell中执行
reptyr -L bash -i
效果同第一种方案。
6) Windows的挣扎
如果client/server都是Linux,可照搬前文套路。但client是Windows时, "script /dev/null"并不能让cmd里正常使用vi,Windows的telnet客户端是特别处理 过的,会处理屏显相关的控制序列。
server端
nc -l -p
client端
telnet
在telnet得到的简易shell中执行
script /dev/null;
后面有个分号。接着在shell中执行
stty rows 40 columns 120
这个值与cmd的窗口布局设置一样即可。回显处理得有问题,可以这样
stty -echo
之后bash命令行回显正常,但Ctrl-L刷屏效果丢失。无论怎么搞,vi里的回显都不正 常。检查Windows telnet的回显设置:
Ctrl-]
Microsoft Telnet> d ... Local echo off Line feed mode - Causes return key to send CR Current mode: Console ... Preferred term type is VT100 Microsoft Telnet> unset localecho
已经关闭客户端本地回显。我猜因master pty未关联合适的terminal emulator导致 这些问题,当client是Linux时,client自己做了一些补救。
☆ 结束语
跑oclHashcat-plus时没事先丢到screen session里,这个后悔药怎么吃?
原始问题在过去经常碰上,始终没有深究过救急方案。从原理上猜测动用llkm、tty hijacking技术可能可以解决,以前搞过tty hijacking。但涉及内核态编程,稳定性、 可移植性不高。
最近有同事找过来问这个事,当时想当然地以为没有成熟工具,谁知道在用户态有 screenify、reptyr这两种成熟工具,尤其后者进了Debian发行版。
20多年前搞过curses编程,当时仅限于利用各种API实现功能,没有从更深处理解伪 终端机制,这次一并学习了一点点。本文没有原创技术点,汇总从2004年至今在互联 网上能找到的关于此问题的绝大多数有价值的讨论。本文价值在于将这些零散分布的 有用信息归档一处。建议直接阅读英文原文,我是反复过了好几遍。
那些第一次听说screenify、reptyr的人,你们会感谢我的。