武器开发 - MacOS 内存执行
2022-8-19 20:45:25 Author: 0x4d5a(查看原文) 阅读量:19 收藏

0. TL; DR

在开发跨平台 C2 过程中,为了实现 MacOS 内存执行功能,我研究了相关技术并实现武器化,故有此文。

本文分以下三点介绍:

1.首先介绍 MacOS 内存执行两个失败的探索,分别是shm_openNSObjectFileImage2.接着介绍 MacOS dyld 加载流程和武器化实现。3.最后介绍 MacOS Silicon M1 会影响武器开发的几个新安全机制。

作者对于 MacOS 攻防领域属于刚入门的水平,本文如有错误还请指出。

1. MacOS 内存执行的探索

1.1 shm_open

Linux中可以通过 shm_open 创建共享内存,并通过返回的 fd 来做内存执行。然而它的效果并不好,因为能够在 /dev/shm 目录观察到文件操作行为,并且 Linux 上无法创建匿名的 shm,所以它只是在没有 memfd API (Linux < 3.17, glibc < 2.27) 时的后备选择。

MacOS 有 shm_open,并且不会在文件系统体现。但 MacOS shm_open/dev/fd 的抽象程度与 Linux 不同,下面是一些测试。

#include <stdio.h>#include <sys/mman.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <dlfcn.h>
extern char **environ;
int dev_fd(char **argv) { int fd = open("/bin/ls", O_RDONLY); if (-1 == fd) { perror("open"); return -1; }
return fd;}
int dev_fd_shm(char **argv) { shm_unlink("ls");
int shm_fd = shm_open("ls", O_RDWR | O_CREAT | O_EXCL, 0777); if (-1 == shm_fd) { perror("shm_open"); return -1; }
struct stat s = {0}; if (stat("/bin/ls", &s) == -1) { perror("stat"); return -1; }
if (-1 == ftruncate(shm_fd, s.st_size)) { perror("ftruncate"); return -1; }
void *ptr = mmap(NULL, s.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); if (!ptr || ptr == -1) { perror("mmap"); return -1; }
int fd = open("/bin/ls", O_RDONLY); if (-1 == fd) { perror("open"); return -1; }
int n = read(fd, ptr, s.st_size); if (-1 == n || n != s.st_size) { perror("read"); return -1; }
munmap(ptr, s.st_size); close(shm_fd); close(fd);
shm_fd = shm_open("ls", O_RDONLY); if (shm_fd == -1) { perror("shm_open"); return -1; }
return shm_fd;}
void test(int fd, char** argv) { struct stat s = {0}; if (-1 == fstat(fd, &s)) { perror("fstat"); return; }
ftruncate(fd, s.st_size);
char buf[8] = {0}; int n = read(fd, buf, 8); if (n == -1 || n != 8) { perror("read"); printf("read %d\n", n); }
for (int i = 0; i < 8; i++) printf("%i ", buf[i]);
char f[64] = {0}; sprintf(f, "/dev/fd/%d", fd);
printf("%s\n", f);
if (-1 == chmod(f, 0777)) perror("chmod");
printf("dlopen %p\n", dlopen(f, RTLD_NOW));
execve(f, argv, environ); perror("execve");}
int main(int argc, char **argv) { printf("===== test /dev/fd\n"); test(dev_fd(argv), argv);
printf("===== test /dev/fd shm\n"); test(dev_fd_shm(argv), argv);}

输出为:

===== test /dev/fd-54 -2 -70 -66 0 0 0 2 /dev/fd/3chmod: Operation not permitteddlopen 0x204d35460execve: Permission denied
===== test /dev/fd shmread: Device not configuredread -10 0 0 0 0 0 0 0 /dev/fd/4chmod: Bad file descriptordlopen 0x0execve: Permission denied

概括来讲,与 Linux 对比有以下限制:

1./dev/fd/<FD> 下的普通文件不能被 chmod,不能被 exec,可以 dlopen2.shm_open 返回的 fd 不能被 read (只能 mmap 后操作) ,不能被 chmod,不能被 exec,不能被 dlopen

综上,MacOS 上无法像 Linux 一样通过 shm_open 做内存执行。

1.2 NSObjectFileImage

MacOS 有一组从 macOS 10.5 已弃用[1]的 API NSCreateObjectFileImageFromMemory[2]NSLinkModule[3],借用一张图

