使用 eBPF 和 无监督学习 进行异常行为检测
2022-8-17 15:0:14 Author: mp.weixin.qq.com(查看原文) 阅读量:22 收藏

开卷有益 · 不求甚解


前言

大家好,我希望你们在经历了两年的 Covid 和封锁之后度过了这个夏天:在这篇文章中,我将描述如何以创造性的方式使用 eBPF 系统调用跟踪,以便在运行时检测进程行为异常一种称为自动编码器的无监督学习模型。

异常

虽然许多项目通过构建允许的系统调用列表并在运行时检查进程是否使用此列表之外的任何内容来解决此问题,但我们将使用一种方法,不仅可以使我们免于显式编译此列表,而且还将考虑该进程使用系统调用的速度,通常允许但仅在每秒的特定使用范围内。这种技术可以潜在地检测进程利用、拒绝服务和其他几种类型的攻击。

你会像往常一样在我的 Github 上找到完整的源代码。

什么是 eBPF?

eBPF是一种允许在不使用内核模块的情况下拦截 Linux 内核运行时的多个方面的技术。eBPF 的核心是在内核中运行的虚拟机,它在加载 eBPF 程序操作码之前对其执行完整性检查,以确保运行时安全。

从eBPF.io页面:

eBPF(不再是任何东西的首字母缩写词)是一项革命性技术,起源于 Linux 内核,可以在特权上下文(如操作系统内核)中运行沙盒程序。它用于安全有效地扩展内核的功能,而无需更改内核源代码或加载内核模块。  

从历史上看,由于内核具有监督和控制整个系统的特权,操作系统一直是实现可观察性、安全性和网络功能的理想场所。同时,操作系统内核由于其核心作用和对稳定性和安全性的高要求,难以演进。因此,与在操作系统之外实现的功能相比,操作系统级别的创新率传统上较低。

ebpf
eBPF 从根本上改变了这个公式。通过允许在操作系统中运行沙盒程序,应用程序开发人员可以运行 eBPF 程序以在运行时向操作系统添加额外的功能。然后,操作系统保证安全性和执行效率,就像借助即时 (JIT) 编译器和验证引擎进行本地编译一样。这引发了一波基于 eBPF 的项目,涵盖了广泛的用例,包括下一代网络、可观察性和安全功能。

有几个选项可以编译成字节码然后运行 eBPF 程序,例如Cilium Golang eBPF 包、Aya Rust crate和IOVisor Python BCC 包等等。BCC 是最简单的,我们将在这篇文章中使用。请记住,所有这些库都可以完成相同的操作,只有运行时依赖项和性能会发生变化。

使用 eBPF 进行系统调用跟踪

使用 eBPF 跟踪系统调用的常用方法是在我们想要拦截的每个系统调用上创建一个跟踪点或 kprobe,以某种方式获取调用的参数,然后使用 perf 缓冲区或环将每个参数单独报告给用户空间缓冲区。虽然这种方法非常适合单独跟踪每个系统调用并检查它们的参数(例如,检查正在访问哪些文件或程序正在连接到哪些主机),但它有几个问题。

首先,根据系统架构和内核编译标志,读取每个系统调用的参数非常棘手。例如,在某些情况下,在进入系统调用时无法读取参数,但只有在系统调用执行后,通过保存来自 kprobe 的指针,然后从 kretprobe 读取它们。另一个重要的问题是 eBPF 缓冲吞吐量:当目标进程在短时间内执行大量系统调用时(想想一个承受很大压力的 HTTP 服务器,或者一个执行大量 I/O 的进程),事件可以迷失使这种方法不太理想。

穷人的方法

由于我们对系统调用参数不感兴趣,我们将使用不存在上述问题的替代方法。主要思想非常简单:我们将在sys_enter事件上设置一个跟踪点,每次执行任何系统调用时都会触发。我们不会立即通过缓冲区报告对用户空间的调用,而是只增加数组中的相对整数槽,创建直方图。

这个数组长 512 个整数(512 设置为系统调用的最大常数),因此在(例如)系统调用read(数字 0)执行两次和mprotect(数字 10)执行一次之后,我们将有一个向量/直方图看起来像这样:

2,0,0,0,0,0,0,0,0,0,1,0,0,0,.......

相对的 eBPF 非常简单,如下所示:

