[置顶]macOS软件安全系列-软件内幕篇

macOS平台上的软件安全话题讨论在国内仍是软件安全领域的沼泽地带,除了在部分安全论坛上见过几篇破解分析的文章外,鲜有人问津。

如今,macOS平台的安全问题已经被越来越多的人关注,本系列的文章为macOS软件安全系列的一个篇章-软件内幕篇,探讨macOS系统上常见的文件格式与它们的管理方式,让各位关注软件安全领域的朋友,对macOS系统上的软件安全有直观的认识。

声明

本系列文章为非虫(微信:feicongcn)原创,任何个人与组织未经允许,不得转载与摘抄,否则,作者保留一切追究法律责任的权利。

macOS平台软件的下载与安装

Mach-O文件格式

dylib动态库加载过程分析

静态库的管理与文件格式分析

PKG安装包的管理与文件格式分析

DMG文件管理

Android免Root环境下Hook框架Legend原理分析

0x1 应用场景

现如今,免Root环境下的逆向分析已经成为一种潮流!

在2015年之前的iOS软件逆向工程领域,要想对iOS平台上的软件进行逆向工程分析,越狱iOS设备与安装Cydia是必须的!几乎绝大多数的逆向相关的动态调试工具、Hook注入框架都依赖于获取IOS设备的最高访问权限,就技术本身上而言,对当前程序进行Hook与动态调试,只需要拥有与当前程序相同的权限即可,理论上无需对设备进行Root越狱,实际上,在2015年就出现了在非越狱设备上进行插件开发的实用案例,2016年的iOS软件逆向工程界,更是一发不可收拾,各种名越狱环境下的逆向工具与逆向技巧被安全人员所发掘,在没有越狱的iOS设备上进行软件的动态调试与逆向工程已经是主流的趋势胃。这样的情况下,最直接的影响是安全研究人员不再对iOS设备越狱有着强烈的追求了,越狱需求的下降可能会直接影响到iOS设备越狱工具的发布与技术的更新迭代。

同样的,在Android设备的免Root环境下,进行软件动态调试与逆向工程分析的需求更加强烈。免Root环境下动态调试与逆向工程就技术本质而言是可行的,安全研究人员的智慧更是有力的证明了这一点,LBE发布免Root环境下APK双开工具平行空间就是最好的例子,它是打破逆向工程技术的原始格局的第一个大锤!随后的,各种APK多开框架、免Root环境下的Hook、免Root环境下的动态调试等技术都被研究人员公开,这是Android软件逆向工程界的福音,逆向工程人员在以后的逆向分析过程中,可能再也不需要为自己的手机能否越狱而感到苦恼,手上在吃灰淘汰的Android小米机可能就是你的逆向必备工具之一。

好了,说了这么多,无非是告诉大家,开发技术在更新迭代,软件的逆向工程技术也在不停的更新,各位研究软件安全的朋友们,你们跟上了时代的脚步吗?!

0x2 Legend框架简介

Legend是Lody开源的一个Android免Root环境下的一个APK Hook框架,代码放在github上:https://github.com/asLody/legend。 该框架代码设计简洁,通用性高,适合逆向工程时一些Hook场景。

先来看看如何使用它。框架提供了两种使用方法:基于Annotation注解与代码直接调用。基于Annotation注解的Hook技术不是第一次被发现了,在Java开发的世界里,这种技术被广泛使用,大名鼎鼎的基于AOP开发的Aspectj就大量使用这种技术。使用Annotation方式编写的Java代码有着很强的灵活与扩展性。LegendAnnotation方式的Hook这样使用:

1
2
3
4
5
6
7
8
@Hook("android.app.Activity::startActivity@android.content.Intent")
public static void Activity_startActivity(Activity thiz, Intent intent) {
if (!ALLOW_LAUNCH_ACTIVITY) {
Toast.makeText(thiz, "I am sorry to turn your Activity down :)", Toast.LENGTH_SHORT).show();
} else {
HookManager.getDefault().callSuper(thiz, intent);
}
}

@Hook("xxx")部分指明需要Hook的类与方法以及方法的签名,此处的Activity_startActivity()是自己实现的替换android.app.Activity::startActivity()的方法,HookManager.getDefault().callSuper(thiz, intent);调用是调用原方法。

这种方式Hook的方法,需要执行一次Hook应用操作来激活所有注解Hook,方法是执行下面的方法,传入的YourClass.class是包含了注解的类:

1
HookManager.getDefault().applyHooks(YourClass.class);

另一种代码方式进行Hook使用起来更简单,Hook操作只需要一行代码:

1
HookManager.getDefault().hookMethod(originMethod, hookMethod);

这是Legend提供的demo展示的一个完整实例:

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
package com.legend.demo;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.widget.Toast;
import com.lody.legend.Hook;
import com.lody.legend.HookManager;
/**
* @author Lody
* @version 1.0
*/
public class App extends Application {
public static boolean ENABLE_TOAST = true;
public static boolean ALLOW_LAUNCH_ACTIVITY = true;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
HookManager.getDefault().applyHooks(App.class);
}
@Hook("android.app.Application::onCreate")
public static void Application_onCreate(Application app) {
Toast.makeText(app, "Application => onCreate()", Toast.LENGTH_SHORT).show();
HookManager.getDefault().callSuper(app);
}
@Hook("android.telephony.TelephonyManager::getSimSerialNumber")
public static String TelephonyManager_getSimSerialNumber(TelephonyManager thiz) {
return "110";
}
@Hook("android.widget.Toast::show")
public static void Toast_show(Toast toast) {
if (ENABLE_TOAST) {
HookManager.getDefault().callSuper(toast);
}
}
@Hook("android.app.Activity::startActivity@android.content.Intent")
public static void Activity_startActivity(Activity activity, Intent intent) {
if (!ALLOW_LAUNCH_ACTIVITY) {
Toast.makeText(activity, "I am sorry to turn your Activity down :)", Toast.LENGTH_SHORT).show();
}else {
HookManager.getDefault().callSuper(activity, intent);
}
}
}

0x3 原理分析

先来看看Hook注解的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Legend/legendCore/src/main/java/com/lody/legend/Hook.java
package com.lody.legend;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Lody
* @version 1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Hook {
String value() default "";
}

`@Target(ElementType.METHOD)`指明Hook注解用于修饰类中的Method,与之类似的还有@Target(ElementType.FIELD)用来修饰类中的Field。如果想让注解同时修饰类的FieldMethod,可以这么写:

1
2
3
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Hook{}
......

@Retention(RetentionPolicy.RUNTIME)指明Hook注解以何种形式进行保留。RetentionPolicy是一个enum类型,声明如下:

1
2
3
4
5
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}

`SOURCE`表明该注解类型的信息只保留在程序源码里,源码经过编译之后,注解的数据就会消失;CLASS表明注解类型的信息除了保留在程序源码里,同时也保留在编译好的class文件里面,但在执行的时候,并不会把这些信息加载到内存中去;RUNTIME是最大范围的保留,表示同时在源码与编译好的class文件中保留信息,并且在执行的时候会把这些信息加载到内存中去。

定义好了Hook注解,看它是如何使用的,这就是HookManager.getDefault().applyHooks()方法要做的工作,它的代码如下:

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
// Legend/legendCore/src/main/java/com/lody/legend/HookManager.java
public void applyHooks(Class<?> holdClass) {
for (Method hookMethod : holdClass.getDeclaredMethods()) {
Hook hook = hookMethod.getAnnotation(Hook.class);
if (hook != null) {
String statement = hook.value();
String[] splitValues = statement.split("::");
if (splitValues.length == 2) {
String className = splitValues[0];
String[] methodNameWithSignature = splitValues[1].split("@");
if (methodNameWithSignature.length <= 2) {
String methodName = methodNameWithSignature[0];
String signature = methodNameWithSignature.length == 2 ? methodNameWithSignature[1] : "";
String[] paramList = signature.split("#");
if (paramList[0].equals("")) {
paramList = new String[0];
}
try {
Class<?> clazz = Class.forName(className);
boolean isResolve = false;
for (Method method : clazz.getDeclaredMethods()) {
if (method.getName().equals(methodName)) {
Class<?>[] types = method.getParameterTypes();
if (paramList.length == types.length) {
boolean isMatch = true;
for (int N = 0; N < types.length; N++) {
if (!types[N].getName().equals(paramList[N])) {
isMatch = false;
break;
}
}
if (isMatch) {
hookMethod(method, hookMethod);
isResolve = true;
Logger.d("[+++] %s have hooked.", method.getName());
}
}
}
if (isResolve) {
break;
}
}
if (!isResolve) {
Logger.e("[---] Cannot resolve Method : %s.", Arrays.toString(methodNameWithSignature));
}
} catch (Throwable e) {
Logger.e("[---] Error to Load Hook Method From : %s." , hookMethod.getName());
e.printStackTrace();
}
}else {
Logger.e("[---] Can't split method and signature : %s.", Arrays.toString(methodNameWithSignature));
}
}else {
Logger.e("[---] Can't understand your statement : [%s].", statement);
}
}
}
}

该方法遍历类的所有方法,查找匹配注解信息中指定的方法,方法是:对于需要Hook的ClassholdClass,调用它的getDeclaredMethods()获取所有声明的方法,依次调用每个类的方法的getAnnotation()获取注解信息,取到的注解信息保存在String类型的statement变量中,类与完整的方法签名以“::”进行分隔,方法签名中的方法名与参数签名使用“@”进行分隔,参数签名中每个参数之间使用“#”进行分隔,取完一个方法所有的信息后,与类中的方法进行比较,如果完全匹配说明找到了需要Hook的方法,这个时候,调用hookMethod()方法进行Hook操作,注意这里的hookMethod()方法,即Legend框架支持的第二种Hook方式。

hookMethod()调用Runtime.isArt()判断当前代码执行在Art还是Dalvik模式,如果是Art模式,执行hookMethodArt()来完成Hook操作,如果是Dalvik模式,执行hookMethodDalvik()完成Hook。

Runtime.isArt()的代码只有一行,即判断虚拟机版本字符串是否以字符2开头,如下:

1
2
3
4
5
6
7
public static boolean isArt() {
return getVmVersion().startsWith("2");
}
public static String getVmVersion() {
return System.getProperty("java.vm.version");
}

执行完Hook后会返回一个backupMethod,这是一个原始方法的备份,最后将backupMethod放入以methodName命令的backupList,在methodNameToBackupMethodsMap备份就完事了。

接下来看看hookMethodArt()都干了啥:

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
private static Method hookMethodArt(Method origin, Method hook) {
ArtMethod artOrigin = ArtMethod.of(origin);
ArtMethod artHook = ArtMethod.of(hook);
Method backup = artOrigin.backup().getMethod();
backup.setAccessible(true);
long originPointFromQuickCompiledCode = artOrigin.getEntryPointFromQuickCompiledCode();
long originEntryPointFromJni = artOrigin.getEntryPointFromJni();
long originEntryPointFromInterpreter = artOrigin.getEntryPointFromInterpreter();
long originDeclaringClass = artOrigin.getDeclaringClass();
long originAccessFlags = artOrigin.getAccessFlags();
long originDexCacheResolvedMethods = artOrigin.getDexCacheResolvedMethods();
long originDexCacheResolvedTypes = artOrigin.getDexCacheResolvedTypes();
long originDexCodeItemOffset = artOrigin.getDexCodeItemOffset();
long originDexMethodIndex = artOrigin.getDexMethodIndex();
long hookPointFromQuickCompiledCode = artHook.getEntryPointFromQuickCompiledCode();
long hookEntryPointFromJni = artHook.getEntryPointFromJni();
long hookEntryPointFromInterpreter = artHook.getEntryPointFromInterpreter();
long hookDeclaringClass = artHook.getDeclaringClass();
long hookAccessFlags = artHook.getAccessFlags();
long hookDexCacheResolvedMethods = artHook.getDexCacheResolvedMethods();
long hookDexCacheResolvedTypes = artHook.getDexCacheResolvedTypes();
long hookDexCodeItemOffset = artHook.getDexCodeItemOffset();
long hookDexMethodIndex = artHook.getDexMethodIndex();
ByteBuffer hookInfo = ByteBuffer.allocate(ART_HOOK_INFO_SIZE);
hookInfo.putLong(originPointFromQuickCompiledCode);
hookInfo.putLong(originEntryPointFromJni);
hookInfo.putLong(originEntryPointFromInterpreter);
hookInfo.putLong(originDeclaringClass);
hookInfo.putLong(originAccessFlags);
hookInfo.putLong(originDexCacheResolvedMethods);
hookInfo.putLong(originDexCacheResolvedTypes);
hookInfo.putLong(originDexCodeItemOffset);
hookInfo.putLong(originDexMethodIndex);
hookInfo.putLong(hookPointFromQuickCompiledCode);
hookInfo.putLong(hookEntryPointFromJni);
hookInfo.putLong(hookEntryPointFromInterpreter);
hookInfo.putLong(hookDeclaringClass);
hookInfo.putLong(hookAccessFlags);
hookInfo.putLong(hookDexCacheResolvedMethods);
hookInfo.putLong(hookDexCacheResolvedTypes);
hookInfo.putLong(hookDexCodeItemOffset);
hookInfo.putLong(hookDexMethodIndex);
artOrigin.setEntryPointFromQuickCompiledCode(hookPointFromQuickCompiledCode);
artOrigin.setEntryPointFromInterpreter(hookEntryPointFromInterpreter);
artOrigin.setDeclaringClass(hookDeclaringClass);
artOrigin.setDexCacheResolvedMethods(hookDexCacheResolvedMethods);
artOrigin.setDexCacheResolvedTypes(hookDexCacheResolvedTypes);
artOrigin.setDexCodeItemOffset((int) hookDexCodeItemOffset);
artOrigin.setDexMethodIndex((int) hookDexMethodIndex);
int accessFlags = origin.getModifiers();
if (Modifier.isNative(accessFlags)) {
accessFlags &= ~ Modifier.NATIVE;
artOrigin.setAccessFlags(accessFlags);
}
long memoryAddress = Memory.alloc(ART_HOOK_INFO_SIZE);
Memory.write(memoryAddress,hookInfo.array());
artOrigin.setEntryPointFromJni(memoryAddress);
return backup;
}

原方法与替换的方法分别为artOriginartHook,执行artOriginbackup()完成方法的备份操作,backup()内部通过反射获取AbstractMethod类的artMethod字段,然后使用当前类的method进行填充,实际的操作就是复制一份当前类的method,此处不展开它的代码。

接下来的代码是获取artOriginartHook的重要字段,然后构造ByteBuffer类型的hookInfo,最后调用以下三行代码来完成Hook:

1
2
3
long memoryAddress = Memory.alloc(ART_HOOK_INFO_SIZE);
Memory.write(memoryAddress,hookInfo.array());
artOrigin.setEntryPointFromJni(memoryAddress);

ArtMethod在底层的内存结构定义仅次于Android源码的“art/runtime/art_method.h”文件,不同系统版本的Android这个结构体都可能会发现变化,为了保持兼容性,Legend在Java层手动定义保存了它们的字段偏移信息,与“Legend/legendCore/src/main/java/com/lody/legend/art/ArtMethod.java”文件保存在同一目录,在调用ArtMethod::of()方法构造ArtMethod时,会根据不同的系统版本来构造不同的对象。

Memory.write()方法底层调用的LegendNative.memput(),它是一个native方法,对应的实现是android_memput(),代码如下:

1
2
3
4
5
6
7
8
9
10
// Legend/Native/jni/legend_native.cpp
void android_memput(JNIEnv * env, jclass clazz, jlong dest, jbyteArray src) {
jbyte *srcPnt = env->GetByteArrayElements(src, 0);
jsize length = env->GetArrayLength(src);
unsigned char * destPnt = (unsigned char *)dest;
for(int i = 0; i < length; ++i) {
destPnt[i] = srcPnt[i];
}
env->ReleaseByteArrayElements(src, srcPnt, 0);
}

可以看出,馐的内存写操作是直接使用底层指定长度的字节流覆盖的,简单与暴力,而能够这样操作的原因,是当前操作的内存是自己的内存,想怎么干就怎么干!

setEntryPointFromJni()直接将原方法起始地址的指针内容,通过构造的memoryAddress覆盖写入!如此这般,Art模式下的Hook就完成了,当然,这其中很多小细节没有讲到,读者可以看行阅读它的代码。

接下来看看Dalvik下的Hook方法hookMethodDalvik()都干了啥:

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
// Legend/legendCore/src/main/java/com/lody/legend/HookManager.java
private static Method hookMethodDalvik(Method origin, Method hook) {
DalvikMethodStruct dvmOriginMethod = DalvikMethodStruct.of(origin);
DalvikMethodStruct dvmHookMethod = DalvikMethodStruct.of(hook);
byte[] originClassData = dvmOriginMethod.clazz.read();
byte[] originInsnsData = dvmOriginMethod.insns.read();
byte[] originInsSizeData = dvmOriginMethod.insSize.read();
byte[] originRegisterSizeData = dvmOriginMethod.registersSize.read();
byte[] originAccessFlags = dvmOriginMethod.accessFlags.read();
byte[] originNativeFunc = dvmOriginMethod.nativeFunc.read();
byte[] hookClassData = dvmHookMethod.clazz.read();
byte[] hookInsnsData = dvmHookMethod.insns.read();
byte[] hookInsSizeData = dvmHookMethod.insSize.read();
byte[] hookRegisterSizeData = dvmHookMethod.registersSize.read();
byte[] hookAccessFlags = dvmHookMethod.accessFlags.read();
byte[] hookNativeFunc = dvmHookMethod.nativeFunc.read();
dvmOriginMethod.clazz.write(hookClassData);
dvmOriginMethod.insns.write(hookInsnsData);
dvmOriginMethod.insSize.write(hookInsSizeData);
dvmOriginMethod.registersSize.write(hookRegisterSizeData);
dvmOriginMethod.accessFlags.write(hookAccessFlags);
ByteBuffer byteBuffer = ByteBuffer.allocate(DVM_HOOK_INFO_SIZE);
byteBuffer.put(originClassData);
byteBuffer.put(originInsnsData);
byteBuffer.put(originInsSizeData);
byteBuffer.put(originRegisterSizeData);
byteBuffer.put(originAccessFlags);
byteBuffer.put(originNativeFunc);
//May leak
long memoryAddress = Memory.alloc(DVM_HOOK_INFO_SIZE);
Memory.write(memoryAddress, byteBuffer.array());
dvmOriginMethod.nativeFunc.write(memoryAddress);
return origin;
}

分析完Art模式,Dalvik下的就不难看懂的,DalvikMethodStruct.of()会返回DalvikMethodStruct类型结构体,它是Dalvik虚拟机内部DalvikMethod结构体的内线性布局表示。

dvmOriginMethoddvmHookMethod分别代表原方法与Hook替换的方法,同样的,使用底层内存的写操作,对所有需要替换的字段进行替换。

