以原生速度在 macOS 上模糊测试 iOS 代码

阅读量    137958 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x0 概述

随着Apple Silicon Mac的推出,在 Mac 上原生运行 iOS 应用程序成为可能。这归咎于 (1) iPhone 和 Apple Silicon Macs 都使用 arm64 指令集架构 (2) macOS 使用一组最大程度兼容的运行时库和框架,同时还提供 macOS 上不存在的 iOS 运行时组件 /System/iOSSupport。因此,不仅可以运行完整的应用程序,还可以在 Mac 上运行独立的 iOS 二进制文件或库。由于多种原因,这可能很有趣,包括:

  • 它允许在 Mac 上对为 iOS 编译的闭源代码进行模糊测试
  • 它允许在更“友好”的环境中动态分析 iOS 代码

这篇文章解释了如何在实践中实现这一目标。可以在此处找到相应的代码, 并允许在 macOS 上本地执行任意 iOS 二进制文件和库代码。该工具假定SIP 已被禁用并已在 macOS 11.2 和 11.3 上进行测试。启用 SIP 后,某些步骤可能会失败。

我们最初开发这个工具是为了对第三方 iOS 消息应用程序进行模糊测试。虽然该项目没有产生任何有趣的结果,但我们正在将该工具公之于众,因为它可以帮助降低 iOS 安全研究的进入门槛。

 

0x01 目标

该项目的最终目标是在 macOS 上执行为 iOS 原生编译的代码。虽然可以通过交换 mach-o 二进制文件中的平台标识符来实现这一目标(至少对于某些二进制文件/库),但我们将改为使用现有基础架构在 macOS 上运行 iOS 应用程序。这有两个好处:

  • 它将保证 iOS 代码的所有依赖系统库都存在。实际上,这意味着如果 macOS 上不存在依赖库,它将自动从 /System/iOSSupport 加载
  • 运行时(OS 服务、框架等)将在必要时模拟其 iOS 行为,因为它们将知道该进程是一个 iOS 进程。

首先,我们将使用一段简单的 C 源代码并为 iOS 编译它:

> cat hello.c
#include <stdio.h>
int main() {
    puts("Hello from an iOS binary!");
    return 0;
}
> clang -arch arm64 hello.c -o hello -isysroot \
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk
> file hello
hello: Mach-O 64-bit executable arm64
> otool -l hello
…
Load command 10
        cmd LC_BUILD_VERSION
  cmdsize 32
  platform 2           # Platform 2 is iOS
      minos 14.4
          sdk 14.4
      ntools 1
         tool 3
    version 609.8
…

 

0x02 内核空间

尝试执行新编译的二进制文件(在 macOS 11.2 上)会导致:

> ./hello
[1]    13699 killed     ./hello

虽然退出状态通知我们进程已通过 SIGKILL 终止,但它不包含任何关于具体原因的附加信息。但是,该进程似乎在execve(2) 或posix_spawn(2) 系统调用期间被内核终止。事实上,系统生成的崩溃报告指出:终止原因:EXEC,[0xe] 二进制平台错误

此错误对应到位于内核中的 EXEC_EXIT_REASON_WRONG_PLATFORM ,该常量只在一个函数引用:check_for_signature

