Cómo descubrí un huevo de pascua en la seguridad de Android y no conseguí un trabajo en Google

Google ama los huevos de pascua. Los ama tanto, de hecho, que podría encontrarlos en prácticamente todos sus productos. La tradición de los huevos de pascua de Android comenzó en las primeras versiones del sistema operativo (creo que todos saben lo que sucede cuando ingresas a la configuración general y tocas el número de versión varias veces).

Pero a veces puedes encontrar un huevo de pascua en el lugar más improbable. Incluso hay una leyenda urbana que un día, un programador buscó en Google "bloqueo de mutex", pero en lugar de los resultados de búsqueda aterrizó en foo.bar, resolvió todas las tareas y consiguió un trabajo en Google.

Reconstrucción
imagen

Me pasó lo mismo (excepto sin el final feliz). Mensajes ocultos donde definitivamente no podría haber ninguno, invirtiendo el código Java y sus bibliotecas nativas, una máquina virtual secreta, una entrevista de Google, todo eso está a continuación.

Droidguard


Una noche aburrida restablecí mi teléfono de fábrica y pude configurarlo de nuevo. Lo primero es lo primero, una nueva instalación de Android me pidió que inicie sesión en la cuenta de Google. Y me preguntaba: ¿cómo funciona el proceso de inicio de sesión en Android? Y la noche de repente se volvió menos aburrida.

Utilizo Burps Suite de PortSwigger para interceptar y analizar el tráfico de red. La versión comunitaria gratuita es suficiente para nuestros propósitos. Para ver las solicitudes https, primero debemos instalar el certificado de PortSwigger en el dispositivo. Como dispositivo de prueba, elegí un Samsung Galaxy S de 8 años con Android 4.4. Algo más nuevo que eso y es posible que tenga problemas con la fijación de certificados y otras cosas.

Honestamente, no hay nada particularmente especial con las solicitudes de API de Google. El dispositivo envía información sobre sí mismo y recibe tokens en respuesta ... El único paso curioso es una solicitud POST al servicio contra el abuso.



Después de que se realiza la solicitud, entre numerosos parámetros muy normales aparece uno interesante, llamado droidguard_result . Es una cadena Base64 muy larga:



DroidGuard es el mecanismo de Google para detectar bots y emuladores entre dispositivos reales. SafetyNet, por ejemplo, también usa los datos de DroidGuard. Google también tiene algo similar para los navegadores: Botguard.

Pero, ¿qué son esos datos? Averigüemos

Tampones de protocolo


¿Qué genera ese enlace ( www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI ) y qué dentro de Android hace esta solicitud? Después de una breve investigación, resultó que el enlace, en esta forma exacta, se encuentra dentro de una de las clases ofuscadas de Google Play Services:

public bdd(Context var1, bdh var2) { this(var1, "https://www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI", var2); } 

Como ya hemos visto en Burp, las solicitudes POST en este enlace tienen Content-Type - application / x-protobuf (Google Protocol Buffers, el protocolo de Google para la serialización binaria). Sin embargo, no es json: es difícil descubrir qué se envía exactamente.

Los búferes de protocolo funcionan así:

  • Primero describimos la estructura del mensaje en un formato especial y lo guardamos en un archivo .proto;
  • Luego compilamos archivos .proto, y el compilador de protocolos genera el código fuente en un idioma elegido (en el caso de Android es Java);
  • Finalmente, usamos las clases generadas en nuestro proyecto.

Tenemos dos formas de decodificar mensajes protobuf. El primero es usar un analizador de protobuf e intentar recrear la descripción original de los archivos .proto. El segundo es eliminar las clases generadas por el protocolo de los Servicios de Google Play, que es lo que decidí hacer.

Tomamos el archivo .apk de Google Play Services de la misma versión que está instalada en el dispositivo (o, si el dispositivo está rooteado, simplemente tome el archivo directamente desde allí). Usando dex2jar, convertimos el archivo .dex nuevamente en .jar y lo abrimos en un descompilador de elección. Personalmente me gusta el Fernflower de JetBrains. Funciona como un complemento para IntelliJ IDEA (o Android Studio), por lo que simplemente iniciamos Android Studio y abrimos el archivo con el enlace que estamos tratando de analizar. Si proguard no lo intentaba demasiado, el código Java descompilado para crear mensajes protobuf podría simplemente copiarse en su proyecto.