最后就是Hook后的方法调用原方法了,它的代码如下:

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
// Legend/legendCore/src/main/java/com/lody/legend/HookManager.java
public <T> T callSuper(Object who, Object... args) {
StackTraceElement[] traceElements = Thread.currentThread().getStackTrace();
StackTraceElement currentInvoking = traceElements[3];
String invokingClassName = currentInvoking.getClassName();
String invokingMethodName = currentInvoking.getMethodName();
Map<String,List<Method>> methodNameToBackupMethodsMap = classToBackupMethodsMapping.get(invokingClassName);
if (methodNameToBackupMethodsMap != null) {
List<Method> methodList = methodNameToBackupMethodsMap.get(invokingMethodName);
if (methodList != null) {
Method method = matchSimilarMethod(methodList, args);
if (method != null) {
try {
if (Runtime.isArt()) {
return callSuperArt(method, who, args);
}else {
return callSuperDalvik(method, who, args);
}
} catch (Throwable e) {
Logger.e("[---] Call super method with error : %s, detail message please see the [Logcat :system.err].", e.getMessage());
e.printStackTrace();
}
}else {
Logger.e("[---] Super method cannot found in backup map.");
}
}
}
return null;
}

这段代码是在之前保存的methodNameToBackupMethodsMap中查找备份的方法,找到后对ArtDalvik模式分别调用callSuperArt()callSuperDalvik(),前者比较简单,只是调用方法的invoke()就完事,而Dalvik模式由于没有像Art那样做备份,所以多出了一个字段回替换的操作,完事也是调用的invoke()来执行原方法。

0x4 一些感想

分析完上面的代码,可以出来Legend尽管实现了ArtDalvik双模式下的Hook,但在实际逆向Hook中,还是有一些不足:

  1. 不能Hook字段。在很多应用场景中可能会用到,这里有一个迂回的替代的方案是:在字段较敏感的方法中对方法做Hook,然后在Hook代码中反射操作字段。
  2. Hook自定义的类加载器加载的类方法。由于反射查找的类的方法列表依赖于类的查找,对于部分自定义ClassLoader的情况,获取Class本身就存在着难度,更别说Hook它的方法了。
  3. 兼容性。只支持4.2到6.0,当然,根据技术原理,从2.3到7.1应该都是可以做到的。
  4. 稳定性。与该框架技术原理类似的还有很多,比较alibaba的AndFix,在系统自定义修改较多的情况下,框加要的稳定性存疑,当然,逆向工程时使用的稳定性远没有做产品要求的高,一些全新思路的Hook修改方案如Tinker可能也是一个不错的选择,留待以后测试了!

最后,讲完了它的原理,并没有讲如何在逆向工程中使用,这个交给聪明的安全研究人员作为思维发散。

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不太一样,更加简单与完整,这里不再赘述了!

DMG文件管理

dmg是苹果电脑上专用的磁盘镜像(disk image)文件,类似于Windows平台上的iso镜像,dmg类似于一个压缩文档,支持压缩与加密,将程序与文档打包成dmg是一种比较流行的软件发布形式。

0x1 构建dmg

苹果官方系统自带的磁盘管理工具Disk Utility可以很方便的构建dmg文件,最简单的方法是启动/Applications/Utilies/Disk Utility,点击菜单File->New Image->Image From Folder…,从文件夹创建镜像,这一步,选择上一节的app目录,如图所示:
disk_utility
在Save As处输入要保存的文件名;Encryption处选择是否进行加密,none表示不加密,128-bit AES encryption是macOS版本10.3以前之前支持的128位的AES加密,258-bit AES encryption则是macOS版本10.5以后才开始支持的256位AES加密,在选择这两种中任意一种加密方式后,会弹出输入密码的对话框,提示输入的密码不是AES算法的加密key,只是一个用户自已设置的密码;设置好密码后,在Image Format处设置镜像的格式,read-only表示创建只读的镜像,compressed表示对镜像进行压缩,read/write表示镜像可读可写,DVD/CD master表示创建DVD镜像,hybrid image表示创建混合镜像。选择好选项后,点击Save铵钮,dmg就创建成功了。

除了使用图形界面创建dmg外,还可以使用命令行工具hdiutil来创建,例如为app目录下的myframeworktest.app创建一个AES128加密,密码为abc123的dmg镜像只需要执行如下命令即可:

1
2
3
4
$ hdiutil create -fs HFS+ -volname myframework -srcfolder ../pkg_install_script/app -encryption AES-128 -stdinpass -o myframeworktest_cmd.dmg
Enter disk image passphrase: //此处输入密码123
..
created: /Users/macbook/code/chapter4/dmg/myframeworktest_cmd.dmg

如果觉得从文件夹中创建的dmg不够个性化,完全可以使用Disk Utility创建自定义的dmg,自定义的dmg包括为dmg指定图标,背景图片,以及dmg文件的显示方式及大小。只需要打开Disk Utility,点击菜单File->New Image->Blank Image…,创建一个空白的镜像,在保存对话框中,设置镜像的大小、加密方式、分区格式及镜像格式,需要注意的是,此处镜像格式需要选择read/write disk image,创建成功后,打开镜像,将app目录中的文件复制进去,如果需要更换背景图片,只需要将背景图片复制到镜像中,使用chflags命令设置成隐藏格式,或者放放一个点“.”结尾的目录(点目录默认会隐藏显示),在镜像上右键,在弹出的菜单中选择Get Info,然后设置背景图片即可。操作完后,点击Disk Utility菜单的Images->Convert…,,选择操作后的dmg镜像,将该镜像压缩保存一下就可以发布了。

除了官网的Disk Utility外,也可以使用上一小节中介绍的Luggage工具,编译脚本后,执行“make dmg”来生成dmg文件。最后,还有一些第三方的工具也可以创建dmg镜像,比较知名的有DropDMG(下载地址:http://c-command.com/dropdmg ),从软件的名称上就可以判断,它支持快速的从文件拖放来创建dmg镜像,有兴趣的读者可以试试,它的使用比较简单,此处不再赘述。

0x2 管理dmg

dmg文件格式不是开放的,要想探索它的文件格式,可以逆向hidutil工具处理dmg的部分代码。在使用dmg的过程中,一种典型的可能遇到的场景是将dmg转换格式后,在Windows或Linux平台上使用,针对早期版本的dmg,网上有第三方的开方人员开发了dmg2img工具(下载地址:https://github.com/Lekensteyn/dmg2img ),方便将dmg转换成可以在Linux系统上挂载的镜像,还有一个工具dmg2iso(下载地址:https://sourceforge.net/projects/dmg2iso ),可以将dmg转换成Windows平台上使用的iso镜像,实际上该工具的底层是调用的hdiutil

使用hdiutil来管理dmg已经足够了,它提供了查看、创建、转换dmg等功能,例如,查看myframeworktest.dmg的信息可以执行如下命令:

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
$ hdiutil imageinfo myframeworktest.dmg
Format Description: UDIF read-only compressed (zlib)
Class Name: CUDIFDiskImage
Checksum Type: CRC32
Size Information:
Compressed Ratio: 0.022532451628704386
Total Empty Bytes: 500224
Sector Count: 5060
Total Bytes: 2590720
CUDIFEncoding-bytes-wasted: 7963
Total Non-Empty Bytes: 2090496
CUDIFEncoding-bytes-in-use: 47410
Compressed Bytes: 47410
CUDIFEncoding-bytes-total: 55373
Checksum Value: $404B6F25
Segments:
......
-1:
Name: Protective Master Boot Record (MBR : 0)
Partition Number: -1
Checksum Type: CRC32
Checksum Value: $0492F534
2:
Name: (Apple_Free : 3)
Partition Number: 2
Checksum Type: CRC32
Checksum Value: $00000000
Format: UDZO
Backing Store Information:
......
partitions:
partition-scheme: GUID
block-size: 512
partitions:
0:
partition-name: Protective Master Boot Record
partition-start: 0
partition-synthesized: true
partition-length: 1
partition-hint: MBR
......
7:
partition-name: GPT Header
partition-start: 5059
partition-synthesized: true
partition-length: 1
partition-hint: Backup GPT Header
burnable: false
udif-ordered-chunks: false
Properties:
Encrypted: false
Kernel Compatible: true
Checksummed: true
Software License Agreement: false
Partitioned: false
Compressed: true
Resize limits (per hdiutil resize -limits):
min cur max
5060 5060 5060

将myframeworktest_cmd.dmg密码abc123更改为123abc只需执行如下命令:

1
2
3
4
$ hdiutil chpass ./myframeworktest_cmd.dmg
Enter password to access "myframeworktest_cmd.dmg": //abc123
Enter a new password to secure "myframeworktest_cmd.dmg": //123abc
Re-enter new password: //123abc

将myframeworktest.dmg转换成iso格式可以执行如下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ hdiutil convert ./myframeworktest.dmg -format UDTO -o ./myframeworktest.cdr
Reading Protective Master Boot Record (MBR : 0)…
Reading GPT Header (Primary GPT Header : 1)…
Reading GPT Partition Data (Primary GPT Table : 2)…
Reading (Apple_Free : 3)…
Reading disk image (Apple_HFS : 4)…
..............................................................................
Reading (Apple_Free : 5)…
Reading GPT Partition Data (Backup GPT Table : 6)…
..............................................................................
Reading GPT Header (Backup GPT Header : 7)…
..............................................................................
Elapsed Time: 7.868ms
Speed: 314.0Mbytes/sec
Savings: 0.0%
created: /Users/.../code/chapter4/dmg/myframeworktest.cdr
$ mv ./myframeworktest.cdr ./myframeworktest.iso

另外,DropDMG也提供了很方便的dmg管理功能。例如,在文件夹上点击右键,在弹出的菜点中,选择Services->DropDMG:Use Current Configration,DropDMG就会使用当前默认的配置为文件夹在当前目录创建一个dmg,或者在dmg上点击右键,选择DropDMG:Ask for Options,来对dmg做一些修改,例如设置图标、修改密码、更改格式等。

PKG安装包的管理与文件格式分析

不同的操作系统都有专属于自己的软件安装包格式。如Ubuntu系统上的deb安装包,Windows系统上的msi安装包等。macOS系统使用pkg作为软件安装包格式。

大多数macOS上开发的程序都不需要安装程序,它们只是一个以app结尾的Bundle包,使用zip压缩一下,或者dmg制作一份镜像,是这类程序的主要发布方式。然而,一些App有一些特定的需求,比如:向系统配置面板写配置程序、安装屏幕保护程序、读写特定的目录与文件等。此时就可以制作pkg安装包程序来安装这类特殊的程序了。当然,由于这些特殊性,pkg安装程序无法通过苹果官方商店来发布。

0x1 构建pkg

pkg安装程序能够扩展程序安装内容以及读写特定目录的特性,来源于pkg支持的脚本特性,pkg安装程序允许开发人员在程序的安装过程中,运行自己编写的Bash脚本程序。

苹果官方在低版本的Xcode工具中提供了PackageMaker用来制作pkg,该工具没有直接包含在XCode开发套件中,如果要使用它,需要到苹果的开发者官网上去下载它。下载安装好该工具后,运行PackageMaker.app就可以制作pkg了。

本小节制作一个pkg安装包,完成以下目标:将上一小节的myframeworktest程序安装到Applications目录下,随便将myframework.framework框架安装到~/Library/frameworks目录中。启动PackageMaker,点击菜单File->New,在弹出的对话框中,在Organization一栏输入机构信息,如“com.macbook”,点击OK返回程序主界面,点击File->Save,将工程保存为pkg_install.pmdoc。

将上一小节的myframeworktest程序放到app目录下,将app目录直接拖入PackageMaker的主界面,会自己加程序添加配置信息,点击Configuration可以设置一些安装时的配置信息,如图所示:
pkgmaker
install指定要安装的程序路径,这里已经指定好了;Destination指定程序要安装的位置,默认为“/Applications”目录;取消勾选“Allow custom location”选项,让程序只能安装到/Applications目录下,Package Identifier指定安装包的标识符,macOS记录安装过的pkg就是通过它来识别的,手动卸载pkg时需要用到它;Package Version指定安装包的版本,版本号是识别pkg版本升级的关键,为pkg指定升级脚本时需要用到;Restart Action指定pkg安装完成后,是否需要执行注销、关机或重启等操作;Require admin authentication复选框指定安装器需要管理员权限,为pkg指定的安装脚本如果需要管理员权限的话,就需要在此勾选上。
配置好后,点击Contents标签,配置需要安装的内容,PackageMaker已经默认选择好了要安装的内容为myframeworktest.app,并且文件的读写与执行权限也自动设置好了,如图所示:
pkgmaker_contents
关于文件的读写权限设罢,一个建议的设置如下表所示:

表4.1 Contents权限设置

Owner Group Permissions
Applications root admin rwxrwxr-x
System root admin rwxrwxr-x
Library root admin rwxrwxr-x
Extensions root admin rwxrwxr-x

关于拖入Contents中的程序,这里有一个技巧!macOS系统会为操作过的文件夹中生成一个隐藏的.DS_Store文件,如果系统开启了显示隐藏文件的选项,直接打包程序会在安装包中包含隐藏的.DS_Store文件,需要在拖入Contents前将它们全部删除,可以使用如下的命令:

1
find ./ -name ".DS_Store" -exec rm -f {} \;

点击Components标签,配置组件信息。取消掉“Allow Relocation”复选框,否则即使提示安装完成,在/Applications目录下也看不到安装后的程序。如图所示:
pkgmaker_components
点击Scripts标签,配置运行安装器时需要执行的脚本。将编写好的脚本分别保存为preflight与postflight,然后将它们放到script目录下,执行以下命令为它们赋上可执行权限:

1
2
$ chmod a+x ./preinstall
$ chmod a+x ./postflight

将script目录直接拖入Scripts Directory旁的文本框中,此时,Preflight与Postflight脚本会自动设置完成。如图所示:
pkgmaker_script
可以设置的脚本有六个,它们按照执行顺序分别是:

  • preflight:点击安装界面上的Install按钮时运行此脚本。该脚本在程序每次安装时都会运行。
  • preinstall/preupgrade:针对单程序安装包(pkg),该脚本会在preflight脚本运行之后运行,针对多程序安装包(mpkg),该脚本会在用户按下Install铵钮后执行。preinstall与preupgrade的区别在于:preinstall只会在用户第一次安装该程序时执行,而preupgrade相反,如果之前安装过该程序,那么该脚本才会执行,preupgrade用于软件升级时使用。区分程序是否为第一次安装是通过pkg安装器Installer.app来完成的,Installer.app通过查看/private/var/db/receipts目录,查看目录中是否有以程序包名命名的pkg文件,如果存在,说明已经安装过,反之,为第一次安装。
  • postinstall/postupgrade:该脚本在程序安装完之后才运行。它们的区别与preinstall/preupgrade一样。
  • postflight:该脚本在postinstall/postupgrade脚本之后运行。
    PackageMaker支持Shell脚本与Perl脚本,此处编写的是Shell脚本,preinstall脚本的内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env bash
echo "Running myframeworktest.app preinstall script."
echo "Killing myframeworktest.app."
killall "myframeworktest"
echo "Finding old versions of myframeworktest."
mdfind -onlyin /Applications "kMDItemCFBundleIdentifier=='fc.myframeworktest'" | xargs -I % rm -rf %
echo "Removed old versions of myframeworktest.app, if any."
echo "Ran myframeworktest.app preinstall script."
exit 0

这段脚本首先使用killall杀掉正在运行的myframeworktest.app进程;接着使用mdfind在/Applications目录下查找程序标识符为”fc.myframeworktest“的程序包路径,找到后使用rm -rf将其删除掉。

再来看看postflight脚本的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env bash
echo "Running myframeworktest.app postinstall script."
echo "Installing myframework.framework."
rm -rf ~/Library/Frameworks/myframework.framework
mkdir ~/Library/Frameworks/myframework.framework
cp -r /Applications/myframeworktest.app/Contents/Frameworks/myframework.framework/* ~/Library/Frameworks/myframework.framework
chmod -R 6777 ~/Library/Frameworks/myframework.framework
echo "Ran myframeworktest.app postinstall script."
exit 0

该脚本运行时,myframeworktest.app程序包已经安装到了/Applications目录下,将myframework.framework拷贝到~/Library/Frameworks目录下,然后修改它的权限为任何人都可读可写可执行,最后执行完后调用”exit 0”退出脚本。

配置好要安装的内容与执行脚本后,点击Contents上面的图标,对pkg进行配置,点击Configuration,在Title旁的文本框中输入安装包的标题,例如”mframeworktest installer“;User Sees处选择Easy Install Only(简单安装)即可;Install Destination处勾选Volume selected by user。

点击Requirements标签,设置pkg运行的系统要求。点击界面左下角的加号”+“按钮,添加两条规则:一条是System OS Version(e.g. 10.x.x),另一条是Target OS Version(e.g. 10.x.x),都设置成”>=“10.6,如图所示:
pkgmaker_requirements

最后的Actions标签页不用去管它。配置完了后,还可以点击界面右上角的Edit interface按钮来编辑安装程序的界面。包括:Background、Introduction、Read me、License与Finish up。它们每一项都是一个页面,内容可以选择系统默认的Default,也可以直接写一段文本甙入(Embedded)进去,或者选择一个外部的rtf文档或html网页。如图所示,为一段手写的Read Me:
pkgmaker_readme

以上所有操作完成后,点击PackageMaker左上角的Build铵钮进行构建,或者Build and Run按钮构建成功后直接运行。构建完成后会针对单程序安装包或多程序安装包生成一个pkg或mpkg文件,该文件是最终可以发布的产品,接下来只需要对其进行安装测试,没问题就可以发布了。

在新版本的XCode中,提供了命令行工具productbuild来打包制作pkg。本小节PackageMaker操作的步骤可以执行以下命令完成:

1
$ productbuild --component app/myframeworktest.app /Applications --scripts script ~/Desktop/out.pkg

命令执行完后,就会在当前用户桌面上生成pkg文件,当然编译时可以指定--sign参数来为pkg签名,pkg的签名不是使用codesign,如果创建pkg时没有对其进行签名,或者手动修改过pkg的内容,可以使用工具productsign来对pkg进行签名。

介绍了官方的pkg创建工具后,再来看看目前市面上常用的pkg制作工具。
喜欢命令行编译的开发人员一定会喜欢工具Luggage(下载地址:“https://github.com/unixorn/luggage”),它提供了一种自定义脚本的方式来编译构建pkg文件。该工具的使用方法很简单,只需要将整个github上的文件复制到/usr/local/share/luggage就完成了安装。至于脚本如何编写,可以参考`Luggage`提供的样例,
地址为“https://github.com/unixorn/luggage-examples”,以编译样例中的fex程序为例,在命令行下执行以下命令:

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
$ make
Usage
make clean - clean up work files.
make dmg - roll a pkg, then stuff it into a dmg file.
make zip - roll a pkg, then stuff it into a zip file.
make pkg - roll a pkg.
make pkgls - list the bill of materials that will be generated by the pkg.
$ make pkg
Password:
make -f Makefile -e pack-fex
Disabling bundle relocation.
If you need to override permissions or ownerships, override modify_packageroot in your Makefile
Creating /tmp/the_luggage/Fex-20160902/payload/Fex-20160902.pkg with /usr/bin/pkgbuild.
sudo /usr/bin/pkgbuild --root /tmp/the_luggage/Fex-20160902/root \
--component-plist /tmp/the_luggage/Fex-20160902/luggage.pkg.component.plist \
--identifier com.huronhs.Fex \
--filter "/CVS$" --filter "/\.svn$" --filter "/\.cvsignore$" --filter "/\.cvspass$" --filter "/(\._)?\.DS_Store$" --filter "/\.git$" --filter "/\.gitignore$" \
--scripts /tmp/the_luggage/Fex-20160902/scripts \
--version 20160902 \
--ownership preserve --quiet \
/tmp/the_luggage/Fex-20160902/payload/Fex-20160902.pkg
$ ls
Fex-20160902.pkg Makefile fex

从输出中可以看出,除了构建pkg,Luggage还支持生成dmg与zip打包的程序,非常方便。

Luggage类似的还有createOSXinstallPkg(下载地址:“https://github.com/munki/createOSXinstallPkg”),使用方法也很简单,有兴趣的读者可以到github页面上查看如何使用。

最后,还有一款免费强大的pkg安装包制作工具Iceberg(下载地址:http://s.sudre.free.fr/Software/Iceberg.html ),该工具可以修改安装程序界面的背景图片,此处就不去讨论它的用法了,有兴趣的读者可以去它的官网下载了试试。

0x2 pkg的安装与卸载

安装pkg很简单,只要双击pkg,或者双击mpkg,就会弹出安装向导,按照步骤不停点击Next,直到安装完成,安装过程中,可能执行一些操作可能会需要管理器权限,系统会弹出提示要求用户输入管理员密码,按照操作输入密码即可。除了双击安装外,还可以使用命令行工具installer进行静默安装,执行以下命令可以安装上一小节的pkg:

1
2
3
4
5
$ sudo installer -pkg ./myframework_installer.pkg -target LocalSystem
Password:
installer: Package name is myframework installer
installer: Upgrading at base path /
installer: The upgrade was successful.

pkg的卸载就没这么简单了!苹果公司没有提供直接卸载pkg的方法,上一小节没有制作pkg格式的卸载程序,而是编写了一个简单的脚本,只需要双击运行它就可以卸载上一节制作的pkg。脚本的代码如下:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env bash
if [ -d ~/Library/Frameworks/myframework.framework ]; then
/bin/rm -rf ~/Library/Frameworks/myframework.framework
fi
if [ -d /Application/myframework.app ]; then
/bin/rm -rf /Applications/myframeworktest.app
fi
echo done.

对于没有提供卸载程序的pkg,卸载它们就只能手动或者依赖第三方的工具。例如UninstallPKG(下载地址:http://www.corecode.at/uninstallpkg ),这是一个收费软件,安装并运行该软件后,它会收集系统中安装的所有pkg软件,然后使用列表形式展示出来,如图所示:
uninstallpkg
点击View Package…按钮,可以查看pkg在系统中写入了哪些文件内容,点击Uninstall Package…可以直接卸载pkg。

UninstallPKG是如何做到收集与卸载系统中安装的pkg呢?其实它的原理并不难。它通过读取/private/var/db/receipts下的pkg列表,然后使用lsbom查看这些pkg文件的bom信息,找到bom文件中保存的文件列表,将它们列举出来,卸载的时候将它们全部删除即可。执行如下的命令列表可以查看上一小节pkg的信息:

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
$ cd /private/var/db/receipts
$ ls | grep macbook
com.macbook.myframeworkInstaller.pkg.bom
com.macbook.myframeworkInstaller.pkg.plist
$ lsbom -pf ./com.macbook.myframeworkInstaller.pkg.bom
.
./myframeworktest.app
./myframeworktest.app/Contents
./myframeworktest.app/Contents/Frameworks
./myframeworktest.app/Contents/Frameworks/myframework.framework
./myframeworktest.app/Contents/Frameworks/myframework.framework/Resources
./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions
./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A
./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/Resources
./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/Resources/Info.plist
./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/_CodeSignature
./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/_CodeSignature/CodeResources
./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/myframework
./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/Current
./myframeworktest.app/Contents/Frameworks/myframework.framework/myframework
./myframeworktest.app/Contents/Info.plist
./myframeworktest.app/Contents/MacOS
./myframeworktest.app/Contents/MacOS/myframeworktest
./myframeworktest.app/Contents/PkgInfo
./myframeworktest.app/Contents/Resources
./myframeworktest.app/Contents/Resources/Base.lproj
./myframeworktest.app/Contents/Resources/Base.lproj/MainMenu.nib
./myframeworktest.app/Contents/_CodeSignature
./myframeworktest.app/Contents/_CodeSignature/CodeResources

可以看出,脚本中执行的命令,在~/Library/Frameworks目录中安装的myframework.framework并没有列出来,而只有在Contents中指定的内容。上面查看bom信息使用的lsbom命令,其实,查看pkg中的内容还有一种更简单的方法,在双击运行pkg后,不要点击Continue按钮,而是点击菜单File->Show Files,pkg中包含的文件内容就一目了然了,如图所示:
pkg_showfiles
除了手动的去/private/var/db/receipts目录下读取pkg列表,还可以使用pkg管理工具pkgutil来查看系统中安装的pkg信息,不过只有查看功能,不能卸载。执行如下命令可以查看上一节安装的pkg信息,效果与上面一样:

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
$ pkgutil --pkgs | grep -i com.macbook
com.macbook.myframeworkInstaller.pkg
$ pkgutil --files com.macbook.myframeworkInstaller.pkg
myframeworktest.app
myframeworktest.app/Contents
myframeworktest.app/Contents/Frameworks
myframeworktest.app/Contents/Frameworks/myframework.framework
myframeworktest.app/Contents/Frameworks/myframework.framework/Resources
myframeworktest.app/Contents/Frameworks/myframework.framework/Versions
myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A
myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/Resources
myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/Resources/Info.plist
myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/_CodeSignature
myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/_CodeSignature/CodeResources
myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/myframework
myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/Current
myframeworktest.app/Contents/Frameworks/myframework.framework/myframework
myframeworktest.app/Contents/Info.plist
myframeworktest.app/Contents/MacOS
myframeworktest.app/Contents/MacOS/myframeworktest
myframeworktest.app/Contents/PkgInfo
myframeworktest.app/Contents/Resources
myframeworktest.app/Contents/Resources/Base.lproj
myframeworktest.app/Contents/Resources/Base.lproj/MainMenu.nib
myframeworktest.app/Contents/_CodeSignature
myframeworktest.app/Contents/_CodeSignature/CodeResources

0x3 pkg文件格式

pkg分为pkg与mpkg,前者是针对单程序安装;后者是针对多程序安装,它包含一个或多个的子包(Sub Package)。pkg本身又有两种格式,一种是与Bundle一样,有着特定组织结构的目录,上一小节生成的pkg的安装包就是这种格式的,还有一种是xar格式的文件,下面分别对这两种格式的安装包进行分析。

首先看myframework_installer.mpkg,使用tree命令(系统默认没有此命令,可以使用“brew install tree”进行安装)查看它的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ tree ./myframework_installer.mpkg/
./myframework_installer.mpkg/
└── Contents
├── Packages
│   └── app.pkg
│   └── Contents
│   ├── Archive.bom
│   ├── Archive.pax.gz
│   ├── Info.plist
│   ├── PkgInfo
│   └── Resources
│   ├── en.lproj
│   │   └── Description.plist
│   ├── package_version
│   ├── postflight
│   └── preflight
├── Resources
│   └── en.lproj
└── distribution.dist
8 directories, 9 files

对于外层的mpkg,它的Packages目录下存放的是pkg文件列表,也就是子包列表;Resources目录存放了pkg用到的资源、如本地化资源、图像、rtf文档、pdf文档等;还有一个distribution.dist文件,这是一个xml文档,包含了要安装的子包、运行时脚本等信息。对于当前的mpkg,它的内容如下:

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
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<installer-script minSpecVersion="1.000000" authoringTool="com.apple.PackageMaker" authoringToolVersion="3.0.6" authoringToolBuild="201">
<title>myframework installer</title>
<options customize="never" allow-external-scripts="no" rootVolumeOnly="false"/>
<installation-check script="pm_install_check();"/>
<volume-check script="pm_volume_check();"/>
<script>function pm_volume_check() {
if(!(my.target.systemVersion &amp;&amp; /* &gt;= */ system.compareVersions(my.target.systemVersion.ProductVersion, '10.6') &gt;= 0)) {
my.result.title = 'Failure';
my.result.message = 'Installation cannot proceed, as not all requirements were met.';
my.result.type = 'Fatal';
return false;
}
return true;
}
function pm_install_check() {
if(!(/* &gt;= */ system.compareVersions(system.version.ProductVersion, '10.6') &gt;= 0)) {
my.result.title = 'Failure';
my.result.message = 'Installation cannot proceed, as not all requirements were met.';
my.result.type = 'Fatal';
return false;
}
return true;
}
</script>
<choices-outline>
<line choice="choice0"/>
</choices-outline>
<choice id="choice0" title="app">
<pkg-ref id="com.macbook.myframeworkInstaller.pkg"/>
</choice>
<pkg-ref id="com.macbook.myframeworkInstaller.pkg" installKBytes="108" version="1.0" auth="Root">file:./Contents/Packages/app.pkg</pkg-ref>
</installer-script>

pm_install_check()pm_volume_check()分别做安装时检查与卷标检查,下面的choices-outline部分指定了安装时使用的choice,也就是选择执行哪个子包,对于当前mpkg包,它只有一个pkg,choice的id为“choice0”,指向的路径是“file:./Contents/Packages/app.pkg”。

app.pkg是要安装的子包,是一个pkg格式的Bundle结构的目录,它包含一个Contents子目录,里面有四个文件与一个目录Resources,它们分别是:

  • Archive.bom:bom信息。存放的要安装写入的文件列表,可以使用“lsbom -pf”命令查看,效果与上一节讲到的一样。
  • Archive.pax.gz:使用pax格式打包后,再使用gzip压缩的压缩包,它的内容就是要安装的内容,此处就是myframeworktest.app程序。可以执行以下命令进行解压:

    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
    $ cd ./myframework_installer.mpkg/Contents/Packages/app.pkg/Contents/
    $ gunzip -d ./Archive.pax.gz
    $ pax -rvf ./Archive.pax
    .
    ./myframeworktest.app
    ./myframeworktest.app/Contents
    ./myframeworktest.app/Contents/_CodeSignature
    ./myframeworktest.app/Contents/_CodeSignature/CodeResources
    ./myframeworktest.app/Contents/Frameworks
    ./myframeworktest.app/Contents/Frameworks/myframework.framework
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/myframework
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/Resources
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/_CodeSignature
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/_CodeSignature/CodeResources
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/myframework
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/Resources
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/A/Resources/Info.plist
    ./myframeworktest.app/Contents/Frameworks/myframework.framework/Versions/Current
    ./myframeworktest.app/Contents/Info.plist
    ./myframeworktest.app/Contents/MacOS
    ./myframeworktest.app/Contents/MacOS/myframeworktest
    ./myframeworktest.app/Contents/PkgInfo
    ./myframeworktest.app/Contents/Resources
    ./myframeworktest.app/Contents/Resources/Base.lproj
    ./myframeworktest.app/Contents/Resources/Base.lproj/MainMenu.nib
  • Info.plist:pkg包的信息。如CFBundleIdentifier为pkg的标识;IFMajorVersion与IFMinorVersion分别为pkg的主版本与子版本号;IFPkgFlagInstalledSize为pkg安装后所需要占用的字节大小。

  • PkgInfo:8字节的标识。表明是一个pkg文件。

Resources目录除了包含资源文件外,还包含了:

  • package_version:它是包版本文件。也就是使用PackageMaker制作pkg时设置的版本号;
  • postflight/preflight:pkg要执行的脚本文件。上一节中讲过,此处是未经过加密明文存放的。

另外一种是xar格式的文件,可以使用如下命令查看myframework_installer.pkg文件的格式:

1
2
$ file ./myframework_installer.pkg
./myframework_installer.pkg: xar archive - version 1

xar是压缩的可扩展归档格式,可以使用xar命令对其进行解压,执行如下命令解压:

1
2
3
4
5
6
7
8
9
$ xar -xvf ./myframework_installer.pkg
Distribution
app.pkg/PackageInfo
app.pkg/Bom
app.pkg/Payload
app.pkg/Scripts
app.pkg
Resources/en.lproj
Resources

Distribution与前面讨论的distribution.dist文件基本一样;Resources目录与前面的也一样;主要看app.pkg,它包含四个文件:

  • Bom:bom信息,存放的要安装写入的文件列表。可以使用“lsbom -pf”命令查看,效果与上一节讲到的一样。
  • PackageInfo:文本文件,包含了包的信息。可以使用cat命令查看它的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat app.pkg/PackageInfo
<pkg-info format-version="2" identifier="com.macbook.myframeworkInstaller.pkg" version="1.0" install-location="/Applications" auth="root">
<payload installKBytes="108" numberOfFiles="25"/>
<scripts>
<preinstall file="./preflight"/>
<postinstall file="./postflight"/>
</scripts>
<bundle id="fc.myframeworktest" CFBundleIdentifier="fc.myframeworktest" path="./myframeworktest.app" CFBundleVersion="1">
<bundle id="fc.myframework" CFBundleIdentifier="fc.myframework" path="./Contents/Frameworks/myframework.framework" CFBundleVersion="1"/>
</bundle>
<bundle-version>
<bundle id="fc.myframeworktest"/>
<bundle id="fc.myframework"/>
</bundle-version>
  • Payload:经过gzip压缩过的数据内容,本处为要安装的myframework.app,可以使用如下命令进行解压:
1
2
$ cat ./Payload | cpio -i
3 blocks

解压成功后就会在当前目录下生成myframework.app。

  • Scripts:经过gzip压缩过的脚本。可以使用如下命令进行解压:
1
2
3
$ cd app.pkg
$ cat ./Scripts | cpio -i
181 blocks

解压成功后就会在当前目录下生成未加密的preflight与postflight脚本。

0x4 破解pkg

对于pkg格式有一定了解后,修改或破解pkg就不会感到多难。破解pkg无非有以下三种:

  • 资源的替换或修改:针对文件夹类型的pkg,未加密,可直接进行修改替换;针对xar类型的pkg,需要先解压xar,然后替换或修改完资源后,重新压缩xar。
  • 安装脚本的替换或修改:针对文件夹类型的pkg,未加密,可直接进行修改替换;针对xar类型的pkg,需要先解压xar,然后解压Scripts,然后替换或修改完脚本后,重新压缩Scripts,最后重新压缩xar。
  • 安装内容的替换或修改:针对文件夹类型的pkg,未加密,但需要先对Archive.pax.gz进行解包,修改完后,需要重新打包回去;针对xar类型的pkg,需要先解压xar,然后解压Payload,替换或修改完数据后,重新压缩Payload,最后重新压缩xar。

以上步骤是操作思路,实际分析过程中,使用工具来做一些辅助工作是可以大大提高效率的,在拿到pkg后,首先快速浏览pkg文件,简单分析出pkg的行为与可能要做的操作。推荐一款工具:Suspicious Package(下载地址:http://www.mothersruin.com/software/SuspiciousPackage ),此工具提供了快速浏览插件,安装完成后,在要操作的pkg上按下空格,就可以快速查看pkg,检索要安装的软件内容,如图所示:
pkg_quicklook
还可以查看要执行的脚本的内容,如图所示:
pkg_quicklook_script
对pkg有了初步了解后,找到需要操作的地方后,下一步就是提取数据内容了,Suspicious Package支持数据的提取,使用Suspicious Package打开pkg文件后,在主界面的All Files列就可以查看所有文件,可以选中要导出的文件,直接拖出到Finder,或者点击Action->Export,都可以将文件导出,操作效果如图所示:
pkg_export

除了Suspicious Package外,介绍另外一款更强大的工具:Pacifist(下载地址:http://www.charlessoft.com ),这款工具支持多种文件数据的提取,其中就包括pkg,如图所示:
pacifist
选中要提取的文件,右键选择“Extract to Custom Location…”,或者直接拖到要保存到的文件夹中,都可以将文件提取出来。

提取出来的文件,分析完成,修改好了后,就要打包回去了,Pacifist不支持将数据打包回去,如果修改的是Payload,可以使用如下命令将app目录下的myframeworktest.app打包回去:

1
find app/* | cpio -o > ./Payload

如果是脚本文件,也可以如法炮制,最后就是将修改好的Payload或Scripts重新打包回去,可以使用执行“xar cvf”命令来操作,这里推荐另一款图形化工具:Flat Package Editor,该工具是苹果官方提供的,与PackageMaker一起提供给开发人员,它可以对pkg直接进行增、删、改操作,使用Flat Package Editor打开要操作的pkg,将修改好的Payload或Scripts拖回去,然后,点击菜单File->Save就保存成功了!如图所示:
flat_pkg_editor
操作完成后,pkg就算修改好了,接下来测试安装没问题就算破解完成了。

静态库的管理与文件格式分析

静态库与动态库都属于Mach-O格式的文件,动态库使用.dylib作为文件的扩展名,静态库的扩展名则是.a;在功能上,动态库通过动态链接的方式向其它程序提供接口,而静态库则是将功能代码直接编译进目标Mach-O文件中去,多个程序使用同一个动态库并不会增加目标文件的大小,使用静态库则会将每份功能代码都拷贝到目标文件中;从运行效率上来说,动态库需要在加载后做符号绑定操作,而静态库代码直接在目标程序中运行,理论上来讲,使用静态库的运行效率比动态库要高一些。

0x1 构建静态库

XCode提供了创建静态库的工程模板,创建静态库的方法与创建动态库几乎一样,唯一不同的是,在项目设置时,Type选择Static。还是与创建动态库一样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//mystaticlib.h
#import <Foundation/Foundation.h>
@interface mystaticlib : NSObject
-(void) hello;
@end
//mystaticlib.m
#import "mystaticlib.h"
@implementation mystaticlib
-(void) hello {
NSLog(@"hello world");
}
@end

分别保存为mystaticlib.h与mystaticlib.m。然后使用xcodebuild编译会有如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ xcodebuild
=== BUILD TARGET mystaticlib OF PROJECT mystaticlib WITH THE DEFAULT CONFIGURATION (Release) ===
Check dependencies
Write auxiliary files
write-file /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/mystaticlib-generated-files.hmap
write-file /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/mystaticlib-all-target-headers.hmap
......
/bin/mkdir -p /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/Objects-normal/x86_64
write-file /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/Objects-normal/x86_64/mystaticlib.LinkFileList
CompileC build/mystaticlib.build/Release/mystaticlib.build/Objects-normal/x86_64/mystaticlib.o mystaticlib/mystaticlib.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler
cd /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib
export LANG=en_US.US-ASCII
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -arch x86_64 -fmessage-length=94 -fdiagnostics-show-note-include-stack -fmacro-backtrace-limit=0 -fcolor-diagnostics -std=gnu99 -fobjc-arc -fmodules -gmodules -fmodules-prune-interval=86400 -fmodules-prune-after=345600 -fbuild-session-file=/var/folders/rd/mts0362j0n92rq0z1cnmdb580000gn/C/org.llvm.clang/ModuleCache/Session.modulevalidation -fmodules-validate-once-per-build-session -Wnon-modular-include-in-framework-module -Werror=non-modular-include-in-framework-module -Wno-trigraphs -fpascal-strings -Os -fno-common -Wno-missing-field-initializers -Wno-missing-prototypes -Werror=return-type -Wunreachable-code -Wno-implicit-atomic-properties -Werror=deprecated-objc-isa-usage -Werror=objc-root-class -Wno-arc-repeated-use-of-weak -Wduplicate-method-match -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function -Wno-unused-label -Wno-unused-parameter -Wunused-variable -Wunused-value -Wempty-body -Wconditional-uninitialized -Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion -Wbool-conversion -Wenum-conversion -Wshorten-64-to-32 -Wpointer-sign -Wno-newline-eof -Wno-selector -Wno-strict-selector-match -Wundeclared-selector -Wno-deprecated-implementations -DNS_BLOCK_ASSERTIONS=1 -DOBJC_OLD_DISPATCH_PROTOTYPES=0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk -fasm-blocks -fstrict-aliasing -Wprotocol -Wdeprecated-declarations -mmacosx-version-min=10.11 -g -Wno-sign-conversion -iquote /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/mystaticlib-generated-files.hmap -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/mystaticlib-own-target-headers.hmap -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/mystaticlib-all-target-headers.hmap -iquote /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/mystaticlib-project-headers.hmap -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/Release/include -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/DerivedSources/x86_64 -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/DerivedSources -F/Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/Release -MMD -MT dependencies -MF /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/Objects-normal/x86_64/mystaticlib.d --serialize-diagnostics /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/Objects-normal/x86_64/mystaticlib.dia -c /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/mystaticlib/mystaticlib.m -o /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/Objects-normal/x86_64/mystaticlib.o
Libtool build/Release/libmystaticlib.a normal x86_64
cd /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib
export MACOSX_DEPLOYMENT_TARGET=10.11
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/libtool -static -arch_only x86_64 -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk -L/Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/Release -filelist /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/mystaticlib.build/Release/mystaticlib.build/Objects-normal/x86_64/mystaticlib.LinkFileList -o /Users/macbook/Documents/macbook/macbook/code/chapter4/mystaticlib/build/Release/libmystaticlib.a
** BUILD SUCCEEDED **

整个编译过程分为: 检查依赖(Check dependencies)、生成辅助文件(Write auxiliary files)、编译(CompileC)、打包生成库(Libtool)等几步。最终打包生成库的环节使用的libtool工具,该工具除了生成静态库,也可以生成动态库,上一节生成动态库使用链接器ld,它底层也是通过libtool来生成动态库的。最后,静态库不需要签名,静态库中的代码最终会被插入到目标程序中,由目标程序来签名。

0x2 静态库格式

上一节讲到的动态库文件,它的格式就是标准的Mach-O文件,它与Mach-O可执行文件不同的是,动态库在Mach-O头部指定文件类型为MH_DYLIB,可执行程序为MH_EXECUTE。与动态库不同的是,静态库文件不是标准的Mach-O,它的格式如下:

1
2
3
4
5
6
7
8
9
Start
Symtab Header
Symbol Table
String Table
Object Header 0
ObjName0.o
......
Object Header N
ObjNameN.o

Start为静态库的开始,它是一个固定长度的签名值“!<arch>\n”,十六进制为“21 3C 61 72 63 68 3E 0A”。

Symtab Header为符号表头,描述了符号表的信息。它使用symtab_header结构体表示,具体定义为:

1
2
3
4
5
6
7
8
9
10
struct symtab_header {
char name[16]; /* 名称 */
char timestamp[12]; /* 库创建的时间戳 */
char userid[6]; /* 用户id */
char groupid[char]; /* 组id */
uint64_t mode; /* 文件访问模式 */
uint64_t size; /* 符号表占总字节大小 */
uint32_t endheader; /* 头结束标志 */
char longname[20]; /* 符号表长名 */
};

Symbol Table为当前静态库导出的符号表。它使用symbol_table结构体表示,具体定义为:

1
2
3
4
5
6
7
8
9
struct symbol_table {
uint32_t size; /* 符号表占用的总字节数 */
symbol_info syminfo[0]; /* 符号信息,它的个数是 size / sizeof(symbol_info) */
};
struct symbol_info {
uint32_t symnameoff; /* 符号名在字符串表数据中的偏移值 */
uint32_t objheaderoff; /* 符号所属的目标文件的文件头在文件中的偏移值 */
};

String Table为字符串表,该结构体存储的字符串信息供符号表使用。使用string_table结构体表示,具体定义为:

1
2
3
4
struct string_table {
uint32_t size; /* 字符串表占用的总字节数 */
char data[size]; /* 字符串数据 */
};

Object Header为目标文件的头,描述了接下来的目标文件的信息。使用object_header结构体表示,具体定义为:

1
2
3
4
5
6
7
8
9
10
struct object_header {
char name[16]; /* 名称 */
char timestamp[12]; /* 目标文件创建的时间戳 */
char userid[6]; /* 用户id */
char groupid[char]; /* 组id */
uint64_t mode; /* 文件访问模式 */
uint64_t size; /* 符号表占总字节大小 */
uint32_t endheader; /* 头结束标志 */
char longname[20]; /* 符号表长名 */
};

object_header结构体的布局与symtab_header基本一样的。

ObjName.o:在object_header结构体下面紧接着就是具体的目标文件内容了。目标文件是以.o结尾的Mach-O格式的文件,它是由编译器生成的中间文件。目标文件在它的Mach-O头部被标识为MH_OBJECT类型的文件。

最后,可以使用MachOView查看本小节生成的libmystaticlib.a的结构信息,效果如图所示:
static_lib

0x3 管理静态库

通过上一小节的分析,我们知道,静态库是由一些头信息加一系统的.o目标文件组成的。在分析静态库中的具体目标文件时,需要先将目标文件解压出来,还好目前主流的静态分析工具都支持直接读取静态库中的目标文件。但如果想要修改静态库中目标文件的内容,就需要先将目标文件取出后,修改后再替换回去,在了解了静态库文件格式后,完全可以自己动手写工具解出静态库中的目标文件,但实现上不用这么麻烦,可以使用库管理工具ar来完成该工作。

执行如下命令就可以解出上一小节生成的静态库中的目标文件:

1
$ ar -x ./libmystaticlib.a

操作成功后没有输出信息,但可以发现,当前目录中已经生成了mystaticlib.o文件。在修改完目标文件后,可以将其打包进原来的库,或者直接生成新的静态库,执行以下的命令:

1
$ ar rcs libmystaticlib_new.a *.o

同样没有输出信息,但ar已经成功将当前目录下所有的目标文件打包进了libmystaticlib_new.a中。

dylib动态库加载过程分析

Windows系统的动态库是DLL文件,Linux系统是so文件,macOS系统的动态库则使用dylib文件作为动态库。
dylib本质上是一个Mach-O格式的文件,它与普通的Mach-O执行文件几乎使用一样的结构,只是在文件类型上一个是MH_DYLIB,一个是MH_EXECUTE
在系统的/usr/lib目录下,存放了大量供系统与应用程序调用的动态库文件,使用file命令查看系统动态库libobjc.dylib的信息,输出如下:

1
2
3
4
5
$ file /usr/lib/libobjc.dylib
/usr/lib/libobjc.dylib: Mach-O universal binary with 3 architectures
/usr/lib/libobjc.dylib (for architecture i386): Mach-O dynamically linked shared library i386
/usr/lib/libobjc.dylib (for architecture x86_64): Mach-O 64-bit dynamically linked shared library x86_64
/usr/lib/libobjc.dylib (for architecture x86_64h): Mach-O 64-bit dynamically linked shared library x86_64

从上面的输出信息可以看出,libobjc.dylib是一个通用的二进制文件,包含了三种cpu架构的Mach-O。另外,
可以使用Mach-O格式文件管理工具otool查看dylib的信息,如查看动态库的依赖库信息如下:

1
2
3
4
5
6
7
$ otool -L /usr/lib/libobjc.dylib
/usr/lib/libobjc.dylib:
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libauto.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libc++abi.dylib (compatibility version 1.0.0, current version 125.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 120.1.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1225.0.0)

0x1 构建动态库

XCode环境提供了创建动态库的工程模板,创建动态库的方法比较简单,在XCode中选择File->New->Project,在打开的工程模选择对话框中,选择标签macOS->Framework & Library,在右侧选择Library,点击Next按钮,在新页面中输入项目名称mylib,Type选择Dynamic,单击Next按钮选择项目保存的路径后,工程就创建好了。接着修改工程文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//mylib.h
#import <Foundation/Foundation.h>
@interface mylib : NSObject
-(void) hello;
@end
//mylib.m
#import "mylib.h"
@implementation mylib
-(void) hello {
NSLog(@"hello world");
}
@end

保存后。觇击菜单Product->Build,或者按键般的COMMAND+B键就编译成功了。命令执行完后,就会生成mylib.dylib文件。
XCode创建的项目是xcodeproj文件,可以使用XCode提供的工具xcodebuild在命令行下编译,在命令行下切换到工程文件所在的目录后,执行xcodebuild会有如下输出:

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
$ xcodebuild
=== BUILD TARGET mylib OF PROJECT mylib WITH THE DEFAULT CONFIGURATION (Release) ===
Check dependencies
Write auxiliary files
write-file /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/mylib-own-target-headers.hmap
write-file /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/mylib-all-non-framework-target-headers.hmap
......
/bin/mkdir -p /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/Objects-normal/x86_64
write-file /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/Objects-normal/x86_64/mylib.LinkFileList
CompileC build/mylib.build/Release/mylib.build/Objects-normal/x86_64/mylib.o mylib/mylib.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler
cd /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib
export LANG=en_US.US-ASCII
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -arch x86_64 -fmessage-length=94 -fdiagnostics-show-note-include-stack -fmacro-backtrace-limit=0 -fcolor-diagnostics -std=gnu99 -fobjc-arc -fmodules -gmodules -fmodules-prune-interval=86400 -fmodules-prune-after=345600 -fbuild-session-file=/var/folders/rd/mts0362j0n92rq0z1cnmdb580000gn/C/org.llvm.clang/ModuleCache/Session.modulevalidation -fmodules-validate-once-per-build-session -Wnon-modular-include-in-framework-module -Werror=non-modular-include-in-framework-module -Wno-trigraphs -fpascal-strings -Os -fno-common -Wno-missing-field-initializers -Wno-missing-prototypes -Werror=return-type -Wunreachable-code -Wno-implicit-atomic-properties -Werror=deprecated-objc-isa-usage -Werror=objc-root-class -Wno-arc-repeated-use-of-weak -Wduplicate-method-match -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function -Wno-unused-label -Wno-unused-parameter -Wunused-variable -Wunused-value -Wempty-body -Wconditional-uninitialized -Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion -Wbool-conversion -Wenum-conversion -Wshorten-64-to-32 -Wpointer-sign -Wno-newline-eof -Wno-selector -Wno-strict-selector-match -Wundeclared-selector -Wno-deprecated-implementations -DNS_BLOCK_ASSERTIONS=1 -DOBJC_OLD_DISPATCH_PROTOTYPES=0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk -fasm-blocks -fstrict-aliasing -Wprotocol -Wdeprecated-declarations -mmacosx-version-min=10.11 -g -Wno-sign-conversion -iquote /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/mylib-generated-files.hmap -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/mylib-own-target-headers.hmap -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/mylib-all-target-headers.hmap -iquote /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/mylib-project-headers.hmap -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/Release/include -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/DerivedSources/x86_64 -I/Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/DerivedSources -F/Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/Release -MMD -MT dependencies -MF /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/Objects-normal/x86_64/mylib.d --serialize-diagnostics /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/Objects-normal/x86_64/mylib.dia -c /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/mylib/mylib.m -o /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/Objects-normal/x86_64/mylib.o
Ld build/Release/libmylib.dylib normal x86_64
cd /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib
export MACOSX_DEPLOYMENT_TARGET=10.11
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -arch x86_64 -dynamiclib -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk -L/Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/Release -F/Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/Release -filelist /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/Objects-normal/x86_64/mylib.LinkFileList -install_name /usr/local/lib/libmylib.dylib -mmacosx-version-min=10.11 -fobjc-arc -fobjc-link-runtime -single_module -compatibility_version 1 -current_version 1 -Xlinker -dependency_info -Xlinker /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/mylib.build/Release/mylib.build/Objects-normal/x86_64/mylib_dependency_info.dat -o /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/Release/libmylib.dylib
GenerateDSYMFile build/Release/libmylib.dylib.dSYM build/Release/libmylib.dylib
cd /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/Release/libmylib.dylib -o /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/Release/libmylib.dylib.dSYM
CodeSign build/Release/libmylib.dylib
cd /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib
export CODESIGN_ALLOCATE=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate
Signing Identity: "-"
/usr/bin/codesign --force --sign - --timestamp=none /Users/macbook/Documents/macbook/macbook/code/chapter4/mylib/build/Release/libmylib.dylib
** BUILD SUCCEEDED **

从上面的日志中可以看出,整个编译过程分为:
检查依赖(Check dependencies)、生成辅助文件(Write auxiliary files)、编译(CompileC)、链接(Ld)、生成调试符号(GenerateDSYMFile)、代码签名(CodeSign)等几步。
编译代码时,使用的编译器是clang,这是苹果公司开发的用来替代gcc的现代化编译器,该编译器目前也广泛用于安卓、Linux平台上的软件开发工作;链接时使用clang前端传入参数给链接器ld,链接完成后dylib动态库就编译成功了;生成调试符号这一步主要用于生成符号的调试信息,供调试器使用;最后一步是代码签名,在没有指定签名证书的情况下,XCode默认使用的adhoc签名。

编译好的动态库可以被其它程序通过头文件声明隐式的调用,也可以像Linux系统那样,使用系统函数dlopen()dlsym()手动进行调用。

0x2 dyld

动态库不能直接运行,而是需要通过系统的动态链接加载器进行加载到内存后执行,动态链接加载器在系统中以一个用户态的可执行文件形式存在,一般应用程序会在Mach-O文件部分指定一个LC_LOAD_DYLINKER的加载命令,此加载命令指定了dyld的路径,通常它的默认值是“/usr/lib/dyld”。系统内核在加载Mach-O文件时,会使用该路径指定的程序作为动态库的加载器来加载dylib。

dyld加载时,为了优化程序启动,启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被dyld映射到内存中,之后,当任何Mach-O映像加载时,dyld首先会检查该Mach-O映像与所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。在程序依赖的系统动态库很多的情况下,这种做法对程序启动性能是有明显提升的。

update_dyld_shared_cache程序确保了dyld的共享缓存是最新的,它会扫描/var/db/dyld/shared_region_roots/目录下paths路径文件,这些paths文件包含了需要加入到共享缓存的Mach-O文件路径列表,update_dyld_shared_cache()会挨个将这些Mach-O文件及其依赖的dylib都加共享缓存中去。

共享缓存是以文件形式存放在/var/db/dyld/目录下的,生成共享缓存的update_dyld_shared_cache程序位于是/usr/bin/目录下,该工具会为每种系统加构生成一个缓存文件与对应的内存地址map表,如下所示:

1
2
3
4
5
6
7
ls -l /var/db/dyld/
total 1741296
-rw-r--r-- 1 root wheel 333085108 Apr 22 15:02 dyld_shared_cache_i386
-rw-r--r-- 1 root wheel 65378 Apr 22 15:02 dyld_shared_cache_i386.map
-rw-r--r-- 1 root wheel 558259294 Apr 25 16:18 dyld_shared_cache_x86_64h
-rw-r--r-- 1 root wheel 129633 Apr 25 16:18 dyld_shared_cache_x86_64h.map
drwxr-xr-x 10 root wheel 340 Apr 7 09:19 shared_region_roots

生成的共享缓存可以使用工具dyld_shared_cache_util查看它的信息,该工具位于dyld源码中的 launch-cache\dyld_shared_cache_util.cpp 文件,需要自己手动编译。另外,也可以使用dyld提供的两个函数dyld_shared_cache_extract_dylibs()dyld_shared_cache_extract_dylibs_progress()来自己解开cache文件,代码位于dyld源码的launch-cache\dsc_extractor.cpp文件中。

update_dyld_shared_cache通常它只在系统的安装器安装软件与系统更新时调用,当然,可以手动运行“sudo update_dyld_shared_cache”来更新共享缓存。新的共享缓存会在系统下次启动后自动更新。

0x3 动态库的加载过程分析

dyld是苹果操作系统一个重要组成部分,而且令人兴奋的是,它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式(下载地址:http://opensource.apple.com/tarballs/dyld),了解系统加载动态库的细节。

系统内核在加载动态库前,会加载dyld,然后调用去执行__dyld_start(),该函数会执行dyldbootstrap::start(),后者会执行_main()函数,dyld的加载动态库的代码就是从_main()开始执行的。下面以dyld源码的360.18版本为蓝本进行分析:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
//第一步,设置运行环境,处理环境变量
uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;
......
CRSetCrashLogMessage("dyld: launch started");
......
setContext(mainExecutableMH, argc, argv, envp, apple);
// Pickup the pointer to the exec path.
sExecPath = _simple_getenv(apple, "executable_path");
// <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
if (!sExecPath) sExecPath = apple[0];
......
sExecShortName = ::strrchr(sExecPath, '/');
if ( sExecShortName != NULL )
++sExecShortName;
else
sExecShortName = sExecPath;
sProcessIsRestricted = processRestricted(mainExecutableMH, &ignoreEnvironmentVariables, &sProcessRequiresLibraryValidation);
if ( sProcessIsRestricted ) {
#if SUPPORT_LC_DYLD_ENVIRONMENT
checkLoadCommandEnvironmentVariables();
#endif
pruneEnvironmentVariables(envp, &apple);
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else {
if ( !ignoreEnvironmentVariables )
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
}
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
getHostInfo(mainExecutableMH, mainExecutableSlide);
......
//第二步,初始化主程序
try {
// add dyld itself to UUID list
addDyldImageToUUIDList();
CRSetCrashLogMessage(sLoadingCrashMessage);
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.processIsRestricted = sProcessIsRestricted;
gLinkContext.processRequiresLibraryValidation = sProcessRequiresLibraryValidation;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
......
//第三步,加载共享缓存
checkSharedRegionDisable();
#if DYLD_SHARED_CACHE_SUPPORT
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
mapSharedCache();
#endif
// Now that shared cache is loaded, setup an versioned dylib overrides
#if SUPPORT_VERSIONED_PATHS
checkVersionedPaths();
#endif
//第四步,加载插入的动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
sInsertedDylibCount = sAllImages.size()-1;
//第五步,链接主程序
gLinkContext.linkingMainExecutable = true;
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
//第六步,链接插入的动态库
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
image->setNeverUnloadRecursive();
}
// only INSERTED libraries can interpose
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing();
}
}
// <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
for (int i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image->inSharedCache() )
continue;
image->registerInterposing();
}
// apply interposing to initial set of images
for(int i=0; i < sImageRoots.size(); ++i) {
sImageRoots[i]->applyInterposing(gLinkContext);
}
//第七步,执行弱符号绑定
gLinkContext.linkingMainExecutable = false;
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
//第八步,执行初始化方法
CRSetCrashLogMessage("dyld: launch, running initializers");
#if SUPPORT_OLD_CRT_INITIALIZATION
// Old way is to run initializers via a callback from crt1.o
if ( ! gRunInitializersOldWay )
initializeMainExecutable();
#else
// run all initializers
initializeMainExecutable();
#endif
//第九步,查找入口点并返回
result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}
}
catch(const char* message) {
syncAllImages();
halt(message);
}
catch(...) {
dyld::log("dyld: launch failed\n");
}
CRSetCrashLogMessage(NULL);
return result;
}

整个方法的代码比较长,将它按功能分成九个步骤进行讲解:

第一步,设置运行环境,处理环境变量

