Explore el error de iOS con Hopper

¡Hola! Mi nombre es Alexander Nikishin, estoy desarrollando aplicaciones para iOS en Badoo. En el artículo, hablaré sobre cómo investigamos un error en UIKit, que Apple no quiso reparar durante seis meses.



Todo comenzó en agosto de 2019 con las primeras versiones beta de iOS 13. Luego, primero encontramos un problema. En las aplicaciones Badoo y Bumble, trabajamos constantemente para mejorar las interfaces y, por ejemplo, tratamos de optimizar el proceso de registro tedioso y poco querido tanto como sea posible. La información predictiva sistemática sobre el teclado es una excelente manera de reducir la cantidad de clics del usuario al ingresar datos. Sin embargo, en la nueva versión de iOS, nos sorprendió descubrir que las indicaciones para ingresar el número de teléfono habían desaparecido.



Cuando salió la versión GM, nos dimos cuenta de que el problema no estaba solucionado. Nos pareció que un error tan obvio simplemente no podía pasarse por alto en las pruebas de regresión, que Apple probablemente tenía en su arsenal, y comenzamos a esperar una corrección en el primer gran paquete de actualización. Otros desarrolladores esperaban esto . Sin embargo, con el lanzamiento de la versión 13.1, nada ha cambiado, y no tuvimos más remedio que abrir el radar, lo que hicimos a principios de octubre. Pasó el tiempo, salió iOS 13.2 y 13.3, y el error aún no se corrigió.

En febrero, nuestro equipo de registro obtuvo algo de tiempo libre trabajando en el trabajo atrasado, y decidí investigar este problema un poco más a fondo.

Antes de comenzar a cavar, tenía que averiguar qué manera de hacerlo. Dado que las sugerencias continuaron funcionando en algunos tipos de teclados, lo primero que se pensó fue estudiar la jerarquía de sus vistas en diferentes versiones de iOS.


iOS 12 vs iOS 13

De inmediato se hizo evidente que Apple en iOS 13 refactorizó la implementación del teclado y resaltó las indicaciones en un controlador separado ( UIPredictionViewController). Obviamente, la tendencia hacia la modularización y la descomposición también lo ha alcanzado (por cierto, Artyom Loenko habló recientemente sobre nuestra experiencia en su aplicación ). Lo más probable es que, debido a esto, haya una regresión de la funcionalidad. El círculo de búsquedas comenzó a estrecharse.

Creo que la mayoría de los desarrolladores de iOS saben que es fácil encontrar interfaces de clases de sistemas privados en el dominio público: solo ingrese una consulta en el motor de búsqueda. En el estudio de una interfaz de clase UIPredictionViewController en el ojo llama la atención uno de sus métodos:



parece que había una pista, que es bastante fácil de verificar, utilizando las buenas funciones de implementación de falsificación de herramientas (Swizzling). Lo usaremos para que una función que esté "bajo sospecha" siempre devuelva el valor verdadero:

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

Al reiniciar el proyecto de prueba, descubrí que las indicaciones del teléfono volvieron a iOS 13 y funcionan "en modo normal". Esto podría finalizar la investigación y, quizás, incluso usar con mucho cuidado esta peligrosa y prohibida solución de pautas de Apple en el conjunto de lanzamiento con la capacidad de habilitar / deshabilitar de forma remota para algunos usuarios (para la opción de administración remota de funciones, puede ver la grabación del informe de mi colega Katerina Trofimenko). Sin embargo, nunca dejé de preguntarme por qué esta función devuelve falso cuando se usa el tipo de teclado telefónico.

Para llegar a la verdad, necesitamos el código fuente de la función. Obviamente, Apple no distribuye el código del componente iOS de derecha a izquierda, por lo que no es posible buscarlo en Google. Solo queda un camino: ingeniería inversa para descompilar el código binario. Anteriormente, había escuchado sobre un producto como Hopper varias veces y leí varios artículos sobre su uso para profundizar bajo el capó de las bibliotecas del sistema, pero nunca lo usé yo mismo. E inmediatamente me sorprendió gratamente: resultó que para jugar y aprender las herramientas, ni siquiera es necesario comprar la versión completa. La versión demo incluye sesiones de trabajo interminables de 30 minutos sin la capacidad de guardar el índice y realizar cambios en los archivos binarios, lo que significa que es una excelente plataforma para la experimentación.