Mirando el código descompilado, vemos que Build. * Constantes se envían dentro del mensaje protobuf. (Está bien, eso no fue demasiado difícil de adivinar).

 ... var3.a("4.0.33 (910055-30)"); a(var3, "BOARD", Build.BOARD); a(var3, "BOOTLOADER", Build.BOOTLOADER); a(var3, "BRAND", Build.BRAND); a(var3, "CPU_ABI", Build.CPU_ABI); a(var3, "CPU_ABI2", Build.CPU_ABI2); a(var3, "DEVICE", Build.DEVICE); ... 

Pero desafortunadamente, en la respuesta del servidor, todos los campos de protobuf se convirtieron en sopa de letras después de la ofuscación. Pero podemos descubrir qué hay allí usando un controlador de errores. Así se verifican los datos que provienen del servidor:

 if (!var7.d()) { throw new bdf("byteCode"); } if (!var7.f()) { throw new bdf("vmUrl"); } if (!var7.h()) { throw new bdf("vmChecksum"); } if (!var7.j()) { throw new bdf("expiryTimeSecs"); } 

Aparentemente, así se llamaban los campos antes de la ofuscación: byteCode , vmUrl , vmChecksum y expiryTimeSecs . Este esquema de nombres ya nos da algunas ideas.

Combinamos todas las clases descompiladas de los Servicios de Google Play en un proyecto de prueba, les cambiamos el nombre, generamos Build Test *. Comandos y lanzamos (imitando cualquier dispositivo que queramos). Si alguien quiere hacerlo él mismo, aquí está el enlace a mi GitHub .

Si la solicitud es correcta, el servidor devuelve esto:
00: 06: 26.761 [main] INFO daresponse.AntiabuseResponse - byteCode size: 34446
00: 06: 26.761 [main] INFO daresponse.AntiabuseResponse - vmChecksum: C15E93CCFD9EF178293A2334A1C9F9B08F115993
00: 06: 26.761 [main] INFO daresponse.AntiabuseResponse - vmUrl: www.gstatic.com/droidguard/C15E93CCFD9EF178293A2334A1C9F9B08F115993
00: 06: 26.761 [main] INFO daresponse.AntiabuseResponse - expiryTimeSecs: 10

Paso 1 completo. Ahora veamos qué hay detrás del enlace vmUrl .

APK secreto


El enlace nos lleva directamente a un archivo .apk, llamado así por su propio hash SHA-1. Es bastante pequeño, solo 150 KB. Y está bastante justificado: si es descargado por cada uno de los 2 mil millones de dispositivos Android, eso es 270 TB de tráfico en los servicios de Google.



DroidGuardService clase DroidGuardService , que forma parte de los servicios de Google Play, descarga el archivo en el dispositivo, lo descomprime, extrae .dex y utiliza la clase com.google.ccc.abuse.droidguard.DroidGuard través de la reflexión. Si hay un error, DroidGuardService cambia de DroidGuard a Droidguasso. Pero esa es otra historia completamente.

Esencialmente, la clase DroidGuard es un simple contenedor JNI alrededor de la biblioteca nativa .so. El ABI de la biblioteca nativa coincide con lo que enviamos en el campo CPU_ABI en la solicitud de protobuf: podemos pedir armeabi, x86 o incluso MIPS.

El servicio DroidGuardService sí no contiene ninguna lógica interesante para trabajar con la clase DroidGuard . Simplemente crea una nueva instancia de DroidGuard , le envía el byteCode desde el mensaje protobuf, llama a un método público, que devuelve una matriz de bytes. Esta matriz se envía al servidor dentro del parámetro droidguard_result .

Para tener una idea aproximada de lo que ocurre dentro de DroidGuard , podemos repetir la lógica de DroidGuardService (pero sin descargar el .apk, ya que ya tenemos la biblioteca nativa). Podemos tomar un archivo .dex del APK secreto, convertirlo a .jar y luego usarlo en nuestro proyecto. El único problema es cómo la clase DroidGuard carga la biblioteca nativa. El bloque de inicialización estática llama al método loadDroidGuardLibrary() :

 static { try { loadDroidGuardLibrary(); } catch (Exception ex) { throw new RuntimeException(ex); } } 

Luego, el método loadDroidGuardLibrary() lee library.txt (ubicado en la raíz del archivo .apk) y carga la biblioteca con ese nombre a través de la llamada System.load(String filename) . No es muy conveniente para nosotros, ya que necesitaríamos construir el .apk de una manera muy específica para poner library.txt y el archivo .so en su raíz. Sería mucho más conveniente mantener el archivo .so en la carpeta lib y cargarlo a través de System.loadLibrary(String libname) .

