武器开发 - MacOS 内存执行

admin 2022年8月20日14:49:40评论250 views字数 13056阅读43分31秒阅读模式

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 %dn", n); }
for (int i = 0; i < 8; i++) printf("%i ", buf[i]);
char f[64] = {0}; sprintf(f, "/dev/fd/%d", fd);
printf("%sn", f);
if (-1 == chmod(f, 0777)) perror("chmod");
printf("dlopen %pn", dlopen(f, RTLD_NOW));
execve(f, argv, environ); perror("execve");}
int main(int argc, char **argv) { printf("===== test /dev/fdn"); test(dev_fd(argv), argv);
printf("===== test /dev/fd shmn"); 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 导出)

武器开发 - MacOS 内存执行

[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 %dn", c); return -1; }
NSModule mod = NSLinkModule(img, "_", NSLINKMODULE_OPTION_NONE); if (mod == -1) { perror("NSLindModule"); return -1; }
printf("mod %pn", mod);
NSSymbol sym = NSLookupSymbolInModule(mod, "_foo"); if (sym == -1) { perror("NSLookupSymbolInModule"); return -1; }
void* addr = NSAddressOfSymbol(sym);
printf("%pn", addr);
((void (*)())(addr))();}


武器开发 - MacOS 内存执行

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-basicn");./testing/test-cases/NSCreateObjectFileImageFromMemory-basic.dtest/main.c:112: printf("[PASS] NSCreateObjectFileImageFromMemory-basicn");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

武器开发 - MacOS 内存执行

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

武器开发 - MacOS 内存执行

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

武器开发 - MacOS 内存执行

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

3. Silicon M1 的安全机制

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

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

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

武器开发 - MacOS 内存执行

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

武器开发 - MacOS 内存执行

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

武器开发 - MacOS 内存执行

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

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

3.2 DEP

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

武器开发 - MacOS 内存执行

上图中 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


原文始发于微信公众号(0x4d5a):武器开发 - MacOS 内存执行

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年8月20日14:49:40
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   武器开发 - MacOS 内存执行http://cn-sec.com/archives/1244227.html

发表评论

匿名网友 填写信息