Mload: la llave del starlet (segunda parte)
Para consultas y debates sobre el contenido de este artículo, visita el Hilo oficial.
Contenido |
Introducción
- En la primera parte se ha visto lo suficiente para entender cómo podemos construir nuestros propios módulos, donde cargarlos y la forma de cargarlos mediante las herramientas que nos proporciona mload. Pero queremos llegar aún más lejos: ya que hemos podido acceder al Starlet con la posibilidad de cargar nuestro propio código ¿por qué no contar con unas herramientas que nos permitan incluso modificar el sistema y saltar las limitaciones que nos impone?.
- Para entender lo siguiente de forma más completa, nos viene de perlas una información que se puede encontrar en wiibrew en lo referente al Starlet, concretamente éste par de enlaces:
- También hay un enlace sobre la arquitectura en general: http://www.arm.com/miscPDFs/14128.pdf
Modos de ejecución y vectores de excepción
- Nuestros módulos se ejecutan en "Modo Usuario", en el cual no tenemos permisos para modificar otras zonas de memoria o modificar ciertos registros y por tanto, no podemos mas que observar el contenido y en ocasiones, ni eso y la única forma de poder hacer algo, es mediante las syscalls que disponemos... ¿pero realmente, es la única forma?.
- Obviamente, se hace necesario ganar dichos permisos, de hecho, las syscalls que se han descrito anteriormente, no son más que un vía para acceder a una excepción que se produce cuando el ARM intenta ejecutar una instrucción no definida (es decir, una instrucción máquina inexistente), lo que provoca un cambio de modo del procesador y un salto a un punto determinado de la memoria, donde si se dispone de algunos de los permisos que necesitamos para hacer algunas de las cosas interesantes que buscamos.
- En Wii la tabla de excepciones se localiza a partir de la dirección 0xFFFF0000 y según el PDF 2, página 70, estos son los vectores que contiene:
OFFSET 0xFFFF0000 Reset (Modo Supervisor) OFFSET 0xFFFF0004 Instrucción Indefinida (Modo Indefinido). Las syscalls de Wii que conocemos OFFSET 0xFFFF0008 Interrupción de Software SWI (Modo Supervisor): Salta a una instrucción de retorno en los IOS, excepto en mload OFFSET 0xFFFF000C Abort (Prefech) (Modo Abort) OFFSET 0xFFFF0010 Abort (Data) (Modo Abort) OFFSET 0xFFFF0014 Reserved OFFSET 0xFFFF0018 IRQ: Interrupciones (Modo IRQ) OFFSET 0xFFFF001C FIQ: Interrupciones rápidas (Modo FIQ). En Wii sin uso |
- Como podemos observar, cada excepción entra en un determinado modo del procesador cuya principal diferencia es, aparte de contar con una serie de permisos que no se tienen en modo de usuario, contar con una serie de registros adicionales, para almacenar la pila o de retorno distintos, aparte de contar con otros registros de estado. Además de los modos que podéis ver ahí, también contamos con el Modo Sistema, que se apoya en los registros del Modo Usuario, aunque evidentemente, la diferencia es que contamos con permisos de los que no disponemos en el Modo Usuario.
- Algo importante que conviene saber, es que cada vez que se produce una excepción se deshabilitan las interrupciones en el registro CPSR.
- Si queréis echarle un vistazo a la tabla de registros disponibles para los distintos modos, en el PDF 2, página 55 la podemos encontrar (modo ARM o 32 bits) y en la 57, la correspondiente al modo Thumb.
Por si no estáis familiarizados con el ARM, los ARM cuentan con un modo de instrucciones completo, de 32 bits y un modo de instrucciones reducidas, llamado Thumb de tan solo 16 bits, para economizar memoria y que tiene limitado también el acceso a ciertos registros. El código que compilamos en C se convierte en instrucciones Thumb, mientras que el modo ARM de 32 Bits, queda reservado para operaciones especiales (como las llamadas a las syscalls) o rutinas que queramos optimizar en velocidad. En los PDF podéis encontrar información de los distintos modos de operación, si estáis interesados.
- En el mismo PDF 2, página 58, podéis ver el registro de estado CPSR con la descripción de los distintos flags (Modo de ejecución, Thumb, Flags que deshabilitan IRQ y FIQ por software y los típicos flags de estado que tiene cualquier procesador).
El Club de la Lucha
- Según su autor, Hermes:
"La primera regla del Club de la Lucha, es que no se habla del Club de la Lucha"
Bien, aquí es donde rompo esa primera regla para contaros mis aventuras y desventuras en el desarrollo del driver EHCI y trabajando con el sistema.
Parece obvio que si queremos obtener permisos, deberíamos tratar o bien de buscar algún tipo de exploit raro que nos plantase en Modo Sistema (porque o yo estoy torpe, o en Modo Usuario el registro CPSR pasa de ti cuando tratas de cambiar de modo, cosa que es lógica porque si no ¿para qué sirven los permisos?) o mucho más fácil, ya que tenemos la posibilidad de crear nuestro cIOS, modificarlo en parte para que alguna de éstas excepciones que hemos visto, nos devolvieran el control.
Hace meses, le sugerí a Waninkoko que se podría utilizar las syscalls, ya fuera utilizando alguna "instrucción" que no se utilizara o modificar alguna de forma que pudiera cumplir con su cometido y además, nos proporcionase algún tipo de control, pero la verdad es que yo no estaba interesado en llevarlo a cabo personalmente y tenía otras preocupaciones.
Pero lamentablemente, el driver EHCI me daba muchos problemas de cuelgues raros que parecían debidos a problemas de sincronización con otros hilos o interrupciones en puntos incómodos (parece ser que es un bug en IOS 36, pero ¿como iba a suponer yo eso?) y cuando ya me había contenido como 100 veces de estampar la Wii contra la pared, fruto de la impotencia y la desesperación, pensé en mirar la forma de usar interrupciones.
Desensamblando a partir del vector de las syscalls, encuentras fácilmente la tabla de saltos de las funciones de tratamiento de dichas syscalls y en concreto la función correspondiente a os_software_IRQ(), que es la fallaba a la hora de fijar la IRQ 4.
Curiosamente, ésta función utiliza también una tabla de saltos para tratar las diferentes interrupciones y fue fácil darse cuenta que permite fijarlas a partir de IRQ 4 y que había una serie de interrupciones que utilizaban una rutina común de retorno...
Mirando la IRQ 4, vi que hacía un chequeo que si no se cumplía, no permitía fijar los flags correspondientes para dicha IRQ. Así que calculé el salto necesario para esquivar ese chequeo al mismo tiempo que adoptaba la IRQ 9, que estaba fuera de uso, para que saltara a un punto del crt0.s de mload, con el fin de obtener permisos (son los dos parcheos actuales que se hacen en el cIOS installer)
El resultado es que a pesar de que la IRQ 4 estaba habilitada, no funcionaba (porque me faltaba una información que provocó que creara éste hilo de divagaciones sobre ehci y su vector de interrupción), pero la IRQ 9 si que funcionaba perfectamente, como mostraba el LED frontal encendido (es uno de los registros prohibidos en Modo Usuario).
Gracias a isobel supe lo que necesitaba para poder poner en marcha las interrupciones (un flag en uno de los registros EHCI reservados), pero ni se resolvían los problemas y además, tenía otros nuevos, debido a la posibilidad de que se produjeran varias interrupciones de golpe (sobre todo cuando pasé a utilizar mi propio vector de interrupciones).
Sin embargo, desensamblar programas produce que indirectamente, observes cosas que no vas buscando. Entre eso y los PDF, ya me había dado cuenta que el vector de SWI estaba fuera de uso, que aquí se llegaba con las interrupciones deshabilitadas, en diferente modo que con las syscalls y que podría utilizar una serie de instrucciones especiales propias para llamar a las diferentes rutinas. Sobre todo necesitaba acceder a las syscalls relacionadas con la caché y que esto no supusiera una complicación extrema.
La verdad es que recuerdo una conversación con marcan de hace mucho, sobre el lío que estaban suponiendo las funciones de caché (invalidate/flush) por que aparte de lo preceptivo en el ARM, había que tontear con una serie de registros desconocidos, de cuando él desarrollaba bootmii. De hecho, si le echáis un ojo a MINI podéis ver que llama a una función más rara que un perro verde, para cerrar dichas operaciones.
Así que una de mis ideas era ver si podía de alguna manera, llamar a las syscalls sin tener que recurrir a usar la tabla de syscalls para llamarlas de forma directa (que en cada IOS suele estar colocada en un sitio diferente) o tener que copiar parte de MINI... al menos para ese tipo de operaciones de caché (necesarias si vas a modificar cosas en caliente). Y eso hacía aún mas interesante trabajar con SWI en vez de con os_software_IRQ(9), que además, se cepillaba uno de los parámetros pasados como registros en dicha función. Además existía el atractivo de poder capturar mensajes del sistema, pues yo sabía del uso de svc 0xab en la función os_puts() de libcios y ya había visto referencias a ella en los módulos.
Total que ese fue el origen del vector de tratamiento de SWI, donde deposité parte de mi confianza, en la creencia de que si se deshabilitaba las interrupciones en determinados puntos críticos, tal vez se resolvieran los errores del driver EHCI tan extraños (lo mismo tenías un error al minuto, que lo tenías al cabo de una hora... y después cinco errores en un intervalo muy corto y luego otra hora y un bloqueo que requería desenchufar. Algo muy raro)
Pronto me dí cuenta que no iba a ser posible utilizar las syscalls como syscalls (se pasa de Modo Indefinido a Modo Sistema y eso hace que por ejemplo, la pila no coincida, ni el retorno), e incluso que estando en "Modo Dios", que no podía acceder a determinados puntos de la memoria (por ejemplo, no podía parchear dev/es), aunque afortunadamente, el propio vector IRQ ya te mostraba como fijaba los permisos de Acceso en Modo Cliente para todos los dominios y los PDF contenían información sobre ello (PDF 1, página 47. En el crt0.s de mload, podeis ver dos funciones read_access_perm() y write_access_perm() relacionadas con ésto)
Al final tenía todo operativo, pero seguía teniendo un driver inestable... entonces se me ocurrió probar con IOS 38, dado que ya tenía su modulo DIP adaptado para funcionar con uLoader y joder, era muy raro que fuera un problema del driver y ya se me habían acabado las posibilidades (era eso, o golpear la Wii contra la pared, seguida por el disco duro ) y..... EUREKA! ni un error de lectura y todo estable: ya solo quedaba limpiar el código de cosas que no fueran necesarias y tratar de añadir al nuevo servicio SWI todo lo que pudiera ser útil y necesario, no solo para trabajar con el driver EHCI o con uLoader, si no con vistas a un uso más profundo y general.
"La regla de oro del Club de la Lucha, es que si no te rindes, puede que no consigas lo que esperabas en un principio, pero también puedes obtener otras recompensas a cambio. A veces incluso, tienes algo de suerte y te llevas mucho más de lo que esperabas en un principio"
Gracias a eso, ahora tengo un driver muy estable, sin un solo error de lectura en mis unidades, trabajando por interrupciones y un conjunto de herramientas que me permiten un acceso pleno y la posibilidad de modificar los IOS en caliente (eso si: después de una buena carga de trabajo y poner en peligro mi estabilidad mental, la salud de la Wii, del disco duro y de la pared)
Los Servicios SWI de mload
- El vector SWI es capturado y preparado para responder a la instrucción en ensamblador "svc x" (también se puede escribir como "swi x"). Al llamar a la instrucción, se produce una excepción y es responsabilidad de la función de tratamiento de dicha excepción, buscar el código de instrucción y obtener el argumento "x". La instrucción svc existe también en modo thumb, pero debido a la limitación de la excepción o a algún problema, solo se ha conseguido que funcione si está alineada a 4 bytes. De todas formas, es preferible usar la versión de 32 bits.
- Conviene saber que las interrupciones deben seguir deshabilitadas por software y que se dispone una pila de 0x900 bytes para trabajar con las diferentes funciones SWI, de los cuales, se gastarán unos cuantos en guardar registros, etc, por lo que hay que tener especial cuidado en procurar que las variables locales, no ocupen mucho espacio.
No se debe llamar a una función que hace uso de SWI dentro de una función SWI.
- Los servicios de mload, se ofrecen en "svc 0xcc" y en libcios. Se dispone de la librería swi_mload para facilitar el acceso a esos servicios. Evidentemente, solo se puede acceder a estas funciones desde el Starlet.
swi_mload
- Estas son las funciones ofrecidas:
void swi_mload_add_handler(u8 svc_code, int (*func) (u32 arg0, u32 arg1, u32 arg2, u32 arg3)); |
Función que permite añadir un handler para tratar las diferentes funciones "svc x", donde x puede se cualquier valor entre 0x0 y 0xff, excepto 0xcc que siempre estará controlado por mload. La razón de ésta función es permitir que otros módulos puedan añadir su propio vector para intercambiar datos, por ejemplo. La función "svc 0xab" se encuentra registrada internamente para redirigir os_puts(), pero puede ser cambiada gracias a ésta función, para ser tratada desde otro lugar.
svc_code: desde 0x0 a 0xff exceptuando 0xcc. Por ejemplo 0xab para "svc 0xab" func: función que tratará el nuevo servicio SWI. La función recibe los argumentos como registros, desde el r0 al r3 y se ejecutará en Modo Supervisor, por lo que acceder a otros argumentos extras, sería algo complicado (las funciones de C reciben los 4 primeros argumentos en los registros r0 a r3, y el resto en el stack). El valor de retorno es retornado como en cualquier otra función de C (registro r0) ret: no retorna nada |
void * swi_mload_EHCI_data(void); |
Esta función retorna la estructura EHCI pre-inicializada desde mload (es lo mismo que se obtenía utilizando la función equivalente en dev/mload)
Su uso se limita a ehcmodule.elf |
u32 swi_mload_get_syscall_base(void); |
Obtiene la dirección de la tabla de las funciones de syscall. A partir de ésta dirección, podemos llamar a algunas syscalls de forma directa.
Por ejemplo, si queremos llamar a os_sync_before_read(), debemos llamar a la syscall 0x3f. Partiendo de la dirección que devuelve ésta función, podemos calcular que: syscall_base+ 0x3f*4, es la dirección de la función que necesitamos. Mas adelante, describiré éste acceso directo de forma practica, aunque podéis observarlo tanto en el crt0.s y el main de ehcmodule, como en los fuentes de mload. ret: Dirección de la tabla de funciones de las syscalls |
u32 swi_mload_get_ios_base(void); |
Retorna la versión usada como IOS base (actualmente 38). Puede detectar IOS 36,37,38 y 60. Es útil para poder adaptar los parches que requiramos de forma dinámica (por ejemplo, ehcmodule utiliza ésta función para adaptar la rutina que tratará el vector de interrupción, para cada IOS)
ret: IOS base. |
void swi_mload_memcpy(void * dst, void * src, int len); |
Función que copia un bloque de memoria desde la dirección fuente a la dirección destino
Lo que la hace especial es:
En la práctica equivale a copiar un bloque de memoria dentro de la caché a otro punto sin que se produzcan interrupciones, ni fallos de acceso (por falta de permisos) y sincronizando los datos de la caché en escritura con la memoria RAM (para poder utilizarlos con una DMA, por ejemplo) |
void swi_mload_memcpy_from_uncached(void * dst, void * src, int len); |
Función que copia un bloque de memoria desde la dirección fuente a la dirección destino, invalidando primero, los datos de la caché desde la dirección fuente
Lo que la hace especial es:
En la práctica equivale a copiar un bloque de memoria desde fuera de la caché (datos procedentes de una DMA, por ejemplo) a otro punto sin que se produzcan interrupciones, ni fallos de acceso (por falta de permisos) y sincronizando los datos de la caché en escritura con la memoria RAM. |
u32 swi_mload_get_register(u32 addr); |
Lectura de un registro de 32 bits
addr: dirección del registro ret: valor del registro |
void swi_mload_put_register(u32 addr, u32 val); |
Escritura de un registro de 32 bits
addr: dirección del registro val: valor a escribir ret: No devuelve nada |
void swi_mload_set_register(u32 addr, u32 val); |
Fija bits en un registro de 32 bits. Equivale a *(addr)'|=val; Los bits a 1 se fijan
addr: dirección del registro val: valor con los bits a fijar ret: No devuelve nada |
void swi_mload_clr_register(u32 addr, u32 val); |
Borrar bits en un registro de 32 bits. Equivale a *(addr)&=~val. Los bits a 1 se borran
addr: dirección del registro val: valor con los bits a borrar ret: No devuelve nada |
int swi_mload_call_func(int (*func) (void *in, void *out), void *in, void *out); |
Función que se utiliza para invocar a otra función que proporcione el usuario, que será llamada en Modo Supervisor, con las Interrupciones desconectadas. Es muy importante que las interrupciones permanezcan deshabilitadas dentro de dicha función (no se debe modificar los flags del registro CPSR. Gracias a ésta función, se hace innecesario ampliar los servicios SWI de mload en el futuro. Se dispone de una pila con 0x900 bytes, así que mucho cuidado con el tamaño de las variables locales. Tambien se recomienda que las funciones sean relativamente rápidas, para no interferir con la respuesta a las interrupciones
fun: función que será llamada desde el modo supervisor. in: parámetro pensado como de entrada (puede ser NULL o un dato pasado como dirección o la dirección de un dato o datos) out: parámetro pensado como de salida (puede ser NULL o un dato pasado como dirección o la dirección de un dato o datos) ret: retorna el valor devuelto por fun(in,out) NOTA: Obviamente, no hay una regla escrita que diga que los parámetros in y out no puedan ser utilizados ambos como entrada o ambos como salida, por ejemplo. Su designación actual es a título informativo |
void swi_mload_led_on(void); void swi_mload_led_off(void); void swi_mload_led_blink(void); |
Funciones que permiten controlar, el encendido, apagado y la alternancia (parpadeo), del led frontal.
Solo destacar que la función blink no produce un parpadeo de forma automática, si no que alterna entre encendido y apagado cada vez que es llamada. |
void swi_mload_os_software_IRQ9_func( int (*system_mode_func)(void)); |
Función que permite registrar una función que responderá a la llamada de os_software_IRQ(9). Es decir, si se llama a dicha syscall, la función pasada como argumento será llamada en Modo Sistema y su valor de retorno, se tomará como el retorno de os_software_IRQ(9).
system_mode_fun: función que recibirá el foco al llamar a os_software_IRQ(9) ret: No retorna nada. |
void * swi_mload_log_func(u32 mode, void *buffer_log, int maxsize_log); |
Función para trabajar con el buffer que recibe el texto de log desde la función os_puts().
mode: Modo de operación: 0-> retorna la dirección del log buffer, 1-> borra el log buffer 2-> Asigna un nuevo log buffer buffer_log: dirección del nuevo log buffer (solo con mode==2) maxsize_log: tamaño del nuevo log buffer (solo con mode==2) ret: dirección del actual log buffer (por defecto 4KB). El log buffer es una cadena de texto terminada con el carácter '\0' |
Trabajando en Modo Supervisor
- La función swi_mload_call_func() y la función swi_mload_add_handler() nos muestran las vías para entrar en Modo Supervisor. Una vez dentro, no podemos utilizar llamadas a syscall y tal vez necesitemos acceder a características especiales que si bien, no se puede proporcionar todos los datos, si se puede proporcionar una serie de funciones que pueden ser necesarias y útiles.
Leyendo y escribiendo los permisos de acceso
- En el crt0.s de mload, podemos ver:
.align 4 .code 32 .global read_access_perm read_access_perm: mrc p15, 0, r0,c3,c0 bx lr .align 4 .code 32 .global write_access_perm write_access_perm: mcr p15, 0, r0,c3,c0 bx lr
- Las funciones se definen como:
u32 read_access_perm(void); |
Lee los permisos actuales. |
void write_access_perm(u32 flags); |
Fija los permisos |
- Y se puede tener acceso completo con write_access_perm(0xffffffff);. Evidentemente, conviene restablecer los permisos una vez finalizado lo que quiera que estemos haciendo (normalmente, esto solo se utiliza cuando toquemos algún área de memoria prohibida). La descripción de los permisos, se puede ver en PDF 1, página 47.
Invalidar la caché de instrucciones
- Cuando queramos hacer algún tipo de parche en el código en ejecución, nos puede interesar asegurarnos de que la caché de instrucciones se actualiza con los valores adecuados. Normalmente, podemos controlar la caché de datos, pero no se dispone de ninguna syscall que haga lo mismo con la caché de instrucciones por lo que debemos ocuparnos nosotros mismos. En el PDF 1, página 51 podemos ver las instrucciones del procesador necesarias.
- En crt0.s de mload podemos ver:
.align 4 .code 32 .global ic_invalidate ic_invalidate: mov r0, #0 mcr p15, 0, r0, c7, c5, 0 bx lr
- Y se define así:
void ic_invalidate(void); |
Función que invalida toda la caché de instrucciones. |
- El problema es que teóricamente, en Wii hay que hacer algo más para actualizar la caché, por lo que lo mejor sería es invocar ésta función y después, llamar a las instrucciones de caché de datos con las que contamos sobre la zona modificada, por si las moscas.
Acceso a Syscalls
- Pero ¿Con que funciones contamos para trabajar con la caché de datos, si no podemos llamar a las syscalls de forma directa?. Ahí es donde interviene la función swi_mload_get_syscall_base().
- Si mirais el main.c y el crt0.s de ehcmodule, podeis ver que en el main.c se declara ésta variable:
u32 syscall_base;
y desde el main() se llama a dicha función de ésta forma (una sola vez):
syscall_base=swi_mload_get_syscall_base(); // obtenemos la dirección de la tabla de syscalls os_sync_after_write((void *) &syscall_base, 4); // aseguramos la concordancia entre caché y memoria
- Con esto ya hemos ajustado lo necesario para poder invocar syscalls de forma directa, utilizando su tabla de saltos. En el crt0.s podemos observar:
.align 4 .code 32 .global direct_syscall direct_syscall: ldr r12, =syscall_base ldr r12, [r12] nop ldr r12, [r12,r11,lsl#2] nop bx r12 .align 4 .code 32 .global direct_os_sync_before_read direct_os_sync_before_read: mov r11, #0x3f b direct_syscall .align 4 .code 32 .global direct_os_sync_after_write direct_os_sync_after_write: mov r11, #0x40 b direct_syscall
- Como se puede observar, se utiliza el registro r11 para contener el índice a la syscall, mientras el registro r12 se utiliza para almacenar la dirección de la tabla y calcular el desplazamiento necesario para poder invocar a la función de syscall requerida.
Tenéis que tener en cuenta que las syscalls relacionadas con los hilos, seguramente fallen, al no estar trabajando en el contexto adecuado, pero realmente, no tenéis por que usarlas aquí.
- Las syscalls que os he mostrado, se declaran así:
void direct_os_sync_before_read(void* ptr, int size); |
Equivalente a os_sync_before_read() |
void direct_os_sync_after_write(void* ptr, int size); |
Equivalente a os_sync_after_write() |
El vector de interrupción
- En crt0.s de ehcmodule, se puede observar la rutina interrupt_vector(). La rutina está ajustada para trabajar con IOS 36, pero como se puede observar en main.c en la función copy_int_vect() se llevan a cabo unos parches en crt0.s para poder ajustar la rutina para trabajar con IOS 36,37,38 y 60.
- El modo de capturar el vector se realiza en la instrucción "tst r8, #0x1" del vector original, modificando esa instrucción y la siguiente para obtener un "ldr pc, =interrupt_vector" . Podéis ver en main.c "static u32 vector[2]={ 0xE51FF004, 0};" utilizada para llevar a cabo los parches necesarios (el segundo valor, sería la dirección de salto)
- El registro r8, almacena los flags de interrupción, después de haber sido enmascarados con la máscara de interrupciones. Por tanto, un bit alzado significa una interrupción en curso.
- El vector de interrupciones hace una serie de comprobaciones en cadena y entre ellas está la interrupción EHCI, que responde a la instrucción "tst r8, #0x10"
- Por ello hacemos dicha comprobación y si no se ha producido interrupción, hacemos la comprobación "tst r8, #0x1" original (vector del timer) y saltamos al punto conveniente del vector original en virtud al resultado que nos de.
- Si la interrupción EHCI está en proceso, la instrucción "bic r8, r8, #0x10" se encarga de que no se vuelva a tratar dicha interrupción más adelante, cuando estemos de vuelta en el vector original de interrupción.
- Las intrucciones "mov r2, #0x10" y "str r2, [r7]" tienen el cometido de anular el flag de interrupción en el registro de interrupciones (registro 0x0d800038, podéis observar dichos registros en libcios, concretamente en starlet.h)
- Después observamos una serie de instrucciones para preservar la pila, cambiarla por otra interna (de 0x200 bytes) y como preservamos una serie de registros, antes de saltar a _ehci_vector_ , desde la que se volverá a saltar a ehci_vector(), en modo thumb (ehci_vector() se declara en ehci_interrupt.c)
- A la vuelta se hace un test con el retorno: si se retorna 1, se llamara a la función int_send_device_message(4) después de haber restablecido la pila, (opción que ahora mismo no se utiliza ya que se invoca a dicha función desde el propio ehci_vector()) pasando después a tratar "tst r8, #0x1", (la interrupción del timer) y retornando en consecuencias al vector original
- La función int_send_device_message() tiene el cometido de enviar un mensaje a la cola asociada al dispositivo, si éste se registro con la syscall os_register_event_handler(). En ehcmodule, cuando estamos transfiriendo datos, el hilo ajusta un timer (timeout) y aguarda a que la cola asociada reciba un mensaje, ya sea desde la interrupción o del timer.
- Espero que con éstas notas, sepáis entender como trabaja el vector de interrupciones. Obviamente, se hace necesario saber algo de ensamblador de ARM, disponer de un buen desensamblador (como IDA Pro) y hacer lo propio partiendo desde la tabla de excepciones que os puse arriba, y en concreto, desde el vector IRQ para tener una vista más apropiada del tema.
DIP Plugin (cIOS 222/223)
- dip_plugin es un módulo binario (no usa formato .elf) desarrollado por Wiigator / Waninkoko para conectar con el módulo DIP y redirigir sus llamadas de forma que podamos simular la lectura de un DVD desde un dispositivo USB, por ejemplo. El código fuente es semi-privado y el autor original no quiere hacerlo público, por lo Hermes ideó un sistema que permitiera adaptarlo a los diferentes IOS y así daros la posibilidad de poder investigar por vuestra cuenta.
- dip_plugin se aloja en la dirección 0x1377E000 de la memoria compartida de mload. Es decir, se aloja al final de la memoria disponible.
dip_plugin necesita conectar con diferentes funciones del módulo DIP, de forma directa, así que Hermes recurrió a un sencillo sistema que incluía una tabla con las direcciones de las diferentes rutinas.
- Por defecto dip_plugin tiene fijada su tabla para trabajar con el módulo DIP de IOS36, así pues si queremos trabajar con el módulo DIP de IOS38 por ejemplo (el módulo actual, pues el IOS base es el 38), tenemos que identificar el módulo DIP rastreando en la memoria del Starlet en búsqueda de un patrón conocido.
Identificando el módulo DIP
- En el caso de uLoader, está preparado para identificar el DIP de IOS 36 y 38. Lo hace de ésta forma:
mload_seek(0x20207c84, SEEK_SET); // dirección en IOS 38 de la cadena DIP a buscar mload_read(patch_datas, 4); // lee 4 bytes sobre un buffer temporal alineado a 32 bytes if(patch_datas[0]==0x6e657665) { is_ios=38; // es IOS 38 } else { is_ios=36; // suponemos que es IOS 36 }
La tabla de direcciones
- El inicio de dip_plugin se compone de una tabla de direcciones con entradas de 4 bytes, obviamente.
+0x00 DI_EmulateCmd -> Función de entrada en dip_plugin (Thumb) +0x04 0x12340001 -> ID de dip_plugin +0x08 dvd_read_controlling_data -> dirección de buffer de datos en dip_plugin (IOS 36: 0x2022DDAC IOS 38: 0x2022cdac) +0x0c handle_di_cmd_reentry+1 -> Punto de retorno a DIP (Thumb) (IOS 36: 0x20201010+1 IOS 38: 0x20200d38+1) +0x10 addr_ios_shared_alloc_aligned+1 -> Función DIP (Thumb) (IOS 36: 0x20200b9c+1 IOS 38: 0x202008c4+1) +0x14 addr_ios_shared_free+1 -> Función DIP (Thumb) (IOS 36: 0x20200b70+1 IOS 38: 0x20200898+1) +0x18 addr_ios_memcpy+1 -> Función DIP (Thumb) (IOS 36: 0x20205dc0+1 IOS 38: 0x20205b80+1) +0x1c addr_ios_fatal_di_error+1 -> Función DIP (Thumb) (IOS 36: 0x20200048+1 IOS 38: 0x20200048+1) +0x20 addr_ios_doReadHashEncryptedState+1 -> Función DIP (Thumb) (IOS 36: 0x20202b4c+1 IOS 38: 0x20202874+1) +0x24 addr_ios_printf+1 -> Función DIP (Thumb) (IOS 36: 0x20203934+1 IOS 38: 0x2020365c+1) +0x28 Reserved +0x2c Reserved +0x30 Reserved +0x34 Reserved +0x38 Reserved +0x3c Reserved +0x40 in_ES_ioctlv -> Punto de entrada de la función ioctlv del módulo dev/es (se usa con 'Skip IOS' en uLoader) |
Conectando el módulo dip_plugin
- El módulo DIP es parcheado en el proceso de instalación con el fin de preparar un salto en una zona de memoria con acceso compartido dentro del propio modulo DIP, con el propósito de poder localizar y desviar fácilmente el curso de ejecución de DIP hacia dip_plugin.
Desde uLoader ésto se hace así:
- Las syscalls que os he mostrado, se declaran así:
IOS36 |
memcpy(ios_36, dip_plugin, 4); // copy the entry_point (preserves entry point) memcpy(dip_plugin, ios_36, 4*10); // copy the adresses from the array mload_seek(0x1377E000, SEEK_SET); // copy dip_plugin in the starlet mload_write(dip_plugin,size_dip_plugin); // enables DIP plugin mload_seek(0x20209040, SEEK_SET); // enables dip_plugin bypass mload_write(ios_36, 4); mload_close(); |
IOS38 |
memcpy(ios_38, dip_plugin, 4); // copy the entry_point (preserves entry point) memcpy(dip_plugin, ios_38, 4*10); // copy the adresses from the array mload_seek(0x1377E000, SEEK_SET); // copy dip_plugin in the starlet mload_write(dip_plugin,size_dip_plugin); // enables DIP plugin mload_seek(0x20208030, SEEK_SET); // enables dip_plugin bypass mload_write(ios_38, 4); mload_close(); |
- Como se puede apreciar, uLoader copia la dirección de la función de entrada de dip_plugin a una tabla, luego copia la tabla correspondiente sobre dip_plugin, después aloja dip_plugin en la memoria del Starlet y por último conecta dip_plugin copiando la dirección de su función de entrada en un punto determinado de memoria compartida en DIP (tendrás que mirar los parches DIP del instalador, desensamblar el módulo DIP (NUS Downloader te permite bajar los IOS desencriptados) y comparar las diferentes direcciones para comprender como trabaja exactamente, si quieres adaptarlo a otros IOS. Si solo buscas la forma de utilizarlo, esto servirá)
El parche dev/es de ioctlv
- Desde uLoader tenemos una opción llamada 'Skip IOS' que así como está implementada, tiene poca utilidad. Además, se redirige hacia dip_plugin para economizar y no tener que añadir otro módulo de corte similar.
La función consiste en capturar la función que trata ioctlv en dev/es. Si le echáis un ojo a libogc, podéis observar en es.c algunas funciones que podréis controlar haciendo éste desvío.
- Desde mload se hacen una serie de parches que provocan el desvío de ésta función a su crt0.s retornando normlamente, si no se programa una función de tratamiento.
La forma de hacerlo seria:
mload_set_ES_ioctlv_vector(in_ES_ioctlv); // thumb in_ES_ioctlv function mload_close();
- En el módulo del Starlet, ésta función sería:
asm.s
.align 2 .code 16 .global in_ES_ioctlv .thumb_func in_ES_ioctlv: push {r2-r6} push {lr} bl ES_ioctlv+1 pop {r1} pop {r2-r6} bx r1 .global out_ES_ioctlv .thumb_func out_ES_ioctlv: push {r4-r6,lr} sub sp, sp, #0x20 ldr r5, [r0,#8] add r1, r0, #0 ldr r3, = 0x201000D5 bx r3
es_ioctlv.c
extern int in_ES_ioctlv(void *); extern int out_ES_ioctlv(void *); struct _ioctl { void *data; u32 len; }; struct _ipcreq { //ipc struct size: 32 u32 cmd; //0 s32 result; //4 union { //8 s32 fd; u32 req_cmd; }; union { struct { char *filepath; u32 mode; } open; struct { void *data; u32 len; } read, write; struct { s32 where; s32 whence; } seek; struct { u32 ioctl; void *buffer_in; u32 len_in; void *buffer_io; u32 len_io; } ioctl; struct { u32 ioctl; u32 argcin; u32 argcio; struct _ioctl *argv; } ioctlv; u32 args[5]; }; } ATTRIBUTE_PACKED; int ES_ioctlv(struct _ipcreq *dat ) { int r; u32 ios,version; ios_sync_before_read(dat, sizeof(struct _ipcreq)); if(dat->ioctlv.ioctl==8) { // reboot ios_sync_before_read( (void *) dat->ioctlv.argv[0].data, dat->ioctlv.argv[0].len); ios=*(((volatile u32 *)dat->ioctlv.argv[0].data)+1) ; version=1; ios_sync_before_read((void *) 0x3140,8); *((volatile u32 *) 0x3140)= ((ios)<<16) | (version & 65535); // write fake IOS version/revision *((volatile u32 *) 0x3188)= ((ios)<<16) | (version & 65535); // write fake IOS version/revision ios_sync_after_write((void *) 0x3140,4); ios_sync_after_write((void *) 0x3188,4); return 0; } r=out_ES_ioctlv(dat); return r; }
- Esto es mas o menos, lo que sucede cuando se activa "Skip IOS" desde uLoader. El código lo podríais incluir en un modulo diferente e investigar otras cosas (la verdad, se le podría sacar un partido enorme a la función, como simular la presencia de IOS que no tenemos instalados y hacer que se ejecuten otros en su lugar, o incluso llevar a cabo la carga de un IOS modificado a conveniencia, aunque eso lo dejo para otros y seguro que lleva muucho trabajo de por medio).