标题: 远程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 [ x`which 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 </dev/null 2>&1 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 d) reptyr 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 | grep ptmx 与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 -e /bin/bash 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 -e /bin/bash 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 = ; eol2 = ; swtch = ; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0; -parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel -iutf8 opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc $ stty raw -echo 在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 -e /bin/bash 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 -e /bin/bash 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 -e /bin/bash 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的人,你们会感谢我的。