Explore iOS bug with Hopper

Hello! My name is Alexander Nikishin, I am developing iOS applications at Badoo. In the article, I will talk about how we investigated a bug in UIKit, which Apple did not want to fix for six months.



It all started in August 2019 with the first beta versions of iOS 13. Then we first encountered a problem. In the Badoo and Bumble applications, we are constantly working on improving the interfaces and, for example, we try to optimize the tedious and unloved registration process as much as possible. Systematic predictive tooltips above the keyboard are a great way to reduce the number of user clicks when entering data. However, in the new version of iOS, we were surprised to find that the prompts for entering the phone number were gone.



When the GM version came out, we realized that the problem was not fixed. It seemed to us that such an obvious bug simply could not be missed by regression tests, which Apple probably had in its arsenal, and we began to wait for a correction in the first big update package. Other developers hoped for this . However, with the release of version 13.1, nothing has changed, and we had no choice but to open the radar, which we did in early October. Time passed, iOS 13.2 and 13.3 came out, and the bug still remained uncorrected.

In February, our registration team got some free time from working on the backlog, and I decided to investigate this problem a little deeper.

Before you start digging, you had to figure out which way to do it. Since the hints continued to work on some types of keyboards, the first thought was to study the hierarchy of its views in different versions of iOS.


iOS 12 vs iOS 13

It immediately became clear that Apple in iOS 13 refactored the keyboard implementation and highlighted the prompts in a separate controller ( UIPredictionViewController). Obviously, the trend towards modularization and decomposition has also reached it (by the way, Artyom Loenko recently spoke about our experience of their application ). Most likely, because of this, there was a regression of functionality. The circle of searches began to narrow.

I think most iOS developers know that it’s easy to find interfaces of private system classes in the public domain: just enter one query in the search engine. In the study of a class interface UIPredictionViewController in the eye catches one of its methods:



It seems there was a clue, which is quite easy to check, using the good old tool spoofing implementation functions (Swizzling). We will use it so that a function that is “under suspicion” always returns the true value:

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

Restarting the test project, I found that the phone prompts returned to iOS 13 and are working “in normal mode”. This could end the investigation and perhaps even very carefully use this dangerous and forbidden Apple guideline solution in the release assembly with the ability to remotely enable / disable for some users (for the option of remote feature management, you can see the recording of the report of my colleague Katerina Trofimenko). Nevertheless, I never ceased to wonder why this function returns false when using the telephone type of keyboard.

To get to the truth, we need the source code of the function. Obviously, Apple doesn’t distribute the iOS component code right and left, so it’s not possible to google it. There is only one way left - reverse engineering to decompile the binary code. Earlier, I had heard about a product such as Hopper several times and read several articles about its use in order to dig deeper under the hood of system libraries, but I never used it myself. And immediately I was pleasantly surprised: it turned out that in order to play around and learn the tools, it is not even necessary to buy the full version. The demo version includes endless 30-minute work sessions without the ability to save the index and make changes to binary files, which means it is an excellent platform for experimentation.

All from the samepublished private interface, you can find out what UIPredictionViewControlleris part of the UIKitCore framework. All that remains is to find it and upload it to Hopper. Binary framework files are located in the depths of Xcode. Here, for example, is the full path to the UIKitCore we need:

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

We make a drag-and-drop file in Hopper, confirm the action in the system dialog and wait for the indexing to complete (in the case of such a large framework as UIKit, this can take from four to six minutes).

The interface of the program is quite simple, I will only note the key elements that were required for my research. In the left panel of the interface you can find the search line through which the code is navigated. By searching the name of the class we are interested in, we quickly get the function under study, its assembler code opens in the main window.



In the upper task bar there are buttons for switching code display modes. From left to right:



  • ASM mode - assembler code;
  • CFG mode - assembler code in the form of a block diagram (tree), where the pieces of code are combined into blocks, and the transitions are shown as branches;
  • Pseudo-code mode - the generated pseudo-code (we will talk more about it below);
  • Hex mode is a hexadecimal representation of a binary file, or abracadabra, which does not help us much in our research.

Now you need to find out what happens inside the function. Her body is quite long, so only ASM gurus, whom I can’t call myself, can understand the logic by looking at the assembler code. To do this, Pseudo-code mode will help, in which Hopper simplifies assembly operations as much as possible, substituting real function names where possible and using register names like variables. It looks like this:



