制造恶作剧切断TCP连接和进程之间的关联

想不想再玩个恶作剧??
很多运维发现系统中有tcp连接异常的时候,会使用netstat/ss命令找出tcp连接对应的处理进程,然后去找研发debug这个进程。比如:

[root@localhost ~]# netstat -ntp Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local AddressForeign AddressStatePID/Program name tcp00 192.168.56.110:22192.168.56.1:50069ESTABLISHED 1420/sshd: root@pts tcp00 192.168.56.110:22192.168.56.1:50048ESTABLISHED 1357/sshd: root@pts tcp00 192.168.56.110:22192.168.56.1:50060ESTABLISHED 1378/sshd: root@pts tcp00 192.168.56.110:22192.168.56.1:50063ESTABLISHED 1399/sshd: root@pts

如果我把tcp连接和进程之间的关联拆除了会怎样?杂耍一个手艺,看看他们的表现?
或者,更甚,我将进程1和进程2处理的tcp连接交换,能不能达到嫁祸于人的目的呢?我让你查,我让你查个毛线球!
若完成此事,必然要先理解在Linux中,tcp连接和进程之间的关联是如何建立的。
OK,让我们开始。
在Linux系统中,一个tcp连接在底层用一个tcp_sock对象来表示。
在sock的OO设计中,存在下面的继承关系,我们从基类sock_common开始:
sock_common <-- sock <-- inet_sock <-- inet_connection_sock <-- tcp_sock
【制造恶作剧切断TCP连接和进程之间的关联】然而…
然而进程和tcp_sock并非相互指向的,因为一个tcp连接并非唯一对应一个进程,我们知道,tcp_sock在进程上下文以文件描述符存在,多个进程可以操作同一个tcp连接。
所以,你可以从一个进程的文件描述符对应到一个tcp连接,但反过来却不行,你无法通过一个tcp_sock确认它所属的进程。
制造恶作剧切断TCP连接和进程之间的关联
文章图片

基于tcp连接和进程并非一一对应的关系,在实现上,若要将一个tcp连接和一个进程建立关联,要分为两个步骤来处理:
  • Linux将所有的tcp_sock导出在/proc/net/tcp中,其中的inode字段指向伯克利套接字层的socket_alloc结构体对象的inode地址的i_ino字段。
  • Linux将所有进程导出在/proc/$pid目录,其中在/proc/$pid/fd子目录导出所有该进程打开的文件描述符。
当你执行netstat -antp的时候,netstat的动作如下:
  1. 将所有/proc/net/tcp中的tcp连接的inode号保存在一个链表。
  2. 遍历所有/proc下导出的进程的/proc/$pid/fd目录中的link,以此link inode号匹配tcp inode链表中的inode号。
  3. 将匹配成功的tcp inode号和/proc/$pid/fd/$fd->inode建立关联。
如果你用ss替代netstat,其过程依然如此,唯一的不同就是tcp inode链表不在再通过/proc/net/tcp获取,而是通过netlink来获取。

这一切你可以通过strace netstat/ss观察到,而无需去分析netstat/ss的源码。
这个过程和Linux内核驱动程序管理机制中在bus上driver/device相互probe的过程非常类似!
为了完成这个恶作剧,常规的想法是把/proc/$pid/fd/x给hook住,让本来是一个tcp socket的link显示为指向其它诸如/dev/null。
但是有了对进程到tcp连接的关联过程的理解,拆除它们之间的关联就更容易了,根本不用去hook什么procfs的文件操作接口,如下图所示:
制造恶作剧切断TCP连接和进程之间的关联
文章图片

