dyen 使用 macOS 自带的 Python 在内存中加载 Mach-O dylib,并在不调用 dlopen 的情况下调用该 dylib 的导出入口点。其目标是使映像永远不会以独立文件的形式出现在目标上,也永远不会注册到 dyld。
作者:cenobyte [email protected] 2026
https://github.com/cenobyte-vincit/dyen
dyen 是一个仅使用标准库的 Mach-O 内存加载器(loader.py),适用于 macOS 上自带的 /usr/bin/python3。它读取普通 dylib、独立的 DYEN v1 blob(加密信封)或末尾附加了该 blob 的载体文件,通过匿名 mmap 映射宿主切片,执行经典的 rebase/bind 修复,并通过 ctypes 函数指针调用 dyld_main。
encrypt_dyld.py 是打包工具:它将 dylib 包装为 DYEN blob 输出到 stdout,或者将载体文件复制为 dyld_<basename> 并附加 blob。附带的 dyld/dyld.c 载荷仅打印 hello world。loader.py 仅支持 macOS;加密器可在 Linux 或 macOS 上运行。
树内加载器读取本地路径或 stdin。投放器(dropper)或第一阶段(stage 1)会通过网络获取载体文件(或原始字节),并将其传递给 read_dyld_bytes(),这样加密后的 dylib 永远不会以独立文件的形式落到目标上。
.dyen 或载体文件。hello world)。诊断信息输出到 stderr。/usr/bin/python3(仅标准库)loader.py 仅支持 macOS。编译 PoC dylib 需要在 Mac 上使用 Apple clang。encrypt_dyld.py 可在 Linux 或 macOS 上针对预构建的 dylib 运行。
clang 和 make 的 macOS(用于编译 dyld/dyld.c)encrypt_dyld.py 的 Python 3(仅标准库)ruff、pylint、cppcheck(brew install ruff pylint cppcheck)make
这会编译 build/libdyld.dylib(通用 x86_64 + arm64),写入 build/libdyld.dyen,并将同一加密 dylib 嵌入 pdf/dyld_c4611_sample_explain.pdf。生成的 PDF 不会被提交;make 会写入它,make clean 会删除它。源载体文件是 pdf/c4611_sample_explain.pdf。
普通 dylib 是中间产物。运行时,加载器在映射之前解密 DYEN blob,无论该 blob 来自独立的 .dyen 文件,还是来自末尾附加了加密 dyld 的载体文件。
# Standalone DYEN blob to stdout (Makefile redirects to build/libdyld.dyen)
python3 encrypt_dyld.py build/libdyld.dylib CHANGEMEASAP > build/libdyld.dyen
# Hide encrypted dyld inside a cover file -> pdf/dyld_c4611_sample_explain.pdf
python3 encrypt_dyld.py build/libdyld.dylib CHANGEMEASAP pdf/c4611_sample_explain.pdf
# Use this Mac's serial number as the encryption key instead of a password (local testing)
python3 encrypt_dyld.py build/libdyld.dylib "$(python3 loader.py --serial)" > build/libdyld.dyen
# Use the target Mac's serial number as the encryption key (fictitious serial)
python3 encrypt_dyld.py build/libdyld.dylib C07XH4V2MD6T > build/libdyld.dyen
encrypt_dyld.py 可在 Linux 或 macOS 上运行。loader.py 仅在 macOS 上运行。
加载器 CLI 接受一个文件路径,或从 stdin 读取。它没有密码标志。解密时先尝试本 Mac 的硬件序列号,然后再尝试 DEFAULT_DYLD_KEY(CHANGEMEASAP)。如果无法读取序列号,则只尝试硬编码密钥。CLI 始终加载输入并调用 dyld_main。将 loader.py 复制到目标上。
/usr/bin/python3 loader.py build/libdyld.dylib
/usr/bin/python3 loader.py pdf/dyld_c4611_sample_explain.pdf
/usr/bin/python3 loader.py build/libdyld.dyen
cat build/libdyld.dylib | /usr/bin/python3 loader.py
cat pdf/dyld_c4611_sample_explain.pdf | /usr/bin/python3 loader.py
cat build/libdyld.dyen | /usr/bin/python3 loader.py
没有附加 blob 的载体文件会失败:
/usr/bin/python3 loader.py pdf/c4611_sample_explain.pdf
loader.py --serial 打印本 Mac 的硬件序列号(仅 macOS):
python3 loader.py --serial
将 LOADER_DEBUG 设置为任意值即可在 stderr 中输出详细跟踪信息:
LOADER_DEBUG=1 /usr/bin/python3 loader.py build/libdyld.dyen
loader.py 和 encrypt_dyld.py:
| Code | 含义 |
|---|---|
| 0 | 成功 |
| 1 | 用法错误、解密失败、缺少序列号或 I/O 错误 |
LOADER_DEBUG(任意值)会在 stderr 中输出详细跟踪信息:输入来源、头部/魔数检查、DYEN blob 检测、解密、map/bind、符号解析。make 默认将 LOADER_DEBUG=1 传递给 encrypt_dyld.py;make LOADER_DEBUG= 可静默加密步骤的跟踪信息。
loader.py 中的 DEFAULT_DYLD_KEY 为 CHANGEMEASAP。加密时需要向 encrypt_dyld.py 提供显式的密码或 Mac 序列号参数。
迭代次数(loader.py 中的 PBKDF2_ITERATIONS)遵循当前 OWASP 密码存储速查表 对 PBKDF2-HMAC-SHA256 的建议(600,000 次迭代)。PKCS #5 / RFC 8018 定义了 PBKDF2,但将迭代次数留给部署者决定;NIST SP 800-63B 允许使用 PBKDF2,但未强制规定具体次数。OWASP 发布的实际目标值会随着硬件性能提升而不断上升,因此应定期重新评估该值。此处是将密码拉伸(password-stretching)指南应用于测试密钥,而非固定的协议常量。
构建主机:
make test
make lint # cppcheck (C) + ruff + pylint (Python)
make check # cppcheck only
make lint-python # ruff + pylint
make test 会运行 linter、构建 PoC dylib、对其加密(独立 .dyen 和 PDF 嵌入),并在 /usr/bin/python3 下运行集成测试。每次加载测试都断言 stdout 中包含 hello world。这些测试在配套的 Mac 上运行。它们并不能作为干净运行时环境的证明。
CrowdStrike Falcon macOS 会检测此技术。其他 EDR 未经测试。
该加载器仅实现经典的 LC_DYLD_INFO rebase/bind。它不处理链式修复(chained fixups)、Objective-C 或 Swift。dlopen 仅用于 Apple 签名的系统依赖(例如 /usr/lib/libSystem.B.dylib)。载荷永远不会注册到 dyld。
在 EOF 处追加的隐藏方式在十六进制转储中可见。
武器化: 在实际使用前更改 DYENCRYPT。DYLD_BLOB_MAGIC(loader.py)中的 PoC 魔数是静态签名。YARA 规则、文件扫描器和简单的 rfind 启发式检测都可以利用它。请将其替换为您选择的唯一值(保持 encrypt 和 loader 同步;长度不固定,但对于嵌入载体文件来说越长越好)。
树内 dylib 仅打印 hello world。