0x00 序言

网络安全技术是在攻防之对抗中发展的。不会攻击的防守者,可能无法猜测攻击方的意图和未来行动;不会防御的攻击者,亦很难猜测漏洞点、绕过各种 waf 和防守策略。于是,网络攻防即是攻心之战,能拟合对手思路的一方,便能在信息量较少的情况下预测对手的行为,从而占据上风。

本站在过去数年间,讨论过大量攻击技术;现在也该转换视角,从防守者一方重新认识网络安全。防守者最重要的工具便是 IDS——网络侧的 NIDS,以及主机侧的 HIDS。典型的 NIDS 会监听网络中的流量,利用特征库识别攻击事件,并报告给值守员。例如,假设我们关注 CVE-2023-1389(TP-Link 某款路由器的 RCE 漏洞),它的攻击报文是:

POST /cgi-bin/luci/;stok=/locale?form=country HTTP/1.1
Host: <target router>
Content-Type: application/x-www-form-urlencoded

operation=write&country=$(id>/tmp/out)

那么,我们便可能写出这样一条规则:“如果 url path 等于 /cgi-bin/luci/;stok=/locale?form=country,则认为是针对 CVE-2023-1389 的攻击”。日后,当攻击者在网络中发送这种报文时,NIDS 产生警报,值守员便可追踪攻击者 IP,进行封堵和溯源工作。

固然,基于规则的 NIDS 对 0day 漏洞是无能为力的。然而,攻击者不太可能在攻击的全程都使用 0day 漏洞,因为攻击者缺乏内网资产信息。想要横向渗透,扫描几乎是不可避免的;一些典型的攻击尝试(例如 SQL 注入)会被 NIDS 发现;木马程序回连 C2 服务器的报文可能会被抓住;攻击者的提权行为可能会被 HIDS 记录下来。因此,我们事实上无需指望 NIDS 发现 0day 漏洞。在足够复杂的渗透过程中,攻击者总会留下踪迹,只要任何一点踪迹被抓住,防守方便获得了主动权。

目前,最流行的开源 NIDS 是 SnortSuricata。在本系列连载文章中,我们将阅读 Suricata 的源码,了解其内部实现,并为二次开发做准备。今天我们先做最基础的工作——把 Suricata 运行起来。

0x01 搭建环境

笔者月初组装了一台 E5-2680v4 洋垃圾服务器,正好用于实验。在 ESXi 上摆出以下网络结构:

共有三台实验机,其中 suricata-host-1 与 suricata-host-2 进行日常通讯,由 suricata-lab 机器运行 IDS。三台机器都用静态 IP 接入实验网络 192.168.25.0/24

💡
配置 ESXi 网络时,需要让虚拟交换机、端口组允许混杂模式。

编辑 IDS 服务器的 /etc/network/interfaces 文件:

allow-hotplug ens224
iface ens224 inet static
  address 192.168.25.10/24

执行 sudo systemctl restart networking 重启网络服务,即可接入实验网络。对 host1、host2 也执行相似步骤,最后给 IDS 服务器的网卡打开混杂模式:

ip link set ens224 promisc on

ip a
#3: ens224: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
#    link/ether 00:0c:29:d9:c8:dd brd ff:ff:ff:ff:ff:ff
#    altname enp19s0
#    inet 192.168.25.10/24 brd 192.168.25.255 scope global ens224
#       valid_lft forever preferred_lft forever
💡
如果不打开混杂模式,则网卡只接受发往自己的包或广播包。打开混杂模式后,网卡不验证 mac 地址,接受所有包,所以抓包局域网时一般要打开混杂模式。

现在来测试 IDS 服务器是否可以抓包。在 host1 运行 HTTP 服务:

python -m http.server -b 0.0.0.0 8000

让 host2 定期访问此服务:

while true; do curl http://192.168.25.21:8000/ > /dev/null 2>&1; sleep 1 ; done

在 lab 机器用 tcpdump 抓包:

sudo tcpdump -i ens224 -c 100 -w out.pcap

抓包成功:

现在我们配完了服务,该开始部署 Suricata 了。

0x02 编译 Suricata

先把源码下载下来,看一眼代码规模:

proxychains wget 'https://www.openinfosecfoundation.org/download/suricata-7.0.8.tar.gz'
tar -zxvf suricata-7.0.8.tar.gz  
cd suricata-7.0.8

cloc .
#     4903 text files.
#     4291 unique files.                                          
#      841 files ignored.
# 
# github.com/AlDanial/cloc v 1.96  T=6.19 s (693.6 files/s, 256749.6 lines/s)
# --------------------------------------------------------------------------------
# Language                      files          blank        comment           code
# --------------------------------------------------------------------------------
# Rust                           2122          59315         105324         700875
# C                               706          62608          58508         319993
# Bourne Shell                     27           9672           8832          57949
# C/C++ Header                    658          13100          29041          40022
# Markdown                        200           6791             58          19265
# reStructuredText                169           8232           2366          17979
# m4                               14           1516            405          14561
# C++                               9           2903           3305          10695
# Text                             16           2202              0           8755
# Python                           62           1714           2102           7635
# TOML                            129            837           1270           4967
# make                             25            230             41           2626
# Perl                              9            103            114            868
# YAML                             11             85            244            533
# PHP                               1             77             72            173
# JSON                            119              0              0            137
# Bourne Again Shell                7             19             15             80
# SVG                               1              0              0             34
# CSS                               1              3              2             29
# Lua                               1             11             10             27
# INI                               2              4              0             22
# Dockerfile                        1              6              0             19
# PowerShell                        1              7              2             13
# --------------------------------------------------------------------------------
# SUM:                           4291         169435         211711        1207257
# --------------------------------------------------------------------------------

