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
这一块的内核源码,别的模块的源码也是同样的错综复杂的。结构体、回调函数、父子继承等等都是有许多关联性的,而且都分散在各个代码文件中,需要仔细捋清楚,这是需要耐心和细心的。即使再复杂的代码也一定能读明白。
最后,代码审计还是有一些小技巧的,例如看代码要抓关键点,不全看,也就是说代码块的阅读的详细程度需要把控好。其次是代码和原理结合起来一起看,先理清代码程序执行流程,再结合着理论原理一起看,基本都能够理解清楚代码块的含义…