- 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
ptraceo con una llamada directa al sistema con el mismo efecto (svc #0x80), por lo que no se detecta solo con un breakpoint simple sobreptrace - La llamada directa al sistema se evita buscando en el binario el patrón
mov w16, #26, poniendo un breakpoint en la posición desvcy usandolldb jumppara 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 conthread returnal 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, ejecutardebugservery depurar la app conectándose desdelldben otra computadora - Si se intenta adjuntar
debugservera esta app del mismo modo, ocurre un Segmentation fault y el attach falla - La causa está relacionada con la solicitud
PT_DENY_ATTACHdeptraceptracees una API privada en iOS y pública en macOSPT_DENY_ATTACHestablece 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
ptracedelibsystem_kernel.dylibusandodlopenydlsym - Este método puede evitarse relativamente fácil poniendo un breakpoint en
ptracey saltando la llamada conthread return
- Como es una API privada de iOS, para la llamada real hay que localizar el símbolo
Por qué no funcionó la evasión fácil
PT_DENY_ATTACHsolo 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
debugserverdirectamente al proceso se lo ejecuta primero y luego enlldbse espera el arranque de la app conprocess 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 ptraceal 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
ptracecontiene como parte central la llamada al sistemasvc #0x80- en
x0va el valor31, que corresponde aPT_DENY_ATTACH - en
x1,x2yx3van los argumentos no usados,0 - en
x16va26, el número de llamada al sistema deptrace
- en
- La app puede no llamar a la función
ptracey, en cambio, configurar esos mismos valores de registros con inline assembly antes de ejecutar directamentesvc #0x80- Este método evita búsquedas sospechosas de APIs privadas como
dlopenydlsym - También hace difícil atraparlo poniendo breakpoints sobre una función compartida como
ptrace
- Este método evita búsquedas sospechosas de APIs privadas como
- 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, #26o 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 D2demov x16, #26para usarla en la búsqueda del binario - No hubo resultados para
mov x16, #26, pero al buscarmov w16, #26aparecieron 4 resultados
- En armconverter.com se puede obtener la secuencia de bytes
- 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, #0x1FMOV X1, #0MOV X2, #0MOV X3, #0MOV W16, #0x1ASVC 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
svcvistas en el desensamblador son0x102A2BB14y0x102A2BB68 - En
lldb, para convertir direcciones basadas en el binario a direcciones reales cargadas, se configuran breakpoints agregando-s TopWidgetbr s -a 0x102A2BB14 -s TopWidgetbr 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 *0x10327bb18hacia la dirección siguiente a la instrucción actual
- En el ejemplo se ejecuta
- 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
lldbseguía adjunto, así que se pudo comprobar que el proceso recibióSIGKILLy revisar el stacktrace - En el stacktrace aparecía un flujo relacionado con captura de pantalla
QuartzCoreconCARenderServerSnapshotUIKitCorecon_UISnapshotScreenWindowsRectAfterCommit- una unnamed symbol interna de
TopWidget
- Con
lldb image lookupse convirtió la dirección en tiempo de ejecución a la dirección basada en binario0x100041898, 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
- llamar a
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.twrry 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 returncuando 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ónBRK, lo que coincide con un crash intencional como un force-unwrap denil - 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
nilen 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:deNSFileManager- Cuando se llama al método original, se ejecuta el método de reemplazo
- El método de reemplazo devuelve
temporaryDirectoryen 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
Flexinyectado
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
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.
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.
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...
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, #26en el código en lugar desvc 0x80.svc 0x80es 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.
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.
Me gustó que también ofrecieras una versión en texto.
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.
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.
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.
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
.isode Windows, y resultó que efectivamente era eso, y se usaba para un widget de prueba de velocidad de red.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.
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?
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.
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.