据 cloc 工具统计,Suricata 共有 4291 个文件,1207257 行代码,算是一个大型项目。与之相比,sqlite3 只有 191532 行代码,AFL 仅 12179 行代码。完整阅读源码显然不现实,我们只能挑感兴趣的看。

编译并安装:

sudo apt install autoconf automake build-essential cargo cbindgen libjansson-dev libpcap-dev libpcre2-dev libtool libyaml-dev make pkg-config rustc zlib1g-dev

./configure
make -j16

du -h ./src/.libs/suricata
# 102M    ./src/.libs/suricata

ldd ./src/.libs/suricata
#         linux-vdso.so.1 (0x00007fff52594000)
#         libhtp.so.2 => not found
#         libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fbe06a88000)
#         libjansson.so.4 => /lib/x86_64-linux-gnu/libjansson.so.4 (0x00007fbe06a78000)
#         libyaml-0.so.2 => /lib/x86_64-linux-gnu/libyaml-0.so.2 (0x00007fbe05fdf000)
#         libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fbe05f45000)
#         libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007fbe05f26000)
#         libpcap.so.0.8 => /lib/x86_64-linux-gnu/libpcap.so.0.8 (0x00007fbe05eda000)
#         libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fbe05eba000)
#         libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbe05cd9000)
#         /lib64/ld-linux-x86-64.so.2 (0x00007fbe06b71000)
#         libdbus-1.so.3 => /lib/x86_64-linux-gnu/libdbus-1.so.3 (0x00007fbe05c83000)
#         libsystemd.so.0 => /lib/x86_64-linux-gnu/libsystemd.so.0 (0x00007fbe05bb3000)
#         libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x00007fbe05ba7000)
#         libgcrypt.so.20 => /lib/x86_64-linux-gnu/libgcrypt.so.20 (0x00007fbe05a60000)
#         liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007fbe05a31000)
#         libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007fbe05975000)
#         liblz4.so.1 => /lib/x86_64-linux-gnu/liblz4.so.1 (0x00007fbe0594f000)
#         libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0 (0x00007fbe05927000)

sudo make install
sudo make install-conf

执行 suricata --build-info ,查看支持的功能:

This is Suricata version 7.0.8 RELEASE
Features: PCAP_SET_BUFF AF_PACKET HAVE_PACKET_FANOUT HAVE_HTP_URI_NORMALIZE_HOOK PCRE_JIT HAVE_NSS HTTP2_DECOMPRESSION HAVE_JA3 HAVE_JA4 HAVE_LIBJANSSON TLS TLS_C11 RUST POPCNT64
SIMD support: SSE_4_2 SSE_4_1 SSE_3 SSE_2
Atomic intrinsics: 1 2 4 8 16 byte(s)
64-bits, Little-endian architecture
GCC version 12.2.0, C version 201112
compiled with _FORTIFY_SOURCE=0
L1 cache line size (CLS)=64
thread local storage method: _Thread_local
compiled with LibHTP v0.5.49, linked against LibHTP v0.5.49

Suricata Configuration:
  AF_PACKET support:                       yes
  AF_XDP support:                          no
  DPDK support:                            no
  eBPF support:                            no
  XDP support:                             no
  PF_RING support:                         no
  NFQueue support:                         no
  NFLOG support:                           no
  IPFW support:                            no
  Netmap support:                          no
  DAG enabled:                             no
  Napatech enabled:                        no
  WinDivert enabled:                       no

  Unix socket enabled:                     yes
  Detection enabled:                       yes

  Libmagic support:                        no
  libjansson support:                      yes
  hiredis support:                         no
  hiredis async with libevent:             no
  PCRE jit:                                yes
  LUA support:                             no
  libluajit:                               no
  GeoIP2 support:                          no
  JA3 support:                             yes
  JA4 support:                             yes
  Non-bundled htp:                         no
  Hyperscan support:                       no
  Libnet support:                          no
  liblz4 support:                          no
  Landlock support:                        yes

  Rust support:                            yes
  Rust strict mode:                        no
  Rust compiler path:                      /usr/bin/rustc
  Rust compiler version:                   rustc 1.63.0
  Cargo path:                              /usr/bin/cargo
  Cargo version:                           cargo 1.65.0

  Python support:                          yes
  Python path:                             /usr/bin/python3
  Install suricatactl:                     yes
  Install suricatasc:                      yes
  Install suricata-update:                 yes

  Profiling enabled:                       no
  Profiling locks enabled:                 no
  Profiling rules enabled:                 no

  Plugin support (experimental):           yes
  DPDK Bond PMD:                           no

Development settings:
  Coccinelle / spatch:                     no
  Unit tests enabled:                      no
  Debug output enabled:                    no
  Debug validation enabled:                no
  Fuzz targets enabled:                    no

Generic build parameters:
  Installation prefix:                     /usr/local
  Configuration directory:                 /usr/local/etc/suricata/
  Log directory:                           /usr/local/var/log/suricata/

  --prefix                                 /usr/local
  --sysconfdir                             /usr/local/etc
  --localstatedir                          /usr/local/var
  --datarootdir                            /usr/local/share

  Host:                                    x86_64-pc-linux-gnu
  Compiler:                                gcc (exec name) / g++ (real)
  GCC Protect enabled:                     no
  GCC march native enabled:                yes
  GCC Profile enabled:                     no
  Position Independent Executable enabled: no
  CFLAGS                                   -g -O2 -fPIC -std=c11 -march=native -I${srcdir}/../rust/gen -I${srcdir}/../rust/dist
  PCAP_CFLAGS                               -I/usr/include
  SECCFLAGS

0x03 首次运行

按照官方文档指引,我们把 Suricata 运行起来,采用默认的 suricata.yaml 文件。

