osquery源码解读之分析process_open_socket

说明

上篇文章主要是对shell_history的实现进行了分析。通过分析可以发现,osquery良好的设计使得源码简单易读。shell_history的整体实现也比较简单,通过读取并解析.bash_history中的内容,获得用户输入的历史命令。本文分析的是process_open_sockets,相比较而言实现更加复杂,对Linux也需要有更深的了解。

使用说明

首先查看process_open_sockets表的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
table_name("process_open_sockets")
description("Processes which have open network sockets on the system.")
schema([
Column("pid", INTEGER, "Process (or thread) ID", index=True),
Column("fd", BIGINT, "Socket file descriptor number"),
Column("socket", BIGINT, "Socket handle or inode number"),
Column("family", INTEGER, "Network protocol (IPv4, IPv6)"),
Column("protocol", INTEGER, "Transport protocol (TCP/UDP)"),
Column("local_address", TEXT, "Socket local address"),
Column("remote_address", TEXT, "Socket remote address"),
Column("local_port", INTEGER, "Socket local port"),
Column("remote_port", INTEGER, "Socket remote port"),
Column("path", TEXT, "For UNIX sockets (family=AF_UNIX), the domain path"),
])
extended_schema(lambda: LINUX() or DARWIN(), [
Column("state", TEXT, "TCP socket state"),
])
extended_schema(LINUX, [
Column("net_namespace", TEXT, "The inode number of the network namespace"),
])
implementation("system/process_open_sockets@genOpenSockets")
examples([
"select * from process_open_sockets where pid = 1",
])

其中有几个列名需要说明一下:

  • fd,表示文件描述符
  • socket,进行网络通讯时,socket通信对应的inode number
  • family,表示是IPv4/IPv6,最后的结果是以数字的方式展示
  • protocol,表示是TCP/UDP。

我们进行一个简单的反弹shell的操作,然后使用查询process_open_sockets表的信息。

1
2
3
4
5
6
7
osquery> select pos.*,p.cwd,p.cmdline from process_open_sockets pos left join processes p where pos.family=2 and pos.pid=p.pid and net_namespace<>0;
+-------+----+----------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+-----------------+-----------+
| pid | fd | socket | family | protocol | local_address | remote_address | local_port | remote_port | path | state | net_namespace | cwd | cmdline |
+-------+----+----------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+-----------------+-----------+
| 37272 | 15 | 52319299 | 2 | 6 | 192.168.2.142 | 172.22.0.176 | 43522 | 9091 | | ESTABLISHED | 4026531956 | /home/xingjun | osqueryi |
| 91155 | 2 | 56651533 | 2 | 6 | 192.168.2.142 | 192.168.2.150 | 53486 | 8888 | | ESTABLISHED | 4026531956 | /proc/79036/net | /bin/bash |
+-------+----+----------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+-----------------+-----------+

process_open_sockets表的实现是位于osquery/tables/networking/linux/process_open_sockets.cpp中。

分析

process_open_sockets的实现全部是在QueryData genOpenSockets(QueryContext &context)一个方法中。
官方给出的分析步骤是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Data for this table is fetched from 3 different sources and correlated.

1. Collect all sockets associated with each pid by going through all files
under /proc/<pid>/fd and search for links of the type socket:[<inode>].
Extract the inode and fd (filename) and index it by inode number. The inode
can then be used to correlate pid and fd with the socket information
collected on step 3. The map generated in this step will only contain
sockets associated with pids in the list, so it will also be used to filter
the sockets later if pid_filter is set.

2. Collect the inode for the network namespace associated with each pid.
Every time a new namespace is found execute step 3 to get socket basic
information.

3. Collect basic socket information for all sockets under a specifc network
namespace. This is done by reading through files under /proc/<pid>/net for
the first pid we find in a certain namespace. Notice this will collect
information for all sockets on the namespace not only for sockets
associated with the specific pid, therefore only needs to be run once. From
this step we collect the inodes of each of the sockets, and will use that
to correlate the socket information with the information collect on steps
1 and 2.

其实大致步骤就是:

  1. 收集进程所对应的fd信息,尤其是socket的inode信息;
  2. 收集进程的namespace的inode信息;
  3. 读取/proc/<pid>/net中的信息,与第一步中的socket的inode信息进行比对,找出pid所对应的网络连接信息。