代码在开始时候,将传入的变量mainExecutableMH赋值给了sMainExecutableMachHeader,这是一个macho_header类型的变量,其结构体内容就是本章前面介绍的mach_header结构体,表示的是当前主程序的Mach-O头部信息,有了头部信息,加载器就可以从头开始,遍历整个Mach-O文件的信息。
接着执行了setContext(),此方法设置了全局一个链接上下文,包括一些回调函数、参数与标志设置信息,代码片断如下:

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
static void setContext(const macho_header* mainExecutableMH, int argc, const char* argv[], const char* envp[], const char* apple[])
{
gLinkContext.loadLibrary = &libraryLocator;
gLinkContext.terminationRecorder = &terminationRecorder;
gLinkContext.flatExportFinder = &flatFindExportedSymbol;
gLinkContext.coalescedExportFinder = &findCoalescedExportedSymbol;
gLinkContext.getCoalescedImages = &getCoalescedImages;
......
gLinkContext.bindingOptions = ImageLoader::kBindingNone;
gLinkContext.argc = argc;
gLinkContext.argv = argv;
gLinkContext.envp = envp;
gLinkContext.apple = apple;
gLinkContext.progname = (argv[0] != NULL) ? basename(argv[0]) : "";
gLinkContext.programVars.mh = mainExecutableMH;
gLinkContext.programVars.NXArgcPtr = &gLinkContext.argc;
gLinkContext.programVars.NXArgvPtr = &gLinkContext.argv;
gLinkContext.programVars.environPtr = &gLinkContext.envp;
gLinkContext.programVars.__prognamePtr=&gLinkContext.progname;
gLinkContext.mainExecutable = NULL;
gLinkContext.imageSuffix = NULL;
gLinkContext.dynamicInterposeArray = NULL;
gLinkContext.dynamicInterposeCount = 0;
gLinkContext.prebindUsage = ImageLoader::kUseAllPrebinding;
#if TARGET_IPHONE_SIMULATOR
gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
#else
gLinkContext.sharedRegionMode = ImageLoader::kUseSharedRegion;
#endif
}

设置的回调函数都是dyld本模块实现的,如loadLibrary方法就是本模块的libraryLocator()方法,负责加载动态库。
在设置完这些信息后,执行processRestricted()方法判断进程是否受限。代码如下:

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
static bool processRestricted(const macho_header* mainExecutableMH, bool* ignoreEnvVars, bool* processRequiresLibraryValidation)
{
#if TARGET_IPHONE_SIMULATOR
gLinkContext.codeSigningEnforced = true;
#else
// ask kernel if code signature of program makes it restricted
uint32_t flags;
if ( csops(0, CS_OPS_STATUS, &flags, sizeof(flags)) != -1 ) {
if (flags & CS_REQUIRE_LV)
*processRequiresLibraryValidation = true;
#if __MAC_OS_X_VERSION_MIN_REQUIRED
if ( flags & CS_ENFORCEMENT ) {
gLinkContext.codeSigningEnforced = true;
}
if ( ((flags & CS_RESTRICT) == CS_RESTRICT) && (csr_check(CSR_ALLOW_TASK_FOR_PID) != 0) ) {
sRestrictedReason = restrictedByEntitlements;
return true;
}
#else
if ((flags & CS_ENFORCEMENT) && !(flags & CS_GET_TASK_ALLOW)) {
*ignoreEnvVars = true;
}
gLinkContext.codeSigningEnforced = true;
#endif
}
#endif
// all processes with setuid or setgid bit set are restricted
if ( issetugid() ) {
sRestrictedReason = restrictedBySetGUid;
return true;
}
// <rdar://problem/13158444&13245742> Respect __RESTRICT,__restrict section for root processes
if ( hasRestrictedSegment(mainExecutableMH) ) {
// existence of __RESTRICT/__restrict section make process restricted
sRestrictedReason = restrictedBySegment;
return true;
}
return false;
}

进程受限会是以下三种可能:
restrictedByEntitlements:在macOS系统上,在需要验证代码签名(Gatekeeper开启)的情况下,且csr_check(CSR_ALLOW_TASK_FOR_PID)返回为真(表示Rootless开启了TASK_FOR_PID标志)时,进程才不会受限,在macOS版本10.12系统上,默认Gatekeeper是开启的,并且Rootless是关闭了CSR_ALLOW_TASK_FOR_PID标志位的,这意味着,默认情况下,系统上运行的进程是受限的。
restrictedBySetGUid:当进程的setuid与setgid位被设置时,进程会被设置成受限。这样做是出于安全的考虑,受限后的进程无法访问DYLD_开头的环境变量,一种典型的系统攻击就是针对这种情况而发生的,在macOS版本10.10系统上,一个由DYLD_PRINT_TO_FILE环境变量引发的系统本地提权漏洞,就是通过向DYLD_PRINT_TO_FILE环境变量传入拥有SUID权限的受限文件,而系统没做安全检测,而这些文件是直接有向系统创建与写入文件权限的。关于漏洞的具体细节可以参看:https://www.sektioneins.de/en/blog/15-07-07-dyld_print_to_file_lpe.html
restrictedBySegment:段名受限。当Mach-O包含一个__RESTRICT/__restrict段时,进程会被设置成受限。

在进程受限后,执行了以下三个方法:
checkLoadCommandEnvironmentVariables():遍历Mach-O中所有的LC_DYLD_ENVIRONMENT加载命令,然后调用processDyldEnvironmentVariable()对不同的环境变量做相应的处理。
pruneEnvironmentVariables():删除进程的LD_LIBRARY_PATH与所有以DYLD_开头的环境变量,这样以后创建的子进程就不包含这些环境变量了。
setContext():重新设置链接上下文。这一步执行的主要目的是由于环境变量发生变化了,需要更新进程的envpapple参数。

第二步,初始化主程序

这一步主要执行了instantiateFromLoadedImage()。它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
static ImageLoader* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return image;
}
throw "main executable not a known format";
}

isCompatibleMachO()主要检查Mach-O的头部的cputypecpusubtype来判断程序与当前的系统是否兼容。如果兼容接下来就调用instantiateMainExecutable()实例化主程序,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
// sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}

sniffLoadCommands()主要获取了加载命令中的如下信息:
compressed:判断Mach-O的Compressed还是Classic类型。判断的依据是Mach-O是否包含LC_DYLD_INFOLC_DYLD_INFO_ONLY加载命令。这2个加载命令记录了Mach-O的动态库加载信息,使用结构体dyld_info_command表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct dyld_info_command {
uint32_t cmd; /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
uint32_t cmdsize; /* sizeof(struct dyld_info_command) */
uint32_t rebase_off; /* file offset to rebase info */
uint32_t rebase_size; /* size of rebase info */
uint32_t bind_off; /* file offset to binding info */
uint32_t bind_size; /* size of binding info */
uint32_t weak_bind_off; /* file offset to weak binding info */
uint32_t weak_bind_size; /* size of weak binding info */
uint32_t lazy_bind_off; /* file offset to lazy binding info */
uint32_t lazy_bind_size; /* size of lazy binding infs */
uint32_t export_off; /* file offset to lazy binding info */
uint32_t export_size; /* size of lazy binding infs */
};

rebase_off与大小rebase_size存储了rebase(重设基址)相关信息,当Mach-O加载到内存中的地址不是指定的首选地址时,就需要对当前的映像数据进行rebase(重设基址)。
bind_offbind_size存储了进程的符号绑定信息,当进程启动时必须绑定这些符号,典型的有dyld_stub_binder,该符号被dyld用来做迟绑定加载符号,一般动态库都包含该符号。
weak_bind_offweak_bind_size存储了进程的弱绑定符号信息。弱符号主要用于面向对旬语言中的符号重载,典型的有c++中使用new创建对象,默认情况下会绑定ibstdc++.dylib,如果检测到某个映像使用弱符号引用重载了new符号,dyld则会重新绑定该符号并调用重载的版本。
lazy_bind_offlazy_bind_size存储了进程的延迟绑定符号信息。有些符号在进程启动时不需要马上解析,它们会在第一次调用时被解析,这类符号叫延迟绑定符号(Lazy Symbol)。
export_offexport_size存储了进程的导出符号绑定信息。导出符号可以被外部的Mach-O访问,通常动态库会导出一个或多个符号供外部使用,而可执行程序由导出_main_mh_execute_header符号供dyld使用。

segCount:段的数量。sniffLoadCommands()通过遍历所有的LC_SEGMENT_COMMAND加载命令来获取段的数量。

libCount:需要加载的动态库的数量。Mach-O中包含的每一条LC_LOAD_DYLIBLC_LOAD_WEAK_DYLIBLC_REEXPORT_DYLIBLC_LOAD_UPWARD_DYLIB加载命令,都表示需要加载一个动态库。

codeSigCmd:通过解析LC_CODE_SIGNATURE来获取代码签名的加载命令。

encryptCmd:通过LC_ENCRYPTION_INFOLC_ENCRYPTION_INFO_64来获取段加密信息。

获取compressed后,根据Mach-O是否compressed来分别调用ImageLoaderMachOCompressed::instantiateMainExecutable()ImageLoaderMachOClassic::instantiateMainExecutable()ImageLoaderMachOCompressed::instantiateMainExecutable()代码如下:

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
// create image for main executable
ImageLoaderMachOCompressed* ImageLoaderMachOCompressed::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path,
unsigned int segCount, unsigned int libCount, const LinkContext& context)
{
ImageLoaderMachOCompressed* image = ImageLoaderMachOCompressed::instantiateStart(mh, path, segCount, libCount);
// set slide for PIE programs
image->setSlide(slide);
// for PIE record end of program, to know where to start loading dylibs
if ( slide != 0 )
fgNextPIEDylibAddress = (uintptr_t)image->getEnd();
image->disableCoverageCheck();
image->instantiateFinish(context);
image->setMapped(context);
if ( context.verboseMapping ) {
dyld::log("dyld: Main executable mapped %s\n", path);
for(unsigned int i=0, e=image->segmentCount(); i < e; ++i) {
const char* name = image->segName(i);
if ( (strcmp(name, "__PAGEZERO") == 0) || (strcmp(name, "__UNIXSTACK") == 0) )
dyld::log("%18s at 0x%08lX->0x%08lX\n", name, image->segPreferredLoadAddress(i), image->segPreferredLoadAddress(i)+image->segSize(i));
else
dyld::log("%18s at 0x%08lX->0x%08lX\n", name, image->segActualLoadAddress(i), image->segActualEndAddress(i));
}
}
return image;
}

ImageLoaderMachOCompressed::instantiateStart()使用主程序Mach-O信息构造了一个ImageLoaderMachOCompressed对象。disableCoverageCheck()禁用覆盖率检查。instantiateFinish()调用parseLoadCmds()解析其它所有的加载命令,后者会填充完ImageLoaderMachOCompressed的一些保护成员信息,最后调用setDyldInfo()设置动态库链接信息,然后调用setSymbolTableInfo()设置符号表信息。

instantiateFromLoadedImage()调用完了ImageLoaderMachO::instantiateMainExecutable()后,接着调用addImage(),代码如下:

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
static void addImage(ImageLoader* image)
{
// add to master list
allImagesLock();
sAllImages.push_back(image);
allImagesUnlock();
// update mapped ranges
uintptr_t lastSegStart = 0;
uintptr_t lastSegEnd = 0;
for(unsigned int i=0, e=image->segmentCount(); i < e; ++i) {
if ( image->segUnaccessible(i) )
continue;
uintptr_t start = image->segActualLoadAddress(i);
uintptr_t end = image->segActualEndAddress(i);
if ( start == lastSegEnd ) {
// two segments are contiguous, just record combined segments
lastSegEnd = end;
}
else {
// non-contiguous segments, record last (if any)
if ( lastSegEnd != 0 )
addMappedRange(image, lastSegStart, lastSegEnd);
lastSegStart = start;
lastSegEnd = end;
}
}
if ( lastSegEnd != 0 )
addMappedRange(image, lastSegStart, lastSegEnd);
if ( sEnv.DYLD_PRINT_LIBRARIES || (sEnv.DYLD_PRINT_LIBRARIES_POST_LAUNCH && (sMainExecutable!=NULL) && sMainExecutable->isLinked()) ) {
dyld::log("dyld: loaded: %s\n", image->getPath());
}
}

这段代码将实例化好的主程序添加到全局主列表sAllImages中,最后调用addMappedRange()申请内存,更新主程序映像映射的内存区。做完这些工作,第二步初始化主程序就算完成了。

第三步,加载共享缓存

这一步主要执行mapSharedCache()来映射共享缓存。该函数先通过_shared_region_check_np()来检查缓存是否已经映射到了共享区域了,如果已经映射了,就更新缓存的slideUUID,然后返回。反之,判断系统是否处于安全启动模式(safe-boot mode)下,如果是就删除缓存文件并返回,正常启动的情况下,接下来调用openSharedCacheFile()打开缓存文件,该函数在sSharedCacheDir路径下,打开与系统当前cpu架构匹配的缓存文件,也就是/var/db/dyld/dyld_shared_cache_x86_64h,接着读取缓存文件的前8192字节,解析缓存头dyld_cache_header的信息,将解析好的缓存信息存入mappings变量,最后调用_shared_region_map_and_slide_np()完成真正的映射工作。部分代码片断如下:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
static void mapSharedCache()
{
uint64_t cacheBaseAddress = 0;
if ( _shared_region_check_np(&cacheBaseAddress) == 0 ) {
sSharedCache = (dyld_cache_header*)cacheBaseAddress;
#if __x86_64__
const char* magic = (sHaswell ? ARCH_CACHE_MAGIC_H : ARCH_CACHE_MAGIC);
#else
const char* magic = ARCH_CACHE_MAGIC;
#endif
if ( strcmp(sSharedCache->magic, magic) != 0 ) {
sSharedCache = NULL;
if ( gLinkContext.verboseMapping ) {
dyld::log("dyld: existing shared cached in memory is not compatible\n");
return;
}
}
const dyld_cache_header* header = sSharedCache;
......
// if cache has a uuid, copy it
if ( header->mappingOffset >= 0x68 ) {
memcpy(dyld::gProcessInfo->sharedCacheUUID, header->uuid, 16);
}
......
}
else {
#if __i386__ || __x86_64__
uint32_t safeBootValue = 0;
size_t safeBootValueSize = sizeof(safeBootValue);
if ( (sysctlbyname("kern.safeboot", &safeBootValue, &safeBootValueSize, NULL, 0) == 0) && (safeBootValue != 0) ) {
struct stat dyldCacheStatInfo;
if ( my_stat(MACOSX_DYLD_SHARED_CACHE_DIR DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME, &dyldCacheStatInfo) == 0 ) {
struct timeval bootTimeValue;
size_t bootTimeValueSize = sizeof(bootTimeValue);
if ( (sysctlbyname("kern.boottime", &bootTimeValue, &bootTimeValueSize, NULL, 0) == 0) && (bootTimeValue.tv_sec != 0) ) {
if ( dyldCacheStatInfo.st_mtime < bootTimeValue.tv_sec ) {
::unlink(MACOSX_DYLD_SHARED_CACHE_DIR DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME);
gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
return;
}
}
}
}
#endif
// map in shared cache to shared region
int fd = openSharedCacheFile();
if ( fd != -1 ) {
uint8_t firstPages[8192];
if ( ::read(fd, firstPages, 8192) == 8192 ) {
dyld_cache_header* header = (dyld_cache_header*)firstPages;
#if __x86_64__
const char* magic = (sHaswell ? ARCH_CACHE_MAGIC_H : ARCH_CACHE_MAGIC);
#else
const char* magic = ARCH_CACHE_MAGIC;
#endif
if ( strcmp(header->magic, magic) == 0 ) {
const dyld_cache_mapping_info* const fileMappingsStart = (dyld_cache_mapping_info*)&firstPages[header->mappingOffset];
const dyld_cache_mapping_info* const fileMappingsEnd = &fileMappingsStart[header->mappingCount];
shared_file_mapping_np mappings[header->mappingCount+1]; // add room for code-sig
......
if (_shared_region_map_and_slide_np(fd, mappingCount, mappings, codeSignatureMappingIndex, cacheSlide, slideInfo, slideInfoSize) == 0) {
sSharedCache = (dyld_cache_header*)mappings[0].sfm_address;
sSharedCacheSlide = cacheSlide;
dyld::gProcessInfo->sharedCacheSlide = cacheSlide;
......
}
else {
#if __IPHONE_OS_VERSION_MIN_REQUIRED
throw "dyld shared cache could not be mapped";
#endif
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file could not be mapped\n");
}
}
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file is invalid\n");
}
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file cannot be read\n");
}
close(fd);
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file cannot be opened\n");
}
}
......
}

共享缓存加载完毕后,接着进行动态库的版本化重载,这主要通过函数checkVersionedPaths()完成。该函数读取DYLD_VERSIONED_LIBRARY_PATHDYLD_VERSIONED_FRAMEWORK_PATH环境变量,将指定版本的库比当前加载的库的版本做比较,如果当前的库版本更高的话,就使用新版本的库来替换掉旧版本的。

第四步,加载插入的动态库

这一步循环遍历DYLD_INSERT_LIBRARIES环境变量中指定的动态库列表,并调用loadInsertedDylib()将其加载。该函数调用load()完成加载工作。load()会调用loadPhase0()尝试从文件加载,loadPhase0()会向下调用下一层phase来查找动态库的路径,直到loadPhase6(),查找的顺序为DYLD_ROOT_PATH->LD_LIBRARY_PATH->DYLD_FRAMEWORK_PATH->原始路径->DYLD_FALLBACK_LIBRARY_PATH,找到后调用ImageLoaderMachO::instantiateFromFile()来实例化一个ImageLoader,之后调用checkandAddImage()验证映像并将其加入到全局映像列表中。如果loadPhase0()返回为空,表示在路径中没有找到动态库,就尝试从共享缓存中查找,找到就调用ImageLoaderMachO::instantiateFromCache()从缓存中加载,否则就抛出没找到映像的异常。部分代码片断如下:

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
ImageLoader* load(const char* path, const LoadContext& context)
{
......
if ( context.useSearchPaths && ( gLinkContext.imageSuffix != NULL) ) {
if ( realpath(path, realPath) != NULL )
path = realPath;
}
ImageLoader* image = loadPhase0(path, orgPath, context, NULL);
if ( image != NULL ) {
CRSetCrashLogMessage2(NULL);
return image;
}
......
image = loadPhase0(path, orgPath, context, &exceptions);
#if __IPHONE_OS_VERSION_MIN_REQUIRED && DYLD_SHARED_CACHE_SUPPORT && !TARGET_IPHONE_SIMULATOR
// <rdar://problem/16704628> support symlinks on disk to a path in dyld shared cache
if ( (image == NULL) && cacheablePath(path) && !context.dontLoad ) {
......
if ( (myerr == ENOENT) || (myerr == 0) )
{
const macho_header* mhInCache;
const char* pathInCache;
long slideInCache;
if ( findInSharedCacheImage(resolvedPath, false, NULL, &mhInCache, &pathInCache, &slideInCache) ) {
struct stat stat_buf;
bzero(&stat_buf, sizeof(stat_buf));
try {
image = ImageLoaderMachO::instantiateFromCache(mhInCache, pathInCache, slideInCache, stat_buf, gLinkContext);
image = checkandAddImage(image, context);
}
catch (...) {
image = NULL;
}
}
}
}
#endif
......
else {
const char* msgStart = "no suitable image found. Did find:";
......
throw (const char*)fullMsg;
}
}

