Entdecken Sie den iOS-Fehler mit Hopper

Hallo! Mein Name ist Alexander Nikishin, ich entwickle iOS-Anwendungen bei Badoo. In dem Artikel werde ich darüber sprechen, wie wir einen Fehler in UIKit untersucht haben, den Apple sechs Monate lang nicht beheben wollte.



Alles begann im August 2019 mit den ersten Beta-Versionen von iOS 13. Dann stießen wir zuerst auf ein Problem. In den Anwendungen Badoo und Bumble arbeiten wir ständig an der Verbesserung der Schnittstellen und versuchen beispielsweise, den langwierigen und ungeliebten Registrierungsprozess so weit wie möglich zu optimieren. Systematische prädiktive Tooltips über der Tastatur sind eine hervorragende Möglichkeit, die Anzahl der Benutzerklicks bei der Dateneingabe zu verringern. In der neuen Version von iOS waren wir jedoch überrascht, dass die Eingabeaufforderungen zur Eingabe der Telefonnummer weg waren.



Als die GM-Version herauskam, stellten wir fest, dass das Problem nicht behoben war. Es schien uns, dass solch ein offensichtlicher Fehler durch Regressionstests, die Apple wahrscheinlich in seinem Arsenal hatte, einfach nicht übersehen werden konnte, und wir begannen, auf eine Korrektur im ersten großen Update-Paket zu warten. Andere Entwickler hofften darauf . Mit der Veröffentlichung von Version 13.1 hat sich jedoch nichts geändert, und wir hatten keine andere Wahl, als das Radar zu öffnen, was wir Anfang Oktober getan haben. Die Zeit verging, iOS 13.2 und 13.3 kamen heraus und der Fehler blieb immer noch unkorrigiert.

Im Februar hatte unser Registrierungsteam etwas Zeit, um am Rückstand zu arbeiten, und ich beschloss, dieses Problem etwas genauer zu untersuchen.

Bevor Sie anfangen zu graben, mussten Sie herausfinden, wie es geht. Da die Hinweise auf einigen Tastaturtypen weiterhin funktionierten, bestand der erste Gedanke darin, die Hierarchie der Ansichten in verschiedenen Versionen von iOS zu untersuchen.


iOS 12 vs iOS 13

Es wurde sofort klar, dass Apple in iOS 13 die Tastaturimplementierung überarbeitet und die Eingabeaufforderungen in einem separaten Controller hervorgehoben hat ( UIPredictionViewController). Offensichtlich hat auch der Trend zur Modularisierung und Zerlegung ihn erreicht ( Artyom Loenko hat übrigens kürzlich über unsere Erfahrungen mit ihrer Anwendung gesprochen ). Aus diesem Grund gab es höchstwahrscheinlich eine Regression der Funktionalität. Der Suchkreis wurde enger.

Ich denke, die meisten iOS-Entwickler wissen, dass es einfach ist, Schnittstellen privater Systemklassen im öffentlichen Bereich zu finden: Geben Sie einfach eine Abfrage in die Suchmaschine ein. Bei der Untersuchung einer Klassenschnittstelle UIPredictionViewController im Auge fällt eine ihrer Methoden auf:



Es scheint einen Hinweis zu geben, der mit den guten alten Tool- Spoofing-Implementierungsfunktionen (Swizzling) recht einfach zu überprüfen ist . Wir werden es verwenden, damit eine Funktion, die "unter Verdacht" steht, immer den wahren Wert zurückgibt:

+(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);
}

Beim Neustart des Testprojekts stellte ich fest, dass die Telefonansagen zu iOS 13 zurückkehrten und „im normalen Modus“ funktionieren. Dies könnte die Untersuchung beenden und diese gefährliche und verbotene Apple-Richtlinienlösung in der Release-Assembly möglicherweise sogar sehr sorgfältig verwenden und für einige Benutzer remote aktivieren / deaktivieren (für die Option der Remote-Funktionsverwaltung sehen Sie die Aufzeichnung des Berichts meiner Kollegin Katerina Trofimenko). Trotzdem habe ich mich immer wieder gefragt, warum diese Funktion bei Verwendung der Telefontastatur false zurückgibt.

Um die Wahrheit herauszufinden, benötigen wir den Quellcode der Funktion. Offensichtlich verteilt Apple den iOS-Komponentencode nicht rechts und links, sodass es nicht möglich ist, ihn zu googeln. Es gibt nur noch einen Weg - Reverse Engineering zum Dekompilieren des Binärcodes. Früher hatte ich mehrmals von einem Produkt wie Hopper gehört und mehrere Artikel über seine Verwendung gelesen, um tiefer unter die Haube von Systembibliotheken zu graben, aber ich habe es selbst nie benutzt. Und sofort war ich angenehm überrascht: Es stellte sich heraus, dass es nicht einmal notwendig ist, die Vollversion zu kaufen, um herumzuspielen und die Werkzeuge zu lernen. Die Demoversion enthält endlose 30-minütige Arbeitssitzungen, ohne dass der Index gespeichert und Änderungen an Binärdateien vorgenommen werden können. Dies bedeutet, dass sie eine hervorragende Plattform zum Experimentieren ist.