static int
check_for_signature(proc_t p, struct image_params *imgp)
{
    …;
#if XNU_TARGET_OS_OSX
        /* Check for platform passed in spawn attr if iOS binary is being spawned */
        if (proc_platform(p) == PLATFORM_IOS) {
                struct _posix_spawnattr *psa = imgp->ip_px_sa;
                if (psa == NULL || psa->psa_platform == 0) {
                    …;
                            signature_failure_reason = os_reason_create(OS_REASON_EXEC,
                                        EXEC_EXIT_REASON_WRONG_PLATFORM);
                            error = EACCES;
                            goto done;
                } else if (psa->psa_platform != PLATFORM_IOS) {
                        /* Simulator binary spawned with wrong platform */
                        signature_failure_reason = os_reason_create(OS_REASON_EXEC,
                             EXEC_EXIT_REASON_WRONG_PLATFORM);
                        error = EACCES;
                        goto done;
                } else {
                        printf("Allowing spawn of iOS binary %s since
                             correct platform was passed in spawn\n", p->p_name);
                }
        }
#endif /* XNU_TARGET_OS_OSX */
    …;
}

这段代码在 macOS 上是活跃的,当执行进程的平台为PLATFORM_IOS时就会执行。本质上,代码检查文档未记录的posix_spawn 属性psa_platform ,如果没有它(或者如果它的值不是PLATFORM_IOS ),将按照我们之前观察到的方式终止进程。

因此,为了避免EXEC_EXIT_REASON_WRONG_PLATFORM , 只需要使用文档未记录的posix_spawnattr_set_platform_np 系统调用将目标平台设置为PLATFORM_IOS ,然后调用posix_spawn 来执行 iOS 二进制文件:

posix_spawnattr_t attr;
posix_spawnattr_init(&attr);
posix_spawnattr_set_platform_np(&attr, PLATFORM_IOS, 0);
posix_spawn(&pid, binary_path, NULL, &attr, argv, environ);

这样做现在将导致:

> ./runner hello
...
[*] Child exited with status 5

不再发生SIGKILL,进步!Exit Status 5 对应于 SIGTRAP,这可能意味着该进程现在正在用户空间中终止。事实上,崩溃报告确认进程在库初始化期间崩溃了。

 

0x03 用户空间

此时我们有一个PLATFORM_IOS进程在 macOS 用户空间中运行。紧接着动态链接器dyld开始映射二进制文件所依赖的所有库并执行它们可能具有的任何初始化程序。不幸的是,正在初始化的第一个库libsystem_secinit.dylib尝试确定它是否应该根据二进制平台及其权限来初始化应用程序沙箱。逻辑大致如下:

initialize_app_sandbox = False
if entitlement(“com.apple.security.app-sandbox”) == True:
    initialize_app_sandbox = True
if active_platform() == PLATFORM_IOS &&
    entitlement(“com.apple.private.security.no-sandbox”) != True:
    initialize_app_sandbox = True

因此,libsystem_secinit 将决定它应该初始化应用沙箱,然后与安全策略初始化守护进程secinitd(8)通信,以获取沙箱配置文件。由于该守护进程无法确定与相关进程对应的应用程序,它将失败,然后libsystem_secinit.dylib 将中止(3)进程:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT
  * frame #0: libsystem_secinit.dylib`_libsecinit_appsandbox.cold.5
    frame #1: libsystem_secinit.dylib`_libsecinit_appsandbox
    frame #2: libsystem_trace.dylib` ...
    frame #3: libsystem_secinit.dylib`_libsecinit_initializer
    frame #4: libSystem.B.dylib`libSystem_initializer
    frame #5: libdyld.dylib`...
    frame #6: libdyld.dylib`...
    frame #7: libdyld.dylib`dyld3::AllImages::runLibSystemInitializer
    frame #8: libdyld.dylib`...
    frame #9: dyld`...
    frame #10: dyld`dyld::_main
    frame #11: dyld`dyldbootstrap::start
    frame #12: dyld`_dyld_start + 56

作为旁注,如上的逻辑揭示一个共同的主题:负责运行时环境的各种组件将对 iOS 二进制文件进行特殊处理,在这种情况下,它们倾向于更积极地执行各种策略。

解决此问题的一种可能方法是使用自签名(和本地信任)代码签名证书对 iOS 二进制文件进行签名, 并授予它“com.apple.private.security.no-sandbox”权限。这将导致libsystem_secinit 不尝试进行初始化应用程序沙箱。不幸的是,虽然 AppleMobileFileIntegrity(AMFI – 实施权限、代码签名检查等各种安全策略的操作系统组件)将允许 macOS 二进制文件在禁用 SIP 的情况下通过本地信任的代码签名证书进行签名,但它对于 iOS 二进制文件却截然不同。相反,它强制执行与 iOS 大致相同的要求,即二进制文件必须由 Apple 直接签名(如果应用程序是从应用程序商店下载的)或必须存在有效的代码签名实体明确允许该权限的的配置文件。因此,这条路似乎是一条死胡同。

解决沙箱初始化的另一种方法是使用dyld 插入 替换xpc_copy_entitlements_for_self ,libsystem_secinit 调用它来获取进程的权限,另一个函数将简单地返回“com.apple.private.security.no-sandbox” 权限。这会反过来阻止libsystem_secinit 尝试初始化沙箱。

不幸的是,iOS 进程受到进一步限制,可能是“强化运行时”套件的一部分,这会导致 dyld 禁用库插入(有关此机制的更多信息可在此处获得)。这个策略也由 AMFI 实现,在 AppleMobileFileIntegrity.kext(AMFI 的内核组件)中:

__int64 __fastcall macos_dyld_policy_library_interposing(proc *a1, int *a2)
{
  int v3; // w8
  v3 = *a2;
  ...
  if ( (v3 & 0x10400) == 0x10000 )   // flag is set for iOS binaries
  {
    logDyldPolicyRejection(a1, "library interposing", "Denying library interposing for iOS app\n");
    return 0LL;
  }
  return 64LL;
}

可以看出,AMFI 将拒绝为所有 iOS 二进制文件插入库。不幸的是,我想不出比在运行时修补 dyld 代码以忽略 AMFI 的政策决定从而允许库插入更好的解决方案;幸运的是,通过使用一些经典的mach API,进行轻量级运行时代码修补相当容易:

  • 在 /usr/lib/dyld 中找到_amfi_check_dyld_policy_self的偏移量,例如使用nm(1)
  • 使用POSIX_SPAWN_START_SUSPENDED 属性启动 iOS 进程,以便它最初被挂起(相当于 SIGSTOP)。此时,内核仅将 dyld 和二进制文件本身映射到进程的内存空间。
  • 使用task_for_pid “附加”到进程
  • 通过vm_region_recurse_64查找dyld在内存中的位置
  • 使用vm_protect (VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY)映射 dyld 的代码部分可写 (其中VM_PROT_COPY 似乎是强制复制页面所必需的,因为它们是共享的)
  • 通过vm_write修补 _amfi_check_dyld_policy_self 以简单地返回 0x5f(表示应该允许 dyld 插入和其他功能)
  • 再次映射 dyld 的代码部分可执行文件

为了能够使用task_for_pid 陷阱,运行程序二进制文件需要“com.apple.security.cs.debugger” 或 root 权限。但是,由于运行器是 macOS 二进制文件,因此可以通过 AFMI 允许的自签名证书为其授予此权限。

因此,在 macOS 上启动 iOS 二进制文件所需的完整步骤是:

  • 使用posix_spawnattr_set_platform_np API 将目标平台设置为PLATFORM_IOS
  • 通过posix_spawn(2)执行新进程 并暂停启动
  • 修补 dyld 以允许库插入
  • 在插入的库中,通过替换 xpc_copy_entitlements_for_self 声称拥有 com.apple.security.cs.debugger 权限
  • 通过发送SIGCONT继续该过程

如上操作可以看到:

> cat hello.c
#include <stdio.h>
int main() {
    puts("Hello from an iOS binary!");
    return 0;
}
> clang -arch arm64 hello.c -o hello -isysroot \
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk interpose.dylib
> ./runner hello
[*] Preparing to execute iOS binary hello
[+] Child process created with pid: 48302
[*] Patching child process to allow dyld interposing...
[*] _amfi_check_dyld_policy_self at offset 0x54d94 in /usr/lib/dyld
[*] /usr/lib/dyld mapped at 0x1049ec000
[+] Successfully patched _amfi_check_dyld_policy_self
[*] Sending SIGCONT to continue child
[*] Faking no-sandbox entitlement in xpc_copy_entitlements_for_self
Hello from an iOS binary!
[*] Child exited with status 0

 

0x04 模糊测试

有了启动 iOS 进程的能力,现在也可以在 macOS 上模糊测试现有的 iOS 原生代码。我决定对Honggfuzz 进行一个简单的 PoC,该 PoC 还使用了轻量级覆盖指导(基于Trapfuzz 检测方法)。这种方法的主要问题是 honggfuzz 使用fork(2) 和execve(2) 的组合来创建子进程,同时还执行各种操作,例如 dup2’ing 文件描述符,设置环境变量等 fork 之后但在执行之前。但是,iOS 二进制文件必须通过posix_spawn执行,这意味着这些操作必须在其他时间执行。此外,由于 honggfuzz 本身仍然是为 macOS 编译的,因此目标二进制文件的某些编译步骤将失败(它们会尝试链接以前编译的 .o 文件,但现在平台不再匹配),因此必须更换。当然有更好的方法来做到这一点(我鼓励读者正确地实现它),但这是我工作最快的方法。

可以在此处找到 honggfuzz 的 PoC 补丁。除了为 arm64 构建 honggfuzz 之外,随后对 honggfuzz 二进制文件进行签名并授予“com.apple.security.cs.debugger”权限,以便task_for_pid 正常工作。

 

0x05 结论

这篇博文讨论了 iOS 应用程序如何在 macOS 上运行,以及如何使用该功能在 macOS 上执行为 iOS 编译的任何原生代码。这反过来又便利于 iOS 代码的动态分析和模糊测试,从而可能使该平台对安全研究人员更加开放。

 

0x06 附件1

// clang -o runner runner.c
// cat <<EOF > entitlements.xml
// <?xml version="1.0" encoding="UTF-8"?>
// <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"\>
// <plist version="1.0">
// <dict>
//     <key>com.apple.security.cs.debugger</key>
//     <true/>
// </dict>
// </plist>
// EOF
// # Find available code signing identities using `security find-identity`
// codesign -s "$IDENTITY" --entitlements entitlements.xml runner

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <signal.h>
#include <unistd.h>
#include <spawn.h>
#include <sys/wait.h>
#include <mach/mach_init.h>
#include <mach/vm_map.h>
#include <mach/vm_page_size.h>

#define page_align(addr) (vm_address_t)((uintptr_t)(addr) & (~(vm_page_size - 1)))
#define PLATFORM_IOS 2

extern char **environ;
extern int posix_spawnattr_set_platform_np(posix_spawnattr_t*, int, int);

void instrument(pid_t pid) {
    kern_return_t kr;
    task_t task;
    puts("[*] Patching child process to allow dyld interposing...");

    // Find patch point
    FILE* output = popen("nm -arch arm64e /usr/lib/dyld  | grep _amfi_check_dyld_policy_self", "r");
    unsigned int patch_offset;
    int r = fscanf(output, "%x t _amfi_check_dyld_policy_self", &patch_offset);
    if (r != 1) {
        printf("Failed to find offset of _amfi_check_dyld_policy_self in /usr/lib/dyld\n");
        return;
    }
    printf("[*] _amfi_check_dyld_policy_self at offset 0x%x in /usr/lib/dyld\n", patch_offset);

    // Attach to the target process
    kr = task_for_pid(mach_task_self(), pid, &task);
    if (kr != KERN_SUCCESS) {
        printf("task_for_pid failed. Is this binary signed and possesses the com.apple.security.cs.debugger entitlement?\n");
        return;
    }

    vm_address_t dyld_addr = 0;
    int headers_found = 0;
    vm_address_t addr = 0;
    vm_size_t size;
    vm_region_submap_info_data_64_t info;
    mach_msg_type_number_t info_count = VM_REGION_SUBMAP_INFO_COUNT_64;
    unsigned int depth = 0;

    while (1) {
        // get next memory region
        kr = vm_region_recurse_64(task, &addr, &size, &depth, (vm_region_info_t)&info, &info_count);
        if (kr != KERN_SUCCESS)
            break;

        unsigned int header;
        vm_size_t bytes_read;

        kr = vm_read_overwrite(task, addr, 4, (vm_address_t)&header, &bytes_read);
        if (kr != KERN_SUCCESS) {
            // TODO handle this, some mappings are probably just not readable
            printf("vm_read_overwrite failed\n");
            return;
        }

        if (bytes_read != 4) {
            // TODO handle this properly
            printf("[-] vm_read read to few bytes\n");
            return;
        }

        if (header == 0xfeedfacf) {
            headers_found++;
        }

        if (headers_found == 2) {
            // This is dyld
            dyld_addr = addr;
            break;
        }
        addr += size;
    }

    if (dyld_addr == 0) {
        printf("[-] Failed to find /usr/lib/dyld\n");
        return;
    }

    printf("[*] /usr/lib/dyld mapped at 0x%lx\n", dyld_addr);
    vm_address_t patch_addr = dyld_addr + patch_offset;

    // VM_PROT_COPY forces COW, probably, see vm_map_protect in vm_map.c
    kr = vm_protect(task, page_align(patch_addr), vm_page_size, false, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY);
    if (kr != KERN_SUCCESS) {
        printf("vm_protect failed\n");
        return;
    }

    // MOV X8, 0x5f
    // STR X8, [X1]
    // RET
    const char* code = "\xe8\x0b\x80\xd2\x28\x00\x00\xf9\xc0\x03\x5f\xd6";

    kr = vm_write(task, patch_addr, (vm_offset_t)code, 12);
    if (kr != KERN_SUCCESS) {
        printf("vm_write failed\n");
        return;
    }

    kr = vm_protect(task, page_align(patch_addr), vm_page_size, false, VM_PROT_READ | VM_PROT_EXECUTE);
    if (kr != KERN_SUCCESS) {
        printf("vm_protect failed\n");
        return;
    }
    puts("[+] Successfully patched _amfi_check_dyld_policy_self");
}

int run(const char** argv) {
    pid_t pid;
    int rv;
    posix_spawnattr_t attr;
    rv = posix_spawnattr_init(&attr);

    if (rv != 0) {
        perror("posix_spawnattr_init");
        return -1;
    }

    rv = posix_spawnattr_setflags(&attr, POSIX_SPAWN_START_SUSPENDED);
    if (rv != 0) {
        perror("posix_spawnattr_setflags");
        return -1;
    }

    rv = posix_spawnattr_set_platform_np(&attr, PLATFORM_IOS, 0);
    if (rv != 0) {
        perror("posix_spawnattr_set_platform_np");
        return -1;
    }

    rv = posix_spawn(&pid, argv[0], NULL, &attr, argv, environ);
    if (rv != 0) {
        perror("posix_spawn");
        return -1;
    }

    printf("[+] Child process created with pid: %i\n", pid);
    instrument(pid);
    printf("[*] Sending SIGCONT to continue child\n");
    kill(pid, SIGCONT);
    int status;
    rv = waitpid(pid, &status, 0);

    if (rv == -1) {
         perror("waitpid");
        return -1;
    }

    printf("[*] Child exited with status %i\n", status);
    posix_spawnattr_destroy(&attr);
    return 0;
}

int main(int argc, char* argv[]) {
    if (argc <= 1) {
        printf("Usage: %s path/to/ios_binary\n", argv[0]);
        return 0;
    }
    printf("[*] Preparing to execute iOS binary %s\n", argv[1]);
    return run(argv + 1);
}

 

0x07 附件2

// clang interpose.c -arch arm64 -o interpose.dylib -shared -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk

#include <stdio.h>
#include <unistd.h>

typedef void* xpc_object_t;
extern xpc_object_t xpc_dictionary_create(void*, void*, int);
extern void xpc_dictionary_set_value(xpc_object_t, const char*, xpc_object_t);
extern xpc_object_t xpc_bool_create(int);
extern xpc_object_t xpc_copy_entitlements_for_self();

// From https://opensource.apple.com/source/dyld/dyld-97.1/include/mach-o/dyld-interposing.h.auto.html
/*
 *  Example:
 *
 *  static int
 *  my_open(const char* path, int flags, mode_t mode)
 *  {
 *    int value;
 *    // do stuff before open (including changing the arguments)
 *    value = open(path, flags, mode);
 *    // do stuff after open (including changing the return value(s))
 *    return value;
 *  }
 *  DYLD_INTERPOSE(my_open, open)
 */

#define DYLD_INTERPOSE(_replacment,_replacee) \
 __attribute__((used)) static struct{ const void* replacment; const void* replacee; } _interpose_##_replacee \
__attribute__ ((section ("__DATA,__interpose"))) = { (const void*)(unsigned long)&_replacment, (const void*)(unsigned long)&_replacee };

xpc_object_t my_xpc_copy_entitlements_for_self() {
    puts("[*] Faking com.apple.private.security.no-sandbox entitlement in interposed xpc_copy_entitlements_for_self");
    xpc_object_t dict = xpc_dictionary_create(NULL, NULL, 0);
    xpc_dictionary_set_value(dict, "com.apple.private.security.no-sandbox", xpc_bool_create(1));
    return dict;
}
DYLD_INTERPOSE(my_xpc_copy_entitlements_for_self, xpc_copy_entitlements_for_self);
分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多