No es difícil de hacer. Usaremos smali / baksmali - ensamblador / desensamblador para archivos .dex. Después de usarlo, classes.dex se convierte en un montón de archivos .smali. La clase com.google.ccc.abuse.droidguard.DroidGuard debe modificarse, de modo que el bloque de inicialización estática llame al método System.loadLibrary("droidguard") lugar de loadDroidGuardLibrary() . La sintaxis de Smali es bastante simple, el nuevo bloque de inicialización se ve así:

 .method static constructor <clinit>()V .locals 1 const-string v0, "droidguard" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V return-void .end method 

Luego usamos backsmali para construirlo todo en .dex, y luego lo convertimos en .jar. Al final obtenemos un archivo .jar que podemos usar en nuestro proyecto, aquí está , por cierto.

Toda la sección relacionada con DroidGuard tiene un par de cadenas de largo. La parte más importante es descargar la matriz de bytes que obtuvimos en el paso anterior después de abordar el servicio anti-abuso y entregarlo al constructor DroidGuard :

 private fun runDroidguard() { var byteCode: ByteArray? = loadBytecode("bytecode.base64"); byteCode?.let { val droidguard = DroidGuard(applicationContext, "addAccount", it) val params = mapOf("dg_email" to "test@gmail.com", "dg_gmsCoreVersion" to "910055-30", "dg_package" to "com.google.android.gms", "dg_androidId" to UUID.randomUUID().toString()) droidguard.init() val result = droidguard.ss(params) droidguard.close() } } 

Ahora podemos usar el perfilador de Android Studio y ver qué sucede durante el trabajo de DroidGuard:



El método nativo initNative () recopila datos sobre el dispositivo y llama a los métodos Java hasSystemFeature(), getMemoryInfo(), getPackageInfo() ... eso es algo, pero todavía no veo ninguna lógica sólida. Bueno, todo lo que queda es desmontar el archivo .so.

libdroidguard.so


Afortunadamente, analizar la biblioteca nativa no es más difícil que hacerlo con archivos .dex y .jar. Necesitaríamos una aplicación similar a la IDA de Hex-Rays y algunos conocimientos de código de ensamblador x86 o ARM. Elegí ARM, ya que tenía un dispositivo rooteado para depurar. Si no tiene una, puede tomar una biblioteca x86 y depurarla utilizando un emulador.

Una aplicación similar a Hex-Rays IDA descompila el binario en algo parecido al código C. Si abrimos el método Java_com_google_ccc_abuse_droidguard_DroidGuard_ssNative , veremos algo como esto:

 __int64 __fastcall Java_com_google_ccc_abuse_droidguard_DroidGuard_initNative(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9) ... v14 = (*(_DWORD *)v9 + 684))(v9, a5); v15 = (*(_DWORD *)v9 + 736))(v9, a5, 0); ... 

No parece demasiado prometedor. Primero necesitamos hacer un par de pasos preliminares para transformar eso en algo más útil. El descompilador no sabe nada sobre JNI, por lo que instalamos Android NDK e importamos el archivo jni.h. Como sabemos, los dos primeros parámetros de un método JNI son JNIEnv* y jobject (this) . Podemos encontrar los tipos de otros parámetros del código Java de DroidGuard. Después de asignar los tipos correctos, las compensaciones sin sentido se convierten en llamadas al método JNI:

 __int64 __fastcall Java_com_google_ccc_abuse_droidguard_DroidGuard_initNative(_JNIEnv *env, jobject thiz, jobject context, jstring flow, jbyteArray byteCode, jobject runtimeApi, jobject extras, jint loggingFd, int runningInAppSide) { ... programLength = _env->functions->GetArrayLength)(_env, byteCode); programBytes = (jbyte *)_env->functions->GetByteArrayElements)(_env, byteCode, 0); ... 

Si tenemos suficiente paciencia para rastrear la matriz de bytes recibida del servidor anti-abuso, estaremos ... decepcionados. Desafortunadamente, no hay una respuesta simple a "¿qué está pasando aquí?". Es código de bytes puro y destilado, y la biblioteca nativa es una máquina virtual. Parte del cifrado AES espolvoreado en la parte superior y luego la VM lee el código de bytes, byte por byte, y ejecuta comandos. Cada byte es un comando seguido de operandos. No hay muchos comandos, solo alrededor de 70: leer int, leer byte, leer cadena, llamar al método Java, multiplicar dos números, if-goto, etc.

