关于该项目
在阅读了一篇关于0days 用 30 行代码 fuzzingradare2的文章后,我认为扩展这项研究并将其代码移植到容器并部署到 Kubernetes 集群中将是一个有趣的周末项目。更进一步,构建 radare2 项目的主分支的新版本,并将其集成到 CI/CD 管道中,然后将容器构建部署到 Kuberentes 集群,这似乎是一种真正过火的好方法,只是吃掉我的全部周末。最终结果最终看起来类似于下图。
本博客将重点介绍如何加速哑模糊测试,并讨论我在使用 Kubernetes 扩展哑模糊测试工具时遇到的一些问题。
最无效的 Fuzz 方法
那么让我们来谈谈房间里的大象,我们为什么要哑 fuzz radare2 呢?那么什么是愚蠢的模糊测试?简而言之,模糊测试的核心是向给定的目标二进制文件(在本例中为radare2)抛出格式错误的输入,以希望导致崩溃。到那时,我们将检查崩溃,看看它是否可以用于本地 DoS 之外的东西。
现在,关于这个格式错误的输入的“愚蠢”部分是我们只是从我们的二进制语料库中任意操作二进制的随机字节,然后在二进制上调用radare2 来处理和分析它。这是在之前链接的tmp.out 文章中的以下代码行中完成的:
代码语言:javascript复制def mutate(data):
mutable_bytes = bytearray(data)
for a in range(30):
r = random.randint(0, len(mutable_bytes)-1)
mutable_bytes[r] = random.randint(0,254)
为什么这是一种无效的模糊测试技术?好吧,底层文件格式(PEs/ElFs/etc...)的知识是广泛可用的,我们可以专注于以更智能的方式操作这些二进制文件的相应标头,以针对目标二进制代码的特定部分。例如,也许我们关心给定二进制文件的 ELF 导入解析部分的最新更改。在这种情况下,我们可以围绕 .dynsym/.dynamic/etc... 部分构建我们的突变。
对一些随机字节进行愚蠢模糊测试的好处是设置起来非常容易。正如原作者在他们的文章中详述的那样,这里的工作流程只是:
- 从二进制语料库中读取二进制文件
- 随机更改二进制文件的一些字节。
- 让 Radare2 尝试分析具有给定超时值的二进制文件。
如果超时或发生崩溃,则会生成一个非零状态代码,指示“发生了一些有趣的事情”,让我们保存这个修改后的二进制文件以供以后手动分析。
为了回答本文开头提出的问题“如果它如此无效,为什么还要愚蠢的模糊?”,美丽在于简单。如果您有备用的 Raspberry Pi 或任何旧计算机,为什么不只是被动地让 CPU 对一些数据进行“brrr”处理,看看是否会产生任何有趣的崩溃?这是一个很好的方式让你的脚趾浸入模糊测试的水中,然后冒险探索一些更高级的主题,比如Gamozo Labs、Fuzzing Labs和H0mbre等人在各自的平台上涵盖的内容。
Fuzzing Radare2 的最新和最好的版本
现在我知道你在想什么“为什么在 $NEW_CI_TOOL 存在时使用 Jenkins?”。詹金就像 1985 年的 F150。我知道当我开车时我会进入什么状态。归根结底,所有这些工具都是美化的远程代码执行实用程序,而 Jenkins 提供了做奇怪事情的灵活性。简单地说,我使用 Jenkins 是因为我喜欢 Jenkins。
Radare2 是一个快速发展的平台,在 Github 上有一个活跃的社区。为了确保我对最新和最棒的构建进行模糊测试,我创建了一个 Jenkins 作业来定期从主分支中提取和构建radare2 bin,然后发布工件。一旦发布了工件,就会触发一个单独的作业以从 Jenkins 服务器获取工件,然后构建一个新的 Docker 容器,该容器具有来自原始 tmp.out 文章的哑 fuzzer 工具以及二进制文件的语料库。Jenkins 的默认状态页面显示两个构建都在 19 小时前成功运行,并且在我进行故障排除时几天前都出现了一些问题。
最后,通过 webhook 向 Discord 服务器发送通知,通知我radare2 构建成功并且图像可用。我发现 webhooks Discord 是将通知引入辅助项目的最简单方法,但如果您在 homelab 中有其他东西的警报设置,您也可以将其集成到那里。
Minikube - 更快地原型 K8s 项目
fuzzing 工具本身是一个简单的 Python 应用程序。在讨论扩展模糊测试或任何应用程序时,在考虑底层工作负载类型时需要考虑很多事情。简单地添加更多 CPU 并不是一个好主意,因为它不能解决应用程序中可能存在的潜在瓶颈。但是,这不是生产工作负载,也不是生产应用程序,所以让我们看看通过让几个实例在少数节点上运行来扩展模糊应用程序的可能最糟糕的*方法。这就是 Kubernetes 发挥作用的地方。
有几种不同的以“开发者”为中心的 K8s 发行版。由于底层运行时和虚拟化驱动程序的灵活性,我非常喜欢用于本地开发的minikube 。默认情况下,它使用 Docker 来建立控制平面,但您可以选择 KVM 并启动专用虚拟机来运行您的工作负载。这还允许您安排和关闭不同的节点,以查看 pod 是否按照您的预期重新分配。通过minikube node add
或来添加和删除节点也很容易minikube node delete
。为了将愚蠢的模糊测试扩展到令人难以置信的高度,我在我的 KVM 主机上部署了五个节点。
部署清单利用 KVM 主机上的底层卷挂载到容器的 cdir(崩溃目录)中,以便将任何成功的崩溃保存到底层主机存储中。据我所知,这是在 Minikube 中实现崩溃持久存储的最简单方法。
代码语言:javascript复制apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: dumb-fuzzer
name: dumb-fuzzer
spec:
replicas: 20
selector:
matchLabels:
app: dumb-fuzzer
template:
metadata:
labels:
app: dumb-fuzzer
spec:
containers:
- image: 192.168.39.136:5000/fuzzer
name: fuzzer
volumeMounts:
- mountPath: /opt/fuzzer/cdir
name: fuzzer-vol
volumes:
- name: fuzzer-vol
hostPath:
path: /opt/fuzzer/
type: DirectoryOrCreate
Minikube 有一个 Docker 注册表插件,如果您在集群启动期间指定不安全的注册表将在给定地址上侦听,所有节点都可以使用该插件。minikube start --insecure-registry="192.168.0.0/16"
(将 CIDR 范围设置为您的虚拟网络)。这使得在您的开发机器上构建、推送和部署容器工作负载的一体化开发环境变得非常容易。新构建的 docker fuzzingharness 镜像构建完成后的最终 Jenkins 构建执行rollout restart以获取 Kubernetes 部署中的新 Docker 镜像。下图显示了在二进制文件上运行和搅动的 pod。
有趣和不那么有趣的崩溃
在让愚蠢的模糊器在二进制语料库上运行几个小时后,产生了许多崩溃。然而,大多数人实际上是自我影响的。起初,我尝试部署 100 个 pod,但发生的情况是底层节点上的资源耗尽导致进程死亡,并且该工具将二进制文件保存为崩溃,而实际上它只是内存不足。所以是的,确实发生了崩溃,但它主要是自我强加的。在将工作负载调整为更合理的 30 个 pod 后,生成了更多文件,但同样,并非所有文件都导致崩溃。模糊测试工具的一个关键组成部分是超时值subprocess.run
如果二进制文件只是长时间挂起进程,它将被视为崩溃并保存到崩溃目录(cdir)。因此,现在是检查这些文件的手动部分,以查看它是否确实是崩溃或是否导致radare2 挂起。
try:
p = subprocess.run(['r2','-qq', '-AA','wdir/tmp'], stdin=None, timeout=10)
except:
return
起初我认为“挂起”是由于文件大小造成的,但仔细一看,这是一个 2.5k 的文件,导致等待时间超过 5 分钟,直到 Radare2 自行终止。这很有趣,绝对值得深入研究,因为人们可以将其视为一种反分析技术,或者可能是冰山一角,如果操作得当,可能会使目标二进制文件崩溃。
最后,在验证 Kubernetes 集群生成的崩溃时,我确实遇到了一个实际上导致立即崩溃的二进制文件。成功!从愚蠢的模糊冒险中真正崩溃。现在是调查的时候了!
分析崩溃
现在我们已经成功崩溃了,我们必须确定潜在的问题。这个特定的文件是一个 MS-DOS 可执行文件,NE(未知 OS 0)。了解格式很重要,因为这将与我们在radare2 项目中检查的C 文件相关联。对来自语料库的原始文件的前 50 行十六进制输出进行快速比较,我们可以看到我们的模糊工具操纵了哪些字节的漂亮图像。请记住,线束只是任意操纵字节,没有押韵或理由选择一个或另一个。
现在让我们将它加载到 gdb 并查看堆栈跟踪。
代码语言:javascript复制gdb> set args -qq -AA crash_<UNIQUE_ID>
gdb> r
这导致成功复制了崩溃,现在我们可以查看回溯。
代码语言:javascript复制gdb> bt
检查第 2 帧,我们看到这是我们去 libc 之前的最后一段radare2 代码。所以让我们检查一下
代码语言:javascript复制gdb> b ne.c:52
在崩溃之前,我们两次击中了先前的突破点。现在,当再次遇到断点时,我们可以单步执行,直到导致问题的代码。
果然,它似乎是__func_name_from_ordstrdup
中的函数调用。根据手册页,此函数调用根据指定为参数的字符串返回指向新字符串的指针。通过 gdb 重新执行程序并检查“模块”参数,我们第一次看到实际上模块中有数据。有趣的是,在 tmp.0ut 的原始研究文章中,strdup 也是导致作者获得两个 CVE(CVE-2020-16269、CVE-2020-17487)的崩溃源。
查看模块参数值的差异,我们可以看到在遇到第二个断点时访问内存的问题,从而导致崩溃。
在执行 strdup 之前,应检查模块以确保它是有效的字符串。为了验证我对这个错误的理解,我创建了一个快速的临时程序来在较小的范围内重新创建问题。
代码语言:javascript复制int main() {
char *tmp = "hello world";
char *tmp2 = NULL;
char *test=strdup(tmp);
printf("%sn", test);
char *test2 =strdup(tmp2);
printf("%sn", test2);
return 0;
}
在打印“hello world”时,tmp2 将导致崩溃作为基础值。通过在 gdb 中逐步执行应用程序的最终验证显示 strdup 存在相同的问题并且无法访问内存。
最后,我们对崩溃是如何发生的有了一个很好的了解,但是为什么?让我们退后一步,看看数据是如何传递给易受攻击的函数的。
数据如何传递给易受攻击的函数
易受攻击的函数__func_name_from_ord只在一个文件中被调用,ne.c特别是在第 488 行。这个函数调用在一个迭代程序段的存根中。在某些情况下 name不会设置为任何值,因此 strdup 没有字符串可以复制。这显示在下面的代码片段中。幸运的是,这是一个简单的解决方法,并且PR 已入站!
结论
从 Kuberentes 到 GDB,这个项目涵盖了我在空闲时间喜欢搞砸的事情。虽然愚蠢的模糊测试并不是发现漏洞的最有效方法,但它确实可以在“下班时间”使用 CPU。想想有些人不在电脑前时是如何挖掘加密货币的。也许他们得到了一些硬币,也许他们没有,但无论如何他们的 CPU 都不会闲着。我认为这是看待它的最佳方式。您还必须从某个地方开始,并且进入门槛低。感谢您花时间阅读本文,如果我有什么问题,请在 Twitter 上与我联系 :) ( @DLL_Cool_J )。
下面是我对当前 Minikube 设置问题的一些额外想法,我无法插入博客。
- 问题:大型 Docker 映像拥有一个专用的测试二进制语料库,通过 NFS 共享安装到节点上,然后安装到底层容器中,这将大大减少 Dockerimage 的大小。我只是在“用剪刀跑步”,这个周末没有花时间去做。
- 问题:复制崩溃 Minikube 会为每个部署的 kvm 节点创建一个专用的 ssh 密钥对。这可以通过 获得
minikbue ssh-key -n NODE_NAME
。然而,更“Kubernetes 方式”将是有一个边车应用程序,用于将崩溃推送到特定的中心位置。也许对于 S3,甚至只是将 NFS 共享作为容器内的单独挂载。