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

想不想再玩个恶作剧??

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

[root@localhost ~]# netstat -ntp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 192.168.56.110:22       192.168.56.1:50069      ESTABLISHED 1420/sshd: root@pts
tcp        0      0 192.168.56.110:22       192.168.56.1:50048      ESTABLISHED 1357/sshd: root@pts
tcp        0      0 192.168.56.110:22       192.168.56.1:50060      ESTABLISHED 1378/sshd: root@pts
tcp        0      0 192.168.56.110:22       192.168.56.1:50063      ESTABLISHED 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_sock并非相互指向的,因为一个tcp连接并非唯一对应一个进程,我们知道,tcp_sock在进程上下文以文件描述符存在,多个进程可以操作同一个tcp连接。

所以,你可以从一个进程的文件描述符对应到一个tcp连接,但反过来却不行,你无法通过一个tcp_sock确认它所属的进程。
在这里插入图片描述

基于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的文件操作接口,如下图所示:
在这里插入图片描述
这显然是一种比hook procfs文件操作接口更加接近本质的做法。我个人很不喜欢hook procfs,复杂,且不优雅,而且没有从根本上解决问题!我上述图示所描述的方案才是本质的方案。

完成这件事的代码如下:

#!/usr/bin/stap -g

function 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 Address           Foreign Address         State       PID/Program name
tcp        0      0 192.168.56.110:22       192.168.56.1:50069      ESTABLISHED -
tcp        0      0 192.168.56.110:22       192.168.56.1:50048      ESTABLISHED -
tcp        0      0 192.168.56.110:22       192.168.56.1:50060      ESTABLISHED -
tcp        0      0 192.168.56.110:22       192.168.56.1:50063      ESTABLISHED -
[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连接,如下所示:
在这里插入图片描述

[root@localhost ~]# netstat -ntp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 192.168.56.110:22       192.168.56.1:52105      ESTABLISHED 1046/sshd: root@pts
tcp        0      0 192.168.56.110:22       192.168.56.1:53346      ESTABLISHED 1523/sshd: root@pts

然后用上述代码将二者的f_path.dentry->d_inode进行交换,就混淆了视听,如下所示:
在这里插入图片描述

[root@localhost ~]# netstat -ntp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 192.168.56.110:22       192.168.56.1:52105      ESTABLISHED 1523/sshd: root@pts
tcp        0      0 192.168.56.110:22       192.168.56.1:53346      ESTABLISHED 1046/sshd: root@pts

仔细看,进程和连接之间的关系交换了,如果这个时候运维发现了异常,是不是可以戏弄他们一把呢,至少可以让他们为此召开一个会议吧,哈哈!

什么?可能会宕机?玩这一行的,忍受宕机是必须的素养,要慢慢打磨,才能出精品,哈哈。

至于如何破解这类把戏,很简单,我们不要沿着procfs这条线索去关联,而是沿着socket/sock这条线索去关联即可:

  • socket/sock这条线索是斩不断的!

在这里插入图片描述

后面我会用crash插件来演示,至于今天的把戏,就先到此为止了。


浙江温州皮鞋湿,下雨进水不会胖。

猜你喜欢

转载自blog.csdn.net/dog250/article/details/108113329
今日推荐