macOS Library Injection

从零开始学习AWS黑客技术,成为专家 htARTE(HackTricks AWS Red Team Expert)

支持HackTricks的其他方式:

dyld的代码是开源的,可以在https://opensource.apple.com/source/dyld/找到,也可以使用类似https://opensource.apple.com/tarballs/dyld/dyld-852.2.tar.gz的URL下载tar文件。

Dyld进程

查看Dyld如何在二进制文件中加载库:

pagemacOS Dyld Process

DYLD_INSERT_LIBRARIES

这类似于Linux上的LD_PRELOAD。它允许指示一个进程将从路径加载特定库(如果启用了环境变量)。

这种技术也可以用作ASEP技术,因为每个安装的应用程序都有一个名为"Info.plist"的属性列表,允许使用名为LSEnvironmental的键分配环境变量

自2012年以来,苹果大大减少了 DYLD_INSERT_LIBRARIES 的权限。

转到代码并检查src/dyld.cpp。在函数**pruneEnvironmentVariables中,您可以看到删除了DYLD_***变量。

在函数**processRestricted**中设置了限制的原因。检查该代码,您会看到限制的原因是:

  • 二进制文件是setuid/setgid

  • 在macho二进制文件中存在__RESTRICT/__restrict部分。

  • 软件具有没有com.apple.security.cs.allow-dyld-environment-variables授权的强化运行时

  • 使用以下命令检查二进制文件的授权codesign -dv --entitlements :- </path/to/bin>

在更新的版本中,您可以在函数**configureProcessRestrictions的第二部分找到此逻辑。但是,在较新版本中执行的是函数的开始检查**(您可以删除与iOS或模拟相关的if,因为这些在macOS中不会使用)。

库验证

即使二进制文件允许使用**DYLD_INSERT_LIBRARIES**环境变量,如果二进制文件检查要加载的库的签名,它将不会加载自定义内容。

为了加载自定义库,二进制文件需要具有以下授权之一:

或者二进制文件不应该具有强化运行时标志库验证标志

您可以使用codesign --display --verbose <bin>检查二进制文件是否具有强化运行时,检查**CodeDirectory中的标志运行时,例如:CodeDirectory v=20500 size=767 flags=0x10000(runtime) hashes=13+7 location=embedded**

如果使用与二进制文件相同的证书签名,也可以加载库。

找到一个关于如何(滥用)利用此功能并检查限制的示例:

pagemacOS Dyld Hijacking & DYLD_INSERT_LIBRARIES

Dylib劫持

请记住以前的库验证限制也适用于执行Dylib劫持攻击。

与Windows一样,在MacOS中,您也可以劫持dylibs以使应用程序执行任意 代码(实际上,从普通用户这样做可能不可能,因为您可能需要TCC权限才能写入.app包并劫持库)。 然而,MacOS应用程序加载库的方式比Windows更受限制。这意味着恶意软件开发人员仍然可以使用此技术进行隐蔽,但是滥用此技术以提升权限的可能性要低得多

首先,更常见的是发现MacOS二进制文件指示库的完整路径。其次,MacOS从不在 $PATH 文件夹中搜索库。

与此功能相关的主要代码部分位于ImageLoader.cpp中的**ImageLoader::recursiveLoadLibraries**中。

Macho二进制文件可以使用4个不同的头部命令来加载库:

  • **LC_LOAD_DYLIB**命令是加载dylib的常见命令。

  • **LC_LOAD_WEAK_DYLIB**命令与前一个命令类似,但如果未找到dylib,则继续执行而不会出现任何错误。

  • **LC_REEXPORT_DYLIB**命令代理(或重新导出)来自不同库的符号。

  • **LC_LOAD_UPWARD_DYLIB**命令在两个库彼此依赖时使用(这称为_向上依赖_)。

