0x01 Struct Relation
以tcp socket为例子,udp等都类似。并以系统调用setsockopt为入口做代码分析。
相关的结构体有这么几种(tcp_sock、inet_connection_sock、inet_sock、sock):
1  | struct tcp_sock {  | 
这里我要说一嘴,日常开发中socket结构体用的是最多的,但是在内核代码中,socket结构体没有上述的几个结构体重要,它更像是一个连接用户态和内核态的存在,处于中间地带,这里在后续的分析中会体现出来。
以上的代码基本就可以看出相互之间的父子关系了(tcp_sock -> inet_connection_sock -> inet_sock -> sock -> sock_common),如下图所示:

有了以上的基础,再来继续看后续的代码。
0x02 Code Analyze
0x0A inet_init
该函数是注册内核网络中一系列结构体和回调函数的初始化函数,相当于网络模块的初始化函数。看如下代码:
1  | static int __init inet_init(void)  | 
inet_init函数简单来说就是注册了三种结构体(tcp_prot、inet_family_ops、inetsw_array),如下图所示:

本文主要分析setsockopt系统调用,setsockopt是需要传入一个socket参数的,因此还需要分析socket的起源。
0x0B Socket Create
先上整体代码的关键部分流程图,后续审计代码可以结合该图一起看:

调用流程为net/socket.c: SYSCALL_DEFINE3 -> __sys_socket -> sock_create,看如下代码:
1  | int __sys_socket(int family, int type, int protocol)  | 
上面的代码中pf->create这一块比较关键,主要是创建sock相关的结构体并初始化,看如下代码:
1  | static int inet_create(struct net *net, struct socket *sock, int protocol,  | 
从sock_init_data中可以看出用户态赋值给socket结构体的属性值,又悄悄地转移给了sock,最终用户态数据的体现显而易见的是sock结构体,该结构体也是后续与其他关键结构体交互的关键(inet_sock等),因此我会说socket更像是连接用户和内核的连接体。
inet_create申请完sock结构体并初始化属性值,又从sock转变为inet_sock初始化属性值,再从sock转变为inet_connection_sock初始化属性值,最终还从sock转变为tcp_sock初始化属性值。基本上可以得出inet_create函数将从tcp_sock到sock的从父到子结构体都做了一遍初始化(结构体属性值赋值、回调函数赋值),以供后续的操作使用。这里的后续操作指的便是setsockopt了。
0x0C Setsockopt Syscall
这里以兼容模式(32位下)举例,setsockopt系统调用的流程为net/compat.c:COMPAT_SYSCALL_DEFINE5 -> __compat_sys_setsockopt,代码如下:
1  | COMPAT_SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,  | 
这里假设用户态的代码为setsockopt(fd, SOL_IP, IPT_SO_SET_REPLACE, &data, sizeof(data));,那么上面的代码就走向了(2),根据前面分析的代码sock->ops已经被赋值为inet_stream_ops:
1  | const struct proto_ops inet_stream_ops = {  | 
这一部分主要结合前面的socket create代码部分一起,梳理清楚整体函数调用流程。具体详细的代码分析这里就略过了,不再细说。

0x03 Summary
简单说一下,其实从上面socket create和setsockopt前期阶段的这一部分就可以看出来,为什么在用户态编写c程序的时候,需要先创建socket后使用setsockopt并且在setsockopt的参数中需要有socket的描述符,因为在setsockopt的代码中出现的一些回调函数都是在socket create过程中创建并关联起来的,有一个先来后到的顺序。在漏洞利用的过程当中其实就是个逆推写代码的过程,通过分析内核代码,去推导出poc的c程序该怎么构造。
其次,不单是socket这一块的内核源码,别的模块的源码也是同样的错综复杂的。结构体、回调函数、父子继承等等都是有许多关联性的,而且都分散在各个代码文件中,需要仔细捋清楚,这是需要耐心和细心的。即使再复杂的代码也一定能读明白。
最后,代码审计还是有一些小技巧的,例如看代码要抓关键点,不全看,也就是说代码块的阅读的详细程度需要把控好。其次是代码和原理结合起来一起看,先理清代码程序执行流程,再结合着理论原理一起看,基本都能够理解清楚代码块的含义…