作者:非虫

0x01 应用场景

此处讨论的脱壳不是class-dump这类脱壳,而是指第三方的软件压缩与加密壳,例如upx这类壳在iOS/macOS上的脱壳。

App Store上的软件是不允许这类壳程序存在的,但在iOS越狱插件开发领域与macOS第三方软件提供商发布平台,自定义加密的MachO与dylib随处可见,到目前为此,没有在网络上看到关于这类程序脱壳方法的研究与讨论,本篇与大家讨论的就是在这种情况下,如何优雅的脱壳!

0x02 找寻脱壳点

首先,虚拟机壳与混淆壳不在本篇讨论范围中,在iOS/macOS平台上,如果有虚拟机壳,也是很久以后的事情了,目前市在上见到最多的可能要属upx类的压缩型的壳,这类壳有一个明显的特点:壳初始运行完后,会将代码的控制权交回给原程序,并且内存中已经是存放好了完整的解密代码,脱壳的思路与Android平台上upx的脱壳一样,主要是找准脱壳时机!

在Android时代,脱upx有一个优雅的方法,就是对DT_INIT的处理部分下断点,当linker加载完so,要执行DT_INIT段指向的初始化函数指针时,对内存中的so进行dump来达到脱壳的目的,到了macOS平台上,就采取同样的思路来开始脱壳探索。

首先是编写测试代码:

#import <Foundation/Foundation.h>
#import <time.h>
#import <dlfcn.h>
#import <stdio.h>
#import <stdlib.h>
#import <unistd.h>
#import <fcntl.h>
#import <string.h>

// clang -x objective-c -std=gnu99 -fobjc-arc -flat_namespace -dynamiclib -o ./libunderstandpatcher.dylib understandpatcher.m

static double (*orig_difftime)(time_t time1, time_t time0) = NULL;

typedef double (*orig_difftime_type)(time_t time1, time_t time0);

__attribute__((constructor))
void init_funcs()
{
    printf("--------init funcs.--------\n");
    void * handle = dlopen("libSystem.dylib", RTLD_NOW);
    orig_difftime = (orig_difftime_type) dlsym(handle, "difftime");
    if(!orig_difftime) {
        printf("get difftime() addr error");
        exit(-1);
    }
    。。。

    printf("--------init done--------\n");
}
...

这只是代码的片断,在下写的macOS平台上understand程序的破解补丁,执行以下代码编译生成dylib:

clang -x objective-c -std=gnu99 -fobjc-arc -flat_namespace -dynamiclib -o ./libunderstandpatcher.dylib understandpatcher.m

完事以后使用MachOView查看生成的dylib,看看init_funcs()以何种形式在Mach-O中存在,如图所示:

有两个地方需要注意:LC_FUNCTION_STARTS,__DATA,mod_init_func。

0x2.1 LC_FUNCTION_STARTS

这个加载命令是一个macho_linkedit_data_command结构体,从名称上判断,它是一个指向了函数起始执行的指针。它的内容如下:

$ otool -l ./libunderstandpatcher.dylib | grep LC_FUNCTION_STARTS -A 3
  cmd LC_FUNCTION_STARTS
cmdsize 16
dataoff 8504
datasize 8

dataoff字段的值8504(0x2138),在MachOView中看到,它指向Function Starts第一项的__init_funcs()函数。

0x2.2 __DATA,mod_init_func

__DATA,__mod_init_func是一个Section,它由编译器生成添加到MachO中,用来标识MachO加载完成后要执行的初始化函数。它的内容如下:

$ otool -s __DATA __mod_init_func ./libunderstandpatcher.dylib
./libunderstandpatcher.dylib:
Contents of (__DATA,__mod_init_func) section
0000000000001050    00 0d 00 00 00 00 00 00

位于文件偏移0x1050处指向的是一个个的初始化函数指针,这里只有一个,它的值是0xD00,其实就是__init_funcs()函数所在的地址:

$ otool -tv ./libunderstandpatcher.dylib
./libunderstandpatcher.dylib:
(__TEXT,__text) section
_init_funcs:
0000000000000d00    pushq    %rbp
0000000000000d01    movq    %rsp, %rbp
0000000000000d04    subq    $0x40, %rsp
0000000000000d08    leaq    0x1e9(%rip), %rdi
0000000000000d0f    movb    $0x0, %al
0000000000000d11    callq    0xe82
0000000000000d16    leaq    0x1f8(%rip), %rdi
0000000000000d1d    movl    $0x2, %esi
0000000000000d22    movl    %eax, -0x14(%rbp)
0000000000000d25    callq    0xe70
0000000000000d2a    leaq    0x1f4(%rip), %rsi
0000000000000d31    movq    %rax, -0x8(%rbp)
0000000000000d35    movq    -0x8(%rbp), %rdi
0000000000000d39    callq    0xe76
0000000000000d3e    movq    %rax, 0x35b(%rip)
0000000000000d45    cmpq    $0x0, 0x353(%rip)
0000000000000d4d    jne    0xd6e
0000000000000d53    leaq    0x1d4(%rip), %rdi
0000000000000d5a    movb    $0x0, %al
0000000000000d5c    callq    0xe82
0000000000000d61    movl    $0xffffffff, %edi
0000000000000d66    movl    %eax, -0x18(%rbp)
0000000000000d69    callq    0xe7c
0000000000000d6e    movq    0x323(%rip), %rax
0000000000000d75    movq    0x304(%rip), %rsi
0000000000000d7c    movq    %rax, %rdi
0000000000000d7f    callq    0xe5e
0000000000000d84    movq    %rax, %rdi
0000000000000d87    callq    0xe64
0000000000000d8c    xorl    %ecx, %ecx
0000000000000d8e    movl    %ecx, %edi
0000000000000d90    movq    %rax, -0x10(%rbp)
0000000000000d94    movq    -0x10(%rbp), %rax
0000000000000d98    movq    %rax, -0x20(%rbp)
0000000000000d9c    callq    0xe88
0000000000000da1    leaq    0x2b0(%rip), %rsi
0000000000000da8    movq    0x2d9(%rip), %rdi
0000000000000daf    movq    -0x20(%rbp), %rdx
0000000000000db3    movq    %rdi, -0x28(%rbp)
0000000000000db7    movq    %rdx, %rdi
0000000000000dba    movq    -0x28(%rbp), %rdx
0000000000000dbe    movq    %rsi, -0x30(%rbp)
0000000000000dc2    movq    %rdx, %rsi
0000000000000dc5    movq    %rax, %rdx
0000000000000dc8    movq    -0x30(%rbp), %rcx
0000000000000dcc    callq    0xe5e
0000000000000dd1    movq    -0x10(%rbp), %rax
0000000000000dd5    movq    0x2b4(%rip), %rsi
0000000000000ddc    movq    %rax, %rdi
0000000000000ddf    callq    0xe5e
0000000000000de4    leaq    0x178(%rip), %rdi
0000000000000deb    movb    %al, -0x31(%rbp)
0000000000000dee    movb    $0x0, %al
0000000000000df0    callq    0xe82
0000000000000df5    xorl    %r8d, %r8d
0000000000000df8    movl    %r8d, %esi
0000000000000dfb    leaq    -0x10(%rbp), %rcx
0000000000000dff    movq    %rcx, %rdi
0000000000000e02    movl    %eax, -0x38(%rbp)
0000000000000e05    callq    0xe6a
0000000000000e0a    addq    $0x40, %rsp
0000000000000e0e    popq    %rbp
0000000000000e0f    retq

0x2.3 dyld执行初始化函数过程

dyld如何执行初始化函数才是我们需要重点关注的。下载dyld源码查看,它启动运行的第一个方法dyldbootstrap::start()代码如下:

uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], intptr_t slide, const struct macho_header* dyldsMachHeader, uintptr_t* startGlue)
{
    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    if ( slide != 0 ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

    // allow dyld to use mach messaging
    mach_init();

    // kernel sets up env pointer to be just past end of agv array
    const char** envp = &argv[argc+1];

    // kernel sets up apple pointer to be just past end of envp array
    const char** apple = envp;
    while(*apple != NULL) { ++apple; }
    ++apple;

    // set up random value for stack canary
    __guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
    // run all C++ initializers inside dyld
    runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif

    // now that we are done bootstrapping dyld, call dyld's main
    uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

在开启DYLD_INITIALIZER_SUPPORT的情况下,会调用runDyldInitializers()执行Mach-O的初始化方法,i当然,目前dyld是支持初始化方法执行的,runDyldInitializers()代码如下:

static void runDyldInitializers(const struct macho_header* mh, intptr_t slide, int argc, const char* argv[], const char* envp[], const char* apple[])
{
    for (const Initializer* p = &inits_start; p < &inits_end; ++p) {
        (*p)(argc, argv, envp, apple);
    }
}

这段代码从inits_start到inits_end之间循环获取Initializer方法并执行,Initializer与这两个地址定义如下:

typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[]);

extern const Initializer  inits_start  __asm("section$start$__DATA$__mod_init_func");
extern const Initializer  inits_end    __asm("section$end$__DATA$__mod_init_func");

可以看出,dyld定位与执行初始化方法是通过”__DATA$mod_init_func”节区完成的。

了解了dyld加载执行初始化方法的地方,接下来就是如何脱壳了!

0x03 如何动手

壳程序加载完成,第一件事要做的就是自己或者调用dyld来执行初始化方法,因此,使用任意一款调试器对runDyldInitializers()下断即可。

断点到达后对内存中的MachO进行dump就完成脱壳了,当然对于防内存dump也是有一些tricks的,逆向搞过Hopper主程序的人就会有感触,以后有机会与大家讨论一下!

最后,Mach-O的dump与ELF不太一样,更加简单与完整,这里不再赘述了!


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/202/