Segunda entrega de la guía de Android Debug Bridge. Tras enseñar a conectar y a manejar la herramienta, ahora hablamos ADB con el teléfono byte a byte, dónde vive realmente cada dato en Android y qué rastros permite recuperar un comando bien elegido.
Por qué una segunda parte
La primera guía de esta serie recorría ADB de cero a cien: qué es, cómo se instala, cómo se autoriza un ordenador, cómo se gestionan aplicaciones y archivos, cómo se abre el shell del dispositivo y cómo se emplea todo ello con las garantías que exige un peritaje. Era un curso de uso, y cumplía su propósito. Al releerla con distancia, sin embargo, se hacen visibles los huecos que deja cualquier texto introductorio, y son huecos que debemos cubrir para usuarios avanzados.
El primero es de profundidad. La guía enseñaba a usar ADB, pero no a entenderlo por dentro. Quien trabaja a diario con la herramienta acaba tropezando con comportamientos que solo se explican mirando el protocolo: por qué un archivo binario llega corrupto si se extrae de una forma y correcto si se extrae de otra, por qué un guion de automatización se traga silenciosamente un error, por qué dos comillas de más cambian por completo el resultado de un comando. Nada de eso es magia, y todo tiene una explicación técnica accesible.
El segundo hueco es de alcance. Android no es solo un teléfono con aplicaciones: es un sistema multiusuario, con perfiles de trabajo, espacios seguros y usuarios secundarios que guardan sus datos en rutas distintas y que resultan invisibles para quien no sepa buscarlos. Es también un sistema que registra su propia actividad con una minuciosidad notable, y esos registros —los de uso de aplicaciones, los de consumo de batería, los de tráfico de red, los de accesos a la cámara o al micrófono— permiten reconstruir lo que ocurrió en un dispositivo con un detalle que sorprende a quien lo ve por primera vez. La primera guía apenas rozó ese territorio.
El tercero es de método. Extraer datos es fácil; extraerlos sin estropearlos por el camino, conservando marcas de tiempo, permisos y diarios de las bases de datos, es otra cosa. Y traducir los errores que devuelve la herramienta, en lugar de tantear a ciegas, ahorra horas de trabajo y evita decisiones equivocadas sobre un dispositivo que quizá no admite una segunda oportunidad.
Esta segunda entrega se ocupa de esos tres frentes. Mantiene el mismo pacto con el usuario que establecimos en la primera: comandos listos para copiar, explicaciones en español claro, esquemas propios y rigor en lo que se afirma. Puede leerse de forma independiente, aunque da por sabido lo que allí se explicó —la arquitectura de cliente, servidor y daemon, la instalación, la autorización por clave RSA, los estados del dispositivo y el manejo básico del shell—, de modo que quien empiece desde cero hará bien en recorrer antes aquella. Y conviene repetir la advertencia que abría el primer texto, porque aquí gana peso: todo lo que sigue se ejerce sobre equipos propios o sobre aquellos para los que se dispone de autorización expresa. Cuanto más profundo es el acceso, más importa la legitimidad de quien accede.
El protocolo por dentro: mensajes, canales y servicios
Cada vez que se teclea un comando de ADB ocurre, por debajo, una conversación muy estructurada. Conocerla no es un lujo académico: explica los límites de la herramienta, sus rarezas y buena parte de los mensajes de error que uno encuentra sin entender de dónde salen.

La unidad básica de esa conversación es el mensaje, y tiene una forma fija: una cabecera de veinticuatro bytes seguida, opcionalmente, de una carga de datos. La cabecera contiene seis campos de cuatro bytes cada uno. El primero es el comando, una constante que identifica la clase de mensaje. Los dos siguientes, llamados arg0 y arg1, son argumentos cuyo significado depende del comando; en la práctica sirven para identificar los extremos de un canal. El cuarto indica la longitud de la carga que viene detrás. El quinto es una suma de comprobación de esa carga, calculada como la simple suma de sus bytes. Y el sexto es el llamado magic, que es el comando invertido bit a bit y funciona como verificación elemental de que la cabecera no está corrupta.
Los comandos son media docena, y sus nombres aparecen constantemente en la documentación y en los mensajes de traza. CNXN es el saludo: lo envía el cliente al conectar, anunciando la versión del protocolo que habla y el tamaño máximo de carga que admite; el dispositivo responde con su propio CNXN y una cadena de identificación con el tipo de dispositivo y sus características. AUTH gestiona la autenticación por clave RSA. OPEN solicita la apertura de un canal hacia un servicio del dispositivo. OKAY confirma. WRTE transporta datos por un canal ya abierto. Y CLSE lo cierra. A esos seis se sumó más tarde STLS, que negocia una capa de cifrado TLS para las conexiones inalámbricas modernas.
El tamaño máximo de la carga se negocia en ese saludo inicial y ha ido creciendo con las versiones del protocolo, desde los cuatro kilobytes de las primeras implementaciones hasta el megabyte de las actuales. Ese detalle, aparentemente menor, es la razón de que las transferencias grandes vayan mucho más rápidas con versiones recientes de las platform-tools en ambos extremos, y de que convenga no arrastrar instalaciones antiguas.
La autenticación merece un párrafo propio, porque la primera guía la describió desde fuera y ahora podemos verla por dentro. Cuando el servidor del ordenador saluda a un dispositivo que no lo conoce, el dispositivo responde con un mensaje AUTH de tipo token: veinte bytes aleatorios. El cliente los firma con su clave privada RSA de 2048 bits y devuelve la firma en otro AUTH, de tipo signature. Si el teléfono guarda la clave pública correspondiente entre las de confianza, la verificación cuadra y la conexión se establece sin intervención humana. Si no la guarda, la firma no cuadra, y entonces el cliente envía un tercer AUTH, de tipo public key, con su clave pública completa; es ese mensaje el que provoca el diálogo en la pantalla del teléfono con la huella de la clave. Al pulsar «Permitir siempre», la clave pública se archiva en el dispositivo, en el fichero de claves autorizadas, y las conexiones sucesivas se resuelven en la primera ronda. Entender esta secuencia aclara de golpe por qué un dispositivo aparece como unauthorized cuando la pantalla está bloqueada —el diálogo existe pero nadie puede aceptarlo—, y por qué borrar las claves del ordenador fuerza que todo el proceso vuelva a empezar.
Sobre esa conexión única se multiplexan canales. Cuando se ejecuta adb shell, no se abre una conexión nueva con el teléfono: se abre un canal dentro de la conexión existente, identificado por el par de argumentos arg0 y arg1. Por eso se puede tener un logcat corriendo en una terminal, una transferencia de archivos en otra y scrcpy mostrando la pantalla, todo a la vez y sobre el mismo cable. Y por eso, también, cuando el servidor se atasca se atascan todas las tareas a la vez: hay un único conducto compartido.
Lo que viaja por cada canal es un servicio. El cliente pide abrir uno con OPEN y una cadena que lo nombra. Los principales son fáciles de reconocer porque coinciden con los comandos que ya conocemos: shell: ejecuta órdenes en el dispositivo, sync: transfiere archivos —es el que usan push y pull—, reboot: reinicia, tcpip: cambia el transporte a red, root: intenta reiniciar el daemon con privilegios, jdwp: expone los puertos de depuración de la máquina virtual y framebuffer: entrega el contenido de la pantalla. Cuando en un mensaje de error aparece algo como failed to start service, lo que ha ocurrido es que el daemon rechazó abrir uno de estos canales.
El servicio sync: tiene su propio subprotocolo, y conviene conocerlo porque de él dependen las transferencias. Dentro del canal se intercambian órdenes de cuatro caracteres: STAT consulta los datos de un archivo, LIST enumera un directorio, SEND sube, RECV descarga, DATA transporta bloques, DONE cierra la operación y FAIL informa del fallo. Los datos viajan en bloques de sesenta y cuatro kilobytes. Este detalle explica una limitación que sorprende a muchos: adb pull no conserva por defecto todos los metadatos del archivo, porque el subprotocolo transmite la ruta y el modo, pero la marca de tiempo solo se preserva si se pide explícitamente. Volveremos sobre ello en la sección dedicada a la extracción con metadatos intactos, porque en trabajo pericial esa diferencia es determinante.
Hablar con el servidor a mano: el puerto 5037 sin `adb`
Todo lo anterior deja de ser teoría en cuanto se comprueba con las manos. El servidor de ADB escucha en el puerto 5037 de la dirección local y atiende un conjunto de servicios propios, los llamados host services, que responden sin necesidad de que haya ningún dispositivo conectado. Se puede conversar con él usando cualquier herramienta capaz de abrir un socket, y hacerlo una vez enseña más sobre ADB que muchas páginas de manual.
El formato de la petición es sencillo: se envía la longitud de la orden expresada en cuatro dígitos hexadecimales, seguida de la orden en texto plano. La respuesta empieza por OKAY o por FAIL, y a continuación viene el resultado, precedido a su vez por su longitud en cuatro dígitos hexadecimales. Nada más.
Preguntar por la versión del servidor es el ejemplo canónico. La cadena host:version tiene doce caracteres, es decir, 000c en hexadecimal:
# En Linux o macOS, con netcat
printf '000chost:version' | nc 127.0.0.1 5037
# Respuesta típica (OKAY, longitud 0004, y la versión en hexadecimal)
OKAY00040029
Ese 0029 es el número 41 en hexadecimal: la versión del protocolo del servidor. La lista de dispositivos se pide igual, y su cadena mide también doce caracteres:
printf '000chost:devices' | nc 127.0.0.1 5037
En Windows, donde no hay netcat de serie, el mismo diálogo se puede sostener desde PowerShell abriendo un cliente TCP:
$c = New-Object System.Net.Sockets.TcpClient('127.0.0.1', 5037)
$s = $c.GetStream()
$b = [Text.Encoding]::ASCII.GetBytes('000chost:version')
$s.Write($b, 0, $b.Length)
$r = New-Object byte[] 64
$n = $s.Read($r, 0, 64)
[Text.Encoding]::ASCII.GetString($r, 0, $n)
$c.Close()
Hay unos cuantos servicios de este tipo que conviene conocer. host:version devuelve la versión. host:devices lista los dispositivos, y host:devices-l añade el detalle descriptivo. host:track-devices deja el canal abierto y notifica cada vez que un dispositivo se conecta o se desconecta, que es exactamente lo que usan los entornos de desarrollo para actualizar su lista sin refrescar nada a mano. host:kill detiene el servidor, igual que adb kill-server. Y host:transport:<número de serie> convierte el canal en una conexión dirigida a un dispositivo concreto, tras lo cual se puede pedir un servicio suyo, como shell:id.
La utilidad práctica de todo esto es doble. Por un lado, permite integrar ADB en programas propios sin depender del ejecutable: cualquier lenguaje con soporte de sockets puede hablar con el servidor, y de hecho así funcionan las bibliotecas que existen en Python, Java o Go. Por otro, es una herramienta de diagnóstico de primer orden: si adb devices se comporta de forma extraña, preguntar directamente al servidor separa con limpieza los problemas del cliente de los del servidor.
A esa misma familia de diagnóstico pertenece una variable de entorno poco conocida y muy útil, ADB_TRACE, que hace que el cliente escriba en la salida todo lo que ocurre por debajo, incluidos los mensajes del protocolo:
# Linux y macOS
ADB_TRACE=all adb devices
# Windows (PowerShell)
$env:ADB_TRACE = 'all'; adb devices
La salida es abundante y críptica al principio, pero se vuelve legible en cuanto se reconocen los CNXN, OPEN, WRTE y CLSE de la sección anterior. Cuando un dispositivo se comporta de manera inexplicable, ese volcado suele señalar el punto exacto en el que la conversación se rompe.
Una última pieza de configuración, imprescindible en flotas y en entornos de integración continua: el puerto del servidor puede cambiarse, lo que permite tener varios servidores independientes en la misma máquina, cada uno gobernando su propio conjunto de dispositivos:
# Arrancar un segundo servidor en otro puerto
adb -L tcp:5038 start-server
# O fijarlo por variable de entorno para toda la sesión
export ANDROID_ADB_SERVER_PORT=5038
Y, en la misma línea, la variable ANDROID_SERIAL fija el dispositivo de destino de todos los comandos, evitando repetir -s una y otra vez y previniendo el clásico error de dirigir una orden al teléfono equivocado cuando hay varios conectados:
export ANDROID_SERIAL=R58M31XDKPL
adb shell getprop ro.product.model
Los tres shells: comillas, terminales y códigos de salida
Pocas cosas generan tanta confusión como el comportamiento de adb shell, y casi toda esa confusión nace de un hecho que rara vez se explica: cuando se ejecuta un comando remoto hay al menos dos intérpretes de comandos implicados, el del ordenador y el del teléfono, y cada uno se come su parte de la línea antes de pasarla al siguiente.

