Auditd字段欺骗:从内核到用户空间的攻防解析

本文深入探讨Linux Auditd审计系统中auid字段的安全特性,通过分析内核源码揭示其运作机制,并演示如何利用CAP_AUDIT_CONTROL能力修改审计标识,最后提出针对性的检测与防护建议。

Auditd字段欺骗:现在你看到我,现在你看不到

引言

2022年6月的一个夜晚,Ethan向团队发出询问:“有人知道如何在Linux上欺骗Auditd吗?我正在尝试修改auid(审计用户ID)字段。即使用户使用su或sudo切换身份,这个字段也保持不变(其他用户ID字段会跟踪这些变化)。”

Ethan还提供了一个参考链接来说明他正在研究的内容。虽然我对Auditd的了解并不深入,但作为Linux的忠实粉丝(也是足够愚蠢的人),我决定加入研究。

Ethan的原始目标是什么?简而言之,SOC团队正在构建一个“金丝雀”脚本,用于欺骗给定的auid,并确定在编写Linux系统检测规则时auid值是否可靠。如果auid字段在用户切换用户(su)或以提升权限运行命令(sudo)时保持跟踪,那么修改此字段的能力可能对攻击者具有潜在价值。

基础知识

在开始研究之前,我们首先需要了解auid字段是什么,如何访问,以及关键的是,如何修改它?

根据Ethan发布的文档链接,auid字段似乎源自uid(用户标识符)字段,旨在在进程的子进程中保持一致,即使子进程的uid被修改。上下文上,uid为0表示root用户,uid值低于1000通常是系统账户,而从1000开始的uid值代表标准用户账户。etc/passwd文件包含uid和用户名之间的映射。这些信息也可以通过getent passwd命令查看。

现在,我们有机会做一件我喜欢找借口做的事情:查看内核代码。为此,我将使用elixir.bootlin.com浏览内核源代码。首先,让我们寻找auid字段的使用位置、初始设置位置以及可能修改的位置。从搜索结果来看,auid字段的日志记录从/kernel/audit.c的第1600行开始(基于内核版本6.0.19)。auid字段通过函数调用audit_getloginuid(current)检索。

“auid”字段在审计日志中的位置

既然我们知道日志服务如何检索auid字段,让我们寻找audit_get_loginuid函数。该函数在审计头文件/include/linux/audit.h中定义和实现。从第199行开始的函数实现中,该函数接受一个指向任务结构(task_struct)的指针,并返回其中的一个名为loginuid的字段。

“audit_get_loginuid”函数的内容

我们越来越接近底层,现在是时候在任务结构定义中找到该字段了。任务结构可以在/include/linux/sched.h中找到,用于包含进程信息。loginuid字段被定义为一个结构字段,但仅当定义了CONFIG_AUDIT时才有意义——如果系统未配置审计,我们不需要loginuid字段(或相关字段)。

审计特定的“loginuid”任务结构字段

从这里开始,我构思了写入该字段的方法。考虑到此任务的上下文,内核补丁甚至可加载内核模块感觉有点过于硬核,因此我认为值得尝试从用户空间解决这个问题。不幸的是,这标志着本次冒险的内核源代码探索部分结束。请记得填写导游评论调查。

在获得对auid字段的一些了解后,我开始寻找修改它的方法。最终,我找到了一个GitHub提交,详细说明了如何(以及在什么条件下)可以修改该字段。

Auditd提交详细说明登录UID可变性

根据该提交的所有信息,当前进程的auid字段应该可以通过/proc/self/loginuid进行可变,只要权限有效,设置了CAP_AUDIT_CONTROL Linux能力,并且未设置AUDIT_FEATURE_LOGINUID_IMMUTABLE和AUDIT_FEATURE_ONLY_UNSET_LOGINUID内核配置选项。有了所有这些信息,我们应该准备好开始解决问题了。

现在你看到我

