0. TL; DR
在开发跨平台 C2 过程中,为了实现 MacOS 内存执行功能,我研究了相关技术并实现武器化,故有此文。
本文分以下三点介绍:
1.首先介绍 MacOS 内存执行两个失败的探索,分别是shm_open
和NSObjectFileImage
。2.接着介绍 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/3
chmod: Operation not permitted
dlopen 0x204d35460
execve: Permission denied
===== test /dev/fd shm
read: Device not configured
read -1
0 0 0 0 0 0 0 0 /dev/fd/4
chmod: Bad file descriptor
dlopen 0x0
execve: Permission denied
概括来讲,与 Linux 对比有以下限制:
1./dev/fd/<FD>
下的普通文件不能被 chmod
,不能被 exec
,可以 dlopen
2.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 %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))();
}
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-> 41844
2022-07-06 17:27:10 - Created file: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T/NSCreateObjectFileImageFromMemory-Ay24wFNH
2022-07-06 17:27:10 - Modified directory: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T
2022-07-06 17:27:10 - Modified file: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T/NSCreateObjectFileImageFromMemory-Ay24wFNH
2022-07-06 17:27:10 - Modified file: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T/NSCreateObjectFileImageFromMemory-Ay24wFNH
2022-07-06 17:27:10 - Deleted file: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T/NSCreateObjectFileImageFromMemory-Ay24wFNH
2022-07-06 17:27:10 - Modified directory: /private/var/folders/xx/6qkl26212b50t_twbbk51rhw0000gn/T
NSObjectFileImage
会在 TMPDIR 下落盘一个 NSCreateObjectFileImageFromMemory-XXXXXXXX
的临时文件。这说明 NSObjectFileImage
并不是内存加载,而会触碰 tmpfs 并存在强特征,也就导致用 NSObjectFileImage
的效果甚至不如自己落盘一个随机文件名的 Mach-O 文件到 /tmp
然后 dlopen
或 execve
。
查看 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.2
HEAD is now at bd2e880 dyld-433.5
========= dyld-519.2.1
Previous HEAD position was bd2e880 dyld-433.5
HEAD 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_ONLY
和 LC_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_OFFSETS
或 S_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.dylib
、cc2_safari_dump.dylib
和 cc2_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
原文始发于微信公众号(0x4d5a):武器开发 - MacOS 内存执行
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论