Alles aus dem gleichenIn der veröffentlichten privaten Oberfläche können Sie herausfinden, was UIPredictionViewControllerTeil des UIKitCore-Frameworks ist. Sie müssen es nur noch finden und auf Hopper hochladen. Binäre Framework-Dateien befinden sich in den Tiefen von Xcode. Hier ist zum Beispiel der vollständige Pfad zum UIKitCore, den wir benötigen:

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

Wir erstellen eine Drag-and-Drop-Datei in Hopper, bestätigen die Aktion im Systemdialog und warten, bis die Indizierung abgeschlossen ist (bei einem so großen Framework wie UIKit kann dies vier bis sechs Minuten dauern).

Die Oberfläche des Programms ist recht einfach, ich werde nur die Schlüsselelemente erwähnen, die für meine Forschung erforderlich waren. Im linken Bereich der Benutzeroberfläche finden Sie die Suchzeile, durch die der Code navigiert wird. Durch die Suche nach dem Namen der Klasse, an der wir interessiert sind, erhalten wir schnell die zu untersuchende Funktion. Der Assembler-Code wird im Hauptfenster geöffnet.



In der oberen Taskleiste befinden sich Schaltflächen zum Umschalten des Codeanzeigemodus. Von links nach rechts:



  • ASM-Modus - Assembler-Code;
  • CFG-Modus - Assembler-Code in Form eines Blockdiagramms (Baums), in dem die Codeteile zu Blöcken zusammengefasst und die Übergänge als Verzweigungen angezeigt werden.
  • Pseudocode-Modus - der generierte Pseudocode (wir werden weiter unten mehr darüber sprechen);
  • Der Hex-Modus ist eine hexadezimale Darstellung einer Binärdatei oder Abrakadabra, was uns bei unserer Forschung nicht viel hilft.

Jetzt müssen Sie herausfinden, was innerhalb der Funktion passiert. Ihr Körper ist ziemlich lang, so dass nur ASM-Gurus, die ich mich nicht nennen kann, die Logik verstehen können, indem sie sich den Assembler-Code ansehen. Dazu hilft der Pseudocode-Modus, in dem Hopper die Montagevorgänge so weit wie möglich vereinfacht, wenn möglich echte Funktionsnamen ersetzt und Registernamen wie Variablen verwendet. Es sieht so aus:



Es bleibt nur die Logik der Funktion durchzugehen und zu verstehen, in welchen ihrer Zweige wir fallen. Symbolische Haltepunkte haben mir dabei geholfen., die bei Systemaufrufen installiert werden kann und gleichzeitig alle erforderlichen Variablen und die Ergebnisse von Funktionsaufrufen an die Xcode-Konsole druckt. Auf diese einfache Weise stellte ich fest, dass die Ausführung der Funktion durch ein vorzeitiges Beenden unterbrochen wird, da eine der Bedingungen im Beispielcodeblock nicht funktioniert. Lassen Sie uns herausfinden, was hier los ist.

Ein kleiner Kontext: Im rbx-Register befindet sich etwas höher im Code (den ich der Einfachheit halber weggelassen habe) eine Verknüpfung zu dem Objekt, das das Protokoll implementiert UITextInputTraits_Private(dies ist eine erweiterte Version des öffentlichen Protokolls UITextInputTraits).

Die erste der Bedingungen besteht also darin, zu überprüfen, ob die Eingabeaufforderungen durch die Konfiguration des Eingabefelds nicht ausgeblendet werden. Im Debug können Sie sehen, dass es ausgeführt wird: EigenschafthidePredictiongibt false zurück. Die zweite Bedingung besteht darin, zu überprüfen, ob sich die Tastatur nicht im Split-Modus befindet (ziehen Sie auf Ihrem iPad die untere rechte Taste nach oben , nur 2-3% der Benutzer wissen davon). Das ist auch in Ordnung.

Mach weiter. In der nächsten Phase beginnen Manipulationen mit keyboardType, die uns darauf hinweisen, dass die Wahrheit irgendwo in der Nähe ist. Es wird zuerst überprüft, ob der aktuelle keyboardTypeWert kleiner oder gleich 0xb ist (oder 11 im Dezimalformat). Wenn wir die Deklaration in Xcode öffnen UIKeyboardType, werden wir sehen, dass es 13 Arten von Tastaturen gibt, und eine davon (UIKeyboardTypeAlphabet) ist veraltet und wird als Referenz auf einen anderen Typ deklariert. Das heißt, es gibt 12 Typen in enum: Wenn Sie bei 0 beginnen, hat letzterer einen Wert von 11. Mit anderen Worten, der Code validiert den Wert in Form einer Überlaufprüfung und wird erneut erfolgreich bestanden.