为了方便说明,我对整个函数的代码进行切割,分步说明。

获取pid信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::set <std::string> pids;
if (context.constraints["pid"].exists(EQUALS)) {
pids = context.constraints["pid"].getAll(EQUALS);
}

bool pid_filter = !(pids.empty() ||
std::find(pids.begin(), pids.end(), "-1") != pids.end());

if (!pid_filter) {
pids.clear();
status = osquery::procProcesses(pids);
if (!status.ok()) {
VLOG(1) << "Failed to acquire pid list: " << status.what();
return results;
}
}
  1. 前面的context.constraints["pid"].exists(EQUALS)pid_filter为了判断在SQL语句中是否存在where子句以此拿到选择的pid。
  2. 调用status = osquery::procProcesses(pids);拿到对应的PID信息。

跟踪进入到osquery/filesystem/linux/proc.cpp:procProcesses(std::set<std::string>& processes):

1
2
3
4
5
6
7
8
9
Status procProcesses(std::set<std::string>& processes) {
auto callback = [](const std::string& pid,
std::set<std::string>& _processes) -> bool {
_processes.insert(pid);
return true;
};

return procEnumerateProcesses<decltype(processes)>(processes, callback);
}

继续跟踪进入到osquery/filesystem/linux/proc.h:procEnumerateProcesses(UserData& user_data,bool (*callback)(const std::string&, UserData&))

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
const std::string kLinuxProcPath = "/proc";
.....
template<typename UserData>
Status procEnumerateProcesses(UserData &user_data,bool (*callback)(const std::string &, UserData &)) {
boost::filesystem::directory_iterator it(kLinuxProcPath), end;

try {
for (; it != end; ++it) {
if (!boost::filesystem::is_directory(it->status())) {
continue;
}

// See #792: std::regex is incomplete until GCC 4.9
const auto &pid = it->path().leaf().string();
if (std::atoll(pid.data()) <= 0) {
continue;
}

bool ret = callback(pid, user_data);
if (ret == false) {
break;
}
}
} catch (const boost::filesystem::filesystem_error &e) {
VLOG(1) << "Exception iterating Linux processes: " << e.what();
return Status(1, e.what());
}

return Status(0);
}

  1. boost::filesystem::directory_iterator it(kLinuxProcPath), end;遍历/proc目录下面所有的文件,
  2. const auto &pid = it->path().leaf().string();..; bool ret = callback(pid, user_data);,通过it->path().leaf().string()判断是否为数字,之后调用bool ret = callback(pid, user_data);
  3. callback 方法_processes.insert(pid);return true;将查询到的pid全部记录到user_data中。

以一个反弹shell的例子为例,使用osqueryi查询到的信息如下:

1
2
3
4
5
6
osquery> select * from process_open_sockets where pid=14960; 
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+
| pid | fd | socket | family | protocol | local_address | remote_address | local_port | remote_port | path | state | net_namespace |
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+
| 14960 | 2 | 307410 | 2 | 6 | 192.168.2.156 | 192.168.2.145 | 51118 | 8888 | | ESTABLISHED | 4026531956 |
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+

获取进程对应的pid和fd信息