最初的内核探索很有启发性,但总体上缺乏上下文。为了获得一些上下文,让我们看看审计条目是如何记录的。请注意,下面显示的内容已稍作修改,以省略无关的周围日志条目,使输出更清晰、更容易跟踪。

日志文件信息可以在/var/log/audit/audit.log中找到(至少在RHEL系统上)。我首先运行sudo ls,然后运行一个命令来打印日志文件的最后六行。请注意,记录的可执行文件是/usr/bin/sudo,相关命令是ls,这符合我们刚刚运行的内容。

使用sudo记录,UID和AUID一致

那么,当我们运行sudo su时,会记录什么?正如我们可能预期的那样,审计日志显示了一个命令执行的条目。往下几个条目,我们看到uid字段已更新为0(root),但auid字段仍与原始用户相同。

使用sudo记录,UID更改,AUID一致

在这里,我们到达了任务:一个简单的程序,假设我们有sudo权限,允许我们通过/proc/$pid/loginuid伪文件手动设置auid字段。

现在你看不到

既然我们已经充分研究了任务,让我们先从简单的想法开始。如果我们能写入伪文件,我们甚至需要编程吗?尽管我喜欢用代码解决问题,但这最初不是我的问题,所以我会稍微小心一些(就这一次)。

简单的方法?

首先,如果我们直接写入文件会发生什么?我们应该有必要的权限来写入我们自己的进程,对吧?

写入操作不允许

显然不行。尽管这不是权限被拒绝的错误,那将表示访问错误。让我们暂时搁置这一点。如果我们echo新值,然后以sudo身份将该内容写入文件呢?

不成功的写入尝试

那次没有错误,但仍然没有成功。

又一次不成功的写入尝试

快速补充:您可能想知道在使用sudo时,每个命令和每个用户上下文生成不同进程时,/proc/self是否可靠。我可以从测试中说,使用当前终端的特定PID时,行为是相同的。

更有趣的是,root用户可以修改自己的loginuid文件,但不能修改其他用户进程拥有的文件。Ethan还确认,尝试给cat或tee赋予CAP_AUDIT_CONTROL能力也没有更好效果。所有这些表明,我最终还是得用代码解决问题。

至此,终于到了开始编写代码的时候了。这里是下一部分的一个有趣“饮酒游戏”:每次读到“能力”或“能力”时,喝一小口水,确保您保持水分充足……

困难但有趣的方式!

在开始编码之前快速说明:在继续之前,您需要确保系统上安装了libcap-dev(Debian)或libcap-devel(RHEL)包,否则必要的头文件将不存在。

除了一些初步的参数验证外,我想出的代码主要归结为以下五个主要组件,其中几个主要是错误检测和如果出现问题则短路:

  1. 获取进程能力,确保支持CAP_AUDIT_CONTROL。
  2. 设置进程能力以启用CAP_AUDIT_CONTROL能力。
  3. 将新的auid字段写入/proc/self/loginuid伪文件。
  4. 验证新的auid字段是否已正确写入。
  5. 生成shell。

鉴于我们需要特定的能力来修改auid字段,第一步是检索我们的进程能力,并确定CAP_AUDIT_CONTROL能力是否在我们的进程中启用。我们如何实现这一点?在没有立即熟悉能力编程的情况下,我在终端中运行了apropos capability,并收到了一些结果。

搜索相关手册页

检查其中一个结果的手册页,似乎大多数都指向头文件sys/capabilities.h。cap_clear页面的“参见”部分包括对cap_get_proc的引用,这是我最终用来检索进程能力的函数。这里关键要注意的是,包含sys/capabilities.h需要代码使用-lcaps编译,以便链接到能力库。

既然我们知道用什么来检索我们的进程能力,检查CAP_AUDIT_CONTROL能力是否在我的系统上受支持将很有用。方便的是,cap_get_proc的手册页包括一个可预测名为CAP_IS_SUPPORTED的宏。手册页描述如下:“CAP_IS_SUPPORTED(cap_value_t cap)提供一个评估为真(1)的函数,如果系统支持指定的能力cap。如果系统不支持该能力,此函数返回0。”