然而,有2种dylib劫持

  • 缺失的弱链接库:这意味着应用程序将尝试加载一个使用LC_LOAD_WEAK_DYLIB配置的不存在的库。然后,如果攻击者将dylib放在预期的位置,它将被加载

  • 链接是“弱”的意思是即使未找到库,应用程序也将继续运行。

  • 与此相关的代码位于ImageLoaderMachO.cppImageLoaderMachO::doGetDependentLibraries函数中,其中lib->required仅在LC_LOAD_WEAK_DYLIB为true时为false

  • 在二进制文件中查找弱链接库(稍后您将看到如何创建劫持库的示例):

    otool -l </path/to/bin> | grep LC_LOAD_WEAK_DYLIB -A 5 cmd LC_LOAD_WEAK_DYLIB
    cmdsize 56
    name /var/tmp/lib/libUtl.1.dylib (offset 24)
    time stamp 2 Wed Jun 21 12:23:31 1969
    current version 1.0.0
    compatibility version 1.0.0
  • 配置为@rpath:Mach-O二进制文件可以具有**LC_RPATHLC_LOAD_DYLIB命令。根据这些命令的值**,库将从不同目录加载。

  • LC_LOAD_DYLIB 包含要加载的特定库的路径。这些路径可以包含 @rpath,它将被 LC_RPATH 中的值 替换。如果 LC_RPATH 中有多个路径,则每个路径都将用于搜索要加载的库。例如:

  • 如果 LC_LOAD_DYLIB 包含 @rpath/library.dylib,而 LC_RPATH 包含 /application/app.app/Contents/Framework/v1//application/app.app/Contents/Framework/v2/。那么这两个文件夹都将用于加载 library.dylib。如果库在 [...]/v1/ 中不存在,攻击者可以将其放在那里以劫持在 [...]/v2/ 中的库加载,因为会按照 LC_LOAD_DYLIB 中路径的顺序进行加载。

  • 使用以下命令在二进制文件中 查找 rpath 路径和库otool -l </path/to/binary> | grep -E "LC_RPATH|LC_LOAD_DYLIB" -A 5

@executable_path:是包含 主可执行文件目录路径

@loader_path:是包含包含加载命令的 Mach-O 二进制文件目录路径

  • 当在可执行文件中使用时,@loader_path 实际上与 @executable_path 相同

  • 当在 dylib 中使用时,@loader_path 给出了 dylib路径

利用这种功能进行 权限提升 的方式是在罕见情况下,由 root 执行的 应用程序 正在 查找 一些 ,而攻击者具有写权限的某个文件夹中存在该库。

一个很好的 扫描工具,用于查找应用程序中的 缺失库Dylib Hijack ScannerCLI 版本。 关于这种技术的一个带有技术细节的不错的 报告 可以在 这里 找到。

示例

pagemacOS Dyld Hijacking & DYLD_INSERT_LIBRARIES

Dlopen 劫持

请记住,执行 Dlopen 劫持攻击时也适用 先前的库验证限制

来自 man dlopen

  • 当路径 不包含斜杠字符(即只是一个叶子名称)时,dlopen() 将进行搜索。如果在启动时设置了 $DYLD_LIBRARY_PATH,dyld 将首先在该目录中查找。接下来,如果调用的 mach-o 文件或主可执行文件指定了 LC_RPATH,那么 dyld 将在这些目录中查找。接下来,如果进程是 不受限制的,dyld 将在 当前工作目录 中搜索。最后,对于旧的二进制文件,dyld 将尝试一些回退。如果在启动时设置了 $DYLD_FALLBACK_LIBRARY_PATH,dyld 将在 这些目录中搜索,否则,dyld 将在 /usr/local/lib/ 中查找(如果进程是不受限制的),然后在 /usr/lib/ 中查找(此信息取自 man dlopen)。

  1. $DYLD_LIBRARY_PATH

  2. LC_RPATH

  3. CWD(如果不受限制)

  4. $DYLD_FALLBACK_LIBRARY_PATH

  5. /usr/local/lib/(如果不受限制)

  6. /usr/lib/

如果名称中没有斜杠,有两种方法可以进行劫持:

  • 如果任何 LC_RPATH可写的(但会检查签名,因此您还需要二进制文件是不受限制的)

  • 如果二进制文件是 不受限制的,那么可以从 CWD 中加载内容(或滥用其中提到的环境变量之一)

  • 当路径 看起来像一个框架 路径(例如 /stuff/foo.framework/foo),如果在启动时设置了 $DYLD_FRAMEWORK_PATH,dyld 将首先在该目录中查找 框架部分路径(例如 foo.framework/foo)。接下来,dyld 将尝试使用 提供的路径(对于相对路径,使用当前工作目录)。最后,对于旧的二进制文件,dyld 将尝试一些回退。如果在启动时设置了 $DYLD_FALLBACK_FRAMEWORK_PATH,dyld 将搜索这些目录。否则,它将搜索 /Library/Frameworks(在 macOS 上,如果进程是不受限制的),然后在 /System/Library/Frameworks 中搜索。

  1. $DYLD_FRAMEWORK_PATH

  2. 提供的路径(对于相对路径,如果不受限制,使用当前工作目录)

  3. $DYLD_FALLBACK_FRAMEWORK_PATH

  4. /Library/Frameworks(如果不受限制)

  5. /System/Library/Frameworks