(该 API 虽然已标记弃用,但直到现在 (MacOS 12.2) 依然从 dyld 导出)

[https://slyd0g.medium.com/understanding-and-defending-against-reflective-code-loading-on-macos-e2e83211e48f]

流程为:

1.调用 NSCreateObjectFileImageFromMemory 从内存加载 bundle 文件,该函数接受一个内存 buffer 参数2.调用 NSLinkModule 将上一步创建的 NSObjectFileImage 连接到全局模块3.调用 NSLookupSymbolInModule 获取符号对象4.调用 NSAddressOfSymbol 获取符号地址


该 API 有一个限制,只能加载 bundle 文件。

NSObjectFileImageReturnCode APIs::NSCreateObjectFileImageFromMemory(const void* memImage, size_t memImageSize, NSObjectFileImage* ofi){    ...
// this API can only be used with bundles if ( !mf->isBundle() ) { return NSObjectFileImageInappropriateFile; }
...}

MacOS 的 bundle 文件[4]有几种不同含义:打包的 APP Bundle、Framework Bundle 和 Loadable Bundle。此处为 Loadable Bundle,该文件格式与 dylib 相同。

在 Intel 芯片的 MacOS 上使用该 API 加载 dylib 或 executable 只需修改 Mach-O 文件头的 filetype(M1 芯片无法通过改文件头的方式实现,原因留到后文讲)。

编写一个简单的 loader ,再编译 x86 的 dylib 和 bundle 文件进行测试:

#include <mach-o/dyld.h>#include <stdio.h>#include <unistd.h>#include <fcntl.h>#include <sys/mman.h>#include <sys/stat.h>#include <stdlib.h>
int main(int argc, char** argv) { int fd = open(argv[1], O_RDONLY); if (fd == -1) { perror("open"); return -1; }
struct stat s = { 0 }; if (-1 == fstat(fd, &s)) { perror("fstat"); return -1; }
char* buf = malloc(s.st_size); if (!buf) { perror("malloc"); return -1; }
int total = 0; do { int n = read(fd, buf + total, s.st_size - total); if (n == -1) break;
total += n; } while (total < s.st_size);
if (total != s.st_size) { perror("read"); return -1; }
((struct mach_header_64*)buf)->filetype = MH_BUNDLE;
NSObjectFileImage img; NSObjectFileImageReturnCode c = NSCreateObjectFileImageFromMemory(buf, s.st_size, &img); if (c != NSObjectFileImageSuccess) { printf("NSCreateObjectFileImageFromMemory %d\n", c); return -1; }
NSModule mod = NSLinkModule(img, "_", NSLINKMODULE_OPTION_NONE); if (mod == -1) { perror("NSLindModule"); return -1; }
printf("mod %p\n", mod);
NSSymbol sym = NSLookupSymbolInModule(mod, "_foo"); if (sym == -1) { perror("NSLookupSymbolInModule"); return -1; }
void* addr = NSAddressOfSymbol(sym);
printf("%p\n", addr);
((void (*)())(addr))();}

1.3 再探 NSObjectFileImage

在最初研究时,我逆了 CrossC2 想要学习它内存加载的实现,发现它是通过上文 NSObjectFileImage 的方式做的,但随着我挖掘 Cross C2 Beacon 动态特征时却发现了下面的行为:

[cc2_mem]: delete executable file backup in memory![cc2_mem]: run executable file from memory![runExec]: add jobInfo-> cc2_frp, PID-> 418442022-07-06 17:27:10 - Created file: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T/NSCreateObjectFileImageFromMemory-Ay24wFNH2022-07-06 17:27:10 - Modified directory: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T2022-07-06 17:27:10 - Modified file: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T/NSCreateObjectFileImageFromMemory-Ay24wFNH2022-07-06 17:27:10 - Modified file: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T/NSCreateObjectFileImageFromMemory-Ay24wFNH2022-07-06 17:27:10 - Deleted file: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T/NSCreateObjectFileImageFromMemory-Ay24wFNH2022-07-06 17:27:10 - Modified directory: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T

NSObjectFileImage 会在 TMPDIR 下落盘一个 NSCreateObjectFileImageFromMemory-XXXXXXXX 的临时文件。这说明 NSObjectFileImage 并不是内存加载,而会触碰 tmpfs 并存在强特征,也就导致用 NSObjectFileImage 的效果甚至不如自己落盘一个随机文件名的 Mach-O 文件到 /tmp 然后 dlopenexecve

查看 dyld 源码中 dyld4::NSLinkModule 的逻辑,发现了相关逻辑。它通过向 $TMPDIR 目录创建 NSCreateObjectFileImageFromMemory-XXXXXXXX 格式的临时文件,并写入 image 的内容,最后通过 dlopen 加载。

NSModule APIs::NSLinkModule(NSObjectFileImage ofi, const char* moduleName, uint32_t options){    ...
if ( ofi->memSource != nullptr ) { // make temp file with content of memory buffer ofi->path = nullptr; char tempFileName[PATH_MAX]; const char* tmpDir = this->libSystemHelpers->getenv("TMPDIR"); if ( (tmpDir != nullptr) && (strlen(tmpDir) > 2) ) { strlcpy(tempFileName, tmpDir, PATH_MAX); if ( tmpDir[strlen(tmpDir) - 1] != '/' ) strlcat(tempFileName, "/", PATH_MAX); } else strlcpy(tempFileName, "/tmp/", PATH_MAX); strlcat(tempFileName, "NSCreateObjectFileImageFromMemory-XXXXXXXX", PATH_MAX); int fd = this->libSystemHelpers->mkstemp(tempFileName); if ( fd != -1 ) { ssize_t writtenSize = ::pwrite(fd, ofi->memSource, ofi->memLength, 0); if ( writtenSize == ofi->memLength ) { ofi->path = (char*)this->libSystemHelpers->malloc(strlen(tempFileName)+1); ::strcpy((char*)(ofi->path), tempFileName); } else { //log_apis("NSLinkModule() => NULL (could not save memory image to temp file)\n"); } ::close(fd); } // <rdar://74913193> support old licenseware plugins openMode = RTLD_UNLOADABLE | RTLD_NODELETE; }
...
// dlopen the binary outside of the read lock as we don't want to risk deadlock ofi->handle = dlopen(ofi->path, openMode); if ( ofi->handle == nullptr ) { if ( config.log.apis ) log("NSLinkModule(%p, %s) => NULL (%s)\n", ofi, moduleName, dlerror()); return nullptr; }
...}

但实际上我注意到这项内存加载技术已经存在非常久了,不大可能从来没人发现这个缺陷,因此我猜测是内部实现变了。接着我 clone 了 dyld 仓库,尝试在不同的 branch 中寻找这个临时文件名:

import os
os.system('git tag --sort=v:refname > /tmp/tags')
tags = open('/tmp/tags').readlines()
os.chdir('/Users/x/Desktop/ld-test/dyld')
for t in tags: print('=========', t) os.system('git checkout ' + t) os.system('grep -rn NSCreateObjectFileImageFromMemory-')

输出结果为:

...
========= dyld-433.5
Previous HEAD position was 7691c4c dyld-421.2HEAD is now at bd2e880 dyld-433.5========= dyld-519.2.1
Previous HEAD position was bd2e880 dyld-433.5HEAD is now at 628c97f dyld-519.2.1./dyld3/APIs_macOS.cpp:153: ofi->path = ::tempnam(nullptr, "NSCreateObjectFileImageFromMemory-");./testing/test-cases/NSCreateObjectFileImageFromMemory-basic.dtest/main.c:3:// BUILD: $CC main.c -o $BUILD_DIR/NSCreateObjectFileImageFromMemory-basic.exe -Wno-deprecated-declarations./testing/test-cases/NSCreateObjectFileImageFromMemory-basic.dtest/main.c:6:// RUN: ./NSCreateObjectFileImageFromMemory-basic.exe $RUN_DIR/foo.bundle./testing/test-cases/NSCreateObjectFileImageFromMemory-basic.dtest/main.c:103: printf("[BEGIN] NSCreateObjectFileImageFromMemory-basic\n");./testing/test-cases/NSCreateObjectFileImageFromMemory-basic.dtest/main.c:112: printf("[PASS] NSCreateObjectFileImageFromMemory-basic\n");Binary file ./.git/index matches
...

显而易见,临时文件名是从 dyld-519.2.1 才开始出现的,那么回到 dyld-433.5 看看当时是怎么实现的:

NSObjectFileImageReturnCode NSCreateObjectFileImageFromMemory(const void* address, size_t size, NSObjectFileImage *objectFileImage){    ...
ImageLoader* image = dyld::loadFromMemory((const uint8_t*)address, size, NULL); if ( ! image->isBundle() ) { // this API can only be used with bundles... dyld::garbageCollectImages(); return NSObjectFileImageInappropriateFile; } // Note: We DO NOT link the image! NSLinkModule will do that if ( image != NULL ) { *objectFileImage = createObjectImageFile(image, address, size); return NSObjectFileImageSuccess; } }
...
return NSObjectFileImageFailure;}
ImageLoader* loadFromMemory(const uint8_t* mem, uint64_t len, const char* moduleName){ // if fat wrapper, find usable sub-file const fat_header* memStartAsFat = (fat_header*)mem; uint64_t fileOffset = 0; uint64_t fileLength = len; if ( memStartAsFat->magic == OSSwapBigToHostInt32(FAT_MAGIC) ) { if ( fatFindBest(memStartAsFat, &fileOffset, &fileLength) ) { mem = &mem[fileOffset]; len = fileLength; } ... }
// try each loader if ( isCompatibleMachO(mem, moduleName) ) { ImageLoader* image = ImageLoaderMachO::instantiateFromMemory(moduleName, (macho_header*)mem, len, gLinkContext); // don't add bundles to global list, they can be loaded but not linked. When linked it will be added to list if ( ! image->isBundle() ) addImage(image); return image; }
// try other file formats here...
// throw error about what was found switch (*(uint32_t*)mem) { case MH_MAGIC: case MH_CIGAM: case MH_MAGIC_64: case MH_CIGAM_64: throw "mach-o, but wrong architecture"; default: throwf("unknown file type, first eight bytes: 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X", mem[0], mem[1], mem[2], mem[3], mem[4], mem[5], mem[6],mem[7]); }}

NSCreateObjectFileImageFromMemory 中调用 dyld::loadFromMemory 做了简单的解析校验工作,然后创建了 ImageLoader,整个过程是纯内存的。

NSModule NSLinkModule(NSObjectFileImage objectFileImage, const char* moduleName, uint32_t options){        ...
// support private bundles if ( (options & NSLINKMODULE_OPTION_PRIVATE) != 0 ) objectFileImage->image->setHideExports();
// set up linking options bool forceLazysBound = ( (options & NSLINKMODULE_OPTION_BINDNOW) != 0 );
// load libraries, rebase, bind, to make this image usable dyld::link(objectFileImage->image, forceLazysBound, false, ImageLoader::RPathChain(NULL,NULL), UINT32_MAX);
// bump reference count to keep this bundle from being garbage collected objectFileImage->image->incrementDlopenReferenceCount();
// run initializers unless magic flag says not to if ( (options & NSLINKMODULE_OPTION_DONT_CALL_MOD_INIT_ROUTINES) == 0 ) dyld::runInitializers(objectFileImage->image);
return ImageLoaderToNSModule(objectFileImage->image);
...}

NSLinkModule 调用 dyld::link,其内部调用 ImageLoader::link,完成了 Image 的加载,接着调用 ctor 函数。

总结一下,在 dyld-433.5 及以前的 dyld 是纯内存加载的,从 dyld-5 开始(大概在 2017.9),NSObjectFileImage 已经成为攻防领域一项死去的技术。

2. Mach-O dyld 加载流程

上述两项技术探索失败意味着只能通过实现 Mach-O 加载流程来实现内存加载,所以我对照 dyld 实现了一个 Mach-O Loader,可以兼容 Intel / M1 芯片系统和 MacOS 12 以上和以下。

武器化实现 Mach-O Loader 的流程:(在看之前建议先学习 Mach-O 文件的格式[5]

1.解析所有的 LC_LOAD_DYLIB,加载所需 dylib。2.遍历 LC_SEGMENT(_64) 计算大小,调用 vm_allocate 为所有 segments 分配一块连续的内存。3.遍历 LC_SEGMENT(_64) 将所有 segments 映射到第二步的内存中。4.在 MacOS 12 以下,解析 LC_DYLD_INFO_ONLYLC_DYLD_INFO,处理 rebase、bind、weak_bind 和 lazy_bind 的 bytecodes。5.在 MacOS 12 以上,解析 LC_DYLD_CHAINED_FIXUPS,处理 DYLD_CHAINED_IMPORT*DYLD_CHAINED_PTR*6.初始化 TLV,处理 tlv descriptors。7.调用 vm_protect 恢复 segments 的保护属性。8.遍历寻找带有 S_INIT_FUNC_OFFSETSS_MOD_INIT_FUNC_POINTERS 的 sections,调用其中的 Initializer 函数。

第 4. 5. 步中,MacOS 12 & iOS 15 以上和以下采用了两种不同的数据结构记录 rebase 和 bind 信息。MacOS 12 & iOS 15 以下使用 bytecodes 结构来存储,解析过程可见 ImageLoaderMachOCompressed::eachBind[6]。MacOS 12 & iOS 15 以上存储在一个叫 fixup chains 的链表结构,具体解析过程可参考 LIEF[7]

第 6. 8. 中处理 TLS 和 Initializer,与 Windows 不同,CRT 中 TLS 的初始化在 MacOS 上是在 Initializer 中做的,而不像 Windows 会在 main / DllMain 外包一层 CRTStartup

在集成到 C2 时我使用了先 fork 后执行的方法。其一,因为 Windows 和 nix 的进程创建逻辑不同,nix 上执行命令是通过 fork + exec 的方式实现,所以这里的 fork 并不是敏感行为。其二,也就是 CS fork & run 的优点:更稳定和更容易捕获输出。


Mach-O Loader 的测试分为以下几项:

1.MacOS 12 以上的 iox.dylib,区分 amd64 / arm64

2.MacOS 12 以上的 hack-browser-data.dylib,区分 amd64 / arm64

3.MacOS 12 以下因为没有现成环境,借用 CrossC2 Kit 里的 crossc2_frp.dylibcc2_safari_dump.dylibcc2_keylogger.dylib

因为这个 Loader 一方面被集成在 C2 中,另一方面还被用作 MacOS 加壳免杀,所以暂时不考虑开源。

3. Silicon M1 的安全机制

3.1 代码强制签名和系统完整性保护 (SIP)

M1 引入了代码强制性签名,签名校验失败将无法执行程序或加载库。

下面演示修改数据段的一个字节会导致进程直接被 kill,同样的,加载一个被 patch 而导致签名异常的 dylib 也会失败。

但我测试的时候发现此限制仅针对于 M1 中的原生 ARM 程序,跑在模拟器里的 x86 程序则无限制。

使用 codesign 查看签名,x86 程序编译时不会生成签名,也就无从校验。顺着这个思路,尝试删除 arm64 程序的签名,但依然无法执行。

目前找到的唯一办法是进入保护模式关闭 SIP,这在实战中几乎没有可操作性,因此这个限制会导致:

1.某些工具的生成无法通过 patch 二进制的方式。2.前文 NSObjectFileImage 加载需要 patch 文件头格式为 bundle 也无法再实现。

3.2 DEP

M1 芯片系统不允许创建 WX 或 RWX 的内存块,如下:

上图中 READ 为1,WRITE 为2,EXEC 为4,返回值0为 KERN_SUCCESS,2为 KERN_PROTECTION_FAILURE。可见 WX 和 RWX 会导致 vm_protect 返回失败。另一点,和 code sign 类似,M1 下跑在模拟器里的 x86 程序无此限制。

References

[1] macOS 10.5 已弃用: https://github.com/apple-oss-distributions/dyld/blob/main/libdyld/libdyldGlue.cpp#L214
[2] NSCreateObjectFileImageFromMemory: https://github.com/apple-oss-distributions/dyld/blob/main/dyld/DyldAPIs.cpp#L2727
[3] NSLinkModule: https://github.com/apple-oss-distributions/dyld/blob/main/dyld/DyldAPIs.cpp#L2788
[4] bundle 文件: https://en.wikipedia.org/wiki/Bundle_(macOS)
[5] Mach-O 文件的格式: https://evilpan.com/2020/09/06/macho-inside-out/
[6] ImageLoaderMachOCompressed::eachBind: https://github.com/opensource-apple/dyld/blob/3f928f32597888c5eac6003b9199d972d49857b5/src/ImageLoaderMachOCompressed.cpp#L934
[7] LIEF: https://github.com/lief-project/LIEF/blob/master/src/MachO/ChainedFixup.cpp


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg5MjU2NTc1Mg==&mid=2247483754&idx=1&sn=49e1e46cf8f4568b7364bcedc906c33c&chksm=c03d60fdf74ae9eb584ff847dc8542652163d3d2a923c02959b62fea9a16ecdd937795234a4e#rd
如有侵权请联系:admin#unsafe.sh