1 puntos por GN⁺ 2025-02-19 | 1 comentarios | Compartir por WhatsApp
  • La ingeniería inversa de apps de iOS requiere poder observar y manipular la app en ejecución, pero esta app de widgets usa en conjunto bloqueo del depurador, bloqueo de inyección de código y detección de jailbreak
  • El bloqueo principal puede implementarse con PT_DENY_ATTACH de ptrace o con una llamada directa al sistema con el mismo efecto (svc #0x80), por lo que no se detecta solo con un breakpoint simple sobre ptrace
  • La llamada directa al sistema se evita buscando en el binario el patrón mov w16, #26, poniendo un breakpoint en la posición de svc y usando lldb jump para saltar esa instrucción
  • La acción de hacer soft-reboot/respring del teléfono tras detectar jailbreak estaba conectada a una función que llamaba a snapshotViewAfterScreenUpdates: en un bucle infinito, y se evitó saltándola con thread return al inicio de la función
  • El crash tras la inyección de código parecía deberse menos a una verificación especial de frameworks en tiempo de ejecución y más a la pérdida de permisos de App Group; con un swizzle que cambia containerURLForSecurityApplicationGroupIdentifier: por un directorio temporal, pudo ejecutarse también en dispositivos normales

Una app de widgets para iOS con varias capas de protección

  • El objetivo era una app de widgets de la App Store, con mecanismos defensivos más fuertes que los de una app de widgets común
    • bloqueo de attach del depurador
    • cierre de la app al inyectar código
    • si se ejecuta en estado de jailbreak, todo el teléfono hace soft-reboot/respring
  • No es raro que una app de iOS incluya protecciones como detección de jailbreak u ofuscación de código, pero esta app combina varios métodos
  • Otros comportamientos interesantes dentro de la app quedan para un texto posterior

Bloqueo del attach del depurador con PT_DENY_ATTACH

  • En dispositivos con jailbreak, normalmente se puede entrar por ssh, ejecutar debugserver y depurar la app conectándose desde lldb en otra computadora
  • Si se intenta adjuntar debugserver a esta app del mismo modo, ocurre un Segmentation fault y el attach falla
  • La causa está relacionada con la solicitud PT_DENY_ATTACH de ptrace
    • ptrace es una API privada en iOS y pública en macOS
    • PT_DENY_ATTACH establece una bandera que rechaza el trace del proceso padre a partir de ese momento
    • Si ya está siendo trazado, la app termina con estado ENOTSUP
    • Si el padre intenta trazar un proceso con esa bandera, se produce una segmentation violation del lado del padre
  • La implementación simple puede hacerse con una llamada ptrace(PT_DENY_ATTACH, 0, 0, 0)
    • Como es una API privada de iOS, para la llamada real hay que localizar el símbolo ptrace de libsystem_kernel.dylib usando dlopen y dlsym
    • Este método puede evitarse relativamente fácil poniendo un breakpoint en ptrace y saltando la llamada con thread return

Por qué no funcionó la evasión fácil

  • PT_DENY_ATTACH solo bloquea al depurador después de ser llamado, así que si se hace attach antes de que corra el código de la app, se puede crear un punto de evasión
  • Si en vez de adjuntar debugserver directamente al proceso se lo ejecuta primero y luego en lldb se espera el arranque de la app con process attach --name TopWidget --waitfor, es posible hacer attach antes de la ejecución del código de la app
  • Pero en esta app el breakpoint b ptrace al principio no se resolvía y, aun cuando luego se resolvía, la app terminaba sin que llegara a activarse
  • La razón es que la app no llamaba a la función ptrace, sino que usaba una llamada directa al sistema con el mismo efecto

Encontrar la ubicación de la llamada directa al sistema

  • El desensamblado de la función ptrace contiene como parte central la llamada al sistema svc #0x80
    • en x0 va el valor 31, que corresponde a PT_DENY_ATTACH
    • en x1, x2 y x3 van los argumentos no usados, 0
    • en x16 va 26, el número de llamada al sistema de ptrace
  • La app puede no llamar a la función ptrace y, en cambio, configurar esos mismos valores de registros con inline assembly antes de ejecutar directamente svc #0x80
    • Este método evita búsquedas sospechosas de APIs privadas como dlopen y dlsym
    • También hace difícil atraparlo poniendo breakpoints sobre una función compartida como ptrace
  • Para evitarlo hay que abrir el binario descifrado de la app en un desensamblador y encontrar la ubicación de la llamada al sistema de ptrace
  • Lo que se busca es mov x16, #26 o su vista de 32 bits para el mismo registro, mov w16, #26
    • En armconverter.com se puede obtener la secuencia de bytes 50 03 80 D2 de mov x16, #26 para usarla en la búsqueda del binario
    • No hubo resultados para mov x16, #26, pero al buscar mov w16, #26 aparecieron 4 resultados
  • De esos, dos no coincidían con el patrón esperado por las instrucciones cercanas, y en el tercer resultado se confirmó el siguiente código
    • MOV X0, #0x1F
    • MOV X1, #0
    • MOV X2, #0
    • MOV X3, #0
    • MOV W16, #0x1A
    • SVC 0x80
  • El cuarto resultado era otra branch de la misma función, y se confirmó que esa función era donde se bloqueaba el attach del depurador

Saltarse la instrucción svc

  • Las direcciones svc vistas en el desensamblador son 0x102A2BB14 y 0x102A2BB68
  • En lldb, para convertir direcciones basadas en el binario a direcciones reales cargadas, se configuran breakpoints agregando -s TopWidget
    • br s -a 0x102A2BB14 -s TopWidget
    • br s -a 0x102A2BB68 -s TopWidget
  • Al continuar la ejecución, el breakpoint se activa en la ubicación de svc #0x80
  • La evasión más sencilla es hacer jump a la dirección de la siguiente instrucción, para no ejecutar la llamada al sistema
    • En el ejemplo se ejecuta jump *0x10327bb18 hacia la dirección siguiente a la instrucción actual
  • Tras este proceso, se puede entrar a la app con el depurador ya adjunto

El comportamiento que hace soft-reboot del teléfono

  • Incluso después de evitar el attach del depurador, la app ejecuta un mecanismo de protección que hace soft-reboot/respring del teléfono
  • Esta vez lldb seguía adjunto, así que se pudo comprobar que el proceso recibió SIGKILL y revisar el stacktrace
  • En el stacktrace aparecía un flujo relacionado con captura de pantalla
    • QuartzCore con CARenderServerSnapshot
    • UIKitCore con _UISnapshotScreenWindowsRectAfterCommit
    • una unnamed symbol interna de TopWidget
  • Con lldb image lookup se convirtió la dirección en tiempo de ejecución a la dirección basada en binario 0x100041898, y en el desensamblador se revisó esa función
  • El resultado decompilado mostró que la función solo repetía en un bucle infinito estas acciones
    • llamar a +[UIScreen mainScreen]
    • llamar snapshotViewAfterScreenUpdates: sobre el objeto de pantalla devuelto
    • no usar el resultado y liberarlo
  • snapshotViewAfterScreenUpdates: es una API pública para crear snapshots de vistas, pero aquí la app la invocaba infinitamente de una forma intensiva en memoria
  • En el video enlazado se confirma que el origen de esta llamada está relacionado con la notificación com.apple.tw.twrr y que, si no se supera cierto risk check sobre el teléfono, se produce el respring
  • La evasión es posible poniendo un breakpoint al inicio de esa función y usando thread return cuando se active, para saltarse su ejecución

El crash al inyectar código

  • Durante la depuración se pueden inyectar a la app frameworks que implementen utilidades complejas, como registrar información de accesibilidad de botones en pantalla, y luego invocarlas desde el depurador
  • Incluso sin un dispositivo con jailbreak, es posible explorar inicialmente la app inyectando Frida o Flex
  • Normalmente esta inyección se hace con herramientas que vuelven a firmar la app, como Sideloadly
  • En esta app, al ejecutarla después de resignarla, se cierra de inmediato con crash
  • Si se observa en el desensamblador la dirección superior del stack del crash, 0x1002027D4, aparece una instrucción BRK, lo que coincide con un crash intencional como un force-unwrap de nil
  • En el código decompilado, la llamada que parecía problemática era containerURLForSecurityApplicationGroupIdentifier:
    • Este método devuelve la URL de una carpeta a la que pueden acceder en conjunto apps y extensiones del mismo group
    • El App Group se define durante el proceso de firmado de código
    • Al inyectar código y volver a firmar, se descarta la firma original de la app y también se pierden los permisos del App Group
    • Por eso es probable que devuelva nil en lugar de una URL, y que la app haga force-unwrap de ese valor y provoque el crash
  • Como una app de widgets debe compartir App Group con la extensión encargada de mostrar el widget en la pantalla de inicio, esto parece menos una protección intencional y más un problema común de code signing

Evitar el problema de App Group

  • La solución más fácil es no volver a firmar la app
    • En un dispositivo con jailbreak se puede inyectar el framework como jailbreak tweak sin resignar la app
    • También es posible ejecutar código con firmas inválidas o añadir la app deseada al group deseado
  • Si no hay dispositivo con jailbreak y sí hace falta resignar, todavía puede evitarse con un framework pequeño que haga swizzle del método
  • El código de ejemplo reemplaza containerURLForSecurityApplicationGroupIdentifier: de NSFileManager
    • Cuando se llama al método original, se ejecuta el método de reemplazo
    • El método de reemplazo devuelve temporaryDirectory en vez del shared container
  • Este reemplazo no es equivalente al comportamiento original de la app
    • La app original quiere una carpeta compartida accesible tanto por la app como por la extensión
    • Un directorio temporal no es una carpeta compartida de ese tipo
  • Aun así, puede bastar si el objetivo es revisar solo el comportamiento de la app principal
    • Un parche pequeño como este puede romper la app extension
    • Si solo se necesitan las funciones básicas de la app principal, eso puede no ser un problema
  • Una evasión más grande sería crear un App Group nuevo, resignar con ese group la app principal y todas sus extensiones, y luego hacer swizzle para que los métodos relacionados usen el nuevo group identifier
    • Es mucho más trabajo y, si no es estrictamente necesario, puede ser mejor conseguir un dispositivo con jailbreak
  • Si este framework se inyecta junto con una herramienta como Flex, la app puede ejecutarse normalmente incluso en dispositivos comunes
  • En dispositivos con jailbreak todavía hay que volver a evitar las protecciones anti-debugging y de respring mencionadas antes, pero después ya se puede entrar en la app con Flex inyectado

Estado final

  • Al final, la app quedó en un estado donde ya eran posibles el attach del depurador, la evasión de la detección de jailbreak y la inyección de código
  • Qué era exactamente lo que se quería observar dentro de la app queda para el siguiente texto

1 comentarios

 
GN⁺ 2025-02-19
Opiniones de Hacker News
  • Bryce Bostwick hace un trabajo realmente genial e inspirador en depuración de apps e ingeniería inversa.
    Lo descubrí en YouTube y, después de ver su video donde modifica TikTok para que solo muestre videos de gatos (https://youtu.be/YW3jL2gI9IE), probé modificar Instagram para dejar solo la función de mensajes que uso y quitar todo lo demás.
    Desde hace tiempo quería profundizar más en formas de modificar Windows al estilo de Windhawk (https://windhawk.net/), especialmente en modding e ingeniería inversa, y Bryce muestra muy bien ese tipo de trabajo en iOS con videos paso a paso en tiempo real.

    • Si hay alguien parecido del lado de Android, me gustaría aprender más.
      He visto que se pueden hacer cosas increíbles con Revanced, pero no parece haber muchas buenas guías que expliquen cómo empezar con ese tipo de trabajo.
    • Voy a intentar hacer lo que explicó.
      Si alguien documenta el procedimiento, me interesaría.
      Odio que, incluso para subir una foto o chatear con amigos, tenga que quedar expuesto a Reels.
  • Las técnicas de anti-depuración, e incluso las técnicas para impedir que se bloquee la anti-depuración, son comunes desde hace mucho en DOS/Windows.
    Si uno mira materiales antiguos sobre cracking o unpacking, estos temas se tratan con distintos niveles de profundidad.
    Qué tan fácil puede controlar el usuario el comportamiento de una app es inversamente proporcional a qué tan hostil al usuario es la plataforma.
    PT_DENY_ATTACH parece una función creada justamente para esa hostilidad hacia el usuario.
    Que yo sepa, Windows no tiene una función así; en su lugar se usa una técnica que hace que la app se adjunte a sí misma.
    https://www.x86matthew.com/view_post?id=selfdebug
    https://anti-debug.checkpoint.com/techniques/interactive.htm...

    • Correcto. PT_DENY_ATTACH es una función que Apple creó literalmente en su momento como parte de la solución de DRM de iTunes.
  • Me sorprende un poco que la revisión de la App Store de Apple no rechace apps que hacen llamadas directas al sistema.
    En las plataformas de Apple, las llamadas al sistema no son una ABI estable, así que todas las llamadas al sistema deberían pasar por libSystem; una app que hace llamadas directas al sistema sin pasar por libSystem básicamente está haciendo algo que no debería.
    Del mismo modo, también me da curiosidad por qué el autor buscó mov w16, #26 en el código en lugar de svc 0x80.

    • svc 0x80 es la instrucción que ejecuta cualquier llamada al sistema, y cuál llamada se ejecuta exactamente depende del registro x16.
      La app hará muchísimas llamadas al sistema no relacionadas, así que poner un breakpoint ahí no sería útil.
      Al menos así lo explicó en el video.
    • El compilador a veces puede inlinar wrappers de llamadas al sistema, así que no es tan fácil verificarlo estáticamente.
      Por la misma razón, si buscas instrucciones SVC vas a obtener una cantidad enorme de resultados.
      Si buscas el ID exacto de la llamada al sistema que se mueve a X16, puedes encontrarlo de inmediato.
  • Soy el autor del artículo. Si tienen preguntas, las respondo. Gracias a xmprt por compartirlo.

    • Vi el video en YouTube y me pareció interesante.
      Me gustó que también ofrecieras una versión en texto.
    • Fue un artículo muy interesante, y siempre quise ver más textos sobre este tipo de ingeniería inversa de bajo nivel en teléfonos.
      Quisiera preguntarle algunas cosas al autor: si la herramienta comercial más conocida es Guardsquare, me da curiosidad si consideras que ofrece algo nuevo que impida este tipo de desensamblado sencillo.
      También me interesa saber si TopWidgets estaba usando protecciones parecidas, o si era algo más bien hecho por ellos mismos.
    • Los videos son muy interesantes, y me sorprende que no los haya visto más gente o que no hayan leído el artículo.
      Personalmente uso Android, así que no se aplica directamente en lo técnico, pero aun así obtengo mucho valor de aprender cómo funciona la depuración de bajo nivel en iOS.
    • Me pregunto si en iOS existe algo como PTRACE_SYSCALL, que permita engancharse en el punto de entrada de una llamada al sistema para cambiar el valor de retorno, o detectar desde dónde ocurre una SVC.
  • El video de arriba es de los mejores videos de programación que he visto.
    Avanza rápido, asume exactamente el nivel adecuado de conocimiento previo y tiene excelentes demostraciones que no interrumpen el flujo del video.

  • Excelente artículo.
    De verdad me da curiosidad si era una app normal excesivamente paranoica, o si era una app que ya se estaba depurando porque se sospechaba que era malware.
    Si no, el esfuerzo invertido parece bastante excesivo.

    • A mi juicio, se acerca más a una app simplemente excesivamente paranoica.
      Parece que estaban haciendo cosas bastante interesantes con widgets y querían protegerlas. Aunque esas estrategias, de todos modos, ya empezaban a filtrarse de a poco.
      También había cosas interesantes dentro del binario.
      En un momento intentaba entender por qué estaba viendo código que parecía descargar un .iso de Windows, y resultó que efectivamente era eso, y se usaba para un widget de prueba de velocidad de red.
    • O también podría ser que intentaran demostrar una infracción de copyright si la propia app era recompilada con otro logo u otros cambios.
  • Hay un modo todavía más difícil que “bypassear PT_DENY_ATTACH (modo difícil)”.
    Hace tiempo, en macOS, parcheé el kernel para que PT_DENY_ATTACH no hiciera nada.
    En Mac, ejecutar un kernel parcheado en realidad es bastante fácil, pero en iOS parece que sería mucho más engorroso por cosas como KTRR.
    XNU técnicamente es open source, pero fue más fácil aplicar el parche con un editor hexadecimal que recompilarlo.

    • También se puede usar el kernel task port para voltear un bit en la estructura proc.
      Además, se pueden permitir páginas de código sin firmar y RWX, lo que también habilita JIT.
  • Si “al ejecutarla con el jailbreak activado se crashea todo el teléfono”, ¿no bastaría con reportarla a la tienda como malware?
    Hacer que el teléfono crashee es claramente un comportamiento de malware, y habría que sospechar qué otras acciones maliciosas podría estar intentando ocultar.
    ¿No se supone que impedir esta basura era la justificación del ecosistema cerrado de Apple?

    • “Con el jailbreak activado” significa que estás fuera del ecosistema cerrado.
      No creo que a Apple le importe demasiado que una app crashee en un teléfono con jailbreak.
  • Me da mucha curiosidad la notificación com.apple.tw.twrr.
    ¿Por qué empieza con com.apple?
    La app en cuestión aquí no parece ser una app de Apple, sino una llamada Top Widgets.

    • El nombre de la notificación es una cadena arbitraria.
      Es habitual usar un “nombre completo” de ese estilo para evitar colisiones.
      En este caso, parece que el desarrollador eligió ese prefijo por casualidad.
  • Con herramientas distintas, esto se siente exactamente igual que romper la protección anticopia de Apple II en los años 80 mediante rastreo de arranque.
    Algunas cosas no cambian.