Außerdem sehen wir einen sehr seltsamen Zustand if (!COND), und ich konnte lange Zeit nicht verstehen, was überprüft wurde, da die COND-Variable an keiner anderen Stelle im Code deklariert wurde. Darüber hinaus haben meine symbolischen Haltepunkte gezeigt, dass gerade die Nichterfüllung dieser Bedingung zu einem vorzeitigen Verlassen der Funktion führt. Und hier haben wir keine andere Wahl, als in den ASM-Modus zurückzukehren und diesen Teil des Codes in Assembler-Form zu studieren.

Um diese Bedingung in der ASM-Liste zu finden, können Sie die Option „Keine Codeduplizierung“ verwenden, bei der Pseudocode nicht in Form von Quellcode mit if-else-Bedingungen angezeigt wird, sondern in Form von eindeutigen Codeblöcken und Übergängen in Form von goto. In diesem Fall sehen wir die Position des Anfangs dieser Blöcke in der Binärdatei und verwenden diesen Zeiger, um im ASM-Modus zu suchen. Also fand ich heraus, dass sich der Block, an dem wir interessiert sind, unter fe79f4 befindet:



Wenn wir zum ASM-Modus zurückkehren, können wir diesen Block leicht finden:



Wir kommen zum schwierigsten Teil, wo wir die drei Zeilen des Assembler-Codes analysieren werden.

Ich erkannte die erste Zeile aus meinem Gedächtnis (dank des Assembler-Unterrichts in meiner Heimat MIET ist endlich der Moment in meinem Leben gekommen, in dem die Hochschulbildung nützlich war!). Hier ist alles ganz einfach: Die Konstante 0x930 (100100110000 im Binärformat) wird in das ecx-Register gestellt.

In der zweiten Zeile sehen wir den Befehl bt, der in den Registern excund ausgeführt wird eax. Der Wert von einem, den wir bereits kennen, der Wert des zweiten ist im vorherigen Screenshot zu sehen: rax = [rbx keyboardType]- Er enthält den aktuellen Tastaturtyp. rax- Dies ist das gesamte 64-Bit-Register. eax- Dies ist der 32-Bit-Teil.

Nachdem die Daten festgelegt wurden, bleibt die Logik des Teams zu verstehen. Google führt uns zu dieser Beschreibung :
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.

Der Befehl extrahiert ein Bit aus dem ersten Operanden (Konstante 0x930) an der durch den zweiten Operanden (Tastaturtyp) angegebenen Position und platziert es in CF ( Übertragsflag ). Das heißt, die CF enthält je nach Tastaturtyp 0 oder 1.

Wir fahren mit der letzten Operation fort jb, die die folgende Beschreibung hat :

Jump short if below (CF=1)

Es ist leicht zu erraten, dass es einen Übergang zur Funktion gibt (vorzeitiges Beenden), wenn CF 1 ist.

An diesem Punkt beginnt sich das Rätsel zu summieren. Wir haben eine Bitmaske 100100110000 (sie enthält 12 Bit entsprechend der Anzahl der verfügbaren Tastaturtypen) und bestimmt die Bedingung für ein vorzeitiges Beenden. Wenn wir nun die Verfügbarkeit von Hinweisen für alle Tastaturtypen in aufsteigender Reihenfolge überprüfen rawValue, ist alles vorhanden.



In iOS 12 werden wir keine solche Logik finden - die Hinweise dort funktionieren für jede Art von Tastatur. Ich vermute, dass Apple in iOS 13 beschlossen hat, Hinweise für digitale Tastaturen zu deaktivieren, was im Prinzip verständlich ist: Ich kann mir kein Szenario ausdenken, in dem das System Nummern auffordern muss. Anscheinend hat versehentlich auch eine „heiße Hand“ getroffen UIKeyboardTypePhonePad, die einem normalen Ziffernblock sehr ähnlich ist. Gleichzeitig UIKeyboardTypeNamePhonePadzeigte er eine QWERTZ-Tastatur, um nach Telefonkontakten zu suchen, und genau dieselbe deaktivierte digitale Tastatur und zeigte weiterhin Eingabeaufforderungen an.

Zu meiner Überraschung erwies sich die Arbeit mit Hopper als sehr angenehm und unterhaltsam. Lange Zeit bekam ich nicht so viele Fans. Ich habe die Ergebnisse in meinem Fehlerbericht an die Apple-Ingenieure weitergegeben, und der Status wurde im Laufe der Zeit in "Potenzielle Fehlerbehebung - Für ein zukünftiges Betriebssystem-Update" geändert. Ich hoffe, dass das Update Benutzer in zukünftigen Updates erreichen wird. Gleichzeitig kann Hopper nicht nur zum Auffinden der Ursachen Ihrer oder Apple-Fehler verwendet werden, sondern auch zum Erkennen von Apple-Korrekturen für Programme von Drittanbietern .

All Articles