这显然是一种比hook procfs文件操作接口更加接近本质的做法。我个人很不喜欢hook procfs,复杂,且不优雅,而且没有从根本上解决问题!我上述图示所描述的方案才是本质的方案。
完成这件事的代码如下:
#!/usr/bin/stap -gfunction relieve(fd:long) %{ struct dentry *dentry = current->files->fdt->fd[STAP_ARG_fd]->f_path.dentry; struct inode *ino = current->files->fdt->fd[0]->f_path.dentry->d_inode; dentry->d_inode = ino; %}probe kernel.function("__schedule").return { if (pid() == $1) { relieve($2); exit() } }function wakeup(pid:long) %{ struct task_struct *tsk; tsk = pid_task(find_vpid(STAP_ARG_pid), PIDTYPE_PID); if (tsk) wake_up_process(tsk); %}probe timer.ms(500) { wakeup($1) }

超级超级超级简单的代码。
解释一下为什么要hook __schedule.return,因为在这个位置可以确保被调度进程自身没有持有自旋锁(持有自旋锁是禁止schedule的),且同一个CPU上的其它进程没有持有自旋锁(否则它就不会被切换出去)
来来来,看效果。拿本文开头的例子做实验,我的目的是消去netstat -ntp后面进程pid以及进程名的显示:
[root@localhost test]# for pid in $(netstat -ntp|egrep -o [0-9]+\/|egrep -o [0-9]+); do ./relieve.stp $pid 3; done [root@localhost test]# netstat -ntp Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local AddressForeign AddressStatePID/Program name tcp00 192.168.56.110:22192.168.56.1:50069ESTABLISHED - tcp00 192.168.56.110:22192.168.56.1:50048ESTABLISHED - tcp00 192.168.56.110:22192.168.56.1:50060ESTABLISHED - tcp00 192.168.56.110:22192.168.56.1:50063ESTABLISHED - [root@localhost test]# [root@localhost test]# netstat -ntp|egrep -o [0-9]+\/|egrep -o [0-9]+ [root@localhost test]# echo $? 1 [root@localhost test]#

Oh,yes!
有朋友提示说直接搞没了没意思,最好是混淆进程和tcp连接的关系,更具有迷惑性,OK,这实在是太棒了。简单的代码如下所示:
struct dentry *den1 = tsk1->files->fdt->fd[STAP_ARG_fd1]->f_path.dentry; struct dentry *den2 = tsk2->files->fdt->fd[STAP_ARG_fd2]->f_path.dentry; struct inode *tmp; tmp = den1->d_inode; den1->d_inode = den2->d_inode; den2->d_inode = tmp;

用上述替换代码,简单演示一个例子。
首先接入两个tcp连接,如下所示:
制造恶作剧切断TCP连接和进程之间的关联
文章图片

[root@localhost ~]# netstat -ntp Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local AddressForeign AddressStatePID/Program name tcp00 192.168.56.110:22192.168.56.1:52105ESTABLISHED 1046/sshd: root@pts tcp00 192.168.56.110:22192.168.56.1:53346ESTABLISHED 1523/sshd: root@pts

然后用上述代码将二者的f_path.dentry->d_inode进行交换,就混淆了视听,如下所示:
制造恶作剧切断TCP连接和进程之间的关联
文章图片

[root@localhost ~]# netstat -ntp Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local AddressForeign AddressStatePID/Program name tcp00 192.168.56.110:22192.168.56.1:52105ESTABLISHED 1523/sshd: root@pts tcp00 192.168.56.110:22192.168.56.1:53346ESTABLISHED 1046/sshd: root@pts

仔细看,进程和连接之间的关系交换了,如果这个时候运维发现了异常,是不是可以戏弄他们一把呢,至少可以让他们为此召开一个会议吧,哈哈!
什么?可能会宕机?玩这一行的,忍受宕机是必须的素养,要慢慢打磨,才能出精品,哈哈。
至于如何破解这类把戏,很简单,我们不要沿着procfs这条线索去关联,而是沿着socket/sock这条线索去关联即可:
  • socket/sock这条线索是斩不断的!
制造恶作剧切断TCP连接和进程之间的关联
文章图片

后面我会用crash插件来演示,至于今天的把戏,就先到此为止了。
浙江温州皮鞋湿,下雨进水不会胖。

    推荐阅读