有了这些信息,我们可以构建程序的第一个主要部分:将进程能力存储在名为caps的变量中,并检查CAP_AUDIT_CONTROL支持。

段1 – 能力检索和枚举

在代码的这一点上,我们现在有很好的信心认为所需能力至少系统支持。下一步涉及我们创建一个包含CAP_AUDIT_CONTROL的新能力列表,使用cap_set_flag()函数启用它,然后使用cap_set_proc()函数将更新的能力写入进程。

段2 – 能力构建和写入

随着能力写入,是时候获取并将新的auid值写入/proc/self/loginuid了。为此,我们使用标准文件I/O函数打开文件,并将我们的第一个也是唯一一个命令参数写入文件。

段3 – 文件打开和新AUID写入

您可能注意到我们在上一段中没有关闭使用的文件。这是因为下一段涉及从文件中读回新值以确保内容成功写入。这一步并非严格必要,但考虑到我们在没有代码的情况下修改值时遇到的麻烦,我认为安全总比后悔好。一旦我们验证值已正确设置,我们关闭文件并使用cap_free()函数释放为caps变量分配的任何内存。

段4 – 文件内容验证和内存管理

最后,我们使用execve()函数生成一个简单的/bin/bash shell。没有什么华丽或特别安全,但它完成了工作。

段5 – shell生成

代码大致如此。您都明白了吗?很好。

演示

使用gcc -lcaps capybara.c -o capybara编译代码后,我们可以通过以下方式看到它正在工作:

成功修改AUID字段

好吧,这都很好,但更新进程看到的数字只是我们任务的前半部分。我们真正要做的是更改记录到审计文件的值!

再次运行代码并运行sudo whoami(用于演示目的)后,我们可以输出/var/log/audit/audit.log的一些内容,并添加一个俏皮的Perl正则表达式以增加趣味。查看两个突出显示的审计日志行的第一个,我们可以看到我在与Ethan最初开发时未能注意到的东西:有一个old-auid字段!该字段仅在字段被修改时出现。在第二个突出显示的日志文件条目上,我们看到whoami命令以“正确”的auid值“12345”执行,没有提到旧的auid值。

解析的Auditd日志条目显示“old-auid”字段

因此,分析人员有一条途径可以检测Auditd恶作剧:检查变化的old-auid字段以跟踪可能变化的用户ID。也就是说,Ethan后来向我提到,LOGIN事件没有在此特定环境的SIEM中呈现,这给日志可见性问题增加了另一个皱纹。

结论

我想我们应该用一些离别 thoughts 来结束。

在检测方面,确保您的SIEM从Auditd接收LOGIN事件,以便您能意识到任何篡改。在可能的修复方面,请注意前面显示的GitHub提交还讨论了名为AUDIT_FEATURE_LOGINUID_IMMUTABLE和AUDIT_FEATURE_ONLY_UNSET_LOGINUID的配置选项,分别防止覆盖和取消设置auid字段。从企业Linux的角度来看,我想这些将是启用的良好配置。也就是说,我查看了直到Linux 6.1.3的内核版本的配置选项,在粗略搜索中未能找到这两个配置选项。

显然,有auditctl命令可以设置内核不可变性(-e 2)和常规loginuid不可变性(–login-immutability),但到目前为止,我未能让任一命令阻止此技术工作。

总而言之,这项工作并不代表什么突破性的发现,但似乎为Ethan和SOC团队的其余成员带来了满意的结果。更重要的是,这应作为在编写围绕Auditd设施的检测时谨慎的理由。根据给定组织编写检测或监控审计信息的方式,攻击者可能利用类似策略在未配备检测变化auid值的环境中有效消失。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计