Todos de la mismainterfaz privada publicada , puede averiguar qué UIPredictionViewControlleres parte del marco UIKitCore. Todo lo que queda es encontrarlo y subirlo a Hopper. Los archivos de marco binarios se encuentran en las profundidades de Xcode. Aquí, por ejemplo, está el camino completo al UIKitCore que necesitamos:

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

Creamos un archivo de arrastrar y soltar en Hopper, confirmamos la acción en el cuadro de diálogo del sistema y esperamos a que se complete la indexación (en el caso de un marco tan grande como UIKit, esto puede tomar de cuatro a seis minutos).

La interfaz del programa es bastante simple, solo notaré los elementos clave que fueron necesarios para mi investigación. En el panel izquierdo de la interfaz puede encontrar la línea de búsqueda a través de la cual se navega el código. Al buscar el nombre de la clase que nos interesa, obtenemos rápidamente la función en estudio, su código de ensamblador se abre en la ventana principal.



En la barra de tareas superior hay botones para cambiar los modos de visualización del código. De izquierda a derecha:



  • Modo ASM: código de ensamblador;
  • Modo CFG: código de ensamblador en forma de diagrama de bloques (árbol), donde las piezas de código se combinan en bloques y las transiciones se muestran como ramas;
  • Modo de pseudocódigo: el pseudocódigo generado (hablaremos más sobre esto a continuación);
  • El modo hexadecimal es una representación hexadecimal de un archivo binario, o abracadabra, que no nos ayuda mucho en nuestra investigación.

Ahora necesita descubrir qué sucede dentro de la función. Su cuerpo es bastante largo, por lo que solo los gurús de ASM, a quienes no puedo llamar yo mismo, pueden entender la lógica mirando el código del ensamblador. Para hacer esto, el modo de pseudocódigo ayudará, en el cual Hopper simplifica las operaciones de ensamblaje tanto como sea posible, sustituyendo nombres de funciones reales donde sea posible y usando nombres de registro como variables. Se ve así:



solo queda pasar por la lógica de la función y comprender en qué ramas caemos. Los puntos de interrupción simbólicos me ayudaron con esto., que se puede instalar en las llamadas del sistema, imprimiendo simultáneamente todas las variables necesarias y los resultados de las llamadas de función a la consola Xcode. Usando esta forma simple, descubrí que la ejecución de la función se ve interrumpida por una salida temprana debido al hecho de que una de las condiciones no funciona en el bloque de código de ejemplo. Veamos qué está pasando aquí.

Un pequeño contexto: en el registro rbx, un poco más arriba en el código (que omití por simplicidad) hay un enlace al objeto que implementa el protocolo UITextInputTraits_Private(esta es una versión extendida del protocolo público UITextInputTraits).

Entonces, la primera de las condiciones es verificar que las indicaciones no estén ocultas por la configuración del campo de entrada. En la depuración, puede ver que se está ejecutando: propiedadhidePredictiondevuelve falso La segunda condición es verificar que el teclado no esté en modo "dividido" (en su iPad, jale el botón inferior derecho hacia arriba , solo el 2-3% de los usuarios saben de esto). Con esto, también, todo está en orden.

Siga adelante. En la siguiente etapa, comienzan las manipulaciones keyboardType, que nos sugieren que la verdad está en algún lugar cercano. Primero se verifica que el actual sea keyboardTypemenor o igual a 0xb (o 11 en formato decimal). Si abrimos la declaración en Xcode UIKeyboardType, veremos que hay 13 tipos de teclados, y uno de ellos (UIKeyboardTypeAlphabet) está en desuso y se declara como referencia a otro tipo. Es decir, hay 12 tipos en la enumeración: si comienza desde 0, este último tendrá un valor igual a 11. En otras palabras, el código valida el valor en forma de una verificación de desbordamiento y, nuevamente, pasa con éxito.