sudo suricata -c suricata.yaml -i ens224
# i: suricata: This is Suricata version 7.0.8 RELEASE running in SYSTEM mode
# W: detect: No rule files match the pattern /usr/local/var/lib/suricata/rules/suricata.rules
# W: detect: 1 rule files specified, but no rules were loaded!
# i: threads: Threads created -> W: 16 FM: 1 FR: 1   Engine started.

现在可以在 /usr/local/var/log/suricata/eve.json 看到信息:

{"timestamp":"2024-12-31T14:57:25.849738+0800","flow_id":1667686480329036,"in_iface":"ens224","event_type":"fileinfo","src_ip":"192.168.25.21","src_port":8000,"dest_ip":"192.168.25.22","dest_port":55792,"proto":"TCP","pkt_src":"wire/pcap","http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136},"app_proto":"http","fileinfo":{"filename":"/","gaps":false,"state":"CLOSED","stored":false,"size":1136,"tx_id":0}}
{"timestamp":"2024-12-31T14:57:26.878913+0800","flow_id":1792877521196729,"in_iface":"ens224","event_type":"http","src_ip":"192.168.25.22","src_port":55796,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","pkt_src":"wire/pcap","tx_id":0,"http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136}}
{"timestamp":"2024-12-31T14:57:26.878989+0800","flow_id":1792877521196729,"in_iface":"ens224","event_type":"fileinfo","src_ip":"192.168.25.21","src_port":8000,"dest_ip":"192.168.25.22","dest_port":55796,"proto":"TCP","pkt_src":"wire/pcap","http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136},"app_proto":"http","fileinfo":{"filename":"/","gaps":false,"state":"CLOSED","stored":false,"size":1136,"tx_id":0}}
{"timestamp":"2024-12-31T14:57:27.143629+0800","flow_id":724822544231338,"in_iface":"ens224","event_type":"flow","src_ip":"192.168.25.22","src_port":40386,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","app_proto":"http","flow":{"pkts_toserver":6,"pkts_toclient":5,"bytes_toserver":486,"bytes_toclient":1630,"start":"2024-12-31T14:56:26.299832+0800","end":"2024-12-31T14:56:26.302813+0800","age":0,"state":"closed","reason":"timeout","alerted":false},"tcp":{"tcp_flags":"1b","tcp_flags_ts":"1b","tcp_flags_tc":"1b","syn":true,"fin":true,"psh":true,"ack":true,"state":"closed","ts_max_regions":1,"tc_max_regions":1}}
{"timestamp":"2024-12-31T14:57:27.904702+0800","flow_id":2186168948960977,"in_iface":"ens224","event_type":"http","src_ip":"192.168.25.22","src_port":55800,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","pkt_src":"wire/pcap","tx_id":0,"http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136}}
{"timestamp":"2024-12-31T14:57:27.905031+0800","flow_id":2186168948960977,"in_iface":"ens224","event_type":"fileinfo","src_ip":"192.168.25.21","src_port":8000,"dest_ip":"192.168.25.22","dest_port":55800,"proto":"TCP","pkt_src":"wire/pcap","http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136},"app_proto":"http","fileinfo":{"filename":"/","gaps":false,"state":"CLOSED","stored":false,"size":1136,"tx_id":0}}
{"timestamp":"2024-12-31T14:57:28.138586+0800","flow_id":215858337435564,"in_iface":"ens224","event_type":"flow","src_ip":"192.168.25.22","src_port":40362,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","app_proto":"http","flow":{"pkts_toserver":6,"pkts_toclient":5,"bytes_toserver":486,"bytes_toclient":1630,"start":"2024-12-31T14:56:24.246866+0800","end":"2024-12-31T14:56:24.249848+0800","age":0,"state":"closed","reason":"timeout","alerted":false},"tcp":{"tcp_flags":"1b","tcp_flags_ts":"1b","tcp_flags_tc":"1b","syn":true,"fin":true,"psh":true,"ack":true,"state":"closed","ts_max_regions":1,"tc_max_regions":1}}
{"timestamp":"2024-12-31T14:57:28.932648+0800","flow_id":54022912867887,"in_iface":"ens224","event_type":"http","src_ip":"192.168.25.22","src_port":55804,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","pkt_src":"wire/pcap","tx_id":0,"http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136}}
{"timestamp":"2024-12-31T14:57:28.932907+0800","flow_id":54022912867887,"in_iface":"ens224","event_type":"fileinfo","src_ip":"192.168.25.21","src_port":8000,"dest_ip":"192.168.25.22","dest_port":55804,"proto":"TCP","pkt_src":"wire/pcap","http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136},"app_proto":"http","fileinfo":{"filename":"/","gaps":false,"state":"CLOSED","stored":false,"size":1136,"tx_id":0}}
{"timestamp":"2024-12-31T14:57:29.131677+0800","flow_id":1925890420143592,"in_iface":"ens224","event_type":"flow","src_ip":"192.168.25.22","src_port":45726,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","app_proto":"http","flow":{"pkts_toserver":6,"pkts_toclient":5,"bytes_toserver":486,"bytes_toclient":1630,"start":"2024-12-31T14:56:22.186262+0800","end":"2024-12-31T14:56:22.189601+0800","age":0,"state":"closed","reason":"timeout","alerted":false},"tcp":{"tcp_flags":"1b","tcp_flags_ts":"1b","tcp_flags_tc":"1b","syn":true,"fin":true,"psh":true,"ack":true,"state":"closed","ts_max_regions":1,"tc_max_regions":1}}
{"timestamp":"2024-12-31T14:57:29.961290+0800","flow_id":459079160132775,"in_iface":"ens224","event_type":"http","src_ip":"192.168.25.22","src_port":55810,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","pkt_src":"wire/pcap","tx_id":0,"http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136}}
{"timestamp":"2024-12-31T14:57:29.961553+0800","flow_id":459079160132775,"in_iface":"ens224","event_type":"fileinfo","src_ip":"192.168.25.21","src_port":8000,"dest_ip":"192.168.25.22","dest_port":55810,"proto":"TCP","pkt_src":"wire/pcap","http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136},"app_proto":"http","fileinfo":{"filename":"/","gaps":false,"state":"CLOSED","stored":false,"size":1136,"tx_id":0}}
{"timestamp":"2024-12-31T14:57:30.126511+0800","flow_id":1232570106215586,"in_iface":"ens224","event_type":"flow","src_ip":"192.168.25.22","src_port":40408,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","app_proto":"http","flow":{"pkts_toserver":6,"pkts_toclient":5,"bytes_toserver":486,"bytes_toclient":1630,"start":"2024-12-31T14:56:28.352516+0800","end":"2024-12-31T14:56:28.355172+0800","age":0,"state":"closed","reason":"timeout","alerted":false},"tcp":{"tcp_flags":"1b","tcp_flags_ts":"1b","tcp_flags_tc":"1b","syn":true,"fin":true,"psh":true,"ack":true,"state":"closed","ts_max_regions":1,"tc_max_regions":1}}
{"timestamp":"2024-12-31T14:57:30.126596+0800","flow_id":849355708794600,"in_iface":"ens224","event_type":"flow","src_ip":"192.168.25.22","src_port":40398,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","app_proto":"http","flow":{"pkts_toserver":6,"pkts_toclient":5,"bytes_toserver":486,"bytes_toclient":1630,"start":"2024-12-31T14:56:27.328828+0800","end":"2024-12-31T14:56:27.331637+0800","age":0,"state":"closed","reason":"timeout","alerted":false},"tcp":{"tcp_flags":"1b","tcp_flags_ts":"1b","tcp_flags_tc":"1b","syn":true,"fin":true,"psh":true,"ack":true,"state":"closed","ts_max_regions":1,"tc_max_regions":1}}
{"timestamp":"2024-12-31T14:57:30.986816+0800","flow_id":569585911662863,"in_iface":"ens224","event_type":"http","src_ip":"192.168.25.22","src_port":55818,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","pkt_src":"wire/pcap","tx_id":0,"http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136}}
{"timestamp":"2024-12-31T14:57:30.987070+0800","flow_id":569585911662863,"in_iface":"ens224","event_type":"fileinfo","src_ip":"192.168.25.21","src_port":8000,"dest_ip":"192.168.25.22","dest_port":55818,"proto":"TCP","pkt_src":"wire/pcap","http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136},"app_proto":"http","fileinfo":{"filename":"/","gaps":false,"state":"CLOSED","stored":false,"size":1136,"tx_id":0}}
{"timestamp":"2024-12-31T14:57:31.119378+0800","flow_id":328646049902938,"in_iface":"ens224","event_type":"flow","src_ip":"192.168.25.22","src_port":40378,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","app_proto":"http","flow":{"pkts_toserver":6,"pkts_toclient":5,"bytes_toserver":486,"bytes_toclient":1630,"start":"2024-12-31T14:56:25.273126+0800","end":"2024-12-31T14:56:25.276085+0800","age":0,"state":"closed","reason":"timeout","alerted":false},"tcp":{"tcp_flags":"1b","tcp_flags_ts":"1b","tcp_flags_tc":"1b","syn":true,"fin":true,"psh":true,"ack":true,"state":"closed","ts_max_regions":1,"tc_max_regions":1}}
{"timestamp":"2024-12-31T14:57:31.998449+0800","event_type":"stats","stats":{"uptime":1376,"capture":{"kernel_packets":14776,"kernel_drops":0,"errors":0,"afpacket":{"busy_loop_avg":0,"polls":229465,"poll_signal":0,"poll_timeout":218685,"poll_data":10780,"poll_errors":0,"send_errors":0}},"decoder":{"pkts":14774,"bytes":2834908,"invalid":0,"ipv4":14773,"ipv6":1,"ethernet":14774,"arp":0,"unknown_ethertype":0,"chdlc":0,"raw":0,"null":0,"sll":0,"tcp":14773,"udp":0,"sctp":0,"esp":0,"icmpv4":0,"icmpv6":1,"ppp":0,"pppoe":0,"geneve":0,"gre":0,"vlan":0,"vlan_qinq":0,"vlan_qinqinq":0,"vxlan":0,"vntag":0,"ieee8021ah":0,"teredo":0,"ipv4_in_ipv6":0,"ipv6_in_ipv6":0,"mpls":0,"avg_pkt_size":191,"max_pkt_size":1202,"max_mac_addrs_src":0,"max_mac_addrs_dst":0,"erspan":0,"nsh":0,"event":{"ipv4":{"pkt_too_small":0,"hlen_too_small":0,"iplen_smaller_than_hlen":0,"trunc_pkt":0,"opt_invalid":0,"opt_invalid_len":0,"opt_malformed":0,"opt_pad_required":0,"opt_eol_required":0,"opt_duplicate":0,"opt_unknown":0,"wrong_ip_version":0,"icmpv6":0,"frag_pkt_too_large":0,"frag_overlap":0,"frag_ignored":0},"icmpv4":{"pkt_too_small":0,"unknown_type":0,"unknown_code":0,"ipv4_trunc_pkt":0,"ipv4_unknown_ver":0},"icmpv6":{"unknown_type":0,"unknown_code":0,"pkt_too_small":0,"ipv6_unknown_version":0,"ipv6_trunc_pkt":0,"mld_message_with_invalid_hl":0,"unassigned_type":0,"experimentation_type":0},"ipv6":{"pkt_too_small":0,"trunc_pkt":0,"trunc_exthdr":0,"exthdr_dupl_fh":0,"exthdr_useless_fh":0,"exthdr_dupl_rh":0,"exthdr_dupl_hh":0,"exthdr_dupl_dh":0,"exthdr_dupl_ah":0,"exthdr_dupl_eh":0,"exthdr_invalid_optlen":0,"wrong_ip_version":0,"exthdr_ah_res_not_null":0,"hopopts_unknown_opt":0,"hopopts_only_padding":0,"dstopts_unknown_opt":0,"dstopts_only_padding":0,"rh_type_0":0,"zero_len_padn":0,"fh_non_zero_reserved_field":0,"data_after_none_header":0,"unknown_next_header":0,"icmpv4":0,"frag_pkt_too_large":0,"frag_overlap":0,"frag_invalid_length":0,"frag_ignored":0,"ipv4_in_ipv6_too_small":0,"ipv4_in_ipv6_wrong_version":0,"ipv6_in_ipv6_too_small":0,"ipv6_in_ipv6_wrong_version":0},"tcp":{"pkt_too_small":0,"hlen_too_small":0,"invalid_optlen":0,"opt_invalid_len":0,"opt_duplicate":0},"udp":{"pkt_too_small":0,"hlen_too_small":0,"hlen_invalid":0,"len_invalid":0},"sll":{"pkt_too_small":0},"ethernet":{"pkt_too_small":0},"ppp":{"pkt_too_small":0,"vju_pkt_too_small":0,"ip4_pkt_too_small":0,"ip6_pkt_too_small":0,"wrong_type":0,"unsup_proto":0},"pppoe":{"pkt_too_small":0,"wrong_code":0,"malformed_tags":0},"gre":{"pkt_too_small":0,"wrong_version":0,"version0_recur":0,"version0_flags":0,"version0_hdr_too_big":0,"version0_malformed_sre_hdr":0,"version1_chksum":0,"version1_route":0,"version1_ssr":0,"version1_recur":0,"version1_flags":0,"version1_no_key":0,"version1_wrong_protocol":0,"version1_malformed_sre_hdr":0,"version1_hdr_too_big":0},"vlan":{"header_too_small":0,"unknown_type":0,"too_many_layers":0},"ieee8021ah":{"header_too_small":0},"vntag":{"header_too_small":0,"unknown_type":0},"ipraw":{"invalid_ip_version":0},"ltnull":{"pkt_too_small":0,"unsupported_type":0},"sctp":{"pkt_too_small":0},"esp":{"pkt_too_small":0},"mpls":{"header_too_small":0,"pkt_too_small":0,"bad_label_router_alert":0,"bad_label_implicit_null":0,"bad_label_reserved":0,"unknown_payload_type":0},"vxlan":{"unknown_payload_type":0},"geneve":{"unknown_payload_type":0},"erspan":{"header_too_small":0,"unsupported_version":0,"too_many_vlan_layers":0},"dce":{"pkt_too_small":0},"chdlc":{"pkt_too_small":0},"nsh":{"header_too_small":0,"unsupported_version":0,"bad_header_length":0,"reserved_type":0,"unsupported_type":0,"unknown_payload":0}},"too_many_layers":0},"tcp":{"syn":1338,"synack":1338,"rst":0,"urg":0,"active_sessions":82,"sessions":1338,"ssn_memcap_drop":0,"ssn_from_cache":39,"ssn_from_pool":1299,"pseudo":0,"pseudo_failed":0,"invalid_checksum":0,"midstream_pickups":0,"pkt_on_wrong_thread":0,"ack_unseen_data":94,"segment_memcap_drop":0,"segment_from_cache":3918,"segment_from_pool":48,"stream_depth_reached":0,"reassembly_gap":0,"overlap":0,"overlap_diff_data":0,"insert_data_normal_fail":0,"insert_data_overlap_fail":0,"urgent_oob_data":0,"memuse":9961472,"reassembly_memuse":2166784},"flow":{"memcap":0,"total":1339,"active":82,"tcp":1338,"udp":0,"icmpv4":0,"icmpv6":1,"tcp_reuse":5,"get_used":0,"get_used_eval":0,"get_used_eval_reject":0,"get_used_eval_busy":0,"get_used_failed":0,"wrk":{"spare_sync_avg":100,"spare_sync":16,"spare_sync_incomplete":0,"spare_sync_empty":0,"flows_evicted_needs_work":35,"flows_evicted_pkt_inject":70,"flows_evicted":6,"flows_injected":33,"flows_injected_max":0},"end":{"state":{"new":1,"established":30,"closed":1226,"local_bypassed":0},"tcp_state":{"none":0,"syn_sent":0,"syn_recv":0,"established":0,"fin_wait1":0,"fin_wait2":0,"time_wait":5,"last_ack":0,"close_wait":30,"closing":0,"closed":1221},"tcp_liberal":0},"mgr":{"full_hash_pass":193,"rows_per_sec":9175,"rows_maxlen":2,"flows_checked":2760,"flows_notimeout":1509,"flows_timeout":1251,"flows_evicted":1251,"flows_evicted_needs_work":33},"spare":10817,"emerg_mode_entered":0,"emerg_mode_over":0,"recycler":{"recycled":1218,"queue_avg":0,"queue_max":4},"memuse":7509504},"defrag":{"ipv4":{"fragments":0,"reassembled":0},"ipv6":{"fragments":0,"reassembled":0},"max_frag_hits":0},"flow_bypassed":{"local_pkts":0,"local_bytes":0,"local_capture_pkts":0,"local_capture_bytes":0,"closed":0,"pkts":0,"bytes":0},"detect":{"engines":[{"id":0,"last_reload":"2024-12-31T14:34:35.454475+0800","rules_loaded":0,"rules_failed":0,"rules_skipped":0}],"alert":0,"alert_queue_overflow":0,"alerts_suppressed":0},"app_layer":{"flow":{"http":1338,"ftp":0,"smtp":0,"tls":0,"ssh":0,"imap":0,"smb":0,"dcerpc_tcp":0,"dns_tcp":0,"nfs_tcp":0,"ntp":0,"ftp-data":0,"tftp":0,"ike":0,"krb5_tcp":0,"quic":0,"dhcp":0,"snmp":0,"sip":0,"rfb":0,"mqtt":0,"telnet":0,"rdp":0,"http2":0,"bittorrent-dht":0,"failed_tcp":0,"dcerpc_udp":0,"dns_udp":0,"nfs_udp":0,"krb5_udp":0,"failed_udp":0},"tx":{"http":1338,"ftp":0,"smtp":0,"tls":0,"ssh":0,"imap":0,"smb":0,"dcerpc_tcp":0,"dns_tcp":0,"nfs_tcp":0,"ntp":0,"ftp-data":0,"tftp":0,"ike":0,"krb5_tcp":0,"quic":0,"dhcp":0,"snmp":0,"sip":0,"rfb":0,"mqtt":0,"telnet":0,"rdp":0,"http2":0,"bittorrent-dht":0,"dcerpc_udp":0,"dns_udp":0,"nfs_udp":0,"krb5_udp":0},"error":{"http":{"gap":0,"alloc":0,"parser":0,"internal":0},"ftp":{"gap":0,"alloc":0,"parser":0,"internal":0},"smtp":{"gap":0,"alloc":0,"parser":0,"internal":0},"tls":{"gap":0,"alloc":0,"parser":0,"internal":0},"ssh":{"gap":0,"alloc":0,"parser":0,"internal":0},"imap":{"gap":0,"alloc":0,"parser":0,"internal":0},"smb":{"gap":0,"alloc":0,"parser":0,"internal":0},"dcerpc_tcp":{"gap":0,"alloc":0,"parser":0,"internal":0},"dns_tcp":{"gap":0,"alloc":0,"parser":0,"internal":0},"nfs_tcp":{"gap":0,"alloc":0,"parser":0,"internal":0},"ntp":{"gap":0,"alloc":0,"parser":0,"internal":0},"ftp-data":{"gap":0,"alloc":0,"parser":0,"internal":0},"tftp":{"gap":0,"alloc":0,"parser":0,"internal":0},"ike":{"gap":0,"alloc":0,"parser":0,"internal":0},"krb5_tcp":{"gap":0,"alloc":0,"parser":0,"internal":0},"quic":{"gap":0,"alloc":0,"parser":0,"internal":0},"dhcp":{"gap":0,"alloc":0,"parser":0,"internal":0},"snmp":{"gap":0,"alloc":0,"parser":0,"internal":0},"sip":{"gap":0,"alloc":0,"parser":0,"internal":0},"rfb":{"gap":0,"alloc":0,"parser":0,"internal":0},"mqtt":{"gap":0,"alloc":0,"parser":0,"internal":0},"telnet":{"gap":0,"alloc":0,"parser":0,"internal":0},"rdp":{"gap":0,"alloc":0,"parser":0,"internal":0},"http2":{"gap":0,"alloc":0,"parser":0,"internal":0},"bittorrent-dht":{"gap":0,"alloc":0,"parser":0,"internal":0},"failed_tcp":{"gap":0},"dcerpc_udp":{"alloc":0,"parser":0,"internal":0},"dns_udp":{"alloc":0,"parser":0,"internal":0},"nfs_udp":{"alloc":0,"parser":0,"internal":0},"krb5_udp":{"alloc":0,"parser":0,"internal":0}},"expectations":0},"memcap_pressure":14,"memcap_pressure_max":14,"http":{"memuse":17972,"memcap":0},"ftp":{"memuse":0,"memcap":0},"file_store":{"open_files":0}}}
{"timestamp":"2024-12-31T14:57:32.012900+0800","flow_id":1170911115404055,"in_iface":"ens224","event_type":"http","src_ip":"192.168.25.22","src_port":55822,"dest_ip":"192.168.25.21","dest_port":8000,"proto":"TCP","pkt_src":"wire/pcap","tx_id":0,"http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136}}
{"timestamp":"2024-12-31T14:57:32.013155+0800","flow_id":1170911115404055,"in_iface":"ens224","event_type":"fileinfo","src_ip":"192.168.25.21","src_port":8000,"dest_ip":"192.168.25.22","dest_port":55822,"proto":"TCP","pkt_src":"wire/pcap","http":{"hostname":"192.168.25.21","http_port":8000,"url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1136},"app_proto":"http","fileinfo":{"filename":"/","gaps":false,"state":"CLOSED","stored":false,"size":1136,"tx_id":0}}

eve 文件中出现了几种 event_type,分别是 fileinfohttpflowstats。其中,类型为 http 的 eve json 会包含一个 http 字段,内容大致是:

    "http": {
        "hostname": "192.168.25.21",
        "http_port": 8000,
        "url": "/",
        "http_user_agent": "curl/7.88.1",
        "http_content_type": "text/html",
        "http_method": "GET",
        "protocol": "HTTP/1.1",
        "status": 200,
        "length": 1136
    },

根据文档,我们可以配置哪些字段需要记录,例如 cookie 等。而 fileinfo 是:

    "fileinfo": {
        "filename": "/",
        "gaps": false,
        "state": "CLOSED",
        "stored": false,
        "size": 1136,
        "tx_id": 0
    }

我们没有开启文件提取功能,所以 stored=falseflow 是一个通用类型,内容:

    "flow": {
        "pkts_toserver": 6,
        "pkts_toclient": 5,
        "bytes_toserver": 486,
        "bytes_toclient": 1630,
        "start": "2024-12-31T14:56:28.352516+0800",
        "end": "2024-12-31T14:56:28.355172+0800",
        "age": 0,
        "state": "closed",
        "reason": "timeout",
        "alerted": false
    },
    "tcp": {
        "tcp_flags": "1b",
        "tcp_flags_ts": "1b",
        "tcp_flags_tc": "1b",
        "syn": true,
        "fin": true,
        "psh": true,
        "ack": true,
        "state": "closed",
        "ts_max_regions": 1,
        "tc_max_regions": 1
    }

最后是 stats 类型。这种 eve 是 Suricata 自身的状态日志,记录了 Suricata 运行时间、抓包计数、解码计数等信息。

    "stats": {
        "uptime": 1376,
        "capture": {
            "kernel_packets": 14776,
            "kernel_drops": 0,
            "errors": 0,
            "afpacket": {
                "busy_loop_avg": 0,
                "polls": 229465,
                "poll_signal": 0,
                "poll_timeout": 218685,
                "poll_data": 10780,
                "poll_errors": 0,
                "send_errors": 0
            }
        },
        "decoder": {
            "pkts": 14774,
            "bytes": 2834908,
            "invalid": 0,
            "ipv4": 14773,
            "ipv6": 1,
            "ethernet": 14774,
            "arp": 0,
            // ......

于是,我们想要监测网络中的事件,只需观察 eve.json 的新增条目。接下来,我们配置一条告警规则。

0x04 配置告警规则 & 重放 pcap

编写 tplink.rules 文件:

alert http any any -> any any (msg:"CVE-2023-1389 attack"; http.uri; content:"/cgi-bin/luci/\;stok=/locale?form=country"; nocase; sid:1000001; rev:1;)

其中:

  • any any -> any any 表示“任意源 IP、源端口、目的 IP、目的端口”
  • msg:"CVE-2023-1389 attack" 是报告信息
  • http.uri; content:"/cgi-bin/luci/;stok=/locale?form=country"; nocase; 表示在 url 中匹配指定的字符串,忽略大小写
  • sid:1000001 指定了规则 ID,用于区分不同规则
  • rev:1 表示这是规则的第一版

执行 curl "http://192.168.25.21:8000/cgi-bin/luci/;stok=/locale?form=country",观察到以下 eve 记录:

{
  "timestamp": "2024-12-31T15:37:28.311787+0800",
  "flow_id": 204480197161792,
  "in_iface": "ens224",
  "event_type": "alert",
  "src_ip": "192.168.25.22",
  "src_port": 44386,
  "dest_ip": "192.168.25.21",
  "dest_port": 8000,
  "proto": "TCP",
  "pkt_src": "wire/pcap",
  "tx_id": 0,
  "alert": {
    "action": "allowed",
    "gid": 1,
    "signature_id": 1000001,
    "rev": 1,
    "signature": "CVE-2023-1389 attack",
    "category": "",
    "severity": 3
  },
  "http": {
    "hostname": "192.168.25.21",
    "http_port": 8000,
    "url": "/cgi-bin/luci/;stok=/locale?form=country",
    "http_user_agent": "curl/7.88.1",
    "http_content_type": "text/html",
    "http_method": "GET",
    "protocol": "HTTP/1.1",
    "status": 404,
    "length": 0
  },
  "app_proto": "http",
  "direction": "to_server",
  "flow": {
    "pkts_toserver": 4,
    "pkts_toclient": 4,
    "bytes_toserver": 393,
    "bytes_toclient": 792,
    "start": "2024-12-31T15:37:28.309753+0800",
    "src_ip": "192.168.25.22",
    "dest_ip": "192.168.25.21",
    "src_port": 44386,
    "dest_port": 8000
  }
}

在阅读源码的过程中,我们会多次运行 Suricata 程序。为了可复现性,我们不宜依赖于局域网中的实时流量,而应使用 pcap 文件重放流量。指令如下:

sudo suricata -c suricata.yaml -s tplink.rules -r dump.pcap       
# i: suricata: This is Suricata version 7.0.8 RELEASE running in USER mode
# W: detect: No rule files match the pattern /usr/local/var/lib/suricata/rules/suricata.rules
# i: threads: Threads created -> RX: 1 W: 16 FM: 1 FR: 1   Engine started.
# i: suricata: Signal Received.  Stopping engine.
# i: pcap: read 1 file, 100 packets, 16315 bytes

这次,eve 日志不在 /usr/local/var/log/suricata 下,而是在运行目录下。产出与实时监听模式相比,多了一个 pcap_cnt 字段。

0x05 调试模式编译

我们希望追踪 http 请求被处理的过程。然而,我们面前的文件实在太多,且大多数文件与业务无关。想要找到关键逻辑,一个很好的办法是把程序跑一遍,用性能分析工具观察哪些函数被最多次调用。我们选择 gprof 工具。

重新 configure、make:

./configure --enable-shared=no --enable-static=yes --enable-gccprofile=yes CFLAGS="-g -O0"
# 最终生效的 CFLAGS 是:-g -O0 -fPIC -std=c11 -pg -march=native -I${srcdir}/../rust/gen -I${srcdir}/../rust/dist

make -j16

du -h src/suricata
# 101M    src/suricata

运行:

sudo ./suricata-dbg/src/suricata -c ./suricata-dbg/suricata.yaml -s tplink.rules -r 100w.pcap
# i: suricata: This is Suricata version 7.0.8 RELEASE running in USER mode
# W: detect: No rule files match the pattern /usr/local/var/lib/suricata/rules/suricata.rules
# i: threads: Threads created -> RX: 1 W: 16 FM: 1 FR: 1   Engine started.
# i: suricata: Signal Received.  Stopping engine.
# i: pcap: read 1 file, 1000000 packets, 124916062 bytes

运行结束之后,目录下产生了 gmon.out 文件。用 gprof2dot 生成调用图:

gprof ./suricata-dbg/src/suricata gmon.out > profile.txt   
python3 gprof2dot.py profile.txt | dot -Tsvg -o output.svg

结果如下(矢量图,可以放大):

可以发现,关键逻辑应该在 FlowWorker 中。我们日后阅读源码,应当予以关注。

0x06 运行模式

文档提到,Suricata 有不同的运行模式,我们看一眼当前支持的 runmode:

$ ./suricata-dbg/src/suricata --list-runmodes
------------------------------------- Runmodes ------------------------------------------
| RunMode Type      | Custom Mode       | Description 
|----------------------------------------------------------------------------------------
| PCAP_DEV          | single            | Single threaded pcap live mode 
|                   ---------------------------------------------------------------------
|                   | autofp            | Multi-threaded pcap live mode. Packets from each flow are assigned to a consistent detection thread 
|                   ---------------------------------------------------------------------
|                   | workers           | Workers pcap live mode, each thread does all tasks from acquisition to logging 
|----------------------------------------------------------------------------------------
| PCAP_FILE         | single            | Single threaded pcap file mode 
|                   ---------------------------------------------------------------------
|                   | autofp            | Multi-threaded pcap file mode. Packets from each flow are assigned to a consistent detection thread 
|----------------------------------------------------------------------------------------
| PFRING(DISABLED)  | autofp            | Multi threaded pfring mode.  Packets from each flow are assigned to a single detect thread, unlike "pfring_auto" where packets from the same flow can be processed by any detect thread 
|                   ---------------------------------------------------------------------
|                   | single            | Single threaded pfring mode 
|                   ---------------------------------------------------------------------
|                   | workers           | Workers pfring mode, each thread does all tasks from acquisition to logging 
|----------------------------------------------------------------------------------------
| NFQ               | autofp            | Multi threaded NFQ IPS mode with respect to flow 
|                   ---------------------------------------------------------------------
|                   | workers           | Multi queue NFQ IPS mode with one thread per queue 
|----------------------------------------------------------------------------------------
| NFLOG             | autofp            | Multi threaded nflog mode   
|                   ---------------------------------------------------------------------
|                   | single            | Single threaded nflog mode  
|                   ---------------------------------------------------------------------
|                   | workers           | Workers nflog mode          
|----------------------------------------------------------------------------------------
| IPFW              | autofp            | Multi threaded IPFW IPS mode with respect to flow 
|                   ---------------------------------------------------------------------
|                   | workers           | Multi queue IPFW IPS mode with one thread per queue 
|----------------------------------------------------------------------------------------
| ERF_FILE          | single            | Single threaded ERF file mode 
|                   ---------------------------------------------------------------------
|                   | autofp            | Multi threaded ERF file mode.  Packets from each flow are assigned to a single detect thread 
|----------------------------------------------------------------------------------------
| ERF_DAG           | autofp            | Multi threaded DAG mode.  Packets from each flow are assigned to a single detect thread, unlike "dag_auto" where packets from the same flow can be processed by any detect thread 
|                   ---------------------------------------------------------------------
|                   | single            | Singled threaded DAG mode   
|                   ---------------------------------------------------------------------
|                   | workers           | Workers DAG mode, each thread does all  tasks from acquisition to logging 
|----------------------------------------------------------------------------------------
| AF_PACKET_DEV     | single            | Single threaded af-packet mode 
|                   ---------------------------------------------------------------------
|                   | workers           | Workers af-packet mode, each thread does all tasks from acquisition to logging 
|                   ---------------------------------------------------------------------
|                   | autofp            | Multi socket AF_PACKET mode.  Packets from each flow are assigned to a single detect thread. 
|----------------------------------------------------------------------------------------
| AF_XDP_DEV        | single            | Single threaded af-xdp mode 
|                   ---------------------------------------------------------------------
|                   | workers           | Workers af-xdp mode, each thread does all tasks from acquisition to logging 
|----------------------------------------------------------------------------------------
| NETMAP(DISABLED)  | single            | Single threaded netmap mode 
|                   ---------------------------------------------------------------------
|                   | workers           | Workers netmap mode, each thread does all tasks from acquisition to logging 
|                   ---------------------------------------------------------------------
|                   | autofp            | Multi-threaded netmap mode.  Packets from each flow are assigned to a single detect thread. 
|----------------------------------------------------------------------------------------
| DPDK(DISABLED)    | workers           | Workers DPDK mode, each thread does all tasks from acquisition to logging 
|----------------------------------------------------------------------------------------
| UNIX_SOCKET       | single            | Unix socket mode            
|                   ---------------------------------------------------------------------
|                   | autofp            | Unix socket mode            
|----------------------------------------------------------------------------------------
| WINDIVERT(DISABLED) | autofp            | Multi-threaded WinDivert IPS mode load-balanced by flow 
|----------------------------------------------------------------------------------------

可见一共就是 singleautofpworkers 三类。根据文档:

  • workers 模式下,有多个工作线程,每个线程都运行完整的 pipeline。数据包会均衡分配给各个工作线程。
  • autofp 模式下,有 packet capture 线程(负责捕获包、解码)和 packet processing 线程。pcap 文件默认使用这种模式处理。
  • single 模式类似于 workers 模式,但只有一个工作线程。通常用于开发。

那么,我们在 single 模式下,再次运行 Suricata,进行性能分析:

sudo ./suricata-dbg/src/suricata --runmode single -c ./suricata-dbg/suricata.yaml -s tplink.rules -r 100w.pcap
# i: suricata: This is Suricata version 7.0.8 RELEASE running in USER mode
# W: detect: No rule files match the pattern /usr/local/var/lib/suricata/rules/suricata.rules
# i: threads: Threads created -> W: 1 FM: 1 FR: 1   Engine started.
# i: suricata: Signal Received.  Stopping engine.
# i: pcap: read 1 file, 1000000 packets, 124916062 bytes

gprof ./suricata-dbg/src/suricata gmon.out > profile.txt  
python3 gprof2dot.py profile.txt | dot -Tsvg -o output.svg

结果: