4.1 CydiaSubstrate

CydiaSubstrate(如图4-1所示)是绝大部分tweak正常工作的基础,它由MobileHooker、MobileLoader和Safe mode组成。

图4-1 CydiaSubstrate的logo

4.1.1 MobileHooker

MobileHooker的作用是替换系统函数,也就是所谓的hook,它主要包含以下两个函数:


void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP *result);
void MSHookFunction(void* function, void* replacement, void** p_original);

其中MSHookMessageEx作用于Objective-C函数,通过调用method_setImplementation函数将[class selector]的实现改为replacement,达到hook的目的。这是什么意思呢?举个简单的例子,向一个NSString对象发送hasSuffix:消息(即调用[NSString hasSuffix:]),正常情况下它的实现是判断NSString对象有没有某个后缀;如果把这个实现替换成hasPrefix:的实现,那么NSString对象在收到hasSuffix:消息后,实际进行的操作是“口是心非”地判断这个NSString对象有没有某个前缀(prefix)。是不是容易理解一些了?

第3章提到的Logos语法主要是对此函数作了一层封装,让编写针对Objective-C函数的hook代码变得更简单直观了,但其底层实现仍完全基于MSHookMessageEx。对于Objective-C函数的hook,推荐使用更一目了然的Logos语法。如果对MSHookMessageEx本身的使用感兴趣,可以去看它的官方文档,或者Google搜索“cydiasubstrate fuchsiaexample”,以“http://www.cydiasubstrate.com ”开头的那个链接就是了。

MSHookFunction作用于C和C++函数,通过编写汇编指令,在进程执行到function时转而执行replacement,同时保存function的指令及其返回地址,使得用户可以选择性地执行function,并保证进程能够在执行完replacement后继续正常运行。

上面这段话可能有些晦涩难懂,这里用两张图来解释一下,先看图4-2。

在图4-2中,进程先执行一些指令,再执行函数A,接着执行剩下的指令。如果钩住(hook)了函数A(即上面说的function),想用函数B(即replacement)替换它,那么进程的执行流程就变成了图4-3的样子。

图4-2 进程正常执行流程

图4-3 用B替换A

在图4-3中,进程先执行一些指令,在原本应该执行函数A的地方跳转到函数B的位置执行函数B,同时函数A的代码被MobileHooker保存了下来。在函数B中,可以选择是否执行函数A,以及何时执行函数A,在函数B执行完成后,则会继续执行剩下的指令。

值得注意的是,MSHookFunction对function的指令总长度是有要求的,即function里所有指令加起来的长度不能太短(saurik曾在非正式的场合里说过大约是至少8字节,但此数字未经严格验证,请不要以此为准)。那么你可能会问了,如果想钩住(hook)那些短函数,该怎么办呢?

一种变通的方法是钩住(hook)短函数内部调用的其他函数——短函数之所以短,是因为内部一般都调用了其他函数,由其他函数来做出实际操作。因此把长度满足要求的其他函数作为MSHookFunction的目标,然后在replacement里做一些逻辑判断,将它与短函数关联上,再把对短函数的修改写在这里就好了。

如果看了这两段话仍有不解之处,没关系,在了解了MSHookFunction大概的工作原理之后,会以一个简单的例子,解释上面提到的这些内容。需要说明的是,这个例子里会涉及比较多的底层知识,对于新手来说理解会有一定的困难,如果接触逆向工程的时间不长,看不懂下面的例子也没关系,直接跳到4.1.2节吧。当你在实际操作中碰到了类似的情况,再来回看这一小节,相信那时会对这些内容有更好的理解。此外,欢迎来http://bbs.iosre.com参与讨论。

下面一起来完成如下操作。

1)用Theos新建iOSRETargetApp,命令如下:


snakeninnys-MacBook:Code snakeninny$ /opt/theos/bin/nic.pl
NIC 2.0 - New Instance Creator
------------------------------
  [1.] iphone/application
  [2.] iphone/library
  [3.] iphone/preference_bundle
  [4.] iphone/tool
  [5.] iphone/tweak
Choose a Template (required): 1
Project Name (required): iOSRETargetApp
Package Name [com.yourcompany.iosretargetapp]: com.iosre.iosretargetapp
Author/Maintainer Name [snakeninny]: snakeninny
Instantiating iphone/application in iosretargetapp/...
Done.

2)修改RootViewController.mm,命令如下:


#import "RootViewController.h"
class CPPClass
{
      public:
            void CPPFunction(const char *);
};
void CPPClass::CPPFunction(const char *arg0)
{
      for (int i = 0; i < 66; i++) // This for loop makes this function long enough to validate MSHookFunction
      {
            u_int32_t randomNumber;
            if (i % 3 == 0) randomNumber = arc4random_uniform(i);
            NSProcessInfo *processInfo = [NSProcessInfo processInfo];
            NSString *hostName = processInfo.hostName;
            int pid = processInfo.processIdentifier;
            NSString *globallyUniqueString = processInfo.globallyUniqueString;
            NSString *processName = processInfo.processName;
            NSArray *junks = @[hostName, globallyUniqueString, processName];
            NSString *junk = @"";
            for (int j = 0; j < pid; j++)
            {
                  if (pid % 6 == 0) junk = junks[j % 3];
            }
            if (i % 68 == 1) NSLog(@"Junk: %@", junk);
      }
      NSLog(@"iOSRE: CPPFunction: %s", arg0);
}
extern "C" void CFunction(const char *arg0)
{
      for (int i = 0; i < 66; i++) // This for loop makes this function long enough to validate MSHookFunction
      {
            u_int32_t randomNumber;
            if (i % 3 == 0) randomNumber = arc4random_uniform(i);
            NSProcessInfo *processInfo = [NSProcessInfo processInfo];
            NSString *hostName = processInfo.hostName;
            int pid = processInfo.processIdentifier;
            NSString *globallyUniqueString = processInfo.globallyUniqueString;
            NSString *processName = processInfo.processName;
            NSArray *junks = @[hostName, globallyUniqueString, processName];
            NSString *junk = @"";
            for (int j = 0; j < pid; j++)
            {
                  if (pid % 6 == 0) junk = junks[j % 3];
            }
            if (i % 68 == 1) NSLog(@"Junk: %@", junk);
      }
      NSLog(@"iOSRE: CFunction: %s", arg0);
}
extern "C" void ShortCFunction(const char *arg0) // ShortCFunction is too short to be hooked
{
      CPPClass cppClass;
      cppClass.CPPFunction(arg0);
}
@implementation RootViewController
- (void)loadView {
      self.view = [[[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]] autorelease];
      self.view.backgroundColor = [UIColor redColor];
}
- (void)viewDidLoad
{
      [super viewDidLoad];
      CPPClass cppClass;
      cppClass.CPPFunction("This is a C++ function!");
      CFunction("This is a C function!");
      ShortCFunction("This is a short C function!");
}
@end

上面简单地写了一个CPPClass::CPPFunction、一个CFunction和一个ShortCFunction,作为hook的对象。这里特意在CPPClass::CPPFunction和CFuntion里添加了一些无用代码,目的仅仅是增加这两个函数的长度,使得针对它们俩的MSHookFunction生效。而ShortCFunction会因长度太短,导致针对它的MSHookFunction失效。但是在接下来的tweak中会巧妙地回避这个问题。

3)修改iOSRETargetApp的Makefile并安装,命令如下:


THEOS_DEVICE_IP = iOSIP
ARCHS = armv7 arm64
TARGET = iphone:latest:8.0
include theos/makefiles/common.mk
APPLICATION_NAME = iOSRETargetApp
iOSRETargetApp_FILES = main.m iOSRETargetAppApplication.mm RootViewController.mm
iOSRETargetApp_FRAMEWORKS = UIKit CoreGraphics
include $(THEOS_MAKE_PATH)/application.mk
after-install::
      install.exec "su mobile -c uicache"

在上面这段代码中,最后那句“su mobile–c uicache”用来刷新桌面UI缓存,显示出iOSRETargetApp图标。在Terminal里运行“make package install”命令将其安装到设备上。运行iOSRETargetApp,待红色背景显示之后ssh到iOS上,看看产生的输出与期待的结果是否相符,如下:


FunMaker-5:~ root# grep iOSRE: /var/log/syslog
Nov 18 11:13:34 FunMaker-5 iOSRETargetApp[5072]: iOSRE: CPPFunction: This is a C++ function!
Nov 18 11:13:34 FunMaker-5 iOSRETargetApp[5072]: iOSRE: CFunction: This is a C function!
Nov 18 11:13:35 FunMaker-5 iOSRETargetApp[5072]: iOSRE: CPPFunction: This is a short C function!

