只有理解了tweak的工作方式,才能在编写tweak时清楚地知道自己想干什么、在干什么。一般来说,编写tweak会用到C、C++和Objective-C三种语言,有了一个灵感时,该如何自如地运用这三种语言把灵感变成一个好用的tweak呢?事实上,编写tweak的思路是有规律可循的,而且随着你对iOS的了解愈加深入,对编程语言的掌握愈加熟练,这种规律会变得越来越明显。下面将围绕一个简单的tweak例子,从iOS工程师使用最多的Objective-C语言开始分析,总结归纳Objective-C级别的逆向工程理论。
可能有部分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的解剖比别人更彻底!说起来很简单,做起来不简单。
知道自己想要实现什么功能后,就要开始寻找实现这个功能的二进制文件,方法一般有以下几种。
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中,可以就从它着手开始分析。
在找到含有目标功能的二进制文件之后,可以通过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的搜索功能,毕竟在便捷程度相差无几的情况下,图形界面比命令行界面用起来更方便。
在逆向工程中,我们感兴趣的绝大多数函数都是私有的,没有文档可供参考,如果运气足够好,谷歌可能会帮上你的忙,但也可能说明你想做的东西别人已经做过了;如果搜索不到,那么恭喜,你可能发现了一块新大陆,但是,函数的用法和功能需要你自己测试。
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”,看看是什么效果。下一节将会继续说明。
在上面的例子中,函数的参数明确,函数名的含义明显,但我们还是拿不准在调用时到底是传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 的大门向任何讨论开放,欢迎提问!
分析通过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。这两款工具就像两座挡在我们面前的大山,绝大多数逆向工程初学者都没能成功翻越它们,爬到半山腰就打道回府了,而翻越大山的人们顺利跨过逆向工程的门槛,欣赏到了别样的风景。梦想还是要有的,万一实现了呢?我们鼓起勇气,试试看能不能征服它们。