Despierta neo


Decidí ir aún más lejos y descubrir la estructura del código de bytes para esta VM. Hay otro problema con las llamadas: a veces (una vez cada dos semanas) hay una nueva versión de la biblioteca nativa donde se mezclan los pares de bytes y comandos. No me detuvo y decidí recrear la VM usando Java.

Lo que hace el código de bytes es hacer todo el trabajo de rutina para recopilar información sobre el dispositivo. Por ejemplo, carga una cadena con el nombre de un método, obtiene su dirección a través de dlsym y se ejecuta. En mi versión Java de la VM, recreé solo unos 5 métodos y aprendí a interpretar los primeros 25 comandos del código de bytes del servicio contra el abuso. En el comando 26 la VM leyó otra cadena encriptada del código de bytes. De repente resultó que no es un nombre de otro método. Lejos de eso.
Comando de máquina virtual # 26
Invocación de método vm-> vm_method_table [2 * 0x77]
Método vmMethod_readString
el índice es 0x9d
la longitud de la cadena es 0x0066
(se genera una nueva clave)
los bytes de cadena codificados son EB 4E E6 DC 34 13 35 4A DD 55 B3 91 33 05 61 04 C0 54 FD 95 2F 18 72 04 C1 55 E1 92 28 11 66 04 DD 4F B3 94 33 04 35 0A C1 4E B2 DB 12 17 79 4F 92 55 FC DB 33 05 35 45 C6 01 F7 89 29 1F 71 43 C7 40 E1 9F 6B 1E 70 48 DE 4E B8 CD 75 44 23 14 85 14 A7 C2 7F 40 26 42 84 17 A2 BB 21 19 7A 43 DE 44 BD 98 29 1B
los bytes de cadena decodificados son 59 6F 75 27 72 65 20 6E 6F 74 20 6A 75 73 74 20 72 75 6E 6E 69 6E 67 20 73 74 72 69 6E 67 73 20 6F 6E 20 6F 75 72 20 2E 73 6F 21 20 54 61 6C 6B 20 74 6F 20 75 73 20 61 74 20 64 72 6F 69 64 67 75 61 72 64 2D 68 65 6C 6C 6F 2B 36 33 32 36 30 37 35 34 39 39 36 33 66 36 36 31 40 67 6F 6F 67 6C 65 2E 63 6F 6D
el valor de cadena decodificada es ( ¡No solo está ejecutando cadenas en nuestro .so! Hable con nosotros en droidguard@google.com )
Eso es extraño Una máquina virtual nunca me ha hablado antes. Pensé que si comienzas a ver mensajes secretos dirigidos a ti, te estás volviendo loco. Solo para asegurarme de que aún estaba cuerdo, ejecuté un par de cientos de respuestas diferentes del servicio contra el abuso a través de mi VM. Literalmente, cada 25-30 comandos había un mensaje oculto dentro del código de bytes. A menudo se repiten, pero a continuación hay algunos únicos. Sin embargo, edité las direcciones de correo electrónico: cada mensaje tenía una dirección diferente, algo así como "droidguard+tag@google.com", y la etiqueta era única para cada uno.
droidguard@google.com: ¡No seas un extraño!
¡Entraste! Hable con nosotros en droidguard@google.com
¡Saludos del viajero intrépido droidguard@google.com! Di hola!
¿Fue fácil encontrar esto? droidguard@google.com quisiera saber
¡La gente de droidguard@google.com agradecería saber de usted!
¿Qué es todo este gobbledygook? Pregunte a droidguard@google.com ... ¡lo sabrían!
Hey Me alegro de verte aquí. ¿Ya has hablado con droidguard@google.com?
No solo estás ejecutando cadenas en nuestro .so! Hable con nosotros en droidguard@google.com
¿Soy el elegido? Pensé que era hora de dejar de jugar con DroidGuard y hablar con Google, ya que me lo pidieron.

Tu llamada es muy importante para nosotros


Dije mis hallazgos en el correo electrónico que encontré. Para que los resultados sean un poco más impresionantes, automaticé un poco el proceso de análisis. La cuestión es que las cadenas y las matrices de bytes se almacenan en el código de bytes cifrado. La VM los decodifica usando constantes alineadas por el compilador. Usando una aplicación similar a Hex-Rays IDA puede extraerlos con bastante facilidad. Pero con cada nueva versión, las constantes cambian y es bastante inconveniente extraerlas siempre manualmente.