4)用Theos新建iOSREHookerTweak,命令如下:


snakeninnys-MacBook:Code snakeninny$ /opt/theos/bin/nic.pl
NIC 2.0 - New Instance Creator
------------------------------
  [1.] iphone/application
  [2.] iphone/library
  [3.] iphone/preference_bundle
  [4.] iphone/tool
  [5.] iphone/tweak
Choose a Template (required): 5
Project Name (required): iOSREHookerTweak
Package Name [com.yourcompany.iosrehookertweak]: com.iosre.iosrehookertweak
Author/Maintainer Name [snakeninny]: snakeninny
[iphone/tweak] MobileSubstrate Bundle filter [com.apple.springboard]: com.iosre.iosretargetapp
[iphone/tweak] List of applications to terminate upon installation (space-separated, '-' for none) [SpringBoard]: iOSRETargetApp
Instantiating iphone/tweak in iosrehookertweak/...
Done.

5)修改Tweak.xm,命令如下:


#import <substrate.h>
void (*old__ZN8CPPClass11CPPFunctionEPKc)(void *, const char *);
void new__ZN8CPPClass11CPPFunctionEPKc(void *hiddenThis, const char *arg0)
{
      if (strcmp(arg0, "This is a short C function!") == 0) old__ZN8CPPClass11CPPFunctionEPKc(hiddenThis, "This is a hijacked short C function from new__ZN8CPPClass11CPPFunctionEPKc!");
      else old__ZN8CPPClass11CPPFunctionEPKc(hiddenThis, "This is a hijacked C++ function!");
}
void (*old_CFunction)(const char *);
void new_CFunction(const char *arg0)
{
      old_CFunction("This is a hijacked C function!"); // Call the original CFunction
}
void (*old_ShortCFunction)(const char *);
void new_ShortCFunction(const char *arg0)
{
      old_CFunction("This is a hijacked short C function from new_ShortCFunction!"); // Call the original ShortCFunction
}
%ctor
{
      @autoreleasepool
      {
            MSImageRef image = MSGetImageByName("/Applications/iOSRETargetApp.app/iOSRETargetApp");
            void *__ZN8CPPClass11CPPFunctionEPKc = MSFindSymbol(image, "__ZN8CPPClass11CPPFunctionEPKc");
            if (__ZN8CPPClass11CPPFunctionEPKc) NSLog(@"iOSRE: Found CPPFunction!");
            MSHookFunction((void *)__ZN8CPPClass11CPPFunctionEPKc, (void *)&new__ZN8CPPClass11CPPFunctionEPKc, (void **)&old__ZN8CPPClass11CPPFunctionEPKc);
            void *_CFunction = MSFindSymbol(image, "_CFunction");
            if (_CFunction) NSLog(@"iOSRE: Found CFunction!");
            MSHookFunction((void *)_CFunction, (void *)&new_CFunction, (void **)&old_CFunction);
            void *_ShortCFunction = MSFindSymbol(image, "_ShortCFunction");
            if (_ShortCFunction) NSLog(@"iOSRE: Found ShortCFunction!");
            MSHookFunction((void *)_ShortCFunction, (void *)&new_ShortCFunction, (void **)&old_ShortCFunction); // This MSHookFuntion will fail because ShortCFunction is too short to be hooked
      }
}

在这段代码中,有很多需要注意的地方,如下所示。

·MSFindSymbol的作用

简单地说,MSFindSymbol的作用是查找待钩住(hook)的symbol。那symbol又是什么呢?

在计算机中,一个函数的指令被存放在一段内存中,当进程需要执行这个函数时,它必须知道要去内存的哪个地方找到这个函数,然后执行它的指令。也就是说,进程要根据这个函数的名称,找到它在内存中的地址,而这个名称与地址的映射关系,是存储在“symbol table”中的——“symbol table”中的symbol就是这个函数的名称,进程会根据这个symbol找到它在内存中的地址,然后跳转过去执行。