Además, vemos una condición muy extraña if (!COND), y durante mucho tiempo no pude entender lo que estaba comprobando, dado que la variable COND no se declaró en ningún otro lugar del código. Además, mis puntos de interrupción simbólicos mostraron que es precisamente el incumplimiento de esta condición lo que conduce a una salida anticipada de la función. Y aquí no tenemos más remedio que volver al modo ASM y estudiar esta parte del código en forma de ensamblador.

Para encontrar esta condición en la lista de ASM, puede usar la opción "Sin duplicación de código", que mostrará pseudocódigo no en forma de código fuente con condiciones if-else, sino en forma de bloques únicos de código y transiciones en forma de goto. En este caso, veremos la posición del comienzo de estos bloques en el archivo binario y utilizaremos este puntero para buscar en modo ASM. Así que descubrí que el bloque que nos interesa se encuentra en fe79f4:



volviendo al modo ASM, podemos encontrar fácilmente este bloque:



llegamos a la parte más difícil, donde analizaremos las tres líneas de código de ensamblador.

Reconocí la primera línea de mi memoria (gracias a las lecciones de ensamblador en mi MIET nativo, ¡finalmente llegó el momento en mi vida cuando la educación superior fue útil!). Aquí todo es bastante simple: la constante 0x930 (100100110000 en formato binario) se coloca en el registro ecx.

En la segunda línea, vemos la instrucción bt ejecutada en los registros excy eax. El valor de uno que ya conocemos, el valor del segundo se puede ver en la captura de pantalla anterior: rax = [rbx keyboardType]contiene el tipo de teclado actual. rax- este es el registro completo de 64 bits, eax- esta es su parte de 32 bits.

Con los datos decididos, queda por entender la lógica del equipo. Google nos lleva a esta descripción :
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.

La instrucción extrae un bit del primer operando (constante 0x930) en la posición especificada por el segundo operando (tipo de teclado), y lo coloca en CF ( bandera de acarreo ). Es decir, como resultado, el CF contendrá 0 o 1, dependiendo del tipo de teclado.

Pasamos a la última operación jb, tiene la siguiente descripción :

Jump short if below (CF=1)

es fácil adivinar que hay una transición a la función (salida anticipada) si CF es 1.

En este punto, el rompecabezas comienza a sumar. Tenemos una máscara de bits 100100110000 (contiene 12 bits de acuerdo con el número de tipos de teclado disponibles), y determina la condición para la salida anticipada. Ahora, si verificamos la disponibilidad de pistas para todo tipo de teclados en orden ascendente rawValue, todo estará en su lugar.



En iOS 12, no encontraremos esa lógica: las sugerencias allí funcionan para cualquier tipo de teclado. Sospecho que en iOS 13, Apple decidió deshabilitar las sugerencias para los teclados digitales, lo cual es comprensible en principio: no se me ocurre un escenario en el que el sistema necesite solicitar números. Aparentemente, por error, también golpeó una "mano caliente" UIKeyboardTypePhonePad, que es muy similar a un teclado numérico normal. Al mismo tiempo UIKeyboardTypeNamePhonePad, combinando un teclado QWERTY para buscar contactos telefónicos y exactamente el mismo teclado digital deshabilitado, continuó mostrando indicaciones.

Para mi sorpresa, trabajar con Hopper resultó ser muy placentero y entretenido, durante mucho tiempo no tuve tanta afición. Compartí los hallazgos con los ingenieros de Apple en mi informe de error, y su estado cambió con el tiempo a "Posible solución identificada: para una futura actualización del sistema operativo". Espero que la solución llegue a los usuarios en futuras actualizaciones. Al mismo tiempo, Hopper se puede usar no solo para encontrar las causas de sus errores o los de Apple, sino también para detectar soluciones de Apple para programas de terceros .

All Articles