使用Hopper探索iOS错误

你好!我叫Alexander Nikishin,我在Badoo开发iOS应用程序。在本文中,我将讨论我们如何调查UIKit中的错误,而Apple六个月都不想修复该错误。



这一切都始于2019年8月的iOS 13的第一个beta版本。然后我们首先遇到了一个问题。在Badoo和Bumble应用程序中,我们一直在不断改进界面,例如,我们尝试尽可能地优化乏味且不受人欢迎的注册过程。键盘上方的系统预测工具提示是减少输入数据时用户单击次数的好方法。但是,在新版本的iOS中,我们惊讶地发现输入电话号码的提示已消失。



GM版本问世时,我们意识到问题并未得到解决。在我们看来,如此明显的错误根本无法通过Apple可能在其军械库中进行过的回归测试来遗漏,因此我们开始等待第一个大更新包中的更正。其他开发人员对此表示希望。但是,随着版本13.1的发布,一切都没有改变,我们别无选择,只能打开雷达,这是我们在10月初所做的。时间过去了,iOS 13.2和13.3出现了,但该错误仍然没有得到纠正。

2月,我们的注册团队有一些空闲时间来处理积压工作,我决定对这个问题进行更深入的调查。

在开始挖掘之前,您必须弄清楚该怎么做。由于这些提示继续在某些类型的键盘上起作用,因此第一个想法是研究不同版本的iOS中其视图的层次结构。


iOS 12 vs iOS 13

可以立即清楚地知道,iOS 13中的Apple重构了键盘实现,并在单独的控制器(UIPredictionViewController)中突出显示了提示。显然,模块化和分解的趋势也已达到(顺便一句,Artyom Loenko最近谈到了我们在其应用方面的经验)。因此,最有可能是功能的退化。搜索范围开始缩小。

我认为大多数iOS开发人员都知道在公共领域很容易找到私有系统类的接口:只需在搜索引擎中输入一个查询即可。在研究类接口 UIPredictionViewController时,它抓住了其中一种方法:



似乎有一个线索,使用好的老工具欺骗实现功能(Swizzling)很容易检查。我们将使用它,以便“怀疑”的函数始终返回真实值:

+(void)swizzleIsVisibleForInputDelegate {
    SEL targetSelector = sel_getUid("isVisibleForInputDelegate:inputViews:");
    Class targetClass = NSClassFromString(@”UIPredictionViewController”);
    if (targetClass == nil) {
        return;
    }
    if (![targetClass instancesRespondToSelector:targetSelector]) {
        return;
    }

    Method method = class_getInstanceMethod(targetClass, targetSelector);
    if (method == NULL) {
        return;
    }

    IMP originalImplementation = method_getImplementation(method);
    IMP newImp = imp_implementationWithBlock(^BOOL(id me, id delegate, id views) {
        //   ,        . 
        BOOL result = ((bool (*)(id,SEL,id,id))originalImplementation)(me, targetSelector, delegate, views);
        if ([delegate isKindOfClass:[UITextField class]] && [delegate keyboardType] == UIKeyboardTypePhonePad) {
            return YES;
        }
        return result;
    });

    method_setImplementation(method, newImp);
}

重新启动测试项目,我发现电话提示返回到iOS 13,并且在“正常模式”下工作。这可能会结束调查,甚至可能非常小心地在发行程序集中使用此危险且禁止使用的Apple准则解决方案,并具有为某些用户远程启用/禁用的功能(对于远程功能管理,您可以查看我的同事Katerina Trofimenko 的报告记录)。尽管如此,我从未停止怀疑为什么在使用电话的键盘类型时此函数返回false。

要说实话,我们需要函数的源代码。显然,Apple不会左右分配iOS组件代码,因此无法对其进行Google搜索。只剩下一种方法-逆向工程来反编译二进制代码。早些时候,我曾多次听说过Hopper之类的产品,并阅读了几篇有关其用途的文章,以便更深入地研究系统库,但我从来没有亲自使用过。立刻让我感到惊喜的是:事实证明,为了玩转和学习工具,甚至没有必要购买完整版。该演示版本包括无休止的30分钟工作会议,无法保存索引并更改二进制文件,这意味着它是进行实验的绝佳平台。

全部来自同一发布的私有接口,您可以了解UIPredictionViewControllerUIKitCore框架的组成部分。剩下的就是找到它并将其上传到Hopper。二进制框架文件位于Xcode的深度中。例如,这里是我们需要的UIKitCore的完整路径:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore

我们在Hopper中创建一个拖放文件,在系统对话框中确认操作,然后等待索引完成(对于UIKit这样的大型框架,这可能需要四到六分钟的时间)。

该程序的界面非常简单,我只会注意到我的研究所需的关键要素。在界面的左侧面板中,您可以找到用于浏览代码的搜索行。通过搜索我们感兴趣的类的名称,我们可以快速获得正在研究的函数,其汇编代码在主窗口中打开。



在上方的任务栏中,有用于切换代码显示模式的按钮。从左到右:



  • ASM模式-汇编代码;
  • CFG模式-框图(树)形式的汇编代码,其中的代码段被组合成块,并且转换显示为分支;
  • 伪代码模式-生成的伪代码(我们将在下面详细讨论);
  • 十六进制模式是二进制文件或abracadabra的十六进制表示形式,对我们的研究没有太大帮助。