试想这样一个场景:你的软件调用了一个库,这个库里有一个lookup函数,用于到你的服务器上查询信息。另一个软件如果知道了这个函数的symbol,那它岂不是可以导入这个库,然后随意调用lookup,消耗你的服务器资源,为它自己提供便利?

为了避免这种情况,symbol被分为2类,即public symbol与private symbol(其实还有一类stripped symbol,但跟本章关系不大,这里就不介绍了,感兴趣的朋友可以浏览下面提供的参考链接,或自行查阅相关资料)。别人的private symbol不是你想用,想用就能用。也就是说,MSHookFunction直接作用在private symbol上是无效的。所以saurik提供了MSFindSymbol这个API来访问private symbol。如果你仍然不清楚什么是symbol,只需要记住下面的写法就好:


MSImageRef image = MSGetImageByName("/path/to/binary/who/contains/the/implementation/of/symbol");
void *symbol = MSFindSymbol(image, "symbol");

其中MSGetImageByName的参数是“symbol代表的函数其实现(implementation)所在的二进制文件的全路径”。不说绕口令,举个例子,NSLog函数的实现位于Foundation库,所以对于NSLog这个symbol来说,MSGetImageByName的参数就应该是“/System/Library/Frameworks/Foundation.framework/Foundation”。简单吧?

对MSFindSymbol函数更详细的解释可以参考其官方文档http://www.cydiasubstrate.com/api/c/MSFindSymbol/ ;关于symbol的种类及定义,请阅读http://msdn.microsoft.com/en-us/library/windows/hardware/ff553493(v=vs.85).aspx ,以及http://en.wikibooks.org/wiki/Reverse_Engineering/Mac_OS_X#Symbols_Types ,或者自行查阅相关资料。

·symbol的来源

你可能已经注意到了,我们在iOSRETargetApp的RootViewController.mm中定义的3个函数名分别是CPPClass::CPPFunction、CFunction和ShortCFunction,怎么到了iOSREHookerTweak的tweak.xm里,它们却变成了__ZN8CPPClass11CPPFunctionEPKc、_CFunction和_ShortCFunction?简单地说,这是因为编译器对函数名做了进一步的处理。处理过程是什么样的不需要关心,我们关心的是处理结果,这3个以下划线开头的symbol是怎么来的?因为在实战中,我们拿不到被hook函数的源代码,所以一般情况下,这些symbol都是从IDA对二进制文件的分析结果中提取的。下面来看一个简单的例子。

把iOSRETargetApp二进制文件丢到IDA里,初始分析完成后的Functions Window如图4-4所示。

图4-4 Function window

可以看到,CPPClass::CPPFunction(char const*)、_CFunction和_ShortCFunction位列其中。双击“CPPClass::CPPFunction(char const*)”,跳转到其实现上,如图4-5所示。

第4行下划线开头的这个字符串,就是我们要找的symbol。同理,_CFunction和_ShortCFunction的来源也显而易见了,如图4-6和图4-7所示。

图4-5 CPPClass::CPPFunction(char const*)

图4-6 CFunction

图4-7 ShortCFunction

此方法适用于查找任何symbol,在初学阶段,建议不要纠结symbol是怎么生成的,对这个知识点的“不知其所以然”无伤大雅,只需要记住symbol跟函数名不同就够了。在学习逆向工程的整个过程中,symbol的概念一定会潜移默化地融入你的知识体系,无须刻意去强化。

·MSHookFunction的写法

MSHookFunction的三个参数的作用分别是:替换的原函数、替换函数,以及被MobileHooker保存的原函数。红花还需绿叶衬,单独的一个MSHookFunction函数是没有意义的,需要有一套固定的体系来承载它,这个体系的写法如下:


#import <substrate.h>
returnType (*old_symbol)(args);
returnType new_symbol(args)
{
      // Whatever
}
void InitializeMSHookFunction(void) // This function is often called in %ctor i.e. constructor
{
      MSImageRef image = MSGetImageByName("/path/to/binary/who/contains/the/implementation/of/symbol");
      void *symbol = MSFindSymbol(image, "symbol");
      if (symbol)MSHookFunction((void *)symbol, (void *)&new_ symbol, (void **)&old_ symbol);
      else NSLog(@"Symbol not found!");
}

