iOS 逆向工程初探

应用场景

在了解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内存信息的工具。

一、解密,又称砸壳

砸壳的工具网络上现在主要有两种Clutchdumpdecrypted

1. Clutch

Clutch是一个开源的工具,它会生成一个新的进程,然后暂停进程并dump内存。
使用上很简单,只需要从Git仓库中拉取项目并编译,在Build/Clutch文件目录下会生成一个叫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包
完整的Clutch解密过程

接下来同样用scp命令把对应的破解的ipa包拷到手机上,为下一步做准备。

2.dumpdecrypted

dumpdecrypted也是一个开源的工具,与Clutch不同的是它会注入可执行文件,动态地从内存中dump出解密后的内容(一个完整的ipa包),相比Clutch它的操作略显繁琐,但成功率很高,Clutch在破解体积较大的App时往往会使手机卡死直至重启,对不完美越狱来说又得重新越狱一次。
首先也要从Git仓库拉取项目并编译,编译完会生成一个dumpdecrypted.dylib文件。
接下来需要定位要解密的可执行文件,把dumpdecrypted.dylib放到App的沙盒目录Documents下,然后通过DYLD_INSERT_LIBRARIES系统的环境变量把其注入到应用中并开始解密,最后解密的包会生成在App的应用目录下。

获取App应用目录和沙盒目录

3.class-dump

class-dump也是个开源的工具,可以从可执行文件中获取类、方法和属性信息到工具,通过头文件可以快速寻找到想要的方法和属性。
使用上很简单,class-dump -H targetApp -o ./Header就可以,-o后跟的是放头文件的路径

class-dump分析出的头文件

其中一个头文件

上面的头文件可以清楚的看到所有的属性方法,对分析App很有帮助。
4.IDA与Hopper
IDA与Hopper能从解密的应用中分析出实现文件(.m文件)进行反编译,两者的功能类似,但是IDA更强大,反编译后的代码比Hopper容易理解的多,使用上只需要把解密后的文件拖入两个软件之中就会自动分析。


IDA的伪代码
Hop的伪代码

两者相比之下,明显IDA的更容易读懂。

二、从应用界面寻找突入点

这里得提到另一个可以极大提高开发效率的神器--Reveal,Xcode从7.0开始已经集成了Reveal的一部分功能,但是只能再开发自己的应用时使用。
想要使用Reveal查看手机上的任意应用,首先要在Cydia中安装Reveal2Loader插件,接着把Reveal程序用自带的RevealServer.framework导入到越狱设备的/Library/Framework中,并在设置中开启想要查看的应用即可。如下图:

Translate Me的应用界面

下面我们就Translate Me是如何实现自动识别多语言语音的功能展开接下来的探索。

三、找到关键函数

在上图我们可以看到语音识别按钮Memory Address,通过此地址我们能通过Cycript进一步定位到按钮的方法,上文已经说过Cycript可以通过OC语法查看App运行时信息,就像下面我们干的一样。

通过Cycript获取的按钮方法

定位到按钮调用的方法为onMicButton:

四、分析函数的实现逻辑

通过查看IDA的onMicButton:的伪函数,可以看到另一个至关重要的类SDCSpeechViewControllerviewModel,打开头文件发现其属于另一个类,顺藤摸瓜的一路找到了核心类SDCRecognizer!!

SDCRecognizer

当看到SFSpeechRecognitionTaskSFSpeechAudioBufferRecognitionRequestSFSpeechRecognizer这几个类的时候,我就知道自动识别多语言语音的秘密就在此处,因为这几个类都是属于iOS SpeechKit中的核心类,负责语音识别的功能。
这个类肯定通过两个SFSpeechAudioBufferRecognitionRequest实例保存了不同语言的语音的缓存,这是我以前实验过却没有成功的地方,然后通过SFSpeechRecognizerSFSpeechRecognitionTask转换语音对应两种不同语言的文本,根据processPrimaryResult:startBackingRecognition这两个方法名,可以猜测前者识别主语言的语音,或者识别第二种语言的语音,并且两者都调用了averageConfidence方法,通过averageConfidence这个方法名我有理由去相信其获取的是语音对应不同语言的置信度
IDA中processPrimaryResult的伪函数

接下来只要印证它就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调试

lldb需要获取App的方法在内存中的地址才能够打断点,在lldb中可以通过image list -o -f "processName"来获得App运行的基地址,加上IDA中反编译的内存偏移量,就能得到App运行时方法的真正地址。
加了断点的方法

通过lldb打断点

接着在lldb输入c并enter继续运行程序,点击App的语音识别按钮
翻译App
断点执行顺序1
断点执行顺序2

断点执行顺序3

可以明显看到执行顺序为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逆向工程相关的书籍,本文章可以说是此书前几章的概括

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 1. 为什么要逆向工程 iOS 逆向工程主要有两个作用:1,分析目标程序,拿到关键信息,可以归类于安全相关的逆向...
    弦暮阅读 1,227评论 2 4
  • ios 逆向工程 刚进一个新公司,公司的大牛直接分给一个课题研究任务,直接懵逼了,/(ㄒoㄒ)/~~ 想哭,世界就...
    天下林子阅读 29,591评论 39 120
  • 逆向工程的目的 1)分析竞品的最新研究或者产品原型(包括所用的技术,所使用的框架) 2)学术/学习目的。 3)破解...
    零度_不结冰阅读 683评论 0 2
  • 即使花完一个珍贵的半天假,用于在家煲卷福剧,我还是忍不住脑中爆炸的“bored!bored!” 连我自己都不能看到...
    土羊斋阅读 498评论 0 0
  • 【“工匠汾酒”系列之十七·勾调篇】 勾调,是个高端技术活 2018年元旦刚过,三晋大地上的杏花村,被一股寒流袭击,...
    沧桑正道阅读 89,571评论 0 0