如果是框架路径,劫持的方式是:

  • 如果进程是 不受限制的,可以滥用从 CWD 开始的 相对路径 和提到的环境变量(即使在文档中没有提到进程是否受限制,DYLD_* 环境变量会被移除)

  • 当路径 包含斜杠但不是框架路径(即完整路径或指向 dylib 的部分路径),dlopen() 首先在(如果设置了) $DYLD_LIBRARY_PATH 中查找(使用路径的叶子部分)。接下来,dyld 将尝试使用 提供的路径(对于相对路径,仅对于不受限制的进程使用当前工作目录)。最后,对于旧的二进制文件,dyld 将尝试一些回退。如果在启动时设置了 $DYLD_FALLBACK_LIBRARY_PATH,dyld 将在这些目录中搜索,否则,dyld 将在 /usr/local/lib/ 中查找(如果进程是不受限制的),然后在 /usr/lib/ 中查找。

  1. $DYLD_LIBRARY_PATH

  2. 提供的路径(对于相对路径,如果不受限制,使用当前工作目录)

  3. $DYLD_FALLBACK_LIBRARY_PATH

  4. /usr/local/lib/(如果不受限制)

  5. /usr/lib/

如果名称中有斜杠而不是框架,劫持的方式是:

  • 如果二进制文件是 不受限制的,那么可以从 CWD 或 /usr/local/lib 中加载内容(或滥用其中提到的环境变量之一)

注意:没有配置文件来 控制 dlopen 搜索

注意:如果主可执行文件是 set[ug]id 二进制文件或使用授权签名,则会 忽略所有环境变量,只能使用完整路径(查看 DYLD_INSERT_LIBRARIES 限制 获取更详细信息)

注意:Apple 平台使用 "universal" 文件来组合 32 位和 64 位库。这意味着没有 单独的 32 位和 64 位搜索路径

注意:在 Apple 平台上,大多数 OS dylibs 都 合并到 dyld 缓存 中,不存在于磁盘上。因此,调用 stat() 来预先检查 OS dylib 是否存在 不起作用。但是,dlopen_preflight() 使用与 dlopen() 相同的步骤来查找兼容的 mach-o 文件。

检查路径

让我们使用以下代码检查所有选项:

// gcc dlopentest.c -o dlopentest -Wl,-rpath,/tmp/test
#include <dlfcn.h>
#include <stdio.h>

int main(void)
{
void* handle;

fprintf("--- No slash ---\n");
handle = dlopen("just_name_dlopentest.dylib",1);
if (!handle) {
fprintf(stderr, "Error loading: %s\n\n\n", dlerror());
}

fprintf("--- Relative framework ---\n");
handle = dlopen("a/framework/rel_framework_dlopentest.dylib",1);
if (!handle) {
fprintf(stderr, "Error loading: %s\n\n\n", dlerror());
}

fprintf("--- Abs framework ---\n");
handle = dlopen("/a/abs/framework/abs_framework_dlopentest.dylib",1);
if (!handle) {
fprintf(stderr, "Error loading: %s\n\n\n", dlerror());
}

fprintf("--- Relative Path ---\n");
handle = dlopen("a/folder/rel_folder_dlopentest.dylib",1);
if (!handle) {
fprintf(stderr, "Error loading: %s\n\n\n", dlerror());
}

fprintf("--- Abs Path ---\n");
handle = dlopen("/a/abs/folder/abs_folder_dlopentest.dylib",1);
if (!handle) {
fprintf(stderr, "Error loading: %s\n\n\n", dlerror());
}

return 0;
}

如果您编译并执行它,您可以看到每个库未成功搜索的位置。此外,您可以过滤FS日志

sudo fs_usage | grep "dlopentest"

相对路径劫持

如果一个特权二进制应用程序(比如一个SUID或一些拥有强大权限的二进制文件)正在加载一个相对路径库(例如使用@executable_path@loader_path),并且禁用了库验证,那么可能会将二进制文件移动到攻击者可以修改相对路径加载的库的位置,并滥用它来向进程注入代码。

