应用场景
在了解iOS逆向工程之前,我们有必要了解它究竟能做什么,在开发中能够获得哪些帮助?个人觉得有以下四点
- 促进正向开发,深入理解系统原理
- 借鉴别人的设计和实现,实现自己的功能
- 从逆向的角度实现安全保护
- 篡改他人的App(不推荐)
以上四点有自己正在学习的东西,也有已经有了一些了解的东西,当我们知道iOS逆向工程究竟能做什么的时候,下一步就到了应该How do的阶段,我把How do it?根据不同的目地列出以下几个步骤。
- 解密、导出应用程序、导出头文件,为后续工作做准备
- 从应用界面表现入手,获取当前界面布局及控制器
- hook发现的一些相关类,记录输出调用顺序及参数
- 找到关键函数,查看调用堆栈,hook测试效果
- 静态分析加动态调试分析关键函数的实现逻辑
- 模拟或篡改函数调用逻辑
- 制作插件
并移植到非越狱机器(正在研究)
零、准备工作
在砸壳之前,需要做很多准备工作,首先要准备一台越狱的设备,在越狱过程中,会安装一个叫作Cydia的软件,相当于越狱设备的AppStore,可以在上面查找安装各类软件包。
有了越狱设备之后,接下来就要安装各种有助于逆向的插件。
1.SSH:用于远程登录越狱设备、执行命令,iOS10以前需要在Cydia上安装openSSH,iOS10之后需要安装dropbear.
2.Filza:方便的查看iOS的文件目录。
3.Cydia Substrate:一个允许第三方开发者在越狱系统方法里的加一些运行时补丁和扩展方法,是越狱开发的基石。
4.adv-cmds:一个命令行集合工具,提供了很多方便的命令。比如后面会用到的ps -e命令。
5.appsync:直接修改应用文件或结构会破坏应用的签名信息,导致修改后的应用无法安装,所以需要appsync补丁让系统不在验证应用的签名。
6.Cycript:使一个允许开发者使用OC和JS组合语法查看及修改运行时App内存信息的工具。
一、解密,又称砸壳
砸壳的工具网络上现在主要有两种Clutch、dumpdecrypted
1. Clutch
Clutch是一个开源的工具,它会生成一个新的进程,然后暂停进程并dump内存。
使用上很简单,只需要从Git仓库中拉取项目并编译,在Build/Clutch文件目录下会生成一个叫Clutch的命令行工具。
接下来用scp命里通过ssh远程登录拷贝Clutch到设备的/usr/bin目录下,然后通过chmod命令给它附加可执行权限。
scp ./Build/Clutch root@10.20.10.197:/usr/bin
//链接设备
ssh root@10.20.10.197
chmod +x /usr/bing/Clutch
这个时候就能通过Clutch破解手机安装的应用了
Clutch -i//显示手机安装的应用
Clutch -d 应用id//通过-d命令对指定应用执行dump操作,命令执行完会沙盒目录下出现ipa包
接下来同样用scp命令把对应的破解的ipa包拷到手机上,为下一步做准备。
2.dumpdecrypted
dumpdecrypted也是一个开源的工具,与Clutch不同的是它会注入可执行文件,动态地从内存中dump出解密后的内容(一个完整的ipa包),相比Clutch它的操作略显繁琐,但成功率很高,Clutch在破解体积较大的App时往往会使手机卡死直至重启,对不完美越狱来说又得重新越狱一次。
首先也要从Git仓库拉取项目并编译,编译完会生成一个dumpdecrypted.dylib文件。
接下来需要定位要解密的可执行文件,把dumpdecrypted.dylib放到App的沙盒目录Documents下,然后通过DYLD_INSERT_LIBRARIES系统的环境变量把其注入到应用中并开始解密,最后解密的包会生成在App的应用目录下。
3.class-dump
class-dump也是个开源的工具,可以从可执行文件中获取类、方法和属性信息到工具,通过头文件可以快速寻找到想要的方法和属性。
使用上很简单,class-dump -H targetApp -o ./Header就可以,-o后跟的是放头文件的路径
上面的头文件可以清楚的看到所有的属性方法,对分析App很有帮助。
4.IDA与Hopper
IDA与Hopper能从解密的应用中分析出实现文件(.m文件)进行反编译,两者的功能类似,但是IDA更强大,反编译后的代码比Hopper容易理解的多,使用上只需要把解密后的文件拖入两个软件之中就会自动分析。
两者相比之下,明显IDA的更容易读懂。
二、从应用界面寻找突入点
这里得提到另一个可以极大提高开发效率的神器--Reveal,Xcode从7.0开始已经集成了Reveal的一部分功能,但是只能再开发自己的应用时使用。
想要使用Reveal查看手机上的任意应用,首先要在Cydia中安装Reveal2Loader插件,接着把Reveal程序用自带的RevealServer.framework导入到越狱设备的/Library/Framework中,并在设置中开启想要查看的应用即可。如下图:
下面我们就Translate Me是如何实现自动识别多语言语音的功能展开接下来的探索。
三、找到关键函数
在上图我们可以看到语音识别按钮的Memory Address,通过此地址我们能通过Cycript进一步定位到按钮的方法,上文已经说过Cycript可以通过OC语法查看App运行时信息,就像下面我们干的一样。
定位到按钮调用的方法为onMicButton:。
四、分析函数的实现逻辑
通过查看IDA的onMicButton:的伪函数,可以看到另一个至关重要的类SDCSpeechViewController的viewModel,打开头文件发现其属于另一个类,顺藤摸瓜的一路找到了核心类SDCRecognizer!!
当看到SFSpeechRecognitionTask、SFSpeechAudioBufferRecognitionRequest、SFSpeechRecognizer这几个类的时候,我就知道自动识别多语言语音的秘密就在此处,因为这几个类都是属于iOS SpeechKit中的核心类,负责语音识别的功能。
这个类肯定通过两个SFSpeechAudioBufferRecognitionRequest实例保存了不同语言的语音的缓存,这是我以前实验过却没有成功的地方,然后通过SFSpeechRecognizer和SFSpeechRecognitionTask转换语音对应两种不同语言的文本,根据processPrimaryResult:和startBackingRecognition这两个方法名,可以猜测前者识别主语言的语音,或者识别第二种语言的语音,并且两者都调用了averageConfidence方法,通过averageConfidence这个方法名我有理由去相信其获取的是语音对应不同语言的置信度。
接下来只要印证它就OK了。
动态调试-准备工作
这里我们可以通过打断点的方式论证我们的猜想,理清SFSpeechRecognizer的主要函数的调用顺序,在iOS开发中Xcode的断点调试是一大利器--LLDB,当Xcode调试手机App时,Xcode会将debugserver文件复制到手机中,以便在手机上启动一个服务,等待Xcode进行远程调试,这里通过修改debugserver文件达到调试别人App的目的。
1.用命令行或者iTools工具把Developer/usr/bin下的debugserver文件拷贝到电脑上。
2.用Xcode新建一个entitlements.plist文件,加入以下四个键值对
<dict>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>run-unsigned-code</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
</dict>
3.通过命令codesign -s - --eentitlements eentitlements.plist -f debugserver给** debugserver**重新签名
4.把文件拷贝回设备原目录并赋予可执行权限
开始调试
启动App,链接设备,通过以下命令即可对目标App进行调试
lldb需要获取App的方法在内存中的地址才能够打断点,在lldb中可以通过image list -o -f "processName"来获得App运行的基地址,加上IDA中反编译的内存偏移量,就能得到App运行时方法的真正地址。
接着在lldb输入c并enter继续运行程序,点击App的语音识别按钮
可以明显看到执行顺序为start->setup->processPrimaryResult->stopRecording-> processPrimaryResult->averageConfidence->startBackingRecognition-> averageConfidence,可以看出与我们的猜想基本一致,并且这个App是识别完一种语言之后在识别另一种语言,这也解释了为什么App使用时只有说完话才能识别哪种语言的现象,最后我依照SFSpeechRecognizer的逻辑写了自己的函数,如下:
- (void) configAudioEngine:(void (^)(SFSpeechRecognitionResult *, NSError *, BOOL))resultHandler language:(NSString *) languageCode {
//切换AVAudioSession
AVAudioSession * audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryRecord mode:AVAudioSessionModeDefault options:AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryOptionAllowBluetooth error:nil];
[audioSession setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
//设置语言
NSLocale * locale = [NSLocale localeWithLocaleIdentifier:languageCode];
self.speechRecognizer = [[SFSpeechRecognizer alloc] initWithLocale:locale];
self.speechRecognizer.defaultTaskHint = 1;
NSLocale * targetLocale = [NSLocale localeWithLocaleIdentifier:self.translatedLanguageCode];
self.targetSpeechRecognizer = [[SFSpeechRecognizer alloc] initWithLocale:targetLocale];
self.targetSpeechRecognizer.defaultTaskHint = 1;
if (self.recognitionRequest) {
[self.recognitionRequest endAudio];
self.recognitionRequest = nil;
}
if (self.targetRecognitionRequest) {
[self.targetRecognitionRequest endAudio];
self.targetRecognitionRequest = nil;
}
self.recognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];
self.recognitionRequest.shouldReportPartialResults = YES;
self.targetRecognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];
self.targetRecognitionRequest.shouldReportPartialResults = YES;
MJWeakSelf
self.recognitionTask = [self.speechRecognizer recognitionTaskWithRequest:self.recognitionRequest resultHandler:^(SFSpeechRecognitionResult * _Nullable result, NSError * _Nullable error) {
if (result.final) {
NSLog(@"source result === %@", result.bestTranscription.formattedString);
weakSelf.sourceSpeechResult = result;
weakSelf.sourceLanguageConfidence = [weakSelf averageConfidence:result];
[weakSelf recogniTargetLanguage:resultHandler];
}
}];
AVAudioFormat * recordingFormat = [[self.audioEngine inputNode] outputFormatForBus:0];
self.audioEngine = [[AVAudioEngine alloc] init];
[[self.audioEngine inputNode] installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
[self.recognitionRequest appendAudioPCMBuffer:buffer];
[self.targetRecognitionRequest appendAudioPCMBuffer:buffer];
}];
[self.audioEngine prepare];
[self.audioEngine startAndReturnError:nil];
}
- (void) closeAudioEngine {
if (self.audioEngine.isRunning) {
[[self.audioEngine inputNode] removeTapOnBus:0];
[self.audioEngine stop];
}
[self.recognitionRequest endAudio];
self.recognitionRequest = nil;
}
- (void) recogniTargetLanguage:(void (^)(SFSpeechRecognitionResult *, NSError *, BOOL source))resultHandler{
MJWeakSelf
self.targetRecognitionTask = [self.targetSpeechRecognizer recognitionTaskWithRequest:self.targetRecognitionRequest resultHandler:^(SFSpeechRecognitionResult * _Nullable result, NSError * _Nullable error) {
NSLog(@"target result === %@", result.bestTranscription.formattedString);
if (result.final) {
float targetConfidence = [weakSelf averageConfidence:result];
if (targetConfidence < weakSelf.sourceLanguageConfidence) {
resultHandler(weakSelf.sourceSpeechResult, nil, YES);
} else {
resultHandler(result, nil, NO);
}
}
if (error) {
NSLog(@"target Recogni error === %@", error);
}
}];
[self.targetRecognitionRequest endAudio];
self.targetRecognitionRequest = nil;
}
- (float) averageConfidence:(SFSpeechRecognitionResult *) result{
NSMutableArray * array = [NSMutableArray array];
NSArray * resultArray = result.bestTranscription.segments;
for (SFTranscriptionSegment * segment in resultArray) {
[array addObject:[NSNumber numberWithFloat:segment.confidence]];
}
float avgConfidence = [[array valueForKeyPath:@"@avg.self"] floatValue];
NSLog(@"result === %@\nConfidence === %f", result.bestTranscription.formattedString, avgConfidence);
return avgConfidence;
}
我把每一次识别的结果进行回调,当说完“今天天气不错”这句话时中文语言的置信度为0.924333,英文语言的置信度为0.743899,明显中文的置信度更高,至此已初步体会到逆向工程给予我们的帮助----借鉴别人的设计和实现,实现自己的功能,完结撒花~
当然逆向工程还有更多有用的东西我没有分享,比如模拟或篡改函数调用逻辑、制作插件等。
希望下次有机会能给大家在做更深一步的分享~
PS:极力推荐 刘培庆 所著《iOS应用逆向与安全》,是我找到市面上最新讲解iOS逆向工程相关的书籍,本文章可以说是此书前几章的概括