1
2
3
4
5
6
7
8
9
10
11
12
/* Use a set to record the namespaces already processed */
std::set <ino_t> netns_list;
SocketInodeToProcessInfoMap inode_proc_map;
SocketInfoList socket_list;
for (const auto &pid : pids) {
/* Step 1 */
status = procGetSocketInodeToProcessInfoMap(pid, inode_proc_map);
if (!status.ok()) {
VLOG(1) << "Results for process_open_sockets might be incomplete. Failed "
"to acquire socket inode to process map for pid "
<< pid << ": " << status.what();
}

在拿到所有的需要查询的pid信息之后,调用status = procGetSocketInodeToProcessInfoMap(pid, inode_proc_map);,顾名思义就是用于获取进程所对应的socket inode编号。进入到osquery/filesystem/linux/proc.cpp:procGetSocketInodeToProcessInfoMap()中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Status procGetSocketInodeToProcessInfoMap(const std::string &pid,SocketInodeToProcessInfoMap &result) {
auto callback = [](const std::string &_pid,
const std::string &fd,
const std::string &link,
SocketInodeToProcessInfoMap &_result) -> bool {
/* We only care about sockets. But there will be other descriptors. */
if (link.find("socket:[") != 0) {
return true;
}

std::string inode = link.substr(8, link.size() - 9);
_result[inode] = {_pid, fd};
return true;
};

return procEnumerateProcessDescriptors<decltype(result)>(
pid, result, callback);
}

其中的auto callback定义的是一个回调函数,进入到procEnumerateProcessDescriptors()中分析:

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
const std::string kLinuxProcPath = "/proc";
....
template<typename UserData>
Status procEnumerateProcessDescriptors(const std::string &pid,
UserData &user_data,
bool (*callback)(const std::string &pid,
const std::string &fd,
const std::string &link,
UserData &user_data)) {
std::string descriptors_path = kLinuxProcPath + "/" + pid + "/fd";

try {
boost::filesystem::directory_iterator it(descriptors_path), end;

for (; it != end; ++it) {
auto fd = it->path().leaf().string();

std::string link;
Status status = procReadDescriptor(pid, fd, link);
if (!status.ok()) {
VLOG(1) << "Failed to read the link for file descriptor " << fd
<< " of pid " << pid << ". Data might be incomplete.";
}

bool ret = callback(pid, fd, link, user_data);
if (ret == false) {
break;
}
}
} catch (boost::filesystem::filesystem_error &e) {
VLOG(1) << "Exception iterating process file descriptors: " << e.what();
return Status(1, e.what());
}

return Status(0);
}

这个代码写得十分清晰。

  1. 遍历/proc/pid/fd,拿到所有的文件描述符。在本例中即为/proc/14960/fd
  2. 回调bool ret = callback(pid, fd, link, user_data);,即之前在procGetSocketInodeToProcessInfoMap中定义的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        auto callback = [](const std::string &_pid,
    const std::string &fd,
    const std::string &link,
    SocketInodeToProcessInfoMap &_result) -> bool {
    /* We only care about sockets. But there will be other descriptors. */
    if (link.find("socket:[") != 0) {
    return true;
    }

    std::string inode = link.substr(8, link.size() - 9);
    _result[inode] = {_pid, fd};
    return true;
    };

    代码也十分地简单,拿到fd所对应的link,检查是否存在socket:[,如果存在获取对应的inode。由于查询的是process_open_sockets,所以我们仅仅只关心存在socket的link,在本例中就是307410。最终在SocketInodeToProcessInfoMap中的结构就是_result[inode] = {_pid, fd};。以inode作为key,包含了pidfd的信息。

获取进程对应的ns信息

在上一步status = procGetSocketInodeToProcessInfoMap(pid, inode_proc_map);执行完毕之后,得到_result[inode] = {_pid, fd};。将inode与pid和fd进行了关联。接下里就是解析进程对应的ns信息。

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
ino_t ns;
ProcessNamespaceList namespaces;
status = procGetProcessNamespaces(pid, namespaces, {"net"});
if (status.ok()) {
ns = namespaces["net"];
} else {
/* If namespaces are not available we allways set ns to 0 and step 3 will
* run once for the first pid in the list.
*/
ns = 0;
VLOG(1) << "Results for the process_open_sockets might be incomplete."
"Failed to acquire network namespace information for process "
"with pid "
<< pid << ": " << status.what();
}
```
跟踪进入到`status = procGetProcessNamespaces(pid, namespaces, {"net"});`,进入到`osquery/filesystem/linux/proc.cpp:procGetProcessNamespaces()`
```CPP
const std::string kLinuxProcPath = "/proc";
...
Status procGetProcessNamespaces(const std::string &process_id,ProcessNamespaceList &namespace_list,std::vector <std::string> namespaces) {
namespace_list.clear();
if (namespaces.empty()) {
namespaces = kUserNamespaceList;
}
auto process_namespace_root = kLinuxProcPath + "/" + process_id + "/ns";
for (const auto &namespace_name : namespaces) {
ino_t namespace_inode;
auto status = procGetNamespaceInode(namespace_inode, namespace_name, process_namespace_root);
if (!status.ok()) {
continue;
}
namespace_list[namespace_name] = namespace_inode;
}
return Status(0, "OK");
}

遍历const auto &namespace_name : namespaces,之后进入到process_namespace_root中,调用procGetNamespaceInode(namespace_inode, namespace_name, process_namespace_root);进行查询。在本例中namespaces{"net"},process_namespace_root/proc/14960/ns

分析procGetNamespaceInode(namespace_inode, namespace_name, process_namespace_root):

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
Status procGetNamespaceInode(ino_t &inode,const std::string &namespace_name,const std::string &process_namespace_root) {
inode = 0;
auto path = process_namespace_root + "/" + namespace_name;
char link_destination[PATH_MAX] = {};
auto link_dest_length = readlink(path.data(), link_destination, PATH_MAX - 1);
if (link_dest_length < 0) {
return Status(1, "Failed to retrieve the inode for namespace " + path);
}

// The link destination must be in the following form: namespace:[inode]
if (std::strncmp(link_destination,
namespace_name.data(),
namespace_name.size()) != 0 ||
std::strncmp(link_destination + namespace_name.size(), ":[", 2) != 0) {
return Status(1, "Invalid descriptor for namespace " + path);
}

// Parse the inode part of the string; strtoull should return us a pointer
// to the closing square bracket
const char *inode_string_ptr = link_destination + namespace_name.size() + 2;
char *square_bracket_ptr = nullptr;

inode = static_cast<ino_t>(
std::strtoull(inode_string_ptr, &square_bracket_ptr, 10));
if (inode == 0 || square_bracket_ptr == nullptr ||
*square_bracket_ptr != ']') {
return Status(1, "Invalid inode value in descriptor for namespace " + path);
}

return Status(0, "OK");
}

根据procGetProcessNamespaces()中定义的相关变量,得到path/proc/pid/ns/net,在本例中是/proc/14960/ns/net。通过inode = static_cast<ino_t>(std::strtoull(inode_string_ptr, &square_bracket_ptr, 10));,解析/proc/pid/ns/net所对应的inode。在本例中:

所以取到的inode4026531956。之后在procGetProcessNamespaces()中执行namespace_list[namespace_name] = namespace_inode;,所以namespace_list['net']=4026531956。最终ns = namespaces["net"];,所以得到的ns=4026531956

解析进程的net信息

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
// Linux proc protocol define to net stats file name.
const std::map<int, std::string> kLinuxProtocolNames = {
{IPPROTO_ICMP, "icmp"},
{IPPROTO_TCP, "tcp"},
{IPPROTO_UDP, "udp"},
{IPPROTO_UDPLITE, "udplite"},
{IPPROTO_RAW, "raw"},
};
...
if (netns_list.count(ns) == 0) {
netns_list.insert(ns);

/* Step 3 */
for (const auto &pair : kLinuxProtocolNames) {
status = procGetSocketList(AF_INET, pair.first, ns, pid, socket_list);
if (!status.ok()) {
VLOG(1)
<< "Results for process_open_sockets might be incomplete. Failed "
"to acquire basic socket information for AF_INET "
<< pair.second << ": " << status.what();
}

status = procGetSocketList(AF_INET6, pair.first, ns, pid, socket_list);
if (!status.ok()) {
VLOG(1)
<< "Results for process_open_sockets might be incomplete. Failed "
"to acquire basic socket information for AF_INET6 "
<< pair.second << ": " << status.what();
}
}
status = procGetSocketList(AF_UNIX, IPPROTO_IP, ns, pid, socket_list);
if (!status.ok()) {
VLOG(1)
<< "Results for process_open_sockets might be incomplete. Failed "
"to acquire basic socket information for AF_UNIX: "
<< status.what();
}
}

对于icmp/tcp/udp/udplite/raw会调用status = procGetSocketList(AF_INET|AF_INET6|AF_UNIX, pair.first, ns, pid, socket_list);。我们这里仅仅以procGetSocketList(AF_INET, pair.first, ns, pid, socket_list);进行说明(其中的ns就是4026531956)。

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
Status procGetSocketList(int family, int protocol,ino_t net_ns,const std::string &pid, SocketInfoList &result) {
std::string path = kLinuxProcPath + "/" + pid + "/net/";

switch (family) {
case AF_INET:
if (kLinuxProtocolNames.count(protocol) == 0) {
return Status(1,"Invalid family " + std::to_string(protocol) +" for AF_INET familiy");
} else {
path += kLinuxProtocolNames.at(protocol);
}
break;

case AF_INET6:
if (kLinuxProtocolNames.count(protocol) == 0) {
return Status(1,"Invalid protocol " + std::to_string(protocol) +" for AF_INET6 familiy");
} else {
path += kLinuxProtocolNames.at(protocol) + "6";
}
break;

case AF_UNIX:
if (protocol != IPPROTO_IP) {
return Status(1,
"Invalid protocol " + std::to_string(protocol) +
" for AF_UNIX familiy");
} else {
path += "unix";
}

break;

default:
return Status(1, "Invalid family " + std::to_string(family));
}

std::string content;
if (!osquery::readFile(path, content).ok()) {
return Status(1, "Could not open socket information from " + path);
}

Status status(0);
switch (family) {
case AF_INET:
case AF_INET6:
status = procGetSocketListInet(family, protocol, net_ns, path, content, result);
break;

case AF_UNIX:
status = procGetSocketListUnix(net_ns, path, content, result);
break;
}

return status;
}

由于我们的传参是family=AF_INET,protocol=tcp,net_ns=4026531956,pid=14960。执行流程如下:

  1. path += kLinuxProtocolNames.at(protocol);,得到path/proc/14960/net/tcp
  2. osquery::readFile(path, content).ok(),读取文件内容,即/proc/14960/net/tcp所对应的文件内容。在本例中是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
    0: 00000000:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000 26 0 26488 1 ffff912c69c21740 100 0 0 10 0
    1: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 28721 1 ffff912c69c23640 100 0 0 10 0
    2: 00000000:01BB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 27739 1 ffff912c69c21f00 100 0 0 10 0
    3: 0100007F:18EB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 988 0 25611 1 ffff912c69c207c0 100 0 0 10 0
    4: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 27737 1 ffff912c69c226c0 100 0 0 10 0
    5: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 29031 1 ffff912c69c23e00 100 0 0 10 0
    6: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25754 1 ffff912c69c20f80 100 0 0 10 0
    7: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25590 1 ffff912c69c20000 100 0 0 10 0
    8: 9C02A8C0:C7AE 9102A8C0:22B8 01 00000000:00000000 00:00000000 00000000 1000 0 307410 1 ffff912c374887c0 20 0 0 10 -1
  3. 执行procGetSocketListInet(family, protocol, net_ns, path, content, result);

分析

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
static Status procGetSocketListInet(int family,int protocol,ino_t net_ns,const std::string &path,const std::string &content,SocketInfoList &result) {
// The system's socket information is tokenized by line.
bool header = true;
for (const auto &line : osquery::split(content, "\n")) {
if (header) {
if (line.find("sl") != 0 && line.find("sk") != 0) {
return Status(1, std::string("Invalid file header for ") + path);
}
header = false;
continue;
}

// The socket information is tokenized by spaces, each a field.
auto fields = osquery::split(line, " ");
if (fields.size() < 10) {
VLOG(1) << "Invalid socket descriptor found: '" << line
<< "'. Skipping this entry";
continue;
}

// Two of the fields are the local/remote address/port pairs.
auto locals = osquery::split(fields[1], ":");
auto remotes = osquery::split(fields[2], ":");

if (locals.size() != 2 || remotes.size() != 2) {
VLOG(1) << "Invalid socket descriptor found: '" << line
<< "'. Skipping this entry";
continue;
}

SocketInfo socket_info = {};
socket_info.socket = fields[9];
socket_info.net_ns = net_ns;
socket_info.family = family;
socket_info.protocol = protocol;
socket_info.local_address = procDecodeAddressFromHex(locals[0], family);
socket_info.local_port = procDecodePortFromHex(locals[1]);
socket_info.remote_address = procDecodeAddressFromHex(remotes[0], family);
socket_info.remote_port = procDecodePortFromHex(remotes[1]);

if (protocol == IPPROTO_TCP) {
char *null_terminator_ptr = nullptr;
auto integer_socket_state =
std::strtoull(fields[3].data(), &null_terminator_ptr, 16);
if (integer_socket_state == 0 ||
integer_socket_state >= tcp_states.size() ||
null_terminator_ptr == nullptr || *null_terminator_ptr != 0) {
socket_info.state = "UNKNOWN";
} else {
socket_info.state = tcp_states[integer_socket_state];
}
}

result.push_back(std::move(socket_info));
}

return Status(0);
}

整个执行流程如下:

  1. const auto &line : osquery::split(content, "\n");.. auto fields = osquery::split(line, " ");解析文件,读取每一行的内容。对每一行采用空格分割;
  2. 解析信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SocketInfo socket_info = {};
    socket_info.socket = fields[9];
    socket_info.net_ns = net_ns;
    socket_info.family = family;
    socket_info.protocol = protocol;
    socket_info.local_address = procDecodeAddressFromHex(locals[0], family);
    socket_info.local_port = procDecodePortFromHex(locals[1]);
    socket_info.remote_address = procDecodeAddressFromHex(remotes[0], family);
    socket_info.remote_port = procDecodePortFromHex(remotes[1]);

    解析/proc/14960/net/tcp文件中的每一行,分别填充至socket_info结构中。但是在/proc/14960/net/tcp并不是所有的信息都是我们需要的,我们还需要对信息进行过滤。可以看到最后一条的inode307410才是我们需要的。

获取进程连接信息

将解析完毕/proc/14960/net/tcp获取socket_info之后,继续执行genOpenSockets()中的代码。

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
for (const auto &info : socket_list) {
Row r;
auto proc_it = inode_proc_map.find(info.socket);
if (proc_it != inode_proc_map.end()) {
r["pid"] = proc_it->second.pid;
r["fd"] = proc_it->second.fd;
} else if (!pid_filter) {
r["pid"] = "-1";
r["fd"] = "-1";
} else {
/* If we're filtering by pid we only care about sockets associated with
* pids on the list.*/
continue;
}

r["socket"] = info.socket;
r["family"] = std::to_string(info.family);
r["protocol"] = std::to_string(info.protocol);
r["local_address"] = info.local_address;
r["local_port"] = std::to_string(info.local_port);
r["remote_address"] = info.remote_address;
r["remote_port"] = std::to_string(info.remote_port);
r["path"] = info.unix_socket_path;
r["state"] = info.state;
r["net_namespace"] = std::to_string(info.net_ns);

results.push_back(std::move(r));
}

其中关键代码是:

1
2
auto proc_it = inode_proc_map.find(info.socket);
if (proc_it != inode_proc_map.end()) {

通过遍历socket_list,判断在第一步保存在inode_proc_map中的inode信息与info中的inode信息是否一致,如果一致,说明就是我们需要的那个进程的网络连接的信息。最终保存我们查询到的信息results.push_back(std::move(r));
到这里,我们就查询到了进程的所有的网络连接的信息。最终通过osquery展现。

1
2
3
4
5
6
osquery> select * from process_open_sockets where pid=14960; 
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+
| pid | fd | socket | family | protocol | local_address | remote_address | local_port | remote_port | path | state | net_namespace |
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+
| 14960 | 2 | 307410 | 2 | 6 | 192.168.2.156 | 192.168.2.145 | 51118 | 8888 | | ESTABLISHED | 4026531956 |
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+

以上就是整个osquery执行process_open_sockets表查询的整个流程。

扩展

Linux一些皆文件的特性,使得我们能够通过读取Linux下某些文件信息获取系统/进程所有的信息。在前面我们仅仅是从osquery的角度来分析的。本节主要是对Linux中的与网络有关、进程相关的信息进行说明。
/proc/net/tcp/proc/net/udp中保存了当前系统中所有的进程信息,与/proc/pid/net/tcp或者是/proc/pid/net/udp中保存的信息完全相同。
/proc/net/tcp信息如下:

1
2
3
4
5
6
7
8
9
10
11
sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
0: 00000000:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000 26 0 26488 1 ffff912c69c21740 100 0 0 10 0
1: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 28721 1 ffff912c69c23640 100 0 0 10 0
2: 00000000:01BB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 27739 1 ffff912c69c21f00 100 0 0 10 0
3: 00000000:1F40 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 471681 1 ffff912c37488f80 100 0 0 10 0
4: 0100007F:18EB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 988 0 25611 1 ffff912c69c207c0 100 0 0 10 0
5: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 27737 1 ffff912c69c226c0 100 0 0 10 0
6: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 29031 1 ffff912c69c23e00 100 0 0 10 0
7: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25754 1 ffff912c69c20f80 100 0 0 10 0
8: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25590 1 ffff912c69c20000 100 0 0 10 0
9: 9C02A8C0:C7AE 9102A8C0:22B8 01 00000000:00000000 00:00000000 00000000 1000 0 307410 1 ffff912c374887c0 20 0 0 10 -1

/proc/14960/net/tcp信息如下:

1
2
3
4
5
6
7
8
9
10
sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
0: 00000000:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000 26 0 26488 1 ffff912c69c21740 100 0 0 10 0
1: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 28721 1 ffff912c69c23640 100 0 0 10 0
2: 00000000:01BB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 27739 1 ffff912c69c21f00 100 0 0 10 0
3: 0100007F:18EB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 988 0 25611 1 ffff912c69c207c0 100 0 0 10 0
4: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 27737 1 ffff912c69c226c0 100 0 0 10 0
5: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 29031 1 ffff912c69c23e00 100 0 0 10 0
6: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25754 1 ffff912c69c20f80 100 0 0 10 0
7: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25590 1 ffff912c69c20000 100 0 0 10 0
8: 9C02A8C0:C7AE 9102A8C0:22B8 01 00000000:00000000 00:00000000 00000000 1000 0 307410 1 ffff912c374887c0 20 0 0 10 -1

我们每一列的含义都是固定的,我们以最终一列9C02A8C0:C7AE 9102A8C0:22B8 01 00000000:00000000 00:00000000 00000000 1000 0 307410 1 ffff912c374887c0 20 0 0 10 -1为例进行说明。

  1. local_address,本地通讯端口和IP,本例是9C02A8C0:C7AE9C02A8C0,是本地IP。9C02A8C0是十六进制,转换为十进制是2617419968,将其转换为IP地址则是156.2.168.192,倒装一下得到192.168.2.156C7AE转化为十进制是51118。所以当进行网络通信时,得到本地IP是192.168.2.156,端口是51118
  2. rem_address,远程服务器通信端口和IP,本例是9102A8C0:22B89102A8C0是远程IP。分析方法和local_address相同,得到远程IP是192.168.2.145,端口是8888
  3. st,socket的状态,本例是01st的不同的值表示不同的含义。
    • 01: ESTABLISHED,
    • 02: SYN_SENT
    • 03: SYN_RECV
    • 04: FIN_WAIT1
    • 05: FIN_WAIT2
    • 06: TIME_WAIT
    • 07: CLOSE
    • 08: CLOSE_WAIT
    • 09: LAST_ACK
    • 0A: LISTEN
    • 0B: CLOSING
      所以在本例中01则说明是ESTABLISHED状态。
  4. tx_queue, 表示发送队列中的数据长度,本例是00000000
  5. rx_queue, 如果状态是ESTABLISHED,表示接受队列中数据长度;如果是LISTEN,表示已完成连接队列的长度;
  6. tr,定时器类型。为0,表示没有启动计时器;为1,表示重传定时器;为2,表示连接定时器;为3,表示TIME_WAIT定时器;为4,表示持续定时器;
  7. tm->when,超时时间。
  8. retrnsmt,超时重传次数
  9. uid,用户id
  10. timeout,持续定时器或保洁定时器周期性发送出去但未被确认的TCP段数目,在收到ACK之后清零
  11. inode,socket连接对应的inode
  12. 1,没有显示header,表示的是socket的引用数目
  13. ffff912c374887c0,没有显示header,表示sock结构对应的地址
  14. 20,没有显示header,表示RTO,单位是clock_t
  15. 0,用来计算延时确认的估值
  16. 0,快速确认数和是否启用标志位的或元算结果
  17. 10,当前拥塞窗口大小
  18. -1,如果慢启动阈值大于等于0x7fffffff显示-1,否则表示慢启动阈值

proc_net_tcp_decode 这篇文章对每个字段也进行了详细地说明。

通过查看某个具体的pid的fd信息,检查是否存在以socket:开头的文件描述符,如果存在则说明存在网络通信。

在得到了socket所对应的inode之后,就可以在/proc/net/tcp中查询对应的socket的信息,比如远程服务器的IP和端口信息。这样通过socket的inode就可以关联进程信息和它的网络信息。

总结

论读源代码的重要性

以上