在Linux上创建“双面”Rust二进制文件的技术解析

本文详细介绍了如何在Linux环境下创建具有双重行为的Rust二进制文件,通过主机环境数据派生密钥解密隐藏代码,并探讨了内存执行和反检测技术。

在Linux上创建“双面”Rust二进制文件

问题陈述

假设您希望在特定目标机器上运行恶意程序。一种方法是广泛分发该程序,并期望目标最终运行它。虽然具体分发途径不在本文讨论范围内,但可以想象一个预编译的二进制文件,就像开发人员经常在GitHub项目页面下载的那样。

为了最大化到达目标的机会,您可能希望模仿无害程序的行为,并避免任何可能触发检测的可疑行为(例如连接到C&C服务器)。

设计我们的"精神分裂"二进制文件

在本文中,我们将目标主机上要运行的程序称为"隐藏"程序,在其他机器上运行的无害程序称为"正常"程序。

基本方法是在程序启动时早期决定实际运行什么代码:

1
2
3
4
5
if is_running_on_target_host() {
    hidden_program();
} else {
    normal_program();
}

这种方法在基本运行时检测方面有效,但存在以下问题:

  • 隐藏程序仍会在内存中存在并可观察
  • 二进制文件可以被分析和反汇编,暴露"隐藏"程序
  • is_running_on_target_host暴露了我们针对的目标

改进方案是使用加密方法,但不直接将密钥与加密程序一起存储,而是从目标机器的唯一主机数据派生密钥。

程序启动步骤:

  1. 从主机提取唯一识别目标的数据
  2. 使用HKDF将嵌入二进制文件的密钥与先前的主机数据派生新密钥
  3. 使用派生密钥解密嵌入的"隐藏"加密二进制数据
  4. 如果解密成功,运行解密的"隐藏"程序,否则运行"正常"程序

选择派生信息

用于识别目标主机并派生密钥的数据需要仔细选择,需要满足:

  • 足够唯一
  • 随时间稳定
  • 难以被无法访问目标机器的人猜测

候选数据包括:

  • 用户UID:不够唯一,大多数工作站用户值为1000
  • WAN接口IPv6:可能不稳定
  • 硬件序列号:需要root权限读取
  • CPU型号:在虚拟机、公司笔记本电脑群中可能不够唯一
  • 磁盘分区UUID:创建分区时生成的实际随机值,具有良好的熵和唯一性

构建时代码

为了方便开发人员使用,我们将所有这些逻辑集成到单个twoface Rust crate中。Rust除了是现代系统级语言外,还对构建时代码有很好的支持。

我们的库有两个主要部分:

  • 构建时部分:控制"隐藏"二进制文件的加密,并生成要嵌入的数据
  • 运行时部分:执行解密处理,并将执行分派到"正常"或"隐藏"二进制文件

构建过程:

1
2
3
4
5
use std::io;

fn main() -> io::Result<()> {
    twoface::build::build::<twoface::host::HostPartitionUuids>()
}

构建时需要设置环境变量:

1
2
3
4
export TWOFACE_HOST_INFO="/path/to/host_partition_uuids.json"
export TWOFACE_NORMAL_EXE="/path/to/normal_exe"
export TWOFACE_HIDDEN_EXE="/path/to/hidden_exe"
cargo build

构建时步骤:

  1. 加载"正常"可执行文件,生成常量数组供运行时使用
  2. 加载"隐藏"可执行文件并压缩
  3. 从TWOFACE_HOST_INFO文件加载主机数据
  4. 生成随机密钥,生成常量数组供运行时使用
  5. 使用步骤3的主机数据派生密钥
  6. 使用派生密钥加密"隐藏"可执行文件的压缩数据,生成常量数组供运行时使用

从内存运行

我们使用memfd_create系统调用创建不由文件支持的文件描述符。目标二进制文件写入后,fexecve系统调用将用新程序映像替换当前进程映像。

添加另一层乐趣

为了改进写入过程,我们使用不同的方式将解密的"隐藏"程序ELF数据写入目标文件描述符:

  • 使用io_uring:不发出write系统调用
  • 通过mmap内存段:无需跟踪写入,但需要许多系统调用来映射/取消映射每个块
  • 回退到经典write:解密的文件数据仍不会在进程内存中,但write调用容易被跟踪

结果

完整代码可在https://github.com/synacktiv/twoface查看,包含:

  • 示例"无害"/“正常"二进制文件
  • “隐藏”/“恶意"二进制文件
  • twoface库
  • 测试示例

运行test-example将:

  1. 构建"无害"二进制文件
  2. 构建"恶意"二进制文件
  3. 从example/host.json加载分区UUID
  4. 构建包含"无害"和"恶意”(加密)ELF的示例二进制文件
  5. 运行它以查看实际运行的是哪一个

结论

这个概念验证展示了如何利用Rust构建时代码功能创建先进且开发人员友好的机制,实现我们的"双面"二进制文件。

为进一步推进,我们可以:

  • 添加构建时混淆
  • 添加运行时反调试技术
  • 使用内存中的主机特定数据派生密钥
  • 链式多个加载器级别,每个使用不同的派生数据源
  • 使用userfaultfd动态解密ELF内存页
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计