修剪 DYLD_*LD_LIBRARY_PATH 环境变量

在文件 dyld-dyld-832.7.1/src/dyld2.cpp 中,可以找到函数**pruneEnvironmentVariables**,它将删除任何以DYLD_开头和LD_LIBRARY_PATH=的环境变量。

它还会将**DYLD_FALLBACK_FRAMEWORK_PATHDYLD_FALLBACK_LIBRARY_PATH这两个环境变量对于suidsgid二进制文件设置为null**。

如果针对OSX,可以从同一文件的**_main**函数中调用此函数:

#if TARGET_OS_OSX
if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
pruneEnvironmentVariables(envp, &apple);

并且这些布尔标志在代码中的同一文件中设置:

#if TARGET_OS_OSX
// support chrooting from old kernel
bool isRestricted = false;
bool libraryValidation = false;
// any processes with setuid or setgid bit set or with __RESTRICT segment is restricted
if ( issetugid() || hasRestrictedSegment(mainExecutableMH) ) {
isRestricted = true;
}
bool usingSIP = (csr_check(CSR_ALLOW_TASK_FOR_PID) != 0);
uint32_t flags;
if ( csops(0, CS_OPS_STATUS, &flags, sizeof(flags)) != -1 ) {
// On OS X CS_RESTRICT means the program was signed with entitlements
if ( ((flags & CS_RESTRICT) == CS_RESTRICT) && usingSIP ) {
isRestricted = true;
}
// Library Validation loosens searching but requires everything to be code signed
if ( flags & CS_REQUIRE_LV ) {
isRestricted = false;
libraryValidation = true;
}
}
gLinkContext.allowAtPaths                = !isRestricted;
gLinkContext.allowEnvVarsPrint           = !isRestricted;
gLinkContext.allowEnvVarsPath            = !isRestricted;
gLinkContext.allowEnvVarsSharedCache     = !libraryValidation || !usingSIP;
gLinkContext.allowClassicFallbackPaths   = !isRestricted;
gLinkContext.allowInsertFailures         = false;
gLinkContext.allowInterposing         	 = true;

这基本上意味着,如果二进制文件是suidsgid,或者在标头中有一个RESTRICT段,或者使用CS_RESTRICT标志进行签名,那么**!gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache**为真,环境变量将被修剪。

请注意,如果CS_REQUIRE_LV为真,则变量不会被修剪,但库验证将检查它们是否使用与原始二进制文件相同的证书。

检查限制

SUID & SGID

# Make it owned by root and suid
sudo chown root hello
sudo chmod +s hello
# Insert the library
DYLD_INSERT_LIBRARIES=inject.dylib ./hello

# Remove suid
sudo chmod -s hello

区块 __RESTRICT 与段 __restrict

gcc -sectcreate __RESTRICT __restrict /dev/null hello.c -o hello-restrict
DYLD_INSERT_LIBRARIES=inject.dylib ./hello-restrict

强化运行时

在钥匙串中创建一个新证书,并使用它来签署二进制文件:

# Apply runtime proetction
codesign -s <cert-name> --option=runtime ./hello
DYLD_INSERT_LIBRARIES=inject.dylib ./hello #Library won't be injected

# Apply library validation
codesign -f -s <cert-name> --option=library ./hello
DYLD_INSERT_LIBRARIES=inject.dylib ./hello-signed #Will throw an error because signature of binary and library aren't signed by same cert (signs must be from a valid Apple-signed developer certificate)

# Sign it
## If the signature is from an unverified developer the injection will still work
## If it's from a verified developer, it won't
codesign -f -s <cert-name> inject.dylib
DYLD_INSERT_LIBRARIES=inject.dylib ./hello-signed

# Apply CS_RESTRICT protection
codesign -f -s <cert-name> --option=restrict hello-signed
DYLD_INSERT_LIBRARIES=inject.dylib ./hello-signed # Won't work

请注意,即使有用标志**`0x0(none)`**签名的二进制文件,当执行时也可以动态地获得**`CS_RESTRICT`**标志,因此这种技术在其中不起作用。

您可以使用以下命令检查进程是否具有此标志(获取csops here):

csops -status <pid>

参考资料

从零开始学习AWS黑客技术,成为专家 htARTE (HackTricks AWS Red Team Expert)!

支持HackTricks的其他方式:

最后更新于