第五步,链接主程序

这一步执行link()完成主程序的链接操作。该函数调用了ImageLoader自身的link()函数,主要的目的是将实例化的主程序的动态数据进行修正,达到让进程可用的目的,典型的就是主程序中的符号表修正操作,它的代码片断如下:

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
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths)
{
......
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths);
......
context.clearAllDepths();
this->recursiveUpdateDepth(context.imageCount());
this->recursiveRebase(context);
......
this->recursiveBind(context, forceLazysBound, neverUnload);
if ( !context.linkingMainExecutable )
this->weakBind(context); //现在是链接主程序,这里现在不会执行
......
std::vector<DOFInfo> dofs;
this->recursiveGetDOFSections(context, dofs);
context.registerDOFs(dofs);
......
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
this->recursiveApplyInterposing(context); //现在是链接主程序,这里现在不会执行
}
......
}

recursiveLoadLibraries()采用递归的方式来加载程序依赖的动态库,加载的方法是调用contextloadLibrary指针方法,该方法在前面看到过,是setContext()设置的libraryLocator(),该函数只是调用了load()来完成加载,load()加载动态库的过程在上一步已经分析过了。

接着调用recursiveUpdateDepth()对映像及其依赖库按列表方式进行排序。recursiveRebase()则对映像完成递归rebase操作,该函数只是调用了虚函数doRebase()doRebase()ImageLoaderMachO重载,实际只是将代码段设置成可写后调用了rebase(),在ImageLoaderMachOCompressed中,该函数读取映像动态链接信息的rebase_offrebase_size来确定需要rebase的数据偏移与大小,然后挨个修正它们的地址信息。

recursiveBind()完成递归绑定符号表的操作。此处的符号表针对的是非延迟加载的符号表,它的核心是调用了doBind(),在ImageLoaderMachOCompressed中,该函数读取映像动态链接信息的bind_offbind_size来确定需要绑定的数据偏移与大小,然后挨个对它们进行绑定,绑定操作具体使用bindAt()函数,它主要通过调用resolve()解析完符号表后,调用bindLocation()完成最终的绑定操作,需要绑定的符号信息有三种:
BIND_TYPE_POINTER:需要绑定的是一个指针。直接将计算好的新值屿值即可。
BIND_TYPE_TEXT_ABSOLUTE32:一个32位的值。取计算的值的低32位赋值过去。
BIND_TYPE_TEXT_PCREL32:重定位符号。需要使用新值减掉需要修正的地址值来计算出重定位值。

recursiveGetDOFSections()与registerDOFs()主要注册程序的DOF节区,供dtrace使用。

第六步,链接插入的动态库

链接插入的动态库与链接主程序一样,都是使用的link(),插入的动态库列表是前面调用addImage()保存到sAllImages中的,之后,循环获取每一个动态库的ImageLoader,调用link()对其进行链接,注意:sAllImages中保存的第一项是主程序的映像。接下来调用每个映像的registerInterposing()方法来注册动态库插入与调用applyInterposing()应用插入操作。registerInterposing()查找__DATA段的__interpose节区,找到需要应用插入操作(也可以叫作符号地址替换)的数据,然后做一些检查后,将要替换的符号与被替换的符号信息存入fgInterposingTuples列表中,供以后具体符号替换时查询。applyInterposing()调用了虚方法doInterpose()来做符号替换操作,在ImageLoaderMachOCompressed中实际是调用了eachBind()eachLazyBind()分别对常规的符号与延迟加载的符号进行应用插入操作,具体使用的是interposeAt(),该方法调用interposedAddress()fgInterposingTuples中查找要替换的符号地址,找到后然后进行最终的符号地址替换。

第七步,执行弱符号绑定

weakBind()函数执行弱符号绑定。首先通过调用contextgetCoalescedImages()sAllImages中所有含有弱符号的映像合并成一个列表,合并完后调用initializeCoalIterator()对映像进行排序,排序完成后调用incrementCoalIterator()收集需要进行绑定的弱符号,后者是一个虚函数,在ImageLoaderMachOCompressed中,该函数读取映像动态链接信息的weak_bind_offweak_bind_size来确定弱符号的数据偏移与大小,然后挨个计算它们的地址信息。之后调用getAddressCoalIterator(),按照映像的加载顺序在导出表中查找符号的地址,找到后调用updateUsesCoalIterator()执行最终的绑定操作,执行绑定的是bindLocation(),前面有讲过,此处不再赘述。

第八步,执行初始化方法

执行初始化的方法是initializeMainExecutable()。该函数主要执行runInitializers(),后者调用了ImageLoaderrunInitializers()方法,最终迭代执行了ImageLoaderMachOdoInitialization()方法,后者主要调用doImageInit()doModInitFunctions()执行映像与模块中设置为init的函数与静态初始化方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}

第九步,查找入口点并返回

这一步调用主程序映像的getThreadPC()函数来查找主程序的LC_MAIN加载命令获取程序的入口点,没找到就调用getMain()LC_UNIXTHREAD加载命令中去找,找到后就跳到入口点指定的地址并返回了。

到这里,dyld整个加载动态库的过程就算完成了。

另外再讨论下延迟符号加载的技术细节。在所有拥有延迟加载符号的Mach-O文件里,它的符号表中一定有一个dyld_stub_helper符号,它是延迟符号加载的关键!延迟绑定符号的修正工作就是由它完成的。绑定符号信息可以使用XCode提供的命令行工具dyldinfo来查看,执行以下命令可以查看python的绑定信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xcrun dyldinfo -bind /usr/bin/python
for arch i386:
bind information:
segment section address type addend dylib symbol
__DATA __cfstring 0x000040F0 pointer 0 CoreFoundation ___CFConstantStringClassReference
__DATA __cfstring 0x00004100 pointer 0 CoreFoundation ___CFConstantStringClassReference
__DATA __nl_symbol_ptr 0x00004010 pointer 0 CoreFoundation _kCFAllocatorNull
__DATA __nl_symbol_ptr 0x00004008 pointer 0 libSystem ___stack_chk_guard
__DATA __nl_symbol_ptr 0x0000400C pointer 0 libSystem _environ
__DATA __nl_symbol_ptr 0x00004000 pointer 0 libSystem dyld_stub_binder
bind information:
segment section address type addend dylib symbol
__DATA __cfstring 0x1000031D8 pointer 0 CoreFoundation ___CFConstantStringClassReference
__DATA __cfstring 0x1000031F8 pointer 0 CoreFoundation ___CFConstantStringClassReference
__DATA __got 0x100003010 pointer 0 CoreFoundation _kCFAllocatorNull
__DATA __got 0x100003000 pointer 0 libSystem ___stack_chk_guard
__DATA __got 0x100003008 pointer 0 libSystem _environ
__DATA __nl_symbol_ptr 0x100003018 pointer 0 libSystem dyld_stub_binder

所有的延迟绑定符号都存储在_TEXT段的stubs节区(桩节区),编译器在生成代码时创建的符号调用就生成在此节区中,该节区被称为“桩”节区,桩只是一小段临时使用的指令,在stubs中只是一条jmp跳转指令,跳转的地址位于__DATA__la_symbol_ptr节区中,指向的是一段代码,类似于如下的语句:

1
2
push xxx
jmp yyy

其中xxx是符号在动态链接信息中延迟绑定符号数据的偏移值,yyy则是跳转到_TEXT段的stub_helper节区头部,此处的代码通常为:

1
2
3
lea r11, qword [ds:zzz]
push r11
jmp qword [ds:imp___nl_symbol_ptr_dyld_stub_binder]

jmp跳转的地址是__DATA段中__nl_symbol_ptr节区,指向的是符号dyld_stub_binder(),该函数由dyld导出,实现位于dyld源码的“dyld_stub_binder.s”文件中,它调用dyld::fastBindLazySymbol()来绑定延迟加载的符号,后者是一个虚函数,实际调用ImageLoaderMachOCompresseddoBindFastLazySymbol(),后者调用bindAt()解析并返回正确的符号地址,dyld_stub_binder()在最后跳转到符号地址去执行。这一步完成后,__DATA__la_symbol_ptr节区中存储的符号地址就是修正后的地址,下一次调用该符号时,就直接跳转到真正的符号地址去执行,而不用dyld_stub_binder()来重新解析该符号了,

Mach-O文件格式

0x1 通用二进制格式

虽然macOS系统使用了很多UNIX上的特性,但它并没有使用ELF作为系统的可执行文件格式,而是使用自家独创的Mach-O文件格式。

macOS系统一路走来,支持的CPU及硬件平台都有了很大的变化,从早期的PowerPC平台,到后来的x86,再到现在主流的arm、x86-64平台。软件开发人员为了做到不同硬件平台的兼容性,如果需要为每一个平台编译一个可执行文件,这将是非常繁琐的。为了解决软件在多个硬件平台上的兼容性问题,苹果开发了一个通用的二进制文件格式(Universal Binary)。
又称为胖二进制(Fat Binary),通用二进制文件中将多个支持不同CPU架构的二进制文件打包成一个文件,系统在加载运行该程序时,会根据通用二进制文件中提供的多个架构来与当前系统平台做匹配,运行适合当前系统的那个版本。

苹果自家系统中存在着很多通用二进制文件。比如/usr/bin/python,在终端中执行file命令可以查看它的信息:

1
2
3
4
$ file /usr/bin/python
/usr/bin/python: Mach-O universal binary with 2 architectures
/usr/bin/python (for architecture x86_64): Mach-O 64-bit executable x86_64
/usr/bin/python (for architecture i386): Mach-O executable i386

系统提供了一个命令行工具lipo来操作通用二进制文件。它可以添加、提取、删除以及替换通用二进制文件中特定架构的二进制版本。例如提取python中x86_64版本的二进制文件可以执行:

1
lipo /usr/bin/python -extract x86_64 -output ~/Desktop/python.x64

删除x86版本的二进制文件可以执行:

1
lipo /usr/bin/python -remove i386 -output ~/Desktop/python.x64

或者直接瘦身为x86_64版本:

1
lipo /usr/bin/python -thin x86_64 -output ~/Desktop/python.x64

通用二进制的“通用”不止针对可以直接运行的可执行程序,系统中的动态库dylib、静态库.a文件以及框架等都可以是通用二进制文件,对它们也可以同样使用lipo命令来进行管理。
下来看一下通用二进制的文件格式。安装好macOS程序开发的SDK后,或者在xnu的内核源码中,都可以在<mach-o/fat.h>文件中找到通用二进制文件格式的声明。从文件命名上看,将通用二进制称为胖二进制更方便一些。胖二进制头部结构fat_header定义如下:

1
2
3
4
5
6
7
#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */
struct fat_header {
uint32_t magic; /* FAT_MAGIC */
uint32_t nfat_arch; /* number of structs that follow */
};

magic字段被定义为常量FAT_MAGIC,它的取值是固定的0xcafebabe,表示这是一个通用的二进制文件。nfat_arch字段指明了通用二进制中包含多少个Mach-O文件。
每个通用二进制架构信息都使用fat_arch结构表示,在fat_header结构体之后,紧接着的是一个或多个连续的fat_arch结构体,它的定义如下:

1
2
3
4
5
6
7
struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};

cputype指定了具体的cpu类型,它的类型是cpu_type_t,定义位于mach/machine.h中。cpu的常用类型主要有如下几种:

1
2
3
4
5
6
7
8
9
10
11
12
#define CPU_TYPE_X86 ((cpu_type_t) 7)
#define CPU_TYPE_I386 CPU_TYPE_X86
#define CPU_TYPE_X86_64 (CPU_TYPE_X86 | CPU_ARCH_ABI64)
#define CPU_TYPE_MC98000 ((cpu_type_t) 10)
#define CPU_TYPE_HPPA ((cpu_type_t) 11)
#define CPU_TYPE_ARM ((cpu_type_t) 12)
#define CPU_TYPE_ARM64 (CPU_TYPE_ARM | CPU_ARCH_ABI64)
#define CPU_TYPE_MC88000 ((cpu_type_t) 13)
#define CPU_TYPE_SPARC ((cpu_type_t) 14)
#define CPU_TYPE_I860 ((cpu_type_t) 15)
#define CPU_TYPE_POWERPC ((cpu_type_t) 18)
#define CPU_TYPE_POWERPC64 (CPU_TYPE_POWERPC | CPU_ARCH_ABI64

macOS平台上的CPU类型一般为CPU_TYPE_X86_64

cpusubtype指定了cpu的子类型。它的类型是cpu_subtype_t。cpu子类型主要有如下几种:

1
2
3
4
5
6
7
#define CPU_SUBTYPE_MASK 0xff000000
#define CPU_SUBTYPE_LIB64 0x80000000
#define CPU_SUBTYPE_X86_ALL ((cpu_subtype_t)3)
#define CPU_SUBTYPE_X86_64_ALL ((cpu_subtype_t)3)
#define CPU_SUBTYPE_X86_ARCH1 ((cpu_subtype_t)4)
#define CPU_SUBTYPE_X86_64_H ((cpu_subtype_t)8)
......

cpu子类型一般CPU_SUBTYPE_LIB64CPU_SUBTYPE_X86_64_ALL比较常见。

offset字段指明了当前cpu架构数据相对于当前文件开头的偏移值。size字段指明了数据的大小。

align字段指明了数据的内存对齐边界,取值必须是2的次方,它确保了当前cpu架构的目标文件加载到内存中时,数据是经过内存优化对齐的。

可以使用otool工具打印本机安装的python程序的fat_header信息。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
otool -f -V /usr/bin/python
Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture i386
cputype CPU_TYPE_I386
cpusubtype CPU_SUBTYPE_I386_ALL
capabilities 0x0
offset 4096
size 29632
align 2^12 (4096)
architecture x86_64
cputype CPU_TYPE_X86_64
cpusubtype CPU_SUBTYPE_X86_64_ALL
capabilities CPU_SUBTYPE_LIB64
offset 36864
size 29872
align 2^12 (4096)

如果你是UNIX的使用者,经常使用GNU里面binutils提供的objdump查看可执行文件信息的话,在macOS上可以使用它的移植版本gobjdump,使用HomeBrew运行以下命令进行安装:

1
$ brew install binutils

完装完成后,执行下面的命令也可以查看python程序的fat_header信息:

1
2
3
4
5
6
7
8
9
10
11
12
$ gobjdump -f /usr/bin/python
In archive /usr/bin/python:
i386: file format mach-o-i386
architecture: i386, flags 0x00000012:
EXEC_P, HAS_SYMS
start address 0x00001be0
i386:x86-64: file format mach-o-x86-64
architecture: i386:x86-64, flags 0x00000012:
EXEC_P, HAS_SYMS
start address 0x0000000100000e20

fat_arch结构体往下就是具体的Mach-O文件格式了,它的内容复杂得多,将在下一小节进行讨论。

0x2 Mach-O文件格式简介

Mach-O(Mach Object File Format)描述了macOS系统上可执行文件的格式。熟悉Mach-O文件格式,有助于了解苹果底层软件运行机制,更好的掌握dyld加载Mach-O的步骤,为自己动手开发Mach-O相关的加解密工具打下基础。

一个典型的Mach-O文件格式如图所示:
mach-o
通过上图,可以看出Mach-O主要由以下三部分组成:

  • Mach-O头部(mach header)。描述了Mach-O的cpu架构、文件类型以及加载命令等信息。
  • 加载命令(load command)。描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示。
  • Data。Data中的每个段(segment)的数据都保存在这里,段的概念与ELF文件中段的概念类似。每个段都有一个或多个Section,它们存放了具体的数据与代码。

0x3 Mach-O头部

与Mach-O文件格式有关的结构体,都可以直接或间接的在”mach-o/loader.h“文件中找到。
针对32位与64位架构的cpu,分别使用了mach_headermach_header_64结构体来描述Mach-O头部。
mach_header结构体的定义如下:

1
2
3
4
5
6
7
8
9
struct mach_header {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
};

magic字段与fat_header结构体中的magic字段一样,表示Mach-O文件的魔数值,对于32位架构的程序来说,它的取值是MH_MAGIC,固定为0xfeedface。
cputypecpusubtype字段与fat_header结构体中的含义完全相同。
filetype字段表示Mach-O的具体文件类型。它的取值有:

1
2
3
4
5
6
7
8
9
10
11
12
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static */
/* linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug sections */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */

这里主要关注MH_EXECUTEMH_DYLIBMH_DYLIB这3个文件格式。

接下来的ncmds指明了Mach-O文件中加载命令(load commands)的数量。

sizeofcmds字段指明了Mach-O文件加载命令(load commands)所占的总字节大小。

flags字段表示文件标志,它是一个含有一组位标志的整数,指明了Mach-O文件的一些标志信息。可用的值有:

1
2
3
4
5
6
7
#define MH_NOUNDEFS 0x1
#define MH_INCRLINK 0x2
#define MH_DYLDLINK 0x4
#define MH_LAZY_INIT 0x40
#define MH_TWOLEVEL 0x80
#define MH_PIE 0x200000
......

针对64位Mach-O的mach_header_64结构体定义如下:

1
2
3
4
5
6
7
8
9
10
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};

相比mach_header,它多出了一个reserved字段,目前它的取值系统保留。mach_header_64结构体中的字段与mach_header中的基本一致,除了magic字段的取值是MH_MAGIC_64,固定的值为0xfeedfacf。
学习Mach-o文件格式时,可以使用辅助工具查看具体的文件结构,这样效果更加直观。
下图是MachOView查看optool程序Mach64 Header的效果:
mach_header
下图是010 Editor查看optool程序Mach64 Header的效果:
010_editor
下图是Synalyze It!查看optool程序Mach64 Header的效果:
synalyze_it
这三款工具对于学习Mach-O文件格式都是非常有帮助的,读者在实际分析时可以多多使用。

0x4 加载命令

mach_header之后的是Load Command加载命令,这些加载命令在Mach-O文件加载解析时,被内核加载器或者动态链接器调用,基本的加载命令的数据结构如下:

1
2
3
4
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

此结构对应的成员只有2个:cmd字段代表当前加载命令的类型。cmdsize字段代表当前加载命令的大小。
cmd的类型不同,所代表的加载命令的类型就不同,它的结构体也会有所不一样,对于不同类型的加载命令,它们都会在load_command结构体后面加上一个或多个字段来表示自己特定的结构体信息。

macOS系统在进化的过程中,加载命令算是比较频繁被更新的一个数据结构体,截止到macOS 10.12系统,加载命令的类型cmd的取值共有48种。它们的部分定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define LC_SEGMENT 0x1 /* segment of this file to be mapped */
#define LC_SYMTAB 0x2 /* link-edit stab symbol table info */
#define LC_SYMSEG 0x3 /* link-edit gdb symbol table info (obsolete) */
#define LC_THREAD 0x4 /* thread */
#define LC_UNIXTHREAD 0x5 /* unix thread (includes a stack) */
#define LC_LOADFVMLIB 0x6 /* load a specified fixed VM shared library */
#define LC_IDFVMLIB 0x7 /* fixed VM shared library identification */
#define LC_IDENT 0x8 /* object identification info (obsolete) */
#define LC_FVMFILE 0x9 /* fixed VM file inclusion (internal use) */
#define LC_PREPAGE 0xa /* prepage command (internal use) */
#define LC_DYSYMTAB 0xb /* dynamic link-edit symbol table info */
#define LC_LOAD_DYLIB 0xc /* load a dynamically linked shared library */
......
#define LC_ENCRYPTION_INFO_64 0x2C /* 64-bit encrypted segment information */
#define LC_LINKER_OPTION 0x2D /* linker options in MH_OBJECT files */
#define LC_LINKER_OPTIMIZATION_HINT 0x2E /* optimization hints in MH_OBJECT files */
#ifndef __OPEN_SOURCE__
#define LC_VERSION_MIN_TVOS 0x2F /* build for AppleTV min OS version */
#endif /* __OPEN_SOURCE__ */
#define LC_VERSION_MIN_WATCHOS 0x30 /* build for Watch min OS version */

