5.2 tweak的编写套路

只有理解了tweak的工作方式,才能在编写tweak时清楚地知道自己想干什么、在干什么。一般来说,编写tweak会用到C、C++和Objective-C三种语言,有了一个灵感时,该如何自如地运用这三种语言把灵感变成一个好用的tweak呢?事实上,编写tweak的思路是有规律可循的,而且随着你对iOS的了解愈加深入,对编程语言的掌握愈加熟练,这种规律会变得越来越明显。下面将围绕一个简单的tweak例子,从iOS工程师使用最多的Objective-C语言开始分析,总结归纳Objective-C级别的逆向工程理论。

5.2.1 寻找灵感

可能有部分iOS工程师读到这里时,已经能够结合前几章的知识开始开发tweak了,但可能也还有部分人感到无从下手,不知道该写些什么东西。这种有劲儿没处使的感觉确实挺难受,面对这种情况,该怎么办呢?一般情况下,可以从这几个方面找灵感。

1.多使用,多观察

没事就把你的手机拿出来把玩把玩,把系统的每个角落都扫一遍,别光顾着刷朋友圈。虽然iOS的功能已足够多,但也不可能符合每个用户的要求,所以,用得越多,你对iOS的了解就越多,哪些地方用着不爽的感觉就会更强烈。上网看看吧,iOS的用户基数巨大,他们中一定有跟你想法相同的人——你碰到需要解决的实际问题了,这不就是灵感吗?笔者在iOS 6时代开发的Characount for Notes(如图5-4所示)就是这样得来的。当时,笔者经常把微博的内容存成记事本,但微博是有140字限制的,于是就做了一个这样的tweak,用来统计记事本每页的字数,从而控制微博的长度。曾有一位阿拉伯用户还专门给笔者发邮件说很喜欢这个插件,希望加入更多功能把记事本改造成一个Word,但笔者对这个想法不大感兴趣,所以只好对他说声抱歉了。

图5-4 Characount for Notes

2.倾听用户的声音

每个人使用iOS的方式不同,他们的需求各异。如果你自己没有太多灵感,那就多听听果粉们的需求;只要有需求,tweak就有用户。大的项目已经有人做了,我们就针对少数人群定制tweak;水平不足做不了底层的复杂功能,就从高层的简单功能做起;每一版发布后,虚心听取用户的意见和建议,及时改进,快速迭代,你的付出不会没有回报。LowPowerBanner这个iOS 6插件就是笔者听取用户PrimeCode的建议编写的,完成第1版仅用了约5小时,写代码不到50行,但发布后8小时下载量即突破3万次(如图5-5所示),受欢迎程度大大超出笔者的预期。同志们,群众的眼睛是雪亮的,群众的智慧也是无穷的,如果你没有什么灵感,就走到群众中去吧!

图5-5 LowPowerBanner的第一版下载量

3.解剖iOS

当你的能力越大时,能做的事情也就越多。千里之行始于足下,从小程序做起,经过层层磨炼,你对iOS的理解会不断加深;iOS是个封闭的系统,它暴露给我们的只是冰山一角,有太多太多的功能还有待我们进一步挖掘。每次越狱发布后,都会有人把最新的头文件发布出来,Google一下“iOS private headers”即可轻松找到下载链接,省去了自己class-dump的麻烦。Objective-C语言的函数命名很规律,大多数函数都可以望名生义,如SpringBoard.h里的,如下函数:


- (void)reboot;
- (void)relaunchSpringBoard;

和UIViewController.h里的,如下函数:


- (void)attentionClassDumpUser:(id)arg1
yesItsUsAgain:(id)arg2
althoughSwizzlingAndOverridingPrivateMethodsIsFun:(id)arg3
itWasntMuchFunWhenYourAppStoppedWorking:(id)arg4
pleaseRefrainFromDoingSoInTheFutureOkayThanksBye:(id)arg5;