shell y exec-out. · Esquema: elaboración propia.Veámoslo con el ejemplo más común. Al escribir un comando con comodines, la pregunta es quién los expande:
# El comodín lo expande el ordenador (mal, casi nunca es lo que se quiere)
adb shell ls /sdcard/*.jpg
# El comodín viaja intacto y lo expande el teléfono (correcto)
adb shell "ls /sdcard/*.jpg"
En el primer caso, el intérprete del ordenador intenta encontrar archivos que encajen con ese patrón en el ordenador; como no los hay, en Linux y macOS pasa el patrón tal cual y funciona por casualidad, mientras que en otros supuestos produce resultados sorprendentes. En el segundo, las comillas protegen el patrón hasta que llega al teléfono, que es donde debe resolverse. La regla práctica es simple: todo comando remoto que contenga comodines, redirecciones, punto y coma, variables o tuberías va entre comillas. Y cuando dentro hacen falta comillas otra vez, se combinan los dos tipos:
adb shell "for p in \$(pm list packages -3 | cut -d: -f2); do echo \$p; done"
Obsérvese el escape del símbolo del dólar. Sin él, es el ordenador quien intenta resolver la variable, y llega vacía al teléfono. Este es, con diferencia, el error más frecuente en los guiones que se comparten en foros.
La segunda fuente de sorpresas es la terminal virtual. Cuando adb shell se ejecuta sin comando, en modo interactivo, el daemon asigna al canal un pseudoterminal, igual que haría una sesión de SSH. Eso es cómodo para trabajar a mano, pero tiene un efecto colateral serio: el pseudoterminal traduce los saltos de línea, convirtiendo cada carácter de avance de línea en la pareja de retorno de carro y avance de línea. Con texto no se nota; con datos binarios, la salida queda corrupta. Es la razón de que la captura de pantalla clásica llegara rota en Windows durante años, y de que exista la alternativa:
# Puede corromper el PNG: el pseudoterminal altera los bytes
adb shell screencap -p > pantalla.png
# Correcto: exec-out no asigna terminal y entrega los bytes intactos
adb exec-out screencap -p > pantalla.png
La diferencia entre shell y exec-out es exactamente esa: exec-out abre el canal a través del servicio exec:, sin pseudoterminal, con la salida binaria limpia. Para cualquier operación que entregue datos binarios —imágenes, archivos comprimidos, volcados de particiones, bases de datos— la forma correcta es exec-out. Para trabajar a mano dentro del teléfono, shell.
La tercera sorpresa afecta a la automatización, y es la más traicionera: durante años, adb shell no propagaba el código de salida del comando remoto. Cualquiera que fuese el resultado dentro del teléfono, el cliente terminaba devolviendo cero, es decir, éxito. Un guion que comprobara el código de salida daba por buenas operaciones que habían fallado. La solución tradicional consistía en pedir el código explícitamente:
adb shell "ls /ruta/que/no/existe; echo CODIGO=\$?"
Desde la versión 1.0.36 de las platform-tools, con la segunda versión del protocolo del shell, el problema está resuelto: el código de salida se propaga y, además, la salida estándar y la de errores llegan separadas en lugar de mezcladas. Merece la pena comprobar qué comportamiento tiene la instalación con la que se trabaja, porque en entornos con herramientas antiguas la trampa sigue viva:
adb shell false
echo $? # debe imprimir 1 con el protocolo moderno; 0 con el antiguo
Estrechamente relacionados están los comandos de espera, que son la base de cualquier guion serio. adb wait-for-device bloquea hasta que aparece un dispositivo, y admite variantes por transporte y por estado (wait-for-usb-device, wait-for-any-device). Ahora bien, que el dispositivo aparezca no significa que Android haya terminado de arrancar: el daemon responde mucho antes de que el sistema esté operativo, y ese desfase provoca fallos intermitentes en las canalizaciones automatizadas. La comprobación completa consulta la propiedad que el sistema fija al terminar el arranque:
adb wait-for-device
adb shell 'while [ "$(getprop sys.boot_completed)" != "1" ]; do sleep 1; done'
Con esas tres piezas —comillas bien puestas, exec-out para lo binario y espera del arranque completo— desaparece la mayor parte de la fragilidad que se atribuye a ADB en entornos automatizados.
El mapa real del almacenamiento de Android
Para extraer datos con criterio hay que saber dónde están, y el sistema de archivos de Android tiene una geografía particular que la primera guía apenas esbozó. Merece un recorrido detenido, porque casi todas las preguntas prácticas —dónde guarda sus mensajes esta aplicación, por qué no puedo leer aquella carpeta, dónde han ido a parar las fotos de un usuario secundario— se responden con este mapa en la mano.

La primera gran división separa el almacenamiento privado de cada aplicación del almacenamiento compartido. El privado vive bajo /data, y es el territorio interesante: ahí guarda cada aplicación sus bases de datos, sus preferencias y sus archivos internos, protegidos por el aislamiento que Android impone entre aplicaciones. La ruta clásica es /data/data/<paquete>, que todo el mundo conoce, pero conviene saber que es un residuo histórico: hoy es un alias del directorio del usuario principal, y la ruta real y general es /data/user/<identificador de usuario>/<paquete>. Para el usuario principal, el identificador es el cero, de modo que /data/data/com.ejemplo y /data/user/0/com.ejemplo apuntan al mismo sitio.
Junto a esa ruta existe otra, mucho menos conocida y de gran interés pericial: /data/user_de/<identificador>/<paquete>. Las siglas corresponden a device encrypted, almacenamiento cifrado con la clave del dispositivo. Desde que Android adoptó el cifrado basado en archivos, los datos de cada aplicación se reparten en dos zonas. La zona credential encrypted —la de /data/user/— solo se descifra cuando el usuario introduce su PIN, patrón o contraseña por primera vez tras el encendido. La zona device encrypted se descifra en cuanto arranca el sistema, sin intervención humana, para que ciertas funciones básicas —alarmas, accesibilidad, llamadas de emergencia— estén disponibles antes del primer desbloqueo. Esa distinción es la que subyace a los estados que la práctica forense denomina antes del primer desbloqueo y después del primer desbloqueo: en el primero, buena parte del contenido del teléfono existe pero está cifrado y resulta ilegible incluso con acceso físico; en el segundo, las claves están en memoria y el material es accesible. Saber qué zona se está leyendo evita conclusiones erróneas sobre datos «que no aparecen».
El almacenamiento compartido es el que el usuario percibe como «la memoria del teléfono»: fotos, descargas, documentos. Su ruta canónica es /sdcard, pero esa ruta es un enlace simbólico que atraviesa varias capas hasta llegar a /data/media/<identificador de usuario>. En medio hay una vista sintética, gestionada por un sistema de archivos en espacio de usuario, que aplica las reglas del almacenamiento con ámbito introducidas en Android 10 y consolidadas después. La consecuencia práctica es que lo que se ve por /sdcard no siempre coincide exactamente con lo que hay en /data/media, y que ciertos directorios de aplicaciones dentro de Android/data quedan ocultos a según qué procesos. Cuando una extracción devuelve menos de lo esperado, esta es una de las causas habituales.
Bajo /data hay además varias regiones que rara vez se mencionan y que resultan valiosas. En /data/app residen los paquetes instalados por el usuario; desde Android 11 sus rutas incluyen segmentos aleatorios para dificultar la manipulación, de forma que el camino exacto se obtiene mejor preguntando al gestor de paquetes que buscando a mano. En /data/system viven los registros del propio sistema: el inventario de paquetes instalados, la configuración de usuarios, las estadísticas de uso. En /data/misc se guardan datos de subsistemas concretos, como la configuración de redes inalámbricas o el almacén de claves. Y en /data/tombstones, /data/anr y el registro de incidencias del sistema quedan los restos de procesos que fallaron, con volcados que a veces contienen información reveladora sobre qué se estaba ejecutando y cuándo.
Existe, por último, un directorio pequeño y crucial: /data/local/tmp. Es la única zona de /data en la que el usuario shell puede escribir sin privilegios adicionales. Todo lo que ADB necesite dejar temporalmente en el dispositivo —un binario auxiliar, un archivo de configuración para una prueba— acaba ahí. Desde el punto de vista pericial es una zona de riesgo evidente, porque escribir en ella modifica el dispositivo; su uso en una adquisición debe estar justificado y documentado, y en general evitarse.
Comprobar todo esto en un dispositivo real es cuestión de tres comandos:
# Quién soy y a qué grupos pertenezco dentro del teléfono
adb shell id
# Qué puedo ver en las regiones principales
adb shell ls -la /data/ 2>&1 | head -20
adb shell ls -la /sdcard/
# La ruta real del almacenamiento compartido
adb shell "ls -la /sdcard; readlink -f /sdcard"
La salida del primero revela algo importante: el usuario shell tiene identificador 2000 y pertenece a un puñado de grupos que le conceden permisos concretos —leer el almacenamiento compartido, consultar registros, usar la red—, pero no le dan acceso al recinto privado de las aplicaciones. Por eso ls /data/data responde con un lacónico «Permission denied» en un dispositivo normal. Ese muro es intencionado y define el techo de lo que ADB puede hacer sin privilegios elevados.
Hay, sin embargo, una excepción legítima y muy útil que la primera guía no mencionó: el comando run-as. Si una aplicación se compiló como depurable —cosa que ocurre siempre en desarrollo y casi nunca en las versiones publicadas—, el usuario shell puede ejecutar órdenes con la identidad de esa aplicación y, por tanto, acceder a su directorio privado:
# Entrar en el recinto privado de una aplicación depurable
adb shell run-as com.ejemplo.miapp ls -la /data/data/com.ejemplo.miapp
# Copiar su base de datos hacia una zona accesible y traerla
adb shell "run-as com.ejemplo.miapp cat /data/data/com.ejemplo.miapp/databases/app.db" > app.db
Ese segundo comando ilustra un patrón que conviene memorizar: cuando un archivo está fuera del alcance del usuario shell pero sí al alcance de run-as, se vuelca su contenido por la salida estándar y se recoge en el ordenador, sin escribir nada en el dispositivo. Para archivos binarios, la forma correcta es la misma con exec-out. En depuración de aplicaciones propias, run-as es el atajo que ahorra tener que rootear un dispositivo de pruebas.
Usuarios, perfiles de trabajo y espacios que no se ven
Android es multiusuario desde hace más de una década, y esa característica está detrás de uno de los descuidos más costosos que puede cometer quien analiza un dispositivo: examinar solo el usuario principal y dar por completo el examen. Aplicaciones enteras, con todos sus datos, pueden vivir en un usuario secundario, en un perfil de trabajo o en un espacio seguro del fabricante, y no aparecerán en ninguna búsqueda que no las contemple explícitamente.

El inventario de usuarios se obtiene con una sola orden:
adb shell pm list users
La salida enumera cada usuario con su identificador, su nombre, un número de banderas y su estado de ejecución. El usuario cero es siempre el principal, el propietario del dispositivo. Los perfiles de trabajo, creados por las plataformas de gestión de dispositivos corporativos, suelen recibir el identificador diez. Los usuarios secundarios y los perfiles de invitado se numeran a partir de ahí. Y algunos fabricantes emplean identificadores propios para sus espacios cifrados: el espacio seguro de Samsung, por ejemplo, se implementa como un usuario con un identificador alto y reservado, y su contenido resulta invisible desde el usuario principal.
Prácticamente todos los comandos relevantes del shell aceptan el modificador --user, y usarlo cambia por completo lo que se ve:
# Aplicaciones del usuario principal
adb shell pm list packages --user 0
# Aplicaciones del perfil de trabajo
adb shell pm list packages --user 10
# Usuario activo en este momento
adb shell am get-current-user
# Detalle de la configuración de usuarios y perfiles
adb shell dumpsys user
La última orden merece atención especial en trabajo pericial, porque su volcado incluye la fecha de creación de cada usuario, su tipo, las restricciones aplicadas y la relación entre perfiles y sus usuarios padre. Un perfil de trabajo creado tres días antes de los hechos investigados es, en sí mismo, un dato.
Los datos de cada usuario se almacenan, como vimos, en rutas paralelas: /data/user/10/<paquete> para las aplicaciones del perfil diez, /data/media/10 para su almacenamiento compartido. Con acceso privilegiado, esa simetría hace el trabajo previsible. Sin él, el inventario de paquetes por usuario sigue siendo accesible y ya aporta información sustancial: qué aplicaciones existen, cuándo se instalaron y de qué origen.
Una consulta que combina lo anterior y resulta especialmente reveladora es la que enumera los paquetes incluyendo los que fueron desinstalados pero conservan datos:
adb shell pm list packages -u --user 0
El modificador -u incluye los paquetes desinstalados cuyos datos permanecen en el sistema. En un análisis, la diferencia entre esa lista y la lista normal señala aplicaciones que estuvieron instaladas y ya no lo están, lo que es exactamente la clase de rastro que interesa reconstruir. Añadiendo -f se muestra además la ruta del archivo de instalación, y con -i el instalador de origen, es decir, si el paquete llegó desde la tienda oficial, desde otra tienda o por instalación manual, dato con implicaciones evidentes cuando se investiga la presencia de software de vigilancia.
`pm` a fondo: sesiones, paquetes divididos, permisos y `appops`
La primera guía presentó el gestor de paquetes en su uso corriente: instalar, desinstalar, listar. La herramienta da mucho más de sí, y varias de sus capacidades son hoy imprescindibles porque el formato de distribución de aplicaciones ha cambiado.
El cambio principal es que muchas aplicaciones ya no se distribuyen como un único archivo, sino como un paquete dividido: un archivo base más varios complementos con los recursos específicos de la arquitectura del procesador, la densidad de pantalla y el idioma. Instalar solo el base produce un fallo desconcertante, o una aplicación que arranca y se cierra. La orden adecuada los instala todos como un conjunto:
adb install-multiple base.apk split_config.arm64_v8a.apk split_config.xxhdpi.apk split_config.es.apk
Por debajo, esa orden usa el mecanismo de sesiones de instalación, que también puede manejarse a mano cuando se necesita control fino, por ejemplo en dispositivos con poco espacio o para instalar desde un archivo que ya está en el teléfono:
# Crear la sesión, indicando el tamaño total en bytes
adb shell pm install-create -S 15728640
# La orden anterior devuelve un identificador de sesión, por ejemplo 428573649
adb shell pm install-write -S 15728640 428573649 base.apk /data/local/tmp/base.apk
# Confirmar la instalación (o abandonarla con install-abandon)
adb shell pm install-commit 428573649
Los modificadores de adb install merecen también un repaso, porque cada uno resuelve un problema concreto. Con -r se reinstala conservando los datos, y es el que se usa a diario en desarrollo. Con -g se conceden de antemano todos los permisos declarados, lo que ahorra pulsar diálogos en pruebas automatizadas. Con -d se permite instalar una versión anterior a la existente, algo que el sistema rechaza por defecto. Con -t se aceptan paquetes marcados como de prueba. Y con --user se dirige la instalación a un usuario concreto.
En el extremo contrario, la desinstalación tiene una variante que conviene conocer y que se ha popularizado para retirar aplicaciones preinstaladas sin necesidad de privilegios de superusuario:
# Retirar un paquete solo para el usuario 0, conservando sus datos
adb shell pm uninstall -k --user 0 com.fabricante.aplicacion
# Alternativa reversible: deshabilitar en lugar de desinstalar
adb shell pm disable-user --user 0 com.fabricante.aplicacion
adb shell pm enable com.fabricante.aplicacion
La segunda opción es preferible en casi todos los casos, porque es reversible sin restaurar el dispositivo. La primera deja el paquete sin instalar para ese usuario, y aunque una restauración de fábrica lo devuelve, mientras tanto no hay vuelta atrás sencilla. Conviene subrayar que ambas modifican el estado del dispositivo, de modo que en un contexto pericial pertenecen sin discusión a la zona roja.
El gobierno de permisos es otra área donde el shell permite mucho más de lo que ofrece la interfaz. Los permisos declarados y su estado se consultan y se alteran así:
# Ver el estado completo de permisos de un paquete
adb shell dumpsys package com.ejemplo.miapp | grep -A 40 "requested permissions"
# Conceder o retirar un permiso peligroso
adb shell pm grant com.ejemplo.miapp android.permission.ACCESS_FINE_LOCATION
adb shell pm revoke com.ejemplo.miapp android.permission.ACCESS_FINE_LOCATION
Junto al sistema de permisos convive otro mecanismo, menos visible y más granular, llamado operaciones de aplicación. Gobierna acciones concretas —acceder al portapapeles, dibujar sobre otras aplicaciones, leer el registro de llamadas, usar la cámara— y, sobre todo, registra cuándo se ejerció cada una. Ese registro es un artefacto forense de primer orden:
# Estado de las operaciones de una aplicación
adb shell appops get com.ejemplo.miapp
# Volcado completo del sistema, con marcas de tiempo de último acceso
adb shell dumpsys appops
El volcado indica, por aplicación y operación, el momento del último acceso concedido o denegado, distinguiendo si ocurrió con la aplicación en primer o en segundo plano. Determinar que una aplicación accedió al micrófono a una hora concreta, estando en segundo plano, es la clase de hallazgo que decide un análisis, y se obtiene con un comando de lectura pura.
Por último, el propio dumpsys package aplicado a un paquete concreto entrega una ficha completa que en trabajo pericial vale su peso en oro: versión instalada, fecha de primera instalación, fecha de la última actualización, ruta del archivo, firma, usuario propietario, permisos concedidos y, muy señaladamente, el instalador de origen:
adb shell dumpsys package com.ejemplo.miapp | grep -E "versionName|firstInstallTime|lastUpdateTime|installerPackageName|codePath"
Ese puñado de líneas responde a preguntas que suelen ser decisivas: cuándo apareció esta aplicación en el dispositivo, quién la puso ahí y si se actualizó después.
SQLite, diarios y la mitad de la evidencia que se queda atrás
Casi todo lo que guarda una aplicación de Android acaba en una base de datos SQLite: mensajes, contactos, historiales de navegación, registros de ubicación, colas de sincronización. La primera guía lo señaló de pasada. Conviene detenerse, porque copiar esas bases de datos mal es una de las formas más comunes de perder información sin darse cuenta.

SQLite admite dos modos de registro de transacciones. En el modo tradicional, los cambios se escriben directamente en el archivo de base de datos y se usa un diario auxiliar para poder deshacerlos. En el modo de registro de escritura anticipada, que es el que emplean por defecto la mayoría de las aplicaciones modernas de Android, ocurre lo contrario: los cambios se anotan primero en un archivo aparte, con extensión -wal, y solo se trasladan al archivo principal cuando se produce un punto de control, algo que sucede de forma automática al acumularse cierto número de páginas o al cerrarse la conexión de forma ordenada.
La consecuencia es directa y, para quien no la conozca, desconcertante: los datos más recientes pueden no estar en el archivo .db. Un mensaje recibido hace diez minutos es muy probable que viva todavía en el archivo -wal acompañante. Extraer únicamente la base de datos y analizarla en el laboratorio devolverá una foto incompleta, en la que faltará justo el tramo final, que suele ser el más relevante. A esos dos archivos se une un tercero, con extensión -shm, que es la memoria compartida que coordina los accesos concurrentes.
La regla, por tanto, no admite excepción: se extraen siempre los tres archivos, juntos y en el mismo momento.
# Listar la trinidad completa de una aplicación (con acceso privilegiado)
adb shell "ls -la /data/data/com.ejemplo.miapp/databases/"
# Extraer los tres archivos, no solo el principal
adb exec-out "cat /data/data/com.ejemplo.miapp/databases/mensajes.db" > mensajes.db
adb exec-out "cat /data/data/com.ejemplo.miapp/databases/mensajes.db-wal" > mensajes.db-wal
adb exec-out "cat /data/data/com.ejemplo.miapp/databases/mensajes.db-shm" > mensajes.db-shm
Una tentación que conviene resistir es la de abrir la base de datos en el propio dispositivo. Muchos dispositivos incluyen el binario sqlite3, y resulta cómodo lanzar una consulta directa. El problema es que abrir la base de datos con permisos de escritura provoca un punto de control, fusiona el diario en el archivo principal y altera la evidencia. En un contexto pericial eso es inaceptable; en cualquier otro contexto, además, borra la información sobre qué se había escrito recientemente. La consulta se hace siempre sobre la copia, en el ordenador, y nunca sobre el original.
Ese mismo razonamiento explica por qué el análisis posterior debe ir más allá de las consultas al uso. Cuando se borra un registro, SQLite no lo elimina del archivo: marca su espacio como libre y lo deja disponible para futuras escrituras. Hasta que ese espacio se reutiliza, el contenido sigue ahí, en las llamadas páginas libres, y existen herramientas de recuperación capaces de reconstruirlo. Lo mismo ocurre con el diario, que puede conservar versiones anteriores de registros que ya fueron modificados. Una parte sustancial del análisis de aplicaciones de mensajería consiste precisamente en recuperar lo que las consultas normales no ven.
Un último apunte sobre integridad: la comprobación de que la copia es fiel se hace comparando resúmenes criptográficos en ambos extremos, sin necesidad de traer el archivo dos veces.
# Resumen calculado en el dispositivo
adb shell sha256sum /data/local/tmp/mensajes.db
# Resumen calculado sobre la copia en el ordenador
sha256sum mensajes.db
Si ambos coinciden, la transferencia fue íntegra. Este cotejo debe hacerse archivo por archivo y quedar recogido en el registro de la sesión, tal como se describió en la primera guía.
`logcat` avanzado: búferes, formatos y valor probatorio
La primera guía presentó logcat como el registro en vivo del sistema. Es mucho más que eso: es un conjunto de registros separados, con políticas distintas, y saber cuál consultar cambia por completo el rendimiento de una investigación o de una depuración.

logcat, su contenido característico y las opciones que gobiernan la salida. · Esquema: elaboración propia.Android mantiene varios búferes circulares independientes. El búfer main recoge lo que escriben las aplicaciones. El system guarda los mensajes de los componentes del sistema. El crash archiva los fallos graves. El radio registra la actividad del subsistema de telefonía y de la conexión de datos. El kernel recoge los mensajes del núcleo. Y el events, el menos conocido y el más interesante, almacena eventos estructurados del sistema en formato binario: arranques de procesos, cambios de actividad en primer plano, encendidos y apagados de pantalla, niveles de batería. Ese último búfer es una fuente extraordinaria para reconstruir qué estaba haciendo el dispositivo en un momento dado.
Seleccionar el búfer y el formato es cuestión de dos modificadores:
# Volcar todos los búferes y salir, con fecha, hilo y proceso
adb logcat -b all -d -v threadtime > registro_completo.txt
# Solo los eventos estructurados del sistema
adb logcat -b events -d -v threadtime > eventos.txt
# Con marcas absolutas en tiempo universal y año incluido: imprescindible en pericia
adb logcat -b all -d -v threadtime -v year -v UTC > registro_utc.txt
Los formatos de salida merecen conocerse. El formato brief es el clásico y el más pobre. El time añade fecha y hora. El threadtime añade además el identificador de proceso y de hilo, y es el que conviene por defecto. El long separa cada mensaje en varias líneas, cómodo para leer, incómodo para procesar. Y hay tres modificadores que se acumulan a los anteriores y que en trabajo pericial importan más que ninguno: year, que incluye el año en la fecha —sin él, un registro de enero es ambiguo—; UTC, que expresa las marcas en tiempo universal y elimina las dudas sobre el huso configurado; y epoch, que las expresa en segundos desde el origen de época, formato incómodo de leer pero cómodo de correlacionar.
El filtrado evita ahogarse en volumen. Se puede filtrar por etiqueta y prioridad, por proceso o por expresión regular:
# Solo mensajes de error o superiores, silenciando el resto
adb logcat -d "*:E"
# Todo lo de una etiqueta concreta, silenciando lo demás
adb logcat -d ActivityManager:I "*:S"
# Solo los mensajes de un proceso, por nombre de paquete
adb logcat -d --pid=$(adb shell pidof -s com.ejemplo.miapp)
# Búsqueda por expresión regular
adb logcat -d --regex="Exception|FATAL"
Para sesiones largas, logcat puede escribir directamente a archivos con rotación, lo que evita perder información en capturas prolongadas:
# Cuatro archivos de 16 MB, rotando
adb logcat -f /sdcard/captura.log -r 16384 -n 4 -v threadtime
Dos opciones más completan el cuadro. La primera, -g, informa del tamaño de cada búfer, dato que conviene registrar al inicio de una adquisición porque determina cuánta historia cabe. La segunda, -G, lo modifica, y por tanto escribe en el dispositivo: en pericia queda descartada. Y hay una tercera que nunca debe teclearse sobre un dispositivo intervenido, -c, que vacía los búferes; existe para desarrollo, y en el contexto equivocado equivale a destruir evidencia.
Esto último enlaza con el rasgo que define el valor probatorio de estos registros: son volátiles. Los búferes son circulares, tienen un tamaño limitado —del orden de unos pocos megabytes por búfer en la configuración habitual— y se vacían por completo al reiniciar el dispositivo. En un teléfono activo, la historia disponible puede reducirse a unas horas. Esa es la razón por la que el orden de volatilidad expuesto en la primera guía sitúa la captura de registros inmediatamente después de la captura de pantalla: cada minuto de demora es historia perdida, y un reinicio accidental la borra entera.
El informe de errores como fuente estructurada
Existe un comando que reúne de una sola vez casi todo lo que los apartados anteriores obtienen por separado, y que la primera guía mencionó sin explotar: adb bugreport. Su nombre engaña, porque no se limita a informar de un error; genera un volcado exhaustivo del estado del dispositivo.
adb bugreport informe_dispositivo.zip
El resultado es un archivo comprimido que, según el dispositivo y su actividad, ocupa entre unas decenas y unos centenares de megabytes. Dentro hay un archivo de texto principal, de tamaño considerable, que concatena el estado de todos los servicios del sistema, los registros de varios búferes, la lista de procesos, el uso de memoria, la ocupación de disco, las estadísticas de red, el historial de batería, los volcados de aplicaciones que dejaron de responder y la configuración de paquetes. Junto a él viajan archivos auxiliares con la versión del sistema y volcados específicos de la plataforma del fabricante.
Su ventaja es evidente: una sola orden captura un estado completo y coherente, obtenido en el mismo instante, lo que evita las inconsistencias de recopilar los mismos datos mediante veinte comandos ejecutados a lo largo de media hora. Su inconveniente también lo es: la generación tarda varios minutos, consume recursos del dispositivo y, en las versiones modernas de Android, muestra una notificación en la pantalla del teléfono mientras se realiza. Esa visibilidad tiene consecuencias prácticas: no es una operación silenciosa, y en una intervención debe documentarse como interacción con el dispositivo.
Para explotarlo, conviene descomprimirlo y localizar las secciones de interés, que están delimitadas por encabezados reconocibles:
# Ver qué contiene el archivo
unzip -l informe_dispositivo.zip
# Extraer y localizar las secciones principales
unzip informe_dispositivo.zip
grep -n "^------ " bugreport-*.txt | head -60
Cada línea que empieza por una serie de guiones marca el inicio de una sección: el volcado de un servicio, un búfer de registro, una tabla del sistema. Con ese índice a la vista, el archivo deja de ser una masa impenetrable y se convierte en un documento navegable. Existen además herramientas de análisis que consumen directamente este formato, siendo la más conocida la que reconstruye gráficamente el historial de batería y actividad a partir de los datos que el informe contiene.
Reconstruir una línea de tiempo con `dumpsys`
Llegamos a lo que quizá sea la aportación más valiosa de esta segunda entrega. Android registra su propio funcionamiento con un detalle notable, y varios de esos registros llevan marcas de tiempo. Combinándolos se puede reconstruir, con acceso de solo lectura y sin privilegios especiales, una cronología bastante fiel de lo que ocurrió en el dispositivo.

dumpsys que, combinadas, reconstruyen la actividad del dispositivo. · Esquema: elaboración propia.La primera fuente son las estadísticas de uso. Android anota qué aplicación pasó a primer plano, cuándo y durante cuánto tiempo, y conserva esos datos agregados por día, semana, mes y año:
adb shell dumpsys usagestats > uso_aplicaciones.txt
El volcado es extenso y su formato varía entre versiones, pero la estructura se reconoce enseguida: bloques por usuario y por intervalo temporal, con eventos que indican el paso de una aplicación a primer plano, su regreso al segundo plano, el arranque de una actividad o la interacción del usuario con una notificación. Reconstruir a partir de ahí qué aplicación estaba en uso a una hora determinada es un ejercicio directo.
La segunda fuente es el historial de batería, que resulta ser, casi por accidente, el mejor registro cronológico del dispositivo:
adb shell dumpsys batterystats --history > historial_bateria.txt
Ese historial anota, con marcas temporales, encendidos y apagados de pantalla, conexiones y desconexiones de red, activación del receptor de posicionamiento, bloqueos de suspensión adquiridos por cada aplicación, cambios de nivel de carga y conexiones del cargador. Determinar que la pantalla se encendió a una hora precisa, que a continuación una aplicación concreta mantuvo el dispositivo despierto y que en ese intervalo se activó la localización es exactamente el tipo de secuencia que sostiene o desmonta una versión de los hechos.
La tercera fuente son las estadísticas de red, que contabilizan bytes enviados y recibidos por aplicación, por interfaz y por intervalo temporal:
adb shell dumpsys netstats detail > estadisticas_red.txt
Un volumen inusual de datos salientes de una aplicación en un intervalo concreto es un indicio que merece exploración, particularmente cuando se investiga la presencia de software de vigilancia.
La cuarta fuente es el estado de las tareas recientes y de la pila de actividades, que refleja la situación en el momento de la captura:
adb shell dumpsys activity recents | head -40
adb shell dumpsys activity activities | grep -E "mResumedActivity|topResumedActivity"
A esas cuatro se suman otras de interés más específico. dumpsys notification muestra las notificaciones activas y su historial reciente, incluyendo en ocasiones el texto de mensajes recibidos. dumpsys alarm y dumpsys jobscheduler revelan qué tareas tenía programadas cada aplicación. dumpsys wifi y dumpsys connectivity describen la conectividad y, según la versión, las redes conocidas. dumpsys location informa de qué aplicaciones solicitaron posición. Y dumpsys deviceidle documenta los periodos en que el dispositivo estuvo inactivo, dato útil para distinguir el uso real de la actividad automática.
Merece la pena señalar que el volcado completo de todos los servicios se obtiene de una vez, y que ese archivo, sellado con su resumen criptográfico, constituye una instantánea muy completa del estado del sistema:
adb shell dumpsys > dumpsys_completo.txt
sha256sum dumpsys_completo.txt >> HASHES.txt
El problema del tiempo: husos, desfases y anclaje
Todo lo anterior se apoya en marcas temporales, y las marcas temporales de Android no son homogéneas. Conviven al menos tres relojes distintos. El primero es el tiempo de época, expresado en milisegundos desde el 1 de enero de 1970 en tiempo universal, y es el que emplean la mayoría de los registros de alto nivel. El segundo es el tiempo transcurrido desde el arranque, que cuenta milisegundos desde el último encendido e incluye los periodos de suspensión; es el que usa buena parte del historial de batería, y es relativo, de modo que sin conocer el instante del arranque resulta inútil. El tercero es el tiempo de actividad, que no cuenta la suspensión.
Antes de interpretar cualquier cronología hay que anclar esos relojes, y eso se hace con un puñado de comandos de lectura que deben ejecutarse al principio de toda sesión y quedar registrados:
# Hora del dispositivo y huso configurado
adb shell date
adb shell getprop persist.sys.timezone
# Tiempo desde el arranque y momento del último encendido
adb shell uptime
adb shell cat /proc/uptime
# Hora del equipo de referencia, para calcular el desfase
date -u
Comparar la hora del dispositivo con la del equipo forense, ambas anotadas en el mismo instante, permite calcular el desfase y corregir después todas las marcas. Un teléfono cuya hora va cuarenta minutos adelantada produce, si nadie lo advierte, una cronología cuarenta minutos equivocada, con las consecuencias que se pueden imaginar en un procedimiento. Documentar el desfase, el huso horario y si el dispositivo tenía activada la hora automática de red es parte del informe, no un detalle accesorio.
Rendimiento y perfilado: lo que ADB puede medir
Cambiando de terreno, ADB es también una herramienta de medición, y esta faceta quedó fuera de la primera guía pese a ser la que más usan quienes desarrollan aplicaciones. Todo lo que sigue son comandos de lectura, y varios resultan útiles también en diagnóstico de dispositivos lentos.
La medida más básica es el tiempo de arranque de una pantalla, que se obtiene lanzándola con el modificador de espera:
adb shell am start -W -n com.ejemplo.miapp/.MainActivity
La salida informa del tiempo total transcurrido hasta que la actividad quedó lista y del tiempo de espera percibido. Repetir la medida tras un arranque en frío y tras uno en caliente da dos cifras muy distintas, y ambas importan.
El rendimiento gráfico se examina con el volcado del subsistema de dibujo, que en su versión detallada entrega las marcas temporales de los últimos fotogramas renderizados:
# Resumen del rendimiento gráfico de una aplicación
adb shell dumpsys gfxinfo com.ejemplo.miapp
# Detalle por fotograma, apto para volcarlo a una hoja de cálculo
adb shell dumpsys gfxinfo com.ejemplo.miapp framestats
El detalle por fotograma incluye, en nanosegundos, el instante en que se recibió el evento de entrada, en que empezó el dibujo, en que se entregó el fotograma y en que se compuso en pantalla. Restando esas marcas se identifica en qué fase se pierde el tiempo cuando una interfaz se percibe lenta.
La memoria se consulta a dos niveles, el del sistema completo y el de una aplicación concreta:
adb shell dumpsys meminfo
adb shell dumpsys meminfo com.ejemplo.miapp
La cifra que interesa habitualmente es el tamaño proporcional del conjunto residente, que reparte la memoria compartida entre los procesos que la usan y da una idea realista del consumo. Para observar el comportamiento a lo largo del tiempo en lugar de en un instante, el servicio de estadísticas de procesos ofrece agregados por horas:
adb shell dumpsys procstats --hours 3
El uso de procesador se examina con el volcado correspondiente o, de forma más inmediata, con la utilidad clásica dentro del shell:
adb shell dumpsys cpuinfo
adb shell top -m 10 -s cpu -n 1
Para análisis serios, la herramienta moderna de trazado del sistema sustituyó hace años a la anterior y se invoca directamente en el dispositivo, produciendo una traza que después se examina en el visor correspondiente:
adb shell perfetto -o /data/misc/perfetto-traces/traza -t 20s sched freq idle am wm gfx view
adb pull /data/misc/perfetto-traces/traza traza.perfetto-trace
Por último, la grabación de pantalla admite parámetros que la primera guía no detalló y que marcan la diferencia entre un vídeo inservible y uno útil, teniendo en cuenta que la duración máxima por defecto son tres minutos:
adb shell screenrecord --bit-rate 8M --size 720x1280 --time-limit 180 /sdcard/captura.mp4
adb pull /sdcard/captura.mp4
Automatizar pruebas: monos, jerarquías y guiones que no fallan
La automatización con ADB va bastante más allá de recorrer una lista de dispositivos, que fue lo que mostró la primera guía. El sistema incorpora herramientas de prueba que se manejan enteramente desde la línea de comandos y que resultan útiles tanto en desarrollo como en el análisis del comportamiento de una aplicación.
La más veterana es el generador de eventos aleatorios, conocido por el nombre del animal que le da nombre. Lanza sobre una aplicación una secuencia de toques, deslizamientos y pulsaciones al azar, y es una forma sorprendentemente eficaz de encontrar fallos que ninguna prueba dirigida descubre:
# Quinientos eventos sobre una aplicación, con pausa entre ellos y semilla fija
adb shell monkey -p com.ejemplo.miapp -v --throttle 300 -s 42 500
La semilla es el detalle importante: fijándola, la secuencia de eventos se puede repetir de forma idéntica, lo que convierte un fallo casual en un fallo reproducible. Sin ella, encontrar el error dos veces es cuestión de suerte.
La segunda herramienta es el volcado de la jerarquía de la interfaz, que produce una descripción en formato XML de todo lo que hay en pantalla: cada elemento, su texto, su descripción de accesibilidad, sus coordenadas y su estado. Es la base de cualquier automatización por selectores y también una forma cómoda de averiguar las coordenadas exactas de un botón para simular su pulsación:
# Volcar la jerarquía y traerla
adb shell uiautomator dump /sdcard/interfaz.xml
adb pull /sdcard/interfaz.xml
# Variante que no escribe en el dispositivo
adb exec-out uiautomator dump /dev/tty > interfaz.xml
La segunda forma evita dejar archivos en el teléfono, lo que en según qué contextos es preferible por higiene y, en pericia, obligatorio.
La tercera es la ejecución de pruebas instrumentadas, es decir, las pruebas unitarias y de interfaz que acompañan al código de una aplicación. Se lanzan indicando el paquete de pruebas y el ejecutor:
# Toda la batería de pruebas
adb shell am instrument -w com.ejemplo.miapp.test/androidx.test.runner.AndroidJUnitRunner
# Una sola clase
adb shell am instrument -w -e class com.ejemplo.miapp.LoginTest \
com.ejemplo.miapp.test/androidx.test.runner.AndroidJUnitRunner
Un detalle que ahorra muchos falsos negativos en pruebas de interfaz: las animaciones del sistema introducen esperas variables que desestabilizan las comprobaciones. Desactivarlas antes de la batería y restaurarlas después es práctica común en integración continua, y se hace con tres ajustes:
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
Conviene subrayar que estos tres comandos escriben en el dispositivo. En un banco de pruebas es lo normal; sobre un dispositivo bajo análisis, quedan absolutamente descartados.
Con esas piezas, un guion de integración continua completo tiene la forma siguiente, que reúne buena parte de lo visto hasta aquí:
#!/bin/bash
set -euo pipefail
export ANDROID_SERIAL="${1:?Falta el número de serie del dispositivo}"
adb wait-for-device
adb shell 'while [ "$(getprop sys.boot_completed)" != "1" ]; do sleep 1; done'
adb install -r -g -t app-debug.apk
adb install -r -t app-debug-androidTest.apk
adb logcat -c
adb shell am instrument -w com.ejemplo.miapp.test/androidx.test.runner.AndroidJUnitRunner \
| tee resultado_pruebas.txt
adb logcat -b all -d -v threadtime > registro_pruebas.txt
adb exec-out screencap -p > estado_final.png
El uso de set -euo pipefail en la primera línea, combinado con la propagación moderna de códigos de salida, hace que el guion se detenga en cuanto algo falla en lugar de continuar sobre un estado inconsistente. Es la diferencia entre una automatización fiable y una que miente.
Privilegios: `root`, SELinux y tipos de compilación
Uno de los malentendidos más extendidos sobre ADB es creer que adb root concede privilegios de superusuario en cualquier dispositivo. No es así, y entender por qué aclara de paso el techo real de lo que la herramienta puede lograr.

Android se compila en tres variantes. La compilación de tipo user es la que llevan los dispositivos comerciales: está pensada para el usuario final, tiene la depuración limitada y no permite elevar el daemon. La de tipo userdebug mantiene las funciones de la anterior y añade capacidades de diagnóstico, incluida la posibilidad de ejecutar el daemon como superusuario. Y la de tipo eng es la de desarrollo, con todas las restricciones relajadas. El tipo se consulta con una propiedad:
adb shell getprop ro.build.type
adb shell getprop ro.debuggable
En un teléfono comercial, la respuesta será user y cero, y adb root devolverá un mensaje inequívoco: el daemon no puede ejecutarse como superusuario en compilaciones de producción. En un dispositivo de desarrollo o en un emulador, la elevación funciona y con ella se abre el acceso al recinto privado de las aplicaciones:
adb root # reinicia el daemon con privilegios, si la compilación lo permite
adb shell id # ahora debería mostrar uid=0(root)
adb unroot # vuelve al daemon sin privilegios
En dispositivos modificados por el usuario, con un gestor de superusuario instalado, la vía es distinta: el daemon sigue siendo el normal, pero dentro del shell se puede invocar el binario correspondiente para ejecutar una orden concreta con privilegios.
adb shell su -c 'ls -la /data/data/'
Ahora bien, ser superusuario en Android no significa poder hacer cualquier cosa, y aquí entra el segundo mecanismo: SELinux. Android aplica control de acceso obligatorio en modo estricto, de forma que cada proceso opera dentro de un contexto que delimita a qué recursos puede acceder, con independencia de su identificador de usuario. Incluso un proceso con privilegios de superusuario queda confinado por su contexto. Se comprueba así:
adb shell getenforce # devuelve Enforcing en un dispositivo normal
adb shell id -Z # muestra el contexto de seguridad del shell
La tercera capa la aporta el arranque verificado. Las particiones del sistema se verifican criptográficamente en cada encendido, de modo que cualquier modificación las invalida y el dispositivo se niega a arrancar o lo hace con una advertencia. Por eso escribir en las particiones del sistema exige desactivar esa verificación y reiniciar, operación que deja huella permanente:
adb disable-verity
adb reboot
adb remount # ahora las particiones del sistema admiten escritura
Y aquí llega la advertencia más importante de todo este apartado, que en trabajo pericial tiene rango de norma absoluta: desbloquear el cargador de arranque de un dispositivo borra todos los datos de usuario. Es un comportamiento deliberado, diseñado precisamente para que nadie pueda saltarse la protección del dispositivo por esa vía. Quien, ante un teléfono bloqueado y con la esperanza de conseguir acceso privilegiado, ejecute la orden de desbloqueo, destruirá exactamente la evidencia que pretendía obtener, de manera irreversible. No hay procedimiento de recuperación posterior. Es el error más grave que se puede cometer con estas herramientas, y conviene que quede escrito con todas las letras.
De ese conjunto de restricciones se sigue una conclusión práctica que conviene asumir sin frustración: en un dispositivo comercial, moderno, actualizado y cifrado, ADB permite una extracción lógica —lo que el sistema consiente entregar a un usuario sin privilegios— y no una extracción física. Eso sigue siendo mucho: el almacenamiento compartido completo, el inventario de aplicaciones con sus fechas, los registros del sistema, las estadísticas de uso, el historial de batería, las capturas de pantalla y el informe de estado. Pretender más por vías que comprometen el dispositivo es, casi siempre, un mal negocio.
Extraer sin estropear: metadatos, empaquetado y comprobación
Una copia que altera las marcas de tiempo de los archivos es una copia degradada, y en un análisis cronológico puede ser inservible. El comportamiento por defecto de la extracción no conserva esas marcas, de modo que hay que pedirlo explícitamente o recurrir a un empaquetado previo.
La forma sencilla es el modificador que preserva atributos:
# Conserva la marca de tiempo y el modo de cada archivo
adb pull -a /sdcard/DCIM/ ./evidencia_dcim/
La forma robusta, y la preferible cuando la estructura de directorios importa, consiste en empaquetar en el dispositivo y recibir el paquete por la salida estándar, sin escribir nada en el teléfono:
# Empaquetado íntegro con estructura, permisos y tiempos
adb exec-out "tar -cf - /sdcard/DCIM" > dcim.tar
# Comprobación inmediata de integridad, en ambos extremos
adb shell "tar -cf - /sdcard/DCIM | sha256sum"
sha256sum dcim.tar
Esta técnica tiene tres ventajas. Conserva los metadatos que el protocolo de transferencia no transporta. Evita el problema de los enlaces simbólicos, que en una extracción recursiva pueden provocar recorridos infinitos o copias duplicadas. Y produce un único archivo, más fácil de sellar y de custodiar que un árbol de miles de ficheros.
Cuando lo que interesa no es el contenido sino el inventario, una ficha de metadatos de todo el almacenamiento compartido se obtiene en una sola orden y resulta utilísima como anexo del informe:
adb shell "find /sdcard -type f -exec stat -c '%n|%s|%y|%z' {} \;" > inventario_archivos.txt
Cada línea recoge la ruta, el tamaño en bytes, la fecha de última modificación y la de último cambio de estado. Ese inventario, sellado con su resumen criptográfico, documenta el contenido del dispositivo en el momento de la intervención aunque después se discuta qué archivos concretos se llevaron al laboratorio.
En la dirección contraria, y solo para entornos de desarrollo, conviene conocer el modificador de sincronización, que evita retransmitir lo que no ha cambiado y acelera notablemente los ciclos de trabajo:
adb push --sync ./recursos/ /sdcard/recursos/
ADB más allá del teléfono
La última área que quedó fuera de la primera guía es la diversidad de dispositivos que hablan este mismo protocolo. Android está en muchos más sitios que en los teléfonos, y en cada uno ADB presenta particularidades.
En los relojes con Wear OS, la depuración por cable no siempre es posible porque muchos modelos carecen de puerto de datos, de modo que la vía habitual es la conexión inalámbrica desde las opciones de desarrollador del propio reloj. En generaciones anteriores existía además la depuración a través del teléfono emparejado, que tendía un túnel usando el móvil como puente. En cualquier caso, la lógica es la misma: activar depuración, obtener la dirección y conectar por red.
En los televisores y reproductores con Android TV, la conexión es casi siempre por red, y la depuración se activa en el mismo menú oculto que en los teléfonos. La particularidad está en la interacción: sin pantalla táctil, la automatización se apoya en los códigos de tecla del mando en lugar de en coordenadas.
adb connect 192.168.1.45:5555
adb shell input keyevent KEYCODE_DPAD_DOWN
adb shell input keyevent KEYCODE_ENTER
adb shell input keyevent KEYCODE_HOME
En los sistemas de automoción, la plataforma está diseñada desde el principio como multiusuario y con varias pantallas, lo que se refleja en los comandos: al lanzar una aplicación puede ser necesario indicar en qué pantalla debe aparecer. Y en los visores de realidad mixta basados en Android, la herramienta funciona igual que en un teléfono, siendo la vía habitual para instalar aplicaciones al margen de la tienda del fabricante.
Un caso aparte, muy frecuente hoy, es el de quien trabaja desde un entorno Linux dentro de Windows. Ese entorno no accede directamente a los dispositivos USB del sistema anfitrión, de modo que la solución limpia no es intentar redirigir el hardware sino aprovechar la arquitectura de ADB: se deja el servidor corriendo en Windows, con el dispositivo conectado, y el cliente del entorno Linux se dirige a él por red.
# En Windows: arrancar el servidor escuchando en todas las interfaces
adb -a -P 5037 nodaemon server
# En el entorno Linux: apuntar el cliente al servidor de Windows
export ADB_SERVER_SOCKET=tcp:172.28.16.1:5037
adb devices
Ese mismo mecanismo resuelve el uso de ADB dentro de contenedores y en sistemas de integración continua, donde el servidor vive en la máquina que tiene los dispositivos físicos y los trabajos se conectan a él desde donde haga falta. Es, de nuevo, la separación entre cliente, servidor y daemon la que hace posible una solución elegante a un problema que parecía de hardware.
Traducir los errores: catálogo de mensajes frecuentes
Buena parte del tiempo que se pierde con ADB se va en interpretar mensajes de error escritos por y para quien conoce el código fuente. La siguiente tabla reúne los más habituales, lo que significan realmente y qué hacer con ellos.
| Mensaje | Qué significa en realidad | Qué hacer |
|---|---|---|
| Conexión y transporte | ||
more than one device/emulator | Hay varios dispositivos y no se indicó cuál | -s SERIAL · ANDROID_SERIAL |
device unauthorized | El diálogo de la clave RSA está pendiente en la pantalla | Desbloquear y aceptar |
error: closed | El canal se cerró: dispositivo desconectado o autorización perdida | Reconectar y reiniciar el servidor |
protocol fault (couldn't read status) | Cliente y servidor de versiones distintas | Dejar una sola instalación |
| Instalación de aplicaciones | ||
INSTALL_FAILED_ALREADY_EXISTS | El paquete ya está instalado | adb install -r |
INSTALL_FAILED_UPDATE_INCOMPATIBLE | Firma distinta de la versión instalada | Desinstalar antes (se pierden datos) |
INSTALL_FAILED_VERSION_DOWNGRADE | Se intenta instalar una versión anterior | adb install -d |
INSTALL_FAILED_NO_MATCHING_ABIS | El APK no trae la arquitectura del dispositivo | Usar el APK correcto o el bundle |
INSTALL_FAILED_TEST_ONLY | El paquete está marcado como de prueba | adb install -t |
INSTALL_PARSE_FAILED_NO_CERTIFICATES | El APK no está firmado | Firmar el paquete |
INSTALL_FAILED_USER_RESTRICTED | El fabricante exige un permiso adicional | Activar «instalar vía USB» |
INSTALL_FAILED_INSUFFICIENT_STORAGE | No hay espacio suficiente | Liberar espacio |
DELETE_FAILED_INTERNAL_ERROR | Paquete del sistema o de otro usuario | pm uninstall -k –user 0 |
| Archivos y permisos | ||
failed to stat remote object | La ruta no existe o no es accesible para shell | Comprobar la ruta con ls |
Permission denied | Zona fuera del alcance del usuario shell | run-as, o dispositivo con privilegios |
Read-only file system | La partición del sistema no admite escritura | disable-verity + remount |
adbd cannot run as root | La compilación es de tipo user | No hay elevación posible |
Merece la pena detenerse en dos de ellos, porque su causa no es evidente. El fallo de instalación por incompatibilidad de actualización aparece cuando se intenta reinstalar sobre una versión firmada con otra clave; es lo que ocurre al pasar de una compilación de la tienda a una compilación propia, y la única salida es desinstalar la anterior, con la pérdida de datos que eso implica. Y la restricción impuesta al usuario aparece en dispositivos de ciertos fabricantes que exigen activar explícitamente un ajuste adicional en las opciones de desarrollador para permitir instalaciones a través de la depuración; hasta que se activa, cualquier intento fracasa sin explicación clara.
Artefactos de interés: dónde mirar y con qué acceso
Cerramos la parte técnica con una tabla de referencia que reúne las rutas y los volcados mencionados a lo largo del texto, indicando qué contiene cada uno y qué nivel de acceso requiere. Es el mapa que conviene tener delante al planificar una adquisición, porque separa de un vistazo lo que se puede obtener sin privilegios de lo que exige un dispositivo preparado.
| Artefacto o comando | Qué contiene | Acceso |
|---|---|---|
| Identificación y contexto temporal | ||
adb shell date · uptime | Hora del dispositivo y tiempo desde el arranque | shell |
getprop persist.sys.timezone | Huso horario configurado | shell |
dumpsys user | Usuarios y perfiles, con su fecha de creación | shell |
| Inventario de aplicaciones | ||
pm list packages -u -i --user N | Aplicaciones, incluidas las desinstaladas con datos | shell |
dumpsys package <paquete> | Fechas de instalación y actualización, instalador de origen | shell |
dumpsys appops | Último acceso a cámara, micrófono y ubicación por aplicación | shell |
| Actividad y cronología | ||
dumpsys usagestats | Qué aplicación estuvo en primer plano y cuándo | shell |
dumpsys batterystats --history | Pantalla, red, posición y bloqueos de suspensión | shell |
dumpsys netstats detail | Bytes por aplicación, interfaz e intervalo | shell |
dumpsys activity recents | Tareas recientes en el momento de la captura | shell |
logcat -b events -d | Eventos estructurados del sistema | shell |
| Contenido | ||
/sdcard (adb pull -a) | Fotos, descargas y documentos del usuario | shell |
adb bugreport informe.zip | Estado completo del dispositivo en un solo archivo | shell |
/data/user/N/<paq>/databases | Bases de datos SQLite con sus diarios -wal y -shm | root |
/data/user_de/N/<paquete> | La mitad de los datos legible desde el arranque | root |
/data/system/usagestats | Los archivos que respaldan las estadísticas de uso | root |
/data/misc · /data/tombstones | Redes conocidas y restos de procesos caídos | root |
Chuleta avanzada
A la manera de la primera guía, reunimos aquí los comandos introducidos en esta segunda parte, agrupados por función.
| Comando | Para qué sirve |
|---|---|
| Protocolo y servidor | |
printf '000chost:version' | nc 127.0.0.1 5037 | Preguntar al servidor sin usar el ejecutable |
ADB_TRACE=all adb devices | Ver los mensajes del protocolo por dentro |
adb -L tcp:5038 start-server | Un segundo servidor independiente |
export ANDROID_SERIAL=<serie> | Fijar el dispositivo de destino de la sesión |
export ADB_SERVER_SOCKET=tcp:IP:5037 | Usar el servidor de otra máquina (WSL, contenedores) |
| Shell y automatización | |
adb exec-out <comando> | Salida binaria intacta, sin pseudoterminal |
adb shell "orden con comodines" | Que el patrón lo expanda el teléfono |
adb wait-for-device | Esperar a que aparezca el dispositivo |
getprop sys.boot_completed | Esperar a que Android termine de arrancar |
| Paquetes, permisos y usuarios | |
adb install-multiple base.apk split… | Instalar una aplicación de paquete dividido |
pm install-create / write / commit | Sesión de instalación manual |
pm uninstall -k --user 0 <paquete> | Retirar para un usuario conservando datos |
pm disable-user / pm enable | Deshabilitar de forma reversible |
pm grant / revoke · appops get | Gobernar permisos y operaciones de aplicación |
pm list users · am get-current-user | Usuarios, perfiles y usuario activo |
| Registros y diagnóstico | |
logcat -b events -d -v threadtime | Eventos estructurados con proceso e hilo |
logcat -v year -v UTC | Marcas absolutas y sin ambigüedad de huso |
logcat -f ruta -r 16384 -n 4 | Captura larga con rotación de archivos |
adb bugreport informe.zip | Volcado completo del estado del dispositivo |
| Rendimiento | |
am start -W -n paquete/.Actividad | Medir el tiempo de arranque de una pantalla |
dumpsys gfxinfo <paquete> framestats | Detalle por fotograma del dibujo |
dumpsys meminfo · procstats --hours 3 | Memoria instantánea y agregada |
adb shell perfetto -o ruta -t 20s … | Traza del sistema para análisis fino |
| Pruebas | |
monkey -p paquete -s 42 --throttle 300 N | Eventos aleatorios reproducibles |
uiautomator dump /dev/tty | Jerarquía de la interfaz sin escribir en el teléfono |
am instrument -w paquete.test/… | Ejecutar la batería de pruebas instrumentadas |
| Extracción y privilegios | |
adb pull -a origen destino | Conservar marcas de tiempo y modo |
adb exec-out "tar -cf - ruta" > x.tar | Empaquetar preservando estructura y metadatos |
adb shell sha256sum ruta | Sellar la integridad en el propio dispositivo |
adb shell run-as <paquete> … | Entrar en el recinto de una aplicación depurable |
getprop ro.build.type · getenforce | Saber si hay elevación posible y si SELinux confina |
Lo que queda bajo la superficie
Si la primera guía enseñaba a cruzar el puente, esta se ha ocupado de mirar bajo el tablero. Hemos visto cómo se compone cada mensaje que viaja entre el ordenador y el teléfono, cómo se puede conversar con el servidor sin usar el ejecutable, por qué unas comillas mal puestas cambian el resultado de un comando y por qué los datos binarios exigen una vía distinta de la del texto. Hemos recorrido la geografía real del almacenamiento de Android, con sus dos zonas de cifrado y sus rutas paralelas por usuario, y hemos visto cómo un perfil de trabajo puede esconder a plena vista la mitad de lo que interesa. Hemos aprendido a leer las bases de datos sin perder sus diarios, a elegir el búfer correcto de los registros del sistema y a reconstruir una cronología con cuatro volcados de lectura. Y hemos delimitado con precisión el techo de lo que ADB puede hacer en un dispositivo comercial moderno, que es alto, pero tiene un límite claro y unas fronteras que conviene no cruzar.
Queda, como siempre, materia fuera. El análisis de las bases de datos extraídas es una disciplina en sí misma, con herramientas propias y una literatura considerable. La instrumentación dinámica de aplicaciones en ejecución abre otra vía de trabajo que apenas hemos rozado al mencionar la depuración. Y la comparación con el ecosistema de la competencia, donde el equivalente de ADB no existe y las adquisiciones siguen caminos muy distintos, daría por sí sola para un texto entero. Serán, si el interés acompaña, materia de futuras entregas.
Mantenemos el propósito con el que se publicó la primera parte. El conocimiento técnico riguroso, explicado con claridad y en español, es un bien que gana valor al circular. Quien lea estas páginas y descubra en su propio teléfono, con un par de comandos, la cantidad de cosas que el sistema anota sobre su uso, habrá aprendido algo que va más allá de una herramienta: habrá entendido que estos dispositivos llevan un diario detallado, y que saber leerlo es tan útil para quien investiga como necesario para quien quiere protegerse.
Glosario de esta segunda parte
Mensaje ADB. Unidad básica del protocolo: veinticuatro bytes de cabecera con el comando, dos argumentos, la longitud de la carga, su suma de comprobación y el valor de verificación, seguidos opcionalmente de datos.
Servicio local. Cadena que identifica lo que se pide al dispositivo al abrir un canal (shell:, sync:, reboot:, tcpip:), y que determina el comportamiento del canal.
Host service. Servicio atendido por el servidor del ordenador, sin intervención del dispositivo, como host:version o host:devices.
Almacenamiento cifrado con credencial. Zona de datos que solo se descifra tras el primer desbloqueo del usuario con su PIN, patrón o contraseña; corresponde a las rutas bajo /data/user/.
Almacenamiento cifrado con clave de dispositivo. Zona que se descifra al arrancar, sin intervención del usuario, para permitir funciones básicas; corresponde a las rutas bajo /data/user_de/.
Registro de escritura anticipada. Modo de SQLite en el que las modificaciones se anotan primero en un archivo auxiliar con extensión -wal y solo después se consolidan en la base de datos.
Búfer de registro. Cada uno de los almacenes circulares e independientes en los que Android separa sus mensajes de registro (main, system, crash, radio, events, kernel).
Operaciones de aplicación. Mecanismo de control de grano fino, complementario a los permisos, que además anota el momento del último ejercicio de cada acción por cada aplicación.
Tiempo transcurrido desde el arranque. Reloj relativo que cuenta desde el último encendido incluyendo la suspensión; requiere anclaje para convertirse en hora absoluta.
Tipo de compilación. Variante con la que se construyó el sistema (user, userdebug o eng), que determina si el daemon puede ejecutarse con privilegios de superusuario.
Arranque verificado. Comprobación criptográfica de las particiones del sistema en cada encendido, que impide su modificación silenciosa.
Paquete dividido. Aplicación distribuida como un archivo base más varios complementos con recursos específicos de arquitectura, densidad o idioma, que deben instalarse conjuntamente.
Fuentes y lecturas recomendadas
Android Open Source Project. (s. f.). ADB protocol overview. AOSP. Recuperado de https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/main/protocol.txt
Android Open Source Project. (s. f.). SYNC protocol. AOSP. Recuperado de https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/main/SYNC.TXT
Android Open Source Project. (s. f.). File-Based Encryption. AOSP. Recuperado de https://source.android.com/docs/security/features/encryption/file-based
Android Open Source Project. (s. f.). Security-Enhanced Linux in Android. AOSP. Recuperado de https://source.android.com/docs/security/features/selinux
Android Open Source Project. (s. f.). Verified Boot. AOSP. Recuperado de https://source.android.com/docs/security/features/verifiedboot
Google. (s. f.). Android Debug Bridge (adb). Android Developers. Recuperado de https://developer.android.com/tools/adb
Google. (s. f.). logcat command-line tool. Android Developers. Recuperado de https://developer.android.com/tools/logcat
Google. (s. f.). dumpsys. Android Developers. Recuperado de https://developer.android.com/tools/dumpsys
Google. (s. f.). Capture and read bug reports. Android Developers. Recuperado de https://developer.android.com/studio/debug/bug-report
Google. (s. f.). Perfetto: System profiling, app tracing and trace analysis. Recuperado de https://perfetto.dev/docs/
Google. (s. f.). Test from the command line. Android Developers. Recuperado de https://developer.android.com/studio/test/command-line
Google. (s. f.). Configure on-device developer options. Android Developers. Recuperado de https://developer.android.com/studio/debug/dev-options
Hipp, D. R. y equipo de SQLite. (s. f.). Write-Ahead Logging. SQLite. Recuperado de https://www.sqlite.org/wal.html
International Organization for Standardization. (2012). ISO/IEC 27037:2012. Information technology — Security techniques — Guidelines for identification, collection, acquisition and preservation of digital evidence. ISO.
Jefatura del Estado. (2015). Ley Orgánica 13/2015, de 5 de octubre, de modificación de la Ley de Enjuiciamiento Criminal para el fortalecimiento de las garantías procesales y la regulación de las medidas de investigación tecnológica. Boletín Oficial del Estado, núm. 239. https://www.boe.es/eli/es/lo/2015/10/05/13
Créditos de los esquemas
Los ocho esquemas que ilustran esta segunda parte (anatomía del protocolo, recorrido de una orden por los dos intérpretes de comandos, mapa del almacenamiento, usuarios y perfiles, bases de datos con diario, búferes de registro, reconstrucción de la línea de tiempo y capas de privilegio) son de elaboración propia, realizados expresamente para este trabajo. La composición de portada (16:9) es igualmente original.

Deja una respuesta