// defines a per-cpu array in order to avoid race coinditions while updating the histogram
BPF_PERCPU_ARRAY(histogram, u32, MAX_SYSCALLS);

// here's our tracepoint on sys_enter
TRACEPOINT_PROBE(raw_syscalls, sys_enter)
{
    // filter by target pid and return if this activity belongs to a process we'
re not interested in
    u64 pid = bpf_get_current_pid_tgid() >> 32;
    if(pid != TARGET_PID) {
        return 0;
    }

    // populate the histogram, args->id contains the system call number
    u32 key = (u32)args->id;
    u32 value = 0, *pval = NULL;
    pval = histogram.lookup_or_try_init(&key, &value);
    if(pval) {
        *pval += 1;
    }

    return 0;
}

到目前为止,没有将数据传输到用户空间,因此没有丢失任何系统调用调用,并且所有内容都在此直方图中进行了说明。

然后,我们将每 100 毫秒从用户空间对该向量执行一次简单的轮询,通过将向量与其先前状态进行比较,我们将计算每个系统调用的变化率:

# polling loop
while 1:
    # get single histogram from per-cpu arrays
    histogram = [histo_map[s] for s in range(0, MAX_SYSCALLS)]
    # if any change happened
    if histogram != prev:
        # compute the rate of change for every syscall
        deltas = [ 1.0 - (prev[s] / histogram[s]) if histogram[s] != 0.0 else 0.0 for s in range(0, MAX_SYSCALLS)]
        prev = histogram

    # ... SNIPPET ...

    time.sleep(args.time / 1000.0)

这不仅会考虑执行了哪些系统调用(以及未执行的系统调用,因此计数器始终为 0),而且还会考虑它们在正常活动期间在给定时间内执行的速度。

一旦我们将这些数据保存到 CSV 文件中,我们就可以训练一个能够在运行时检测异常的模型。

使用自动编码器进行异常检测

自动编码器是用于无监督学习任务的人工神经网络,能够创建未标记数据的内部表示(因此是“无监督”)并产生相同大小的输出。这种方法可以用于数据压缩(因为内部编码层通常小于输入),当然也可以用于异常检测,就像我们的例子一样。

自动编码器

来源:https://lilianweng.github.io/posts/2018-08-12-vae/

主要思想是训练模型并使用我们的 CSV 数据集作为网络的输入和所需的输出。这样,ANN 将通过正确重建每个向量来了解数据集中的“正常”内容。当输出向量与输入向量有很大不同时,我们就会知道这是一个异常,因为人工神经网络没有经过训练来重建这个特定的向量,这意味着它超出了我们认为的正常活动范围。

我们的自动编码器有 512 个输入(定义为MAX_SYSCALLS常数)和相同数量的输出,而内部表示层的大小只有一半:

n_inputs = MAX_SYSCALLS

# 输入层
inp = Input(shape=(n_inputs,)) 
# 编码器层
encoder = Dense(n_inputs)(inp) 
encoder = ReLU()(encoder) 
# 内部表示层
middle = Dense( int (n_inputs / 2 ))( encoder) 
# 解码器层
decoder = Dense(n_inputs)(middle) 
decoder = ReLU()(decoder) 
decoder = Dense(n_inputs, activation= 'linear' )(decoder) 
m = Model(inp, decoder)

# 我们使用均方误差作为损失函数,因为我们对重建误差
m.compile(optimizer='adam', loss='mse')

为了训练,我们的 CSV 数据集分为训练数据和测试/验证数据。训练后,后者用于计算模型为“正常”数据呈现的最大重建误差:

# 在测试数据上测试模型以计算误差阈值
y_test = model.predict(test
test_err = [] 
# for each vector 
for ind in  range ( len (test)): 
  # 得到绝对误差作为输入的差并重建输出    
  abs_err = np. abs (test[ind, :]-y_test[ind, :]) 
  # 附加每个单独错误的总和
  test_err.append(abs_err.sum ()) 

# 阈值将是我们发现的最大累积错误
threshold = max (test_err)

我们现在有一个自动编码器及其参考错误阈值,我们可以使用它来执行实时异常检测。

例子

让我们看看程序的实际效果。对于这个例子,我决定监控SpotifyLinux 上的进程。由于其高 I/O 强度,Spotify 代表了这种方法演示的一个很好的候选者。我在播放一些音乐并点击播放列表和设置时捕获了训练数据。我在学习阶段没有做的一件事是点击Connect with Facebook按钮,这将是我们的测试。由于此操作会触发 Spotify 通常不执行的系统调用,因此我们可以使用它来检查我们的模型是否真的在运行时检测到异常。

从实时过程中学习

假设 Spotify 的进程 ID 为 1234,我们将从在使用它时捕获一些实时数据开始:

sudo ./main.py --pid 1234 --data spotify.csv --learn

尽可能多地保持这个运行,尽可能多的样本是我们的模型准确检测异常的关键。一旦您对样本数量感到满意,您可以按 Ctrl+C 停止学习步骤。

您的spotify.csv数据集现在已准备好用于训练。

训练模型

我们现在将模型训练 200 个 epoch,您会看到验证损失(重建向量的均方误差)在每一步都在减少,这表明模型确实在从数据中学习:

./main.py --data spotify.csv --epochs 200 --model spotify.h5 --train

训练完成后,模型将保存到spotify.h5文件中,并在屏幕上打印参考误差阈值:

...
时代 195/200 
60/60 [==============================] - 0s 2ms/步 - 损失:1.3071e-05 - val_loss:6.3671e-05
Epoch 196/200 
60/60 [=============================] - 0s 2ms/步 - 损失:1.8221e-05 - val_loss:5.2383e-05
Epoch 197/200 
60/60 [====================== =======] - 0s 2ms/步 - 损失:9.2132e-06 - val_loss:5.3354e-05
Epoch 198/200 
60/60 [=============== ===============] - 0s 2ms/step - loss: 9.2722e-06 - val_loss: 4.9380e-05 
Epoch 199/200 
60/60 [======= =======================] - 0s 2ms/step - loss: 8.0692e-06 - val_loss: 5.1954e-05 
Epoch 200/200 
60/60 [==============================] - 0s 2ms/step - loss: 8.3448e-06 - val_loss: 5.0102e- 05
模型保存到 spotify.h5,获得 106 个样本的错误阈值......

错误阈值=9.969912

检测异常

一旦模型经过训练,它就可以用于实时目标进程来检测异常,在这种情况下,我们使用 10.0 的错误阈值:

sudo ./main.py --pid 1234 --model spotify.h5 --max-error 10.0 --run

当检测到异常时,累积错误将与前 3 个异常系统调用及其各自的错误一起打印。

在此示例中,我单击了Connect with Facebook将使用系统调用的按钮,例如getpriority以前在训练数据中看不到的系统调用。

我们可以从输出中看到模型确实在检测异常:

错误 = 30.605255 - 最大值 = 10.000000 - 前 3:
  b'getpriority' = 0.994272 
  b'writev' = 0.987554 
  b'creat' = 0.969955
成功

结论

这篇文章展示了如何通过使用相对简单的方法并放弃一些系统调用规范(参数),我们可以克服性能问题,并且仍然能够捕获足够的信息来执行异常检测。如前所述,这种方法适用于多种场景,从由于错误导致的简单异常行为到拒绝服务攻击、暴力破解和目标进程的利用。

系统的整体性能可以通过使用诸如Aya之类的本地库及其准确性以及模型的一些超参数调整以及更精细的每个特征错误阈值来提高。

所有这些东西都留给读者作为练

哈哈

译文申明

  • 文章来源为近期阅读文章,质量尚可的,大部分较新,但也可能有老文章。
  • 开卷有益,不求甚解,不需面面俱到,能学到一个小技巧就赚了。
  • 译文仅供参考,具体内容表达以及含义, 以原文为准 (译文来自自动翻译)
  • 如英文不错的,尽量阅读原文。(点击原文跳转)
  • 每日早读基本自动化发布(不定期删除),这是一项测试

最新动态: Follow Me

微信/微博:red4blue

公众号/知乎:blueteams



文章来源: http://mp.weixin.qq.com/s?__biz=MzU0MDcyMTMxOQ==&mid=2247486833&idx=1&sn=3e74cfb12ed33b805a835bce1ab3bfb4&chksm=fb35a4b9cc422daf0013959b07e0cf70c1cb499357ed24729c456ce957f4e0c5caefc57f1f84#rd
如有侵权请联系:admin#unsafe.sh