Mach-O脱壳技巧一则

0x1 应用场景

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

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

0x2 找寻脱壳点

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

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

首先是编写测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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:

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

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

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

0x2.1 LC_FUNCTION_STARTS

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

1
2
3
4
5
$ 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加载完成后要执行的初始化函数。它的内容如下:

1
2
3
4
$ 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()函数所在的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
$ 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()代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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()代码如下:

1
2
3
4
5
6
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_startinits_end之间循环获取Initializer方法并执行,Initializer与这两个地址定义如下:

1
2
3
4
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加载执行初始化方法的地方,接下来就是如何脱壳了!

0x3 如何动手

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

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

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