It remains only to go through the logic of the function and understand which of its branches we fall into. Symbolic Breakpoints helped me with this., which can be installed on system calls, simultaneously printing all the necessary variables and the results of function calls to the Xcode console. Using this simple way, I found that the execution of the function is interrupted by an early exit due to the fact that one of the conditions does not work in the example code block. Let's figure out what's going on here.

A little context: in the rbx register a little higher in the code (which I omitted for simplicity) is a link to the object implementing the protocol UITextInputTraits_Private(this is an extended version of the public protocol UITextInputTraits).

So, the first of the conditions is to verify that the prompts are not hidden by the configuration of the input field. In the debug, you can see that it is running: propertyhidePredictionreturns false. The second condition is to verify that the keyboard is not in “split” mode (on your iPad, pull the lower right button up , only 2-3% of users know about this thing). With this, too, everything is in order.

Move on. At the next stage, manipulations with begin keyboardType, which hint to us that the truth is somewhere nearby. It is first checked that the current one is keyboardTypeless than or equal to 0xb (or 11 in decimal format). If we open the declaration in Xcode UIKeyboardType, we will see that there are 13 types of keyboards, and one of them (UIKeyboardTypeAlphabet) is deprecated and declared as a reference to another type. That is, there are 12 types in enum: if you start from 0, the latter will have a value equal to 11. In other words, the code validates the value in the form of an overflow check, and it, again, passes successfully.

Further we see a very strange condition if (!COND), and for a long time I could not understand what it was checking, given that the COND variable was not declared anywhere else in the code. Moreover, my Symbolic Breakpoints showed that it is precisely the failure to fulfill this condition that leads to an early exit from the function. And here we have no choice but to return to ASM mode and study this part of the code in assembler form.

To find this condition in the ASM listing, you can use the “No code duplication” option, which will show the pseudocode not in the form of source code with if-else conditions, but in the form of unique blocks of code and transitions in the form of goto. In this case, we will see the position of the beginning of these blocks in the binary file and use this pointer to search in ASM mode. So I found out that the block we are interested in is located at fe79f4:



Returning to ASM mode, we can easily find this block:



We come to the most difficult part, where we will parse the three lines of assembler code.

I recognized the first line from my memory (thanks to the assembler lessons in my native MIET, finally the moment has come in my life when higher education came in handy!). Everything is quite simple here: the constant 0x930 (100100110000 in binary format) is placed in the ecx register.

In the second line, we see the bt instruction executed on the excand registers eax. The value of one we already know, the value of the second can be seen in the previous screenshot: rax = [rbx keyboardType]- it contains the current type of keyboard. rax- this is the entire 64-bit register, eax- this is its 32-bit part.

With the data decided, it remains to understand the logic of the team. Google leads us to this description :
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.

The instruction extracts a bit from the first operand (constant 0x930) at the position specified by the second operand (keyboard type), and places it in CF ( carry flag ). That is, as a result, the CF will contain 0 or 1, depending on the type of keyboard.

We proceed to the last operation jb, it has the following description :

Jump short if below (CF=1)

It is easy to guess that there is a transition to the function (early exit) if CF is 1.

At this point, the puzzle begins to add up. We have a bit mask 100100110000 (it contains 12 bits according to the number of keyboard types available), and it determines the condition for early exit. Now, if we check the availability of hints for all types of keyboards in ascending order rawValue, everything will be in place.



In iOS 12, we will not find such logic - the hints there work for any type of keyboard. I suspect that in iOS 13, Apple decided to disable hints for digital keyboards, which is understandable in principle: I can’t come up with a scenario where the system needs to prompt numbers. Apparently, by mistake, a “hot hand” also hit UIKeyboardTypePhonePad, which is very similar to a regular numeric keypad. At the same time UIKeyboardTypeNamePhonePad, combining a QWERTY keyboard to search for telephone contacts and the exact same disabled digital keypad, he continued to show prompts.

To my surprise, working with Hopper turned out to be very pleasant and entertaining, for a long time I did not get so much fan. I shared the findings with Apple engineers in my bug report, and its status changed over time to “Potential fix identified - For a future OS update”. I hope the fix will reach users in future updates. At the same time, Hopper can be used not only to find the causes of your or Apple bugs, but also to detect Apple fixes for third-party programs .

All Articles