在Linux上创建“双面”Rust二进制文件
问题陈述
假设您希望在特定目标机器上运行恶意程序。一种方法是广泛分发该程序,并期望目标最终运行它。虽然具体分发途径不在本文讨论范围内,但可以想象一个预编译的二进制文件,就像开发人员经常在GitHub项目页面下载的那样。
为了最大化到达目标的机会,您可能希望模仿无害程序的行为,并避免任何可能触发检测的可疑行为(例如连接到C&C服务器)。
设计我们的"精神分裂"二进制文件
在本文中,我们将目标主机上要运行的程序称为"隐藏"程序,在其他机器上运行的无害程序称为"正常"程序。
基本方法是在程序启动时早期决定实际运行什么代码:
|
|
这种方法在基本运行时检测方面有效,但存在以下问题:
- 隐藏程序仍会在内存中存在并可观察
- 二进制文件可以被分析和反汇编,暴露"隐藏"程序
is_running_on_target_host暴露了我们针对的目标
改进方案是使用加密方法,但不直接将密钥与加密程序一起存储,而是从目标机器的唯一主机数据派生密钥。
程序启动步骤:
- 从主机提取唯一识别目标的数据
- 使用HKDF将嵌入二进制文件的密钥与先前的主机数据派生新密钥
- 使用派生密钥解密嵌入的"隐藏"加密二进制数据
- 如果解密成功,运行解密的"隐藏"程序,否则运行"正常"程序
选择派生信息
用于识别目标主机并派生密钥的数据需要仔细选择,需要满足:
- 足够唯一
- 随时间稳定
- 难以被无法访问目标机器的人猜测
候选数据包括:
- 用户UID:不够唯一,大多数工作站用户值为1000
- WAN接口IPv6:可能不稳定
- 硬件序列号:需要root权限读取
- CPU型号:在虚拟机、公司笔记本电脑群中可能不够唯一
- 磁盘分区UUID:创建分区时生成的实际随机值,具有良好的熵和唯一性
构建时代码
为了方便开发人员使用,我们将所有这些逻辑集成到单个twoface Rust crate中。Rust除了是现代系统级语言外,还对构建时代码有很好的支持。
我们的库有两个主要部分:
- 构建时部分:控制"隐藏"二进制文件的加密,并生成要嵌入的数据
- 运行时部分:执行解密处理,并将执行分派到"正常"或"隐藏"二进制文件
构建过程:
|
|
构建时需要设置环境变量:
|
|
构建时步骤:
- 加载"正常"可执行文件,生成常量数组供运行时使用
- 加载"隐藏"可执行文件并压缩
- 从TWOFACE_HOST_INFO文件加载主机数据
- 生成随机密钥,生成常量数组供运行时使用
- 使用步骤3的主机数据派生密钥
- 使用派生密钥加密"隐藏"可执行文件的压缩数据,生成常量数组供运行时使用
从内存运行
我们使用memfd_create系统调用创建不由文件支持的文件描述符。目标二进制文件写入后,fexecve系统调用将用新程序映像替换当前进程映像。
添加另一层乐趣
为了改进写入过程,我们使用不同的方式将解密的"隐藏"程序ELF数据写入目标文件描述符:
- 使用io_uring:不发出write系统调用
- 通过mmap内存段:无需跟踪写入,但需要许多系统调用来映射/取消映射每个块
- 回退到经典write:解密的文件数据仍不会在进程内存中,但write调用容易被跟踪
结果
完整代码可在https://github.com/synacktiv/twoface查看,包含:
- 示例"无害"/“正常"二进制文件
- “隐藏”/“恶意"二进制文件
- twoface库
- 测试示例
运行test-example将:
- 构建"无害"二进制文件
- 构建"恶意"二进制文件
- 从example/host.json加载分区UUID
- 构建包含"无害"和"恶意”(加密)ELF的示例二进制文件
- 运行它以查看实际运行的是哪一个
结论
这个概念验证展示了如何利用Rust构建时代码功能创建先进且开发人员友好的机制,实现我们的"双面"二进制文件。
为进一步推进,我们可以:
- 添加构建时混淆
- 添加运行时反调试技术
- 使用内存中的主机特定数据派生密钥
- 链式多个加载器级别,每个使用不同的派生数据源
- 使用userfaultfd动态解密ELF内存页