Pero analizar Java la biblioteca nativa resultó sorprendentemente simple. Usando jelf (una biblioteca para analizar archivos ELF) encontramos el desplazamiento del método Java_com_google_ccc_abuse_droidguard_DroidGuard_initNative en el binario, y luego usando Capstone (un marco de desensamblaje con enlaces para varios idiomas, incluido Java) obtenemos el código del ensamblador y buscamos constantes de carga en registros

Al final, obtuve una aplicación que emulaba todo el proceso de DroidGuard: realiza una solicitud al servicio contra el abuso, descarga el .apk, lo desempaqueta, analiza la biblioteca nativa, extrae las constantes necesarias, selecciona la asignación de los comandos de VM y interpreta el código de byte. Lo compilé todo y lo envié a Google. Mientras lo hacía, comencé a prepararme para una mudanza y busqué Glassdoor para obtener un salario promedio en Google. Decidí no aceptar nada menos de seis cifras.

La respuesta no tardó mucho. Un correo electrónico de un miembro del equipo de DroidGuard simplemente decía: "¿Por qué estás haciendo esto?"



"Porque puedo" - respondí. Un empleado de Google me explicó que se supone que DroidGuard protege a Android de los piratas informáticos (¡no lo dices!) Y sería prudente mantener el código fuente de mi DroidGuard VM para mí. Nuestra conversación terminó allí.

Entrevista


Un mes después recibí otro correo electrónico. El equipo DroidGuard en Zurich necesitaba un nuevo empleado. ¿Estaba interesado en unirme? Por supuesto!

No hay atajos para ingresar a Google. Todo lo que pude hacer fue enviar mi CV al departamento de recursos humanos. Después de eso, tuve que pasar por el habitual rigmarole burocrático y una serie de entrevistas.

Hay muchas historias sobre entrevistas de Google. Los algoritmos, las tareas de Olimpiadas y la programación de Google Docs no son lo mío, así que comencé mis preparativos. Leí docenas de veces el curso de "Algoritmos" de Coursera, resolví cientos de tareas en Hackerrank y aprendí a sortear un gráfico en ambas dimensiones con los ojos cerrados.

Pasaron dos meses. Decir que me sentí preparado sería quedarse corto. Google Docs se convirtió en mi IDE favorito. Sentí que sabía todo lo que hay que saber sobre algoritmos. Por supuesto, conocía mis debilidades y me di cuenta de que probablemente no pasaría la serie de 5 entrevistas en Zurich, pero ir al Disneyland del programador gratis era una recompensa en sí misma. El primer paso fue una entrevista telefónica para eliminar a los candidatos más débiles y no perder el tiempo de los desarrolladores de Zurich en reuniones en persona. El día estaba listo, sonó el teléfono ...



... e inmediatamente fallé mi primera prueba. Tuve suerte: hicieron una pregunta que he visto en Internet antes y que ya resolví. Se trataba de serializar un conjunto de cadenas. Ofrecí codificar cadenas en Base64 y guardarlas a través de un divisor. El entrevistador me pidió que desarrollara un algoritmo Base64. Después de eso, la entrevista se convirtió en una especie de monólogo, donde los entrevistados me explicaron cómo funciona Base64 y traté de recordar las operaciones de bit en Java.

Si alguien en Google está leyendo esto
¡Chicos, ustedes son genios sangrientos si llegaron allí! En serio No puedo imaginar cómo uno puede eliminar todos los obstáculos que ponen delante de usted.

3 días después de la llamada recibí un correo electrónico que decía que no querían entrevistarme más. Y así terminó mi comunicación con Google.

Por qué hay mensajes en DroidGuard que piden chatear, todavía no tengo idea. Probablemente solo por estadísticas. El tipo con el que escribí en primer lugar me dijo que la gente realmente escribe allí, pero la frecuencia varía: a veces reciben 3 respuestas en una semana, a veces 1 al año.

Creo que hay formas más fáciles de obtener una entrevista en Google. Después de todo, podrías preguntarle a cualquiera de los 100,000 empleados (aunque no todos son desarrolladores, sin duda). Pero fue una experiencia divertida, no obstante.

Source: https://habr.com/ru/post/446790/


All Articles