所有的这些加载命令由系统内核加载器直接使用,或由动态链接器处理。其中几个常见的加载命令有LC_SEGMENTLC_LOAD_DYLINKERLC_LOAD_DYLIBLC_MAINLC_CODE_SIGNATURELC_ENCRYPTION_INFO等。

LC_SEGMENT:表示这是一个段加载命令,需要将它加载到对应的进程空间上去。段加载命令将在下一小节进行讨论。

LC_LOAD_DYLIB:表示这是一个需要动态加载的链接库。它使用dylib_command结构体表示。定义如下:

1
2
3
4
5
struct dylib_command {
uint32_t cmd; /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,LC_REEXPORT_DYLIB */
uint32_t cmdsize; /* includes pathname string */
struct dylib dylib; /* the library identification */
};

当cmd类型时LC_ID_DYLIBLC_LOAD_DYLIBLC_LOAD_WEAK_DYLIBLC_REEXPORT_DYLIB时,统一使用dylib_command结构体表示。
它使用dylib结构体来存储要加载的动态库的具体信息。如下:

1
2
3
4
5
6
struct dylib {
union lc_str name; /* library's path name */
uint32_t timestamp; /* library's build time stamp */
uint32_t current_version; /* library's current version number */
uint32_t compatibility_version; /* library's compatibility vers number*/
};

name字段是动态库的完整路径,动态链接器在加载动态库时,通用此路径来进行加载它。
timestamp字段描述了动态库构建时的时间戳。current_versioncompatibility_version指明了前当版本与兼容的版本号。

LC_MAIN:此加载命令记录了可执行文件的主函数main()的位置。它使用entry_point_command结构体表示。定义如下:

1
2
3
4
5
6
struct entry_point_command {
uint32_t cmd; /* LC_MAIN only used in MH_EXECUTE filetypes */
uint32_t cmdsize; /* 24 */
uint64_t entryoff; /* file (__TEXT) offset of main() */
uint64_t stacksize;/* if not zero, initial stack size */
};

entryoff字段中就指定了main()函数的文件偏移。stacksize指定了初始的堆栈大小。

0x5 LC_CODE_SIGNATURE与代码签名过程分析

LC_CODE_SIGNATURE:代码签名加载命令。描述了Mach-O的代码签名信息,它属于链接信息,使用linkedit_data_command结构体表示。定义如下:

1
2
3
4
5
6
7
8
9
struct linkedit_data_command {
uint32_t cmd; /* LC_CODE_SIGNATURE, LC_SEGMENT_SPLIT_INFO,
LC_FUNCTION_STARTS, LC_DATA_IN_CODE,
LC_DYLIB_CODE_SIGN_DRS or
LC_LINKER_OPTIMIZATION_HINT. */
uint32_t cmdsize; /* sizeof(struct linkedit_data_command) */
uint32_t dataoff; /* file offset of data in __LINKEDIT segment */
uint32_t datasize; /* file size of data in __LINKEDIT segment */
};

dataoff字段指明了相对于__LINKEDIT段的文件偏移位置,datasize字段指明了数据的大小。
由于dataoffdatasize分别指明了代码签名的位置与大小,那么笔者在此提个问:如何删除Mach-O中包含的代码签名信息?

与代码签名相关的数据定义可以在xnu内核代码的“bsd/sys/codesign.h”文件中找到。整个代码签名部分的头部使用一个CS_SuperBlob结构体定义,它的原型如下:

1
2
3
4
5
6
7
typedef struct __SC_SuperBlob {
uint32_t magic; /* magic number */
uint32_t length; /* total length of SuperBlob */
uint32_t count; /* number of index entries following */
CS_BlobIndex index[]; /* (count) entries */
/* followed by Blobs in no particular order as indicated by offsets in index */
} CS_SuperBlob;

magic字段指明了Blob的类型,可选值如下:

1
2
3
4
5
6
7
8
9
10
enum {
CSMAGIC_REQUIREMENT = 0xfade0c00, /* single Requirement blob */
CSMAGIC_REQUIREMENTS = 0xfade0c01, /* Requirements vector (internal requirements) */
CSMAGIC_CODEDIRECTORY = 0xfade0c02, /* CodeDirectory blob */
CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0, /* embedded form of signature data */
CSMAGIC_EMBEDDED_SIGNATURE_OLD = 0xfade0b02, /* XXX */
CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xfade7171, /* embedded entitlements */
CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc1, /* multi-arch collection of embedded signatures */
CSMAGIC_BLOBWRAPPER = 0xfade0b01, /* CMS Signature, among other things */
}

对于第一个Blob来说,它的值必定是CSMAGIC_EMBEDDED_SIGNATURE,表示代码签名采用的嵌入式的签名信息。
length字段指明了整个SuperBlob的大小,其中包含马上的介绍的CodeDirectory、Requirement、Entitlement的大小。
count字段指明了接下来会有多少个子条目。
index开始,就是每一个字条目的索引了,它的结构是CS_BlobIndex,定义如下:

1
2
3
4
typedef struct __BlobIndex {
uint32_t type; /* type of entry */
uint32_t offset; /* offset of entry */
} CS_BlobIndex;

type指明了子条目的类型,可选值如下:

1
2
3
4
5
6
7
CSSLOT_CODEDIRECTORY = 0, /* slot index for CodeDirectory */
CSSLOT_INFOSLOT = 1,
CSSLOT_REQUIREMENTS = 2,
CSSLOT_RESOURCEDIR = 3,
CSSLOT_APPLICATION = 4,
CSSLOT_ENTITLEMENTS = 5,
CSSLOT_SIGNATURESLOT = 0x10000, /* CMS Signature */

offset字段指明了子条目距离代码签名数据起始的文件偏移。

通常,签名后的程序,签名数据的第一个子条目指向的是一个typeCSSLOT_CODEDIRECTORY的结构,它是一个CS_CodeDirectory结构体,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct __CodeDirectory {
uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */
uint32_t length; /* total length of CodeDirectory blob */
uint32_t version; /* compatibility version */
uint32_t flags; /* setup and mode flags */
uint32_t hashOffset; /* offset of hash slot element at index zero */
uint32_t identOffset; /* offset of identifier string */
uint32_t nSpecialSlots; /* number of special hash slots */
uint32_t nCodeSlots; /* number of ordinary (code) hash slots */
uint32_t codeLimit; /* limit to main image signature range */
uint8_t hashSize; /* size of each hash in bytes */
uint8_t hashType; /* type of hash (cdHashType* constants) */
uint8_t platform; /* platform identifier; zero if not platform binary */
uint8_t pageSize; /* log2(page size in bytes); 0 => infinite */
uint32_t spare2; /* unused (must be zero) */
/* Version 0x20100 */
uint32_t scatterOffset; /* offset of optional scatter vector */
/* Version 0x20200 */
uint32_t teamOffset; /* offset of optional team identifier */
/* followed by dynamic content as located by offset fields above */
} CS_CodeDirectory;

该结构体数据字段较多,此处只关注与签名相关的字段。hashOffset指明了Hash数据的文件相对偏移,注意是相对于当前结构体CS_CodeDirectoryhashTypehashSize指明了代码签名时使用的算法与每一项签名数据的长度,目前macOS使用的签名算法是SHA-1,长度为20字节。
nSpecialSlotsnCodeSlots指定的代码签名数据条目的个数,前者是针对代码签名中所有的Blob,后者针对程序文件内容。codesign程序在对程序进行签名时,会对SuperBlob中每个子条目进行签名,即对Blob的内容调用SHA-1算法取Hash值,nSpecialSlots的值就是子条目Blob的个数;同时,codesign会以pageSize字段指定的页大小为单位(通常取值是0x1000),对程序数据进行签名,每一页签名后生成一条签名数据,nCodeSlots的值就是签名数据的页数,即程序数据大小除以pageSize字段后的值。

CS_CodeDirectory之后,就是Requirements了,它是一个CS_SuperBlob结构体,指明了Requirement的个数与每一个的偏移。接下来就是每一个Requirement数据了,它是一个CS_GenericBlob结构体,定义如下:

1
2
3
4
5
typedef struct __SC_GenericBlob {
uint32_t magic; /* magic number */
uint32_t length; /* total length of blob */
char data[];
} CS_GenericBlob;

可以看到,它的前两个字段与CS_SuperBlob是一样的,只是后面多出一个data字段,用来存放Blob的数据长度。
在Requirement数据下面,就是Entitlement了,它同样是CS_GenericBlob结构。拿本机Calculator计算器程序来说,它的Entitlement的数据内容是一个xml文件,提取出来内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.print</key>
<true/>
</dict>
</plist>

最后一个Blob通常是签名使用的证书了,Certificates签名证书也是CS_GenericBlob结构,提取它的证书数据后保存为cer文件,使用macOS的文件预览证书内容,效果如图所示:
calc_cer