相信对比了上面的Tweak.xm,你很快就能理解这套体系的含义了。与symbol的情况类似,在实战中,我们拿不到被钩住(hook)的函数的源代码,函数的原型我们是不知道的,因此returnType究竟是什么,args一共有几个,各是什么类型,我们一无所知。这时,就需要借助更高级的逆向工程技术来还原出被钩住(hook)的函数原型了。这部分知识会在第6章重点讲述,现在不理解完全是正常的。建议读者在看完第6章后复习本节的内容,一定会有新的体会。

6)修改iOSREHookerTweak的Makefile并安装,命令如下:


THEOS_DEVICE_IP = iOSIP
ARCHS = armv7 arm64
TARGET = iphone:latest:8.0
include theos/makefiles/common.mk
TWEAK_NAME = iOSREHookerTweak
iOSREHookerTweak_FILES = Tweak.xm
include $(THEOS_MAKE_PATH)/tweak.mk
after-install::
      install.exec "killall -9 iOSRETargetApp"

到这里,请再次运行iOSRETargetApp,看看产生的输出,确定结果是否符合预期,如下:


FunMaker-5:~ root# grep iOSRE: /var/log/syslog
Nov 18 11:29:14 FunMaker-5 iOSRETargetApp[5327]: iOSRE: Found CPPFunction!
Nov 18 11:29:14 FunMaker-5 iOSRETargetApp[5327]: iOSRE: Found CFunction!
Nov 18 11:29:14 FunMaker-5 iOSRETargetApp[5327]: iOSRE: Found ShortCFunction!
Nov 18 11:29:14 FunMaker-5 iOSRETargetApp[5327]: iOSRE: CPPFunction: This is a hijacked C++ function!
Nov 18 11:29:14 FunMaker-5 iOSRETargetApp[5327]: iOSRE: CFunction: This is a hijacked C function!
Nov 18 11:29:14 FunMaker-5 iOSRETargetApp[5327]: iOSRE: CPPFunction: This is a hijacked short C function from new__ZN8CPPClass11CPPFunctionEPKc!

值得一提的是,对短函数(即ShortCFunction)的直接hook失效了(否则会输出“This is a hijacked short C function from new_ShortCFunction!”),而对短函数内部调用的其他函数(即CPPClass::CPPFunction)的hook却是有效的,可通过判断它的参数,推测出它的调用者是ShortCFunction,这样一来,即可间接hook短函数,从而达到围魏救赵的效果。以上介绍的MSHookFunction体系基本涵盖了初学者可能碰到的所有问题,由于Theos仅提供了MSHookMessageEx的封装,掌握这套体系的用法就显得尤为重要了。如果还有什么不明白的地方,可到http://bbs.iosre.com 上讨论。

4.1.2 MobileLoader

MobileLoader的作用是加载第三方dylib。在iOS启动时,会由launchd将MobileLoader载入内存,然后MobileLoader会根据dylib的同名plist文件指定的作用范围,有选择地在不同进程里通过dlopen函数打开目录/Library/MobileSubstrate/DynamicLibraries/下的所有dylib。这个plist文件的格式已在Theos部分详细讲解,此处不再赘述。对于大多数初级iOS逆向工程师来说,MobileLoader的工作过程是完全透明的,此处仅作简单了解即可。

4.1.3 Safe mode

应用的质量良莠不齐,程序崩溃在所难免。因为tweak的本质是dylib,寄生在别的进程里,一旦出错,可能会导致整个进程崩溃,而一旦崩溃的是SpringBoard等系统进程,则会造成iOS瘫痪,所以CydiaSubstrate引入了Safe mode,它会捕获SIGTRAP、SIGABRT、SIGILL、SIGBUS、SIGSEGV、SIGSYS这6种信号,然后进入安全模式,如图4-8所示。

图4-8 安全模式

在安全模式里,所有基于CydiaSubstrate的第三方dylib均会被禁用,便于查错与修复。但是,并不是拥有了Safe mode就能高枕无忧,在很多时候,设备还是会因为第三方dylib的原因而无法进入系统,症状主要有:开机时卡在白苹果上,或者进度圈不停地转。在出现这种情况时,可以同时按住home和lock键硬重启,然后按住音量“+”键来完全禁用CydiaSubstrate,待系统重启完毕后,再来查错与修复。当问题被成功修复后,再重启一次iOS,就能重新启用CydiaSubstrate了,非常方便。