现在,您需要找出函数内部会发生什么。她的身体很长,因此只有ASM专家(我不能称呼自己)可以通过查看汇编代码来理解逻辑。为此,伪代码模式会有所帮助,在这种模式下,Hopper尽可能地简化了汇编操作,在可能的情况下替换实函数名称,并使用变量之类的寄存器名称。看起来像这样:



仅需遍历函数的逻辑并了解我们属于哪个分支。符号断点帮助了我。,可以将其安装在系统调用中,同时将所有必需的变量和函数调用的结果打印到Xcode控制台。使用这种简单的方法,我发现由于条件之一在示例代码块中不起作用,该功能的执行被早期退出中断了。让我们弄清楚这里发生了什么。

一点上下文:在rbx寄存器中,代码中的代码高一点(为简单起见,我省略了)是指向实现协议的对象的链接UITextInputTraits_Private(这是公共协议的扩展版本UITextInputTraits)。

因此,第一个条件是验证提示是否未被输入字段的配置隐藏。在调试中,您可以看到它正在运行:propertyhidePrediction返回false。第二个条件是验证键盘不是处于“拆分”模式(在iPad上,将右下方的按钮向上拉,只有2-3%的用户知道此事)。同样,一切都井井有条。

继续。在下一阶段,使用begin进行操纵keyboardType,这向我们暗示真相就在附近。首先检查当前值是否keyboardType小于或等于0xb(或十进制格式为11)。如果在Xcode中打开声明UIKeyboardType,我们将看到有13种类型的键盘,其中一种(UIKeyboardTypeAlphabet)已弃用,并声明为对另一种类型的引用。也就是说,枚举中有12种类型:如果从0开始,则后者将具有等于11的值。换句话说,代码以溢出检查的形式验证该值,并再次成功通过。

此外,我们看到了一个非常奇怪的情况if (!COND),并且很长一段时间我都不明白它在检查什么,因为COND变量没有在代码中的其他任何地方声明。此外,我的符号断点表明,正是由于未能满足此条件,才导致该功能提前退出。在这里,我们别无选择,只能返回ASM模式,以汇编器形式研究这部分代码。

要在ASM列表中找到此条件,可以使用“无代码重复”选项,该选项将不以带有if-else条件的源代码形式显示伪代码,而是以唯一的代码块形式以及以goto形式进行转换的形式显示伪代码。在这种情况下,我们将在二进制文件中看到这些块开头的位置,并使用此指针在ASM模式下进行搜索。因此,我发现我们感兴趣的代码块位于fe79f4:



回到ASM模式,我们可以轻松找到此代码块:



我们进入最困难的部分,在这里我们将分析三行汇编代码。

我从记忆中认出了第一行(多亏了我的母语MIET中的汇编程序课程,终于有了方便的高等教育进入了我的生活!)。一切都非常简单:常数0x930(二进制格式100100110000)放在ecx寄存器中。

在第二行中,我们看到在exc寄存器上执行了bt指令eax。我们已经知道一个的值,第二个的值可以在前面的屏幕截图中看到:rax = [rbx keyboardType]-它包含当前的键盘类型。rax-这是整个64位寄存器,eax-这是其32位部分。

在确定数据之后,仍然需要了解团队的逻辑。 Google将我们引至以下描述
Selects the bit in a bit string (specified with the first operand, called the bit base) at the bit-position designated by the bit offset operand (second operand) and stores the value of the bit in the CF flag.

该指令从第二个操作数(键盘类型)指定的位置的第一个操作数(常数0x930)中提取一位,并将其放入CF(进位标志)中。也就是说,根据键盘的类型,CF将包含0或1。

我们继续进行最后的操作jb,它具有以下描述

Jump short if below (CF=1)

如果CF为1,则很容易猜到该函数有一个过渡(提前退出)。

这时,难题开始加总。我们有一个位掩码100100110000(根据可用的键盘类型的数目包含12位),它确定了提前退出的条件。现在,如果我们按升序检查所有类型键盘的提示是否可用rawValue,一切都将就绪。



在iOS 12中,我们找不到这种逻辑-那里的提示适用于任何类型的键盘。我怀疑在iOS 13中,Apple决定禁用数字键盘提示,这在原则上是可以理解的:我无法提出系统需要输入数字的情况。显然,错误地也击中了“热手” UIKeyboardTypePhonePad,这与常规数字键盘非常相似。同时UIKeyboardTypeNamePhonePad,结合使用QWERTY键盘搜索电话联系人和完全相同的禁用数字小键盘,他继续显示提示。

令我惊讶的是,与Hopper的合作非常愉快且有趣,很长时间以来,我并没有得到那么多粉丝。我在我的错误报告中与Apple工程师共享了这些发现,并且随着时间的推移,其状态更改为“已确定潜在的修补程序-用于将来的操作系统更新”。我希望此修复程序可以在将来的更新中提供给用户。同时,Hopper不仅可以用来查找导致您或Apple错误的原因,还可以用于检测Apple对第三方程序的修复

All Articles