下面再来看看,系统是如何实施代码签名验证的!内核加载解析Mach-O加载命令的函数是parse_machfile(),位于内核代码”/bsd/kern/mach_loader.c“文件中,部分代码片断如下:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
static load_return_t parse_machfile(
struct vnode *vp,
vm_map_t map,
thread_t thread,
struct mach_header *header,
off_t file_offset,
off_t macho_size,
int depth,
int64_t aslr_offset,
int64_t dyld_aslr_offset,
load_result_t *result
)
{
uint32_t ncmds;
struct load_command *lcp;
struct dylinker_command *dlp = 0;
integer_t dlarchbits = 0;
void * control;
load_return_t ret = LOAD_SUCCESS;
caddr_t addr;
void * kl_addr;
vm_size_t size,kl_size;
size_t offset;
size_t oldoffset; /* for overflow check */
int pass;
proc_t p = current_proc(); /* XXXX */
int error;
int resid=0;
size_t mach_header_sz = sizeof(struct mach_header);
boolean_t abi64;
boolean_t got_code_signatures = FALSE;
int64_t slide = 0;
if (header->magic == MH_MAGIC_64 ||
header->magic == MH_CIGAM_64) {
mach_header_sz = sizeof(struct mach_header_64);
}
......
case LC_CODE_SIGNATURE:
/* CODE SIGNING */
if (pass != 1)
break;
/* pager -> uip ->
load signatures & store in uip
set VM object "signed_pages"
*/
ret = load_code_signature(
(struct linkedit_data_command *) lcp,
vp,
file_offset,
macho_size,
header->cputype,
result);
if (ret != LOAD_SUCCESS) {
printf("proc %d: load code signature error %d "
"for file \"%s\"\n",
p->p_pid, ret, vp->v_name);
/*
* Allow injections to be ignored on devices w/o enforcement enabled
*/
if (!cs_enforcement(NULL))
ret = LOAD_SUCCESS; /* ignore error */
} else {
got_code_signatures = TRUE;
}
if (got_code_signatures) {
unsigned tainted = CS_VALIDATE_TAINTED;
boolean_t valid = FALSE;
struct cs_blob *blobs;
vm_size_t off = 0;
if (cs_debug > 10)
printf("validating initial pages of %s\n", vp->v_name);
blobs = ubc_get_cs_blobs(vp);
while (off < size && ret == LOAD_SUCCESS) {
tainted = CS_VALIDATE_TAINTED;
valid = cs_validate_page(blobs,
NULL,
file_offset + off,
addr + off,
&tainted);
if (!valid || (tainted & CS_VALIDATE_TAINTED)) {
if (cs_debug)
printf("CODE SIGNING: %s[%d]: invalid initial page at offset %lld validated:%d tainted:%d csflags:0x%x\n",
vp->v_name, p->p_pid, (long long)(file_offset + off), valid, tainted, result->csflags);
if (cs_enforcement(NULL) ||
(result->csflags & (CS_HARD|CS_KILL|CS_ENFORCEMENT))) {
ret = LOAD_FAILURE;
}
result->csflags &= ~CS_VALID;
}
off += PAGE_SIZE;
}
}
......

整个代码签名的验证过程大致分为load_code_signature()cs_validate_page()两步,前者负责加载代码签名,后者负责验证数据页面。load_code_signature()在加载代码签名时,通过调用ubc_cs_blob_get()来获取特定CPU的cs_blob指针,ubc_cs_blob_get()第一次调用时,返回的cs_blob指针为空,会调用ubc_cs_blob_add()来加载与验证文件中的Blob信息,以后再调用ubc_cs_blob_get(),就会返回内存中的cs_blob指针,当然不是直接返回,系统会再次判断内存中的cs_blob指针是否损坏或遭到篡改,具体方法是调用ubc_cs_generation_check()做初步的检查,之后调用ubc_cs_blob_revalidate()对blob做重验证。load_code_signature()`函数代码如下:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
static load_return_t
load_code_signature(
struct linkedit_data_command *lcp,
struct vnode *vp,
off_t macho_offset,
off_t macho_size,
cpu_type_t cputype,
load_result_t *result)
{
int ret;
kern_return_t kr;
vm_offset_t addr;
int resid;
struct cs_blob *blob;
int error;
vm_size_t blob_size;
addr = 0;
blob = NULL;
if (lcp->cmdsize != sizeof (struct linkedit_data_command) ||
lcp->dataoff + lcp->datasize > macho_size) {
ret = LOAD_BADMACHO;
goto out;
}
blob = ubc_cs_blob_get(vp, cputype, macho_offset);
if (blob != NULL) {
/* we already have a blob for this vnode and cputype */
if (blob->csb_cpu_type == cputype &&
blob->csb_base_offset == macho_offset &&
blob->csb_mem_size == lcp->datasize) {
/* it matches the blob we want here, lets verify the version */
if(0 != ubc_cs_generation_check(vp)) {
if (0 != ubc_cs_blob_revalidate(vp, blob, 0)) {
ret = LOAD_FAILURE; /* set error same as from ubc_cs_blob_add */
goto out;
}
}
ret = LOAD_SUCCESS;
} else {
/* the blob has changed for this vnode: fail ! */
ret = LOAD_BADMACHO;
}
goto out;
}
blob_size = lcp->datasize;
kr = ubc_cs_blob_allocate(&addr, &blob_size);
if (kr != KERN_SUCCESS) {
ret = LOAD_NOSPACE;
goto out;
}
resid = 0;
error = vn_rdwr(UIO_READ,
vp,
(caddr_t) addr,
lcp->datasize,
macho_offset + lcp->dataoff,
UIO_SYSSPACE,
0,
kauth_cred_get(),
&resid,
current_proc());
if (error || resid != 0) {
ret = LOAD_IOERROR;
goto out;
}
if (ubc_cs_blob_add(vp,
cputype,
macho_offset,
addr,
lcp->datasize,
0)) {
ret = LOAD_FAILURE;
goto out;
} else {
/* ubc_cs_blob_add() has consumed "addr" */
addr = 0;
}
#if CHECK_CS_VALIDATION_BITMAP
ubc_cs_validation_bitmap_allocate( vp );
#endif
blob = ubc_cs_blob_get(vp, cputype, macho_offset);
ret = LOAD_SUCCESS;
out:
if (ret == LOAD_SUCCESS) {
result->csflags |= blob->csb_flags;
result->platform_binary = blob->csb_platform_binary;
result->cs_end_offset = blob->csb_end_offset;
}
if (addr != 0) {
ubc_cs_blob_deallocate(addr, blob_size);
addr = 0;
}
return ret;
}

注意,上面提到的cs_blob指针,其实就是代码签名数据中的CS_SuperBlob指针类型。
ubc_cs_blob_add()的代码比较长,它主要做了三个工作:一是调用cs_validate_csblob()验证cs_blob指针的合法性,cs_validate_csblob()会对CSMAGIC_EMBEDDED_SIGNATURECSMAGIC_CODEDIRECTORY做相应的验证处理,包括调用cs_validate_codedirectory()验证CS_CodeDirectory结构体的合法性,以及调用cs_validate_blob()来验证CS_SuperBlob中每一个CS_GenericBlob是否合法有效;二是调用mac_vnode_check_signature()验证Blob块的代码签名,也就是比较Blob块的SHA1哈希值是否与计算的值相同;三是加载所有的代码签名Hash信息,填充cs_blobs字段,为下一步的内存页签名验证做准备。
ubc_cs_blob_revalidate()做着与ubc_cs_blob_add()几乎相同的验证检查,但前者因为已经有了一些缓存信息,因此检查时会快一些。

load_code_signature()完事以后,会调用ubc_get_cs_blobs()获取cs_blobs指针,最后调用cs_validate_page()以逐页的形式验证文件中每一页的数据的签名。
以上检查做完后,LC_CODE_SIGNATURE就处理完了,没有错误发生就表示代码签名验证通过了。

讲完了代码签名,再讲讲代码加密。Mach-O程序如果使用了代码加密技术,在加载命令列表中会有一个LC_ENCRYPTION_INFO加载命令。它存储了Mach-O的加密信息。关于此加载命令,对于搞过iOS程序逆向的读者应该不会感到陌生。iOS系统由于安全机制的原因,会对App Store中上架的应用默认开启数据加密。
被加密过的App文件,部分段的数据内容是经过加密的,而记录加密数据的关键就是LC_ENCRYPTION_INFO加载命令。分析人员要想对加密过的App进行逆向分析,必须先经过一次解密(俗称“砸壳”)操作。
LC_ENCRYPTION_INFO使用encryption_info_command结构体表示。定义如下(LC_ENCRYPTION_INFO_64使用encryption_info_command_64表示):

1
2
3
4
5
6
7
struct encryption_info_command {
uint32_t cmd; /* LC_ENCRYPTION_INFO */
uint32_t cmdsize; /* sizeof(struct encryption_info_command) */
uint32_t cryptoff; /* file offset of encrypted range */
uint32_t cryptsize; /* file size of encrypted range */
uint32_t cryptid; /* which enryption system, 0 means not-encrypted yet */
};

cryptoffcryptsize字段分别指明了加密数据的文件偏移与大小。cryptid指定了使用的加密系统。
聪明的安全研究人员,根据Mach-O在内存中被加载完后即解密完成的特点,开发了针对iOS平台App的代码解密工具dumpdecrypted
下载地址是:https://github.com/stefanesser/dumpdecrypted 。通过将内存中解密后的数据写回原位置,并将cryptid置0来达到解密App的目的。

再来看看系统是如何处理LC_ENCRYPTION_INFO的,它的解析函数也是parse_machfile(),代码片断如下:

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
static load_return_t parse_machfile(
struct vnode *vp,
vm_map_t map,
thread_t thread,
struct mach_header *header,
off_t file_offset,
off_t macho_size,
int depth,
int64_t aslr_offset,
int64_t dyld_aslr_offset,
load_result_t *result
)
{
......
#if CONFIG_CODE_DECRYPTION
case LC_ENCRYPTION_INFO:
case LC_ENCRYPTION_INFO_64:
if (pass != 3)
break;
ret = set_code_unprotect(
(struct encryption_info_command *) lcp,
addr, map, slide, vp,
header->cputype, header->cpusubtype);
if (ret != LOAD_SUCCESS) {
printf("proc %d: set_code_unprotect() error %d "
"for file \"%s\"\n",
p->p_pid, ret, vp->v_name);
/*
* Don't let the app run if it's
* encrypted but we failed to set up the
* decrypter. If the keys are missing it will
* return LOAD_DECRYPTFAIL.
*/
if (ret == LOAD_DECRYPTFAIL) {
/* failed to load due to missing FP keys */
proc_lock(p);
p->p_lflag |= P_LTERM_DECRYPTFAIL;
proc_unlock(p);
}
psignal(p, SIGKILL);
}
break;
#endif
default:
/* Other commands are ignored by the kernel */
ret = LOAD_SUCCESS;
break;
......

当系统内核被配置为启用代码解密,即定义了CONFIG_CODE_DECRYPTION之后,parse_machfile()函数会解析LC_ENCRYPTION_INFOLC_ENCRYPTION_INFO_64加载命令。
最终是调用了set_code_unprotect()函数来对代码进行解密。该函数通过encryption_info_command中的cryptid来确定使用的加密系统,然后对代码进行内存解密。它的代码片断如下:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#if CONFIG_CODE_DECRYPTION
static load_return_t
set_code_unprotect(
struct encryption_info_command *eip,
caddr_t addr,
vm_map_t map,
int64_t slide,
struct vnode *vp,
cpu_type_t cputype,
cpu_subtype_t cpusubtype)
{
......
if (eip->cmdsize < sizeof(*eip)) return LOAD_BADMACHO;
switch(eip->cryptid) {
case 0:
/* not encrypted, just an empty load command */
return LOAD_SUCCESS;
case 1:
cryptname="com.apple.unfree";
break;
case 0x10:
/* some random cryptid that you could manually put into
* your binary if you want NULL */
cryptname="com.apple.null";
break;
default:
return LOAD_BADMACHO;
}
if (map == VM_MAP_NULL) return (LOAD_SUCCESS);
if (NULL == text_crypter_create) return LOAD_FAILURE;
......
/* set up decrypter first */
crypt_file_data_t crypt_data = {
.filename = vpath,
.cputype = cputype,
.cpusubtype = cpusubtype};
kr=text_crypter_create(&crypt_info, cryptname, (void*)&crypt_data);
FREE_ZONE(vpath, MAXPATHLEN, M_NAMEI);
......
offset = mach_header_sz;
uint32_t ncmds = header->ncmds;
while (ncmds--) {
/*
* Get a pointer to the command.
*/
struct load_command *lcp = (struct load_command *)(addr + offset);
offset += lcp->cmdsize;
switch(lcp->cmd) {
case LC_SEGMENT_64:
seg64 = (struct segment_command_64 *)lcp;
if ((seg64->fileoff <= eip->cryptoff) &&
(seg64->fileoff+seg64->filesize >=
eip->cryptoff+eip->cryptsize)) {
map_offset = seg64->vmaddr + eip->cryptoff - seg64->fileoff + slide;
map_size = eip->cryptsize;
goto remap_now;
}
case LC_SEGMENT:
seg32 = (struct segment_command *)lcp;
if ((seg32->fileoff <= eip->cryptoff) &&
(seg32->fileoff+seg32->filesize >=
eip->cryptoff+eip->cryptsize)) {
map_offset = seg32->vmaddr + eip->cryptoff - seg32->fileoff + slide;
map_size = eip->cryptsize;
goto remap_now;
}
}
}
/* if we get here, did not find anything */
return LOAD_BADMACHO;
remap_now:
/* now remap using the decrypter */
kr = vm_map_apple_protected(map, map_offset, map_offset+map_size, &crypt_info);
if(kr) {
printf("set_code_unprotect(): mapping failed with %x\n", kr);
crypt_info.crypt_end(crypt_info.crypt_ops);
return LOAD_PROTECT;
}
return LOAD_SUCCESS;
}
#endif

text_crypter_create()是一个全局的text_crypter_create_hook_t类型的指针,在内核代码“osfmk/kern/page_decrypt.c”文件中通过text_crypter_create_hook_set()进行设置。
text_crypter_create()在填充完解密所需的信息crypt_info后,会再次计算需要重新解密映射到内存的地址与大小,调用vm_map_apple_protected()进行解密操作。

由于内核的代码可以直接审阅,数据加密在macOS系统上显得意义不大,在目前最新的macOS 10.12系统上,苹果没有启用代码解密功能,LC_ENCRYPTION_INFOLC_ENCRYPTION_INFO_64加载命令也就没那么常见了。

最后,可以使用otool命令行工具来查看Mach-O文件的加载命令信息:

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
otool -l /usr/bin/python
/usr/bin/python:
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot 0x00000000
initprot 0x00000000
nsects 0
flags 0x0
Load command 1
cmd LC_SEGMENT_64
......
Load command 15
cmd LC_DATA_IN_CODE
cmdsize 16
dataoff 17776
datasize 0
Load command 16
cmd LC_CODE_SIGNATURE
cmdsize 16
dataoff 20528
datasize 9344

也可以使用MachOView查看,效果如图所示:
load_command

0x6 LC_SEGMENT

段加载命令LC_SEGMENT,描述了32位Mach-O文件的段的信息,使用segment_command结构体来表示,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};

segname字段是一个16字节大小的空间,用来存储段的名称。
vmaddr字段指明了段要加载的虚拟内存地址。
vmsize字段指明了段所占的虚拟内存的大小。
fileoff字段指明了段数据所在文件中偏移地址。
filesize字段指明了段数据实际的大小。
maxprot字段指明了页面所需要的最高内存保护。
initprot字段指明了页面初始的内存保护。
nsects字段指明了段所包含的节区(section)。
flags字段指明了段的标志信息。

LC_SEGMENT对应的是LC_SEGMENT_64,它使用segment_command_64结构体表示,描述了64位Mach-O文件的段的基本信息,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};

所有的字段含义与32位基本一致。主要讨论一下它最后4个字段。

一个编译后可能执行的程序分成了多个段,不同的类型的数据放入了不同的段中。如程序的代码被称作代码段,
放入一个名为__TEXT的段中,代码段的maxprot字段在编译时被设置成VM_PROT_READ(可读)、VM_PROT_WRITE(可写)、VM_PROT_EXECUTE(可执行),initprot字段被设置成
VM_PROT_READ(可读)与VM_PROT_EXECUTE(可执行),这样做是合理的,一个普通的应用程序,它的代码段部分通常是不可写的,特殊需求的程序,如果要求代码段可写,必须在编译时设置它的
initprot字段为VM_PROT_WRITE(可写)。

nsects字段指定了段加载命令包含几个节区(section),一个段可以包含0个或多个节区。如__PAGEZERO段就不包含任何节区,该段被称为空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用。
当一个段包含多个节区时,节区信息会以数组形式紧随着存储在段加载命令后面。节区使用结构体section表示(64位使用section_64表示),定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};

sectname字段表示节区的名称,segname字段表示节区所在的段名,addrsize指明了节区所在的内存地址与大小,offset指明了区取所在的文件偏移,
align表示节区的内存对齐边界,reloff指明了重定位信息的文件偏移,nreloc表示重定位条目的数目,flags则是节区的一些标志属性。

段加载命令的最后一个字段flags存储了段的一些标志属性,它的取值有如下:

1
2
3
4
#define SG_HIGHVM 0x1
#define SG_FVMLIB 0x2
#define SG_NORELOC 0x4
#define SG_PROTECTED_VERSION_1 0x8

值得关注的是SG_PROTECTED_VERSION_1,当段被设置了该标志位,表示段是经过加密的!在macOS版本10.6以前,系统使用AES算法进行段的加密与解密,10.6的时候,则使用的Blowfish加密算法,著名的iOS逆向工具class-dump(地址:https://github.com/nygard/class-dump) 提供了一个静态数据段解密工具deprotect,有兴趣的读者可以参看它的代码来了解段解密的部分。

最后,使用MachOView工具查看系统python程序的__TEXT段的信息如图所示:
python_seg

macOS平台软件的下载与安装

对于一个操作系统来说,对其影响最大的莫过于运行在它上面的软件,正因为这些形形色色软件的存在,操作系统才得以广泛应用。与Linux、Windows等主流操作系统一样,运行在macOS上的软件也有着自己独有的特点。

macOS上的软件有着自己独特的UI界面与操作方式。macOS的设计师一直秉承着自己的设计理念,设计出的Aqua界面别具风格。银灰的金属色主题始终是分辨macOS系统最快捷的一种方式,与Windows界面将最小按钮与关闭按钮设置到窗口右上角不同,Aqua界面的程序,最小化与关闭按钮位于软件的左上角,并且使用全屏按钮替换了Windows上的最大化按钮。进入全屏模式下的软件会新开一个窗口并占据屏幕的全部空间,可以在触摸板上使用三根手指左右划动来切换不同的全屏窗口。

每个常规的Aqua界面的程序都有一个菜单,菜单显示在用户屏幕的顶端,菜单的最左边始终是一个苹果图标,点击它会弹出系统设置、App Store、关机、重启等多个选项。
macOS使用Dock栏来管理显示常用的软件,它位于用户屏幕的底部,展示效果与Windows的任务栏相似,正在运行的macOS界面程序,都可以在Dock上右键点击该程序,在弹出的菜单中选择Options->Keep in Dock,将程序在Dock上保留下来,以后就可以直接从Dock中单击图标启动程序。
在Windows系统中,用户可以使用开始菜单->所有程序来找到系统中安装的软件并启动它,macOS中则提供了一个Launchpad程序来管理安装在/Applications目录中的软件。Launchpad移植于iOS系统中的SpringBoard,展示的效果也与之类似,它以全屏网格形式的界面显示了所有可以运行的软件与系统工具,如果软件安装过多,会以多个页面来展示。点击Dock上的Launchpad图标可以启动它,如图所示:
launchpad
使用二根手指左右划动可以在不同的页面间切换。

0x0 可执行文件

除了Aqua界面的程序,macOS上还可以运行很多其它种类的文件,这里将所有可以运行在macOS系统上文件的统称为macOS可执行文件。

首先是脚本,macOS提供了UNIX系统中的Shell环境来支持运行脚本与命令行程序,脚本实质是一个文本文件,在脚本文件中指定运行它解释器后,Shell在运行脚本时,会调用解释器来解释运行它,任何一个文件都可以通过执行”chmod +x”命令给它加上可执行权限,拥有可执行权限的文件并不一定能执行,它必须是能满足某种解释器的语法规则才算得上可执行文件。
除了主流的几种Shell脚本支持外,macOS还内置了目前比较流行的PerlPythonRuby等脚本的解释器,任何人都可以直接编写PerlPythonRuby脚本并调用它们的解释器来运行,而不需要安装额外的软件。

另外,苹果公司还开发了一种脚本:AppleScript(苹果脚本)。它的作用是用于运行在macOS的程序并实际自动化工作的。苹果提供了一个单独的脚本编辑器来开发与调试AppleScript。它位于/Applications/Utilities/Script Editor。使用Script Editor编写好的脚本保存的格式为scpt。随着macOS系统的不断升级,AppleScript也在不停的发展,目前甚至可以使用AppleScript来开发macOS上的界面程序。具体的步骤是在XCode中选择File->New->Project,在打开的对话框中选择Other->Cocoa-AppleScript。有兴趣的读者可以参看苹果的官方文档来深入了解AppleScript,地址上:https://developer.apple.com/library/mac/documentation/AppleScript/Conceptual/AppleScriptX/AppleScriptX.html

除了脚本外,可执行文件还包括可以由用户主动执行的程序与被动执行的程序文件。
主动执行的程序包括:GUI界面程序、命令行程序、游戏等;被动执行的程序包括被系统调用的程序,如:Quick Look插件、屏幕保护程序、内核驱动与扩展等,以及被程序调用的框架、库、Bundle、XPC服务等。所有的这些可执行文件都使用苹果独有的可执行文件格式Mach-O。关于Mach-O文件的格式,将在后面的小节中进行详细讲解。

0x1 下载与安装软件

苹果支持从自家的App Store应用商店直接安装软件,也支持从第三方渠道,如其它磁盘介质、网络下载来安装软件。从App Store下载软件是最方便快捷,也是最安全的一种方式,苹果公司一向以软件审查严格闻名,自家商店中的应用软件在界面、功能以及对系统资源的使用上,都是经常严格审查与限制的,对于普通用户来说,这种下载软件的方式最合适不过了,但对于专业用户来说,由于一些专业软件在用户授权协议、资源访问上或其它方面的原因未能在App Store上架,就只能从网络或第三方渠道来获取这类软件了。

0x1.1 免费与付费软件

App Store应用商店上提供了丰富的免费与付费的软件供用户下载使用。下载免费的软件不需要用户付出额外的成本,只需要到官网https://appleid.apple.com/ 注册一个的帐号,使用帐号登录App Store就可以下载上面的免费软件了。
如果要下载App Store上面的收费软件,需要为帐号绑定一张银行卡,购买软件成功后会直接从银行卡中扣取费用。另外,App Store中的软件还支持另一种收费方式:In-App Purchase(应用内付费),简称IAP,这种收费方式在苹果自家的iOS系统中应用非常广泛,它允许开发人员为自己的软件某些特定功能设定为需要购买才能使用,目前,很多软件开发商以此平台作为主要的软件收入来源。

App Store中的收费软件只有IAP与直接购买这两种方式,而网络下载的收费软件的付费形式则丰富很多。部分软件官网只提供软件基础功能的演示版本供用户下载,如果需要使用正式版本,则需要联系软件开发商购买完整版本。例如著名的反汇编软件IDA Pro,官网就只提供了Demo版本可供下载:https://www.hex-rays.com/products/ida/support/download_demo.shtml ,正式版本需要找软件开发商或代理商购买。也有些软件的官网会提供完整版本下载,但会有使用时间或功能限制,如果需要正常无限制使用软件,则需要购买软件授权,如反汇编软件Hopperhttp://hopperapp.com/)。还有部分软件与传统Window付费软件一样,使用用户名与注册码的形式来售卖软件,如`010 Editor`(http://www.sweetscape.com/010editor/)。

0x1.2 安装软件

普通的macOS软件只是一个以扩展名.app结尾的目录,这种目录有着特定的组织结构,macOS将它称之为Bundle,安装这类软件只需要将Bundle复制到系统的/Applications目录即可,复制完成后,Launchpad面板会自动更新安装好的软件图标,启动软件不需要到/Applications目录中去寻找它,只需要打开Launchpad,找到软件的图标并点击就可以运行该程序了。
从网络上下载的软件通常是经过打包后发布的,普通的软件多是zip或其它方式压缩后,以压缩包的形式提供下载,还有的使用磁盘工具将软件打包成一个dmg磁盘镜像文件,这类dmg文件内通常还会有一个Applications目录的软链接,安装的时候,只需要将dmg中的软件直接拖放到该软链接上就算完成安装了。如图所示,是AppDelete的安装镜像:
install_app

macOS上的软件还有一种是以pkg或mpkg结尾的安装包,类似于Windows系统上的msi或exe安装程序,通过不停的点击下一步就可以完成安装,这类软件除了将主程序写入/Applications目录外,一般还会在系统上做一些只有管理员权限才能完成的动作,比如为特定的目录或文件创建软链接、安装与卸载内核扩展、复制命令行程序到用户可执行文件目录/usr/local/bin中等。因此,在安装的过程中,可能会提示输入管理员用户名与密码来执行需要Root权限的操作。

另一种是命令行工具,这类程序的安装使用第一章中介绍的Homebrew即可,此处不再赘述。

0x3 Bundle

Bundle是苹果系统独有的特色,在苹果系统上大量中使用了Bundle。

0x3.1 Bundle目录结构

安装到macOS系统上的软件有着自己特定的格式,它们是以.app扩展名结尾的Bundle目录结构。Bundle有着固定的组织格式,在Finder中查看Bundle的目录内容,可以在程序上点右键,在弹出的菜单中选择Show Package Contents,查看Bundle的目录结构。以/Applications目录下App Store为例,它的目录结构如图所示:
bundle

在App Store.app目录下,只有一个Contents子目录,所有软件的内容都在此目录下。它们包括:

  • CodeSignature目录。此目录下只有一个CodeResources文件,它是一个plist格式的文件,保存了软件包中所有文件的签名信息。
  • info.plist文件。此文件记录了软件的一些信息。如软件构建的机器的版本BuildMachineOSBuild、可执行文件名CFBundleExecutable、软件的标识CFBundleIdentifier、软件包名CFBundleName等。
  • MacOS目录。此目录存放了可执行文件。
  • Pkginfo文件。软件包的8字节的标识符。
  • Resources目录。软件运行所需要的资源,包括.iproj本地化资源、.nib资源、图片、字体、声音、文档以及其它文件。
  • Plugins目录。插件目录,存放了软件用到的插件,插件也是使用Bundle目录结构进行组织的一种程序。

除了Plugins目录外,根据软件需求与实现的不同,Contents下可能还会有Frameworks与XPCServices目录,Frameworks里面存放了软件需要用到的框架,它是以.framework结尾的Bundle结构;XPCServices则存放了软件用到的XPC服务,它是以.xpc结尾的Bundle结构,还有一种以.bundle扩展名结尾的Bundle,它是“纯粹”的Bundle,目录结构与其它的Bundle无异,存放的内容可以是二进制代码,也可以是资源,也可以二者同时存放,如果存放二进制代码的话,可供程序在代码中使用dlopen()函数打开,这种Bundle通常用于制作软件的插件,存放在软件Bundle的Resources目录下。

0x3.2 代码中访问Bundle

在程序中,开发人员可以使用Cocoa框架提供的NSBundle类来获取程序的Bundle信息。调用NSBundlemainBundle()方法可以返回当前程序的主Bundle对象,调用方法如下:

1
NSBundle *bundle = [NSBundle mainBundle]

使用主Bundle对象的infoDictionary()方法可以访问软件Bundle目录下info.plist文件中的信息,它返回的是一个字典对象,如下所示是获取程序标识符的代码:

1
[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"]]

使用主Bundle对象的pathForResource()方法可以访问Bundle目录下任意资源文件。如下所示是访问Bundle根目录下的monkey.png文件:

1
NSString *monkey = [[NSBundle mainBundle] pathForResource:@"monkey" ofType:@"png"];

与主Bundle对应的是自定义Bundle,这一类Bundle的访问可以这样调用:

1
2
3
4
5
6
NSString *resourceBundle = [[NSBundle mainBundle] pathForResource:@"ResPack" ofType:@"bundle"];
NSLog(@"resourceBundle: %@", resourceBundle);
NSString *monkey = [[NSBundle bundleWithPath:resourceBundle] pathForResource:@"monkey"
ofType:@"png" inDirectory:@"Images"];
NSLog(@"monkey path: %@", monkey);

上面这段代码访问了ResPack.bundle中Images目录下的monkey.png文件。

新的篇章

欢迎来到 非虫的博客! 自从12年放弃百度空间后,再没写过博客了。

新的一年,新的篇章,非虫的博客全新开启,以此记录生活点滴,同时作为自己技术分享的平台,希望你会喜欢!