通览这些函数名,是灵感的重要来源之一,也是了解iOS底层的便捷渠道。掌握越多的iOS实现细节,意味着手里握有的零件就越多,因此你就能组装出与别人不同的设备。limneos开发的“Audio Recorder”就是最好的例子,iOS早在2007年就面世了,但通话录音的功能直到7年后才由这位希腊开发者实现。有这个想法的人很多很多,已经动手的人也肯定不在少数,但为什么只有limneos成功了?因为他对iOS的解剖比别人更彻底!说起来很简单,做起来不简单。

5.2.2 定位目标文件

知道自己想要实现什么功能后,就要开始寻找实现这个功能的二进制文件,方法一般有以下几种。

1.固定位置

现阶段我们的逆向目标一般是dylib、bundle或daemon,它们在系统中的位置几乎是固定的:

·基于CydiaSubstrate的dylib全部位于“/Library/MobileSubstrate/DynamicLibraries/”下,几乎不费吹灰之力就可以轻松定位。

·bundle主要分为App和framework两类,其中AppStore App全部位于“/var/mobile/Containers/Bundle/Application/”下,系统App全部位于“/Applications/”下,framework全部位于“/System/Library/Frameworks”或“/System/Library/PrivateFrameworks”下。关于其他类型App的定位,可以来http://bbs.iosre.com一起讨论。

·daemon的配置文件均位于“/System/Library/LaunchDaemons/”、“/Library/Launch-Daemons”或“/Library/LaunchAgents/”下,是一个plist格式的文件。其中的“ProgramArguments”字段,即是daemon可执行文件的绝对路径,如下:


snakeninnys-MacBook:~ snakeninny$ plutil -p /Users/snakeninny/Desktop/com.apple.backboardd.plist 
{
……
  "ProgramArguments" => [
    0 => "/usr/libexec/backboardd"
  ]
……
}

2.Cydia定位

通过“dpkg-i”命令安装的deb包,其内容会被Cydia如实记录,若要查看,在Cydia的“Installed”项中选择“Expert”,如图5-6所示。

然后选择目标软件,进入“Details”界面,如图5-7所示。

图5-6 Cydia的Expert界面

图5-7 Details界面

之后选择“Filesystem Content”,即可浏览软件包里的所有文件,如图5-8所示。

deb包中的每个文件被放在了iOS的哪个路径下,一目了然。

3.PreferenceBundle

PreferenceBundle是寄生在Settings应用里的App,它的功能界定有些模糊,既可以作为单纯的配置文件,由别的进程读取后执行,如图5-9所示的“DimInCall”界面。

也可以含有实际功能,自己来执行一些操作,如图5-10所示的“WLAN”界面。

我们关注的重点是应用的实际功能,因此如何定位PreferenceBundle执行实际功能的二进制文件,就是需要研究的课题之一。来自AppStore的第三方PreferenceBundle仅可作为配置文件存在,不会含有实际功能;来自Cydia的也不是问题,刚才介绍的Cydia定位方式已经完全够用了;但对于iOS自带的PreferenceBundle来说,定位的过程就要复杂一些。

图5-8 Filesystem Content界面

图5-9 DimInCall界面

图5-10 WLAN界面

PreferenceBundle的界面可以用代码编写,也可以用具有固定格式的plist文件构造(格式请参考http://iphonedevwiki.net/index.php/Preferences_specifier_plist )。在逆向此类程序时,如果发现界面中的控件类型全部来自preference specifier plist罗列的标准控件类型,如“About”界面(如图5-11所示),则需注意分辨此界面是用代码编写的,还是用plist构造的。对于iOS自带的PreferenceBundle来说,如果是用代码编写的,一般情况下实际功能就已经包含在二进制文件里了,它们位于“/System/Library/PreferenceBundles/”下;如果是用plist构造的,就需要分析plist和实际功能间的关系,从中找到切入点,定位含有实际功能的二进制文件。总之,PreferenceBundle的情况相对复杂,并不适合作为新手练习。如果对上面的内容一知半解,不要紧,稍后本章会以一个实例来提供参考。更多关于PreferenceBundle的讨论,尽在http://bbs.iosre.com

图5-11 About界面

4.grep命令

grep是一个来自UNIX系统的命令行工具,能够搜索文件中是否含有给定的正则表达式。OSX自带grep命令,iOS上的grep命令则是由Saurik移植过来的,随着Cydia默认安装。在寻找一个字符串的出处时,grep能够快速缩小查找范围。例如,想知道都有哪些地方调用了[IMDAccount initWithAccountID:de faults:service:],可以ssh到iOS后使用grep命令查看一下,如下:


FunMaker-5:~ root# grep -r initWithAccountID:defaults:service: /System/Library/
Binary file /System/Library/Caches/com.apple.dyld/dyld_shared_cache_armv7s matches
grep: /System/Library/Caches/com.apple.dyld/enable-dylibs-to-override-cache: No such file or directory
grep: /System/Library/Frameworks/CoreGraphics.framework/Resources/libCGCorePDF.dylib: No such file or directory
grep: /System/Library/Frameworks/CoreGraphics.framework/Resources/libCMSBuiltin.dylib: No such file or directory
grep: /System/Library/Frameworks/CoreGraphics.framework/Resources/libCMaps.dylib: No such file or directory
grep: /System/Library/Frameworks/System.framework/System: No such file or directory

从运行结果得知,要查找的函数在dyld_shared_cache_armv7s中出现了。再次对decache过的dyld_shared_cache_armv7s使用grep命令,如下:


snakeninnysiMac:~ snakeninny$ grep -r initWithAccountID:defaults:service: /Users/snakeninny/Code/iOSSystemBinaries/8.1_iPhone5
Binary file /Users/snakeninny/Code/iOSSystemBinaries/8.1_iPhone5/dyld_shared_cache_armv7s matches
grep: /Users/snakeninny/Code/iOSSystemBinaries/8.1_iPhone5/System/Library/Caches/com.apple.xpc/sdk.dylib: Too many levels of symbolic links
grep: /Users/snakeninny/Code/iOSSystemBinaries/8.1_iPhone5/System/Library/Frameworks/OpenGLES.framework/libLLVMContainer.dylib: Too many levels of symbolic links
Binary file /Users/snakeninny/Code/iOSSystemBinaries/8.1_iPhone5/System/Library/PrivateFrameworks/IMDaemonCore.framework/IMDaemonCore matches

可以看到,[IMDAccount initWithAccountID:defaults:service:]出现在了IMDaemonCore中,可以就从它着手开始分析。

5.2.3 定位目标函数

在找到含有目标功能的二进制文件之后,可以通过class-dump导出头文件,在里面寻找自己感兴趣的函数。具体做法比较简单,可分为以下两种。

1.OSX自带的搜索功能

不得不承认,OSX自带的搜索功能在笔者用过的操作系统中是最强大的,强大到既能搜索文件名,又能搜索文件内容,而且不论是搜目录还是搜全盘,速度都非常快。利用这一便利工具,可以在大量文件中快速定位目标文件,例如,对iPhone自带的距离感应器(Proximity Sensor)很感兴趣,想看看相关的函数可能会提供哪些功能,可以在Finder中打开保存所有class-dump头文件的文件夹,然后在右上角的搜索栏中输入proximity(大小写不敏感),如图5-12所示。

默认情况下Finder会把本机所有内容中含有proximity关键词的文本文件罗列出来,如图5-13所示。

也可以缩小搜索范围,选择在当前目录下递归搜索文件名。剩下的工作就是找出你感兴趣的文件,然后开始分析喽!

图5-12 在搜索栏输入关键词

图5-13 搜索结果

2.grep命令

是的,你没看错,强大的grep再一次出现了。既然grep能搜索出二进制文件里的字符串,对付文本文件就更不在话下了。对于刚才的例子,使用grep来试试,如下:


snakeninnysiMac:~ snakeninny$ grep -r -i proximity /Users/snakeninny/Code/iOSPrivateHeaders/8.1 
/Users/snakeninny/Code/iOSPrivateHeaders/8.1/Frameworks/CoreLocation/CDStructures.h:            char proximityUUID[512];
/Users/snakeninny/Code/iOSPrivateHeaders/8.1/Frameworks/CoreLocation/CLBeacon.h:    NSUUID *_proximityUUID;
……
/Users/snakeninny/Code/iOSPrivateHeaders/8.1/SpringBoard/SpringBoard.h:- (_Bool)proximityEventsEnabled;
/Users/snakeninny/Code/iOSPrivateHeaders/8.1/SpringBoard/SpringBoard.h:- (void)_proximityChanged:(id)arg1;

虽然grep显示出的结果大而全,但看起来有些乱。推荐使用Finder的搜索功能,毕竟在便捷程度相差无几的情况下,图形界面比命令行界面用起来更方便。

5.2.4 测试函数功能

在逆向工程中,我们感兴趣的绝大多数函数都是私有的,没有文档可供参考,如果运气足够好,谷歌可能会帮上你的忙,但也可能说明你想做的东西别人已经做过了;如果搜索不到,那么恭喜,你可能发现了一块新大陆,但是,函数的用法和功能需要你自己测试。

Objective-C函数的功能测试相对于C/C++函数来说要简单得多,有CydiaSubstrate和Cycript两种方法可供选择。

1.CydiaSubstrate

在测试函数功能时,主要利用CydiaSubstrate来钩住(hook)住一个函数,从而判断这个函数的调用时机。假设怀疑SBScreenShotter.h中的saveScreenshot:在截屏时得到了调用,就可以撰写以下代码来验证:


%hook SBScreenShotter
- (void)saveScreenshot:(BOOL)screenshot
{ 
        %orig; 
        NSLog(@"iOSRE: saveScreenshot: is called");
} 
%end

将filter设置成“com.apple.springboard”,并用Theos制作成deb,安装在iOS中,然后注销(respring)一次。如果感觉有些生疏,不要着急,这是正常的,不求快,但求稳。等锁屏界面完全出现后,同时按下home键和lock键截屏,然后ssh到iOS查看syslog,如下:


FunMaker-5:~ root# grep iOSRE: /var/log/syslog
Nov 24 16:22:06 FunMaker-5 SpringBoard[2765]: iOSRE: saveScreenshot: is called

可以看到,syslog中出现了我们的自定义信息,说明在截屏时,saveScreenshot:得到了调用。此时,你一定会跟笔者一样好奇:这个函数名的含义太明显了,调用这个函数,是不是真就能实现截屏的功能呢?

在iOS的世界中,好奇不会害死猫,就怕你失去好奇心。要满足好奇心,就用Cycript!

2.Cycript

在知道Cycript之前,笔者测试函数功能的工具是Theos。比如,针对上面的例子,笔者会编写下面这样一个tweak。


%hook SpringBoard 
- (void)_menuButtonDown:(id)down
{ 
    %orig;
    SBScreenShotter *shotter = [%c(SBScreenShotter) sharedInstance];
    [shotter saveScreenshot:YES]; // 这里参数传YES是我猜的,等会我们试验一下传NO是什么效果
} 
%end

在tweak生效后,按下home键,就会调用saveScreenshot:函数,然后观察屏幕是不是白光一闪,相册里是不是多了一张截屏。再进入Cydia把tweak删掉,把home键的单纯还给它……

其实如果没有对比,这种方法看起来还算简单,但是当笔者用Cycript达到了相同目的时,才后知后觉地发现,以前浪费了多少“井猜”的“绳命”!

Cycript的用法前面已经介绍过了,因为SBScreenShotter是SpringBoard里的类,所以这里将Cycript注入SpringBoard进程,然后直接调用待测试函数观察实际效果即可,整个编译过程对我们是透明的,测试后无须任何清理工作,简直会让人忍不住哼唱:“测一个简单函数,让我的心情快乐,逆向就像一条河,难免会碰到波折。”

ssh到iOS后输入如下命令:


FunMaker-5:~ root# cycript -p SpringBoard
cy# [[SBScreenShotter sharedInstance] saveScreenshot:YES]

你的屏幕是不是也白光一闪,“咔嚓”一声,截屏一张,与同时按下2个键截屏的效果如出一辙?好了,现在可以确认这个函数能完成截屏操作了。为了进一步满足好奇心和求知欲,在Cycript提示符下按“↑”键,重复上一次输入的命令,然后把“YES”改成“NO”,看看是什么效果。下一节将会继续说明。

5.2.5 解析函数参数

在上面的例子中,函数的参数明确,函数名的含义明显,但我们还是拿不准在调用时到底是传YES还是NO,只能靠猜。浏览通过class-dump导出的头文件时,会发现绝大多数函数的参数类型是id,也就是Objective-C里的泛型,它们是在运行时动态决定的,猜都没法猜。我们从感兴趣的功能开始,一路分析到了对应的函数,只差一步就能闯过第一关了,难道要就此放弃?“不要放弃!”CydiaSubstrate和Theos异口同声地说。

还记得我们是怎样判断函数调用时机的吧?既然能打印一个自定义字符串,就完全能打印出函数参数的信息——description函数能够把对象的内容表示成一个NSString,object_getClassName函数能够把对象的类名表示成一个char*,两者可分别用%@和%s打印出来,这就为解析参数提供了足够参考。对于刚才完成的截屏操作,saveScreenshot:的参数是YES还是NO,唯一的区别好像在于屏幕是否闪现白光。依据这个线索,很快就能定位到可疑的SBScreenFlash类,其中有一个有意思的函数flashColor:withCompletion:——是否闪光可以选择,难道闪光的颜色也可以改变?而且,参数类型似乎就是UIColor吧?编写下面的代码,来满足一下自己的好奇心。


%hook SBScreenFlash 
- (void)flashColor:(id)arg1 withCompletion:(id)arg2
{ 
        %orig; 
        NSLog(@"iOSRE: flashColor: %s, %@", object_getClassName(arg1), arg1); // [arg1 description] 可以直接写成arg1
} 
%end

作为练习,请读者把上面的代码变成一个可用的tweak。

安装完成后,注销(respring)一次,截个屏,再通过ssh命令连接到iOS上看看syslog,你所看到的内容应该如下所示:


FunMaker-5:~ root# grep iOSRE: /var/log/syslog
Nov 24 16:40:33 FunMaker-5 SpringBoard[2926]: iOSRE: flashColor: UICached DeviceWhiteColor, UIDeviceWhiteColorSpace 1 1

可以看出,color是一个UICachedDeviceWhiteColor类型的对象,它的description是“UIDevice WhiteColorSpace 11”。根据命名规则,UICachedDeviceWhiteColor是UIKit中的一个类,但在文档中搜索不到这个类,因此可以断定它是个私有类。在class-dump出的UIKit头文件中找到UICachedDeviceWhiteColor.h,打开看看,如下:


 @interface UICachedDeviceWhiteColor : UIDeviceWhiteColor
{
}
- (void)_forceDealloc;
- (void)dealloc;
- (id)copy;
- (id)copyWithZone:(struct _NSZone *)arg1;
- (id)autorelease;
- (BOOL)retainWeakReference;
- (BOOL)allowsWeakReference;
- (unsigned int)retainCount;
- (id)retain;
- (oneway void)release;
@end

它继承自UIDeviceWhiteColor,于是继续找到UIDeviceWhiteColor.h,如下:


 @interface UIDeviceWhiteColor : UIColor
{
    float whiteComponent;
    float alphaComponent;
    struct CGColor *cachedColor;
    long cachedColorOnceToken;
}
- (BOOL)getHue:(float *)arg1 saturation:(float *)arg2 brightness:(float *)arg3 alpha:(float *)arg4;
- (BOOL)getRed:(float *)arg1 green:(float *)arg2 blue:(float *)arg3 alpha:(float *)arg4;
- (BOOL)getWhite:(float *)arg1 alpha:(float *)arg2;
- (float)alphaComponent;
- (struct CGColor *)CGColor;
- (unsigned int)hash;
- (BOOL)isEqual:(id)arg1;
- (id)description;
- (id)colorSpaceName;
- (void)setStroke;
- (void)setFill;
- (void)set;
- (id)colorWithAlphaComponent:(float)arg1;
- (struct CGColor *)_createCGColorWithAlpha:(float)arg1;
- (id)copyWithZone:(struct _NSZone *)arg1;
- (void)dealloc;
- (id)initWithCGColor:(struct CGColor *)arg1;
- (id)initWithWhite:(float)arg1 alpha:(float)arg2;
@end

UIDeviceWhiteColor继承自UIColor,因为UIColor是一个公开类,所以对参数类型的解析到这个程度就可以了。对其他id类型参数的解析均可重复上述思路。

知道了一个函数的调用效果,解析了它的参数,它的使用文档就可以由我们自行撰写了,建议大家对自己分析的函数作简单记录,这样在下次使用时就能迅速回想起它的用法。

接下来要用Cycript来测试这个函数,看看传进去一个[UIColor magentaColor]是什么效果,如下:


FunMaker-5:~ root# cycript -p SpringBoard
cy# [[SBScreenFlash mainScreenFlasher] flashColor:[UIColor magentaColor] withCompletion:nil]

一抹紫红色的光在屏幕上散开,比白色的闪光有个性多了。检查相册,并没有看到新截屏,因此自然地猜测,这个函数仅仅负责截屏时的闪光功能,而不进行实际截屏操作——一个新的tweak灵感就此产生:我们可以钩住(hook)这个flashColor:with Completion:函数,把自定义的颜色作为参数传递给它,从而使截屏闪光变得丰富多彩起来。这个tweak作为练习,请读者独立完成。

以上的套路是笔者5年多以来的总结,因为iOS逆向工程没有任何官方资料可供参考,笔者个人经验难免有失偏颇,不可能面面俱到,所以,http://bbs.iosre.com 的大门向任何讨论开放,欢迎提问!

5.2.6 class-dump的局限性

分析通过class-dump导出的头文件,我们找到了感兴趣的东西,并在5.2.4节的Cycript试验中看到了对SBScreenShotter类中saveScreenshot:函数传YES和NO两种参数时函数的不同执行效果。

在5.2.5节里,解析了SBScreenFlash类的flashColor:withCompletion:函数参数。从flashColor:withCompletion:的效果来看,我们猜测它应该发生在saveScreenshot:的内部,而如果仅根据class-dump的头文件,结合CydiaSubstrate,最多也只能判断出saveScreenshot:和flashColor:withCompletion:的先后调用顺序,至于两者的实现细节和调用关系则不得而知。

完成了一个tweak,应该小小庆祝一下。从灵感,到文件,到函数,最后到成型的tweak,所有Objective-C级别的逆向工程都遵循这个套路,只是实现细节不同而已。即使完全不懂越狱开发,相信你也能掌握这个套路,它一点都不难。难度低,门槛就低,竞争就多,压力就大,当你掌握了Objective-C级别的逆向工程思路,想要进阶更高的级别,就会发现class-dump不够用了。

在完成一个小tweak之后,我们还应清楚地意识到,与这个tweak相关的很多知识点还没有弄清楚,而通过class-dump得到的信息并不足以支撑我们弄清这些未知的东西,就好像我们身处逆向工程这片茂密的原始森林中,class-dump提供了可以落脚的小屋,但要走出这片森林,还需要一张地图和一个指南针——它们就是IDA和LLDB。这两款工具就像两座挡在我们面前的大山,绝大多数逆向工程初学者都没能成功翻越它们,爬到半山腰就打道回府了,而翻越大山的人们顺利跨过逆向工程的门槛,欣赏到了别样的风景。梦想还是要有的,万一实现了呢?我们鼓起勇气,试试看能不能征服它们。