Crea una cuenta gratis para responder, reaccionar y enterarte cuando haya respuestas en este tema.
Descompilé toDus para saber si la app espía a sus usuarios desde el teléfono. En el cliente no encontré nada escondido. Lo que sí encontré es que el diseño le entrega al servidor casi todo: los mensajes llegan sin cifrado de extremo a extremo, la agenda de contactos se sube con nombres y números, y el servidor puede ordenarle a la app que envíe la ubicación del usuario.
Cómo lo hice
Trabajé con el archivo toDus_latest.apk, versión 2.1.2 (código de versión 30102, paquete cu.todus.android). El certificado de firma dice "ToDus Development Group", organización "UCI", La Habana. Su SHA-256 es:
c0a53eebc2db914535149bbb53162ddce4a8cae251f3da2896d20c27f6b49ce9Descompilé la app con jadx 1.5.1 y leí el manifiesto con androguard 4.1.4. El código propio de toDus ocupa unas 400 000 líneas una vez descompilado. Las librerías vienen ofuscadas, pero las clases de toDus conservan sus nombres, y eso me permitió seguir los datos de una clase a otra.
Todo esto es análisis estático. No pude iniciar sesión, así que no vi la app funcionando ni capturé su tráfico. Lo que cuento aquí es lo que el código puede hacer. Lo que el servidor hace con los datos no lo puedo ver desde el teléfono.
Los mensajes viajan sin cifrado de extremo a extremo
El servidor de toDus recibe el texto de cada mensaje tal como lo escribe el usuario. La conexión entre el teléfono y el servidor va con TLS, y ese cifrado termina en el servidor.
La app usa XMPP, un protocolo de mensajería abierto, a través de la librería Smack. Busqué en todo el APK las librerías habituales de cifrado de extremo a extremo (OMEMO, Signal Protocol, Olm) y sus clases típicas, y no hay ninguna. El único cifrado que programaron ellos está en las copias de seguridad, y más abajo explico por qué tampoco sirve de mucho.
Así se arma un mensaje antes de salir del teléfono. El contenido va directo al cuerpo del mensaje XMPP:
// cu/todus/android/xmpp/mapper/MapperMessageToStanza.java:146-150
EntityBareJid entityBareFrom = JidCreate.entityBareFrom(
... message.getRoomId() + "@im.todus.cu");
org.jivesoftware.smack.packet.Message message7 = new org.jivesoftware.smack.packet.Message();
message7.setStanzaId(message.getId());
message7.setTo(entityBareFrom);
message7.setBody(message.getContent()); // texto del usuario, sin cifrarEl TLS protege el mensaje de quien mire la red, pero quien opera el servidor puede leerlo. En Signal o en WhatsApp el servidor solo ve datos cifrados. En toDus, el operador puede leer, guardar y entregar cualquier conversación.
Las llamadas de voz y vídeo usan WebRTC con DTLS-SRTP entre los dos extremos. La app le pide al servidor credenciales para un servidor de retransmisión TURN (todus:turn:cred). Sin ver el tráfico real no puedo decir si alguna llamada pasa descifrada por un servidor.
Ubicación: el servidor decide si la app la envía
Cada vez que inicia sesión, la app le pregunta al servidor si debe compartir la ubicación del usuario. Si el servidor dice que sí, la app manda latitud y longitud sin que el usuario toque nada. Antes de enviarlas, el teléfono no consulta ningún ajuste guardado en el propio teléfono.
La clase LocationDataSource se crea al arrancar la app si hay una sesión guardada (ToDusApp.java:362). Su constructor lanza un bucle que lee la posición, espera 4 segundos y vuelve a empezar:
// cu/todus/android/location/LocationDataSource.java (bytecode recuperado por jadx)
if (pe.w0.b(4000, r0) != r1) ... // delay(4000): espera 4 segundos
if (autoUpdate(r0) == r1) ... // vuelve a leer la posiciónCada lectura le pide datos al GPS y a la red, con requestLocationUpdates("gps", 500L, 2.0f, ...) y ("network", 1000L, 2.0f, ...). Hace falta que el usuario haya dado permiso de ubicación. En esta versión esas lecturas se quedan en memoria. No encontré código que las mande al servidor cada 4 segundos.
El envío ocurre en otro sitio. Después de autenticarse en XMPP, la app crea un LocationManager que hace esto:
// cu/todus/android/xmpp/managers/LocationManager.java:75-105 (resumido)
if (currentPosition() != null) {
obj = getStatus(this); // pregunta al servidor: tnearp:status:get
}
if ((Boolean) obj) { // respuesta del servidor
if (getOwnerConnection() != Quality.BAD_QUALITY) {
double latitude = currentPosition().getLatitude();
double longitude = currentPosition().getLongitude();
sendNearLocation(latitude, longitude); // envía tnearp:loc:set
}
}Lo que decide si se envía es un atributo que manda el servidor:
// cu/todus/android/xmpp/iq/near/NearGetStatusIQProvider.java:19-21
String attributeValue = parser.getAttributeValue("", "is_show_in");
return new NearGetStatusIQ(Integer.parseInt(attributeValue) == 1);Y este es el paquete que sale del teléfono, con las coordenadas en los atributos lat y lon:
// cu/todus/android/xmpp/iq/near/NearSetIQ.java
public static final String NAMESPACE = "tnearp:loc:set";
xml.attribute(ExtensionLocation.TAG_LAT, String.valueOf(this.lat));
xml.attribute(ExtensionLocation.TAG_LON, String.valueOf(this.lon));
setType(IQ.Type.set);Todo esto forma parte de la función "Personas cerca". El usuario la enciende con un interruptor que dice "Compartiendo ubicación en tiempo real", y desde ese momento la app reenvía su ubicación en cada sesión nueva sin volver a preguntar. El problema es que quien guarda y devuelve el valor is_show_in es el servidor. Si el servidor responde 1 para alguien que nunca encendió la función, el teléfono de esa persona envía su posición igual. No sé si eso pasa en la práctica. Lo que sí puedo afirmar es que el código lo permite.
La ubicación también sale del teléfono cuando el usuario la manda en un chat, cuando marca "Enviar tu ubicación actual" al publicar un producto en el catálogo, y cuando abre un mapa, porque las imágenes se descargan de map.todus.cu. En esos casos es el usuario quien actúa.
El manifiesto declara ACCESS_BACKGROUND_LOCATION, pero el código nunca le pide ese permiso al usuario. Solo pide la ubicación aproximada y la precisa.
La agenda de contactos viaja al servidor
Si el usuario da permiso de contactos, toDus sube al servidor el número y el nombre de cada persona de su agenda. Los números van en claro, sin pasar antes por un hash.
La app lee la agenda con ContactsContract y la sube en lotes, con 150 ms de pausa entre uno y otro:
// cu/todus/android/xmpp/managers/RosterManager.java (uploadContactsBatch)
for (Contact contact : list10) {
arrayList.add(ua.z.a(contact.getPhone(), contact.getFName())); // (teléfono, nombre)
}
RosterUpdateIQ rosterUpdateIQ = new RosterUpdateIQ(i17, arrayList, i18, null);
XMPPInlinesKt.sendIQFlow(xMPPTCPConnection, rosterUpdateIQ);El paquete usa un espacio de nombres propio, todus:roster:update. Los pares se serializan en binario, se codifican en base64 y van al dominio im.todus.cu.
Muchas apps de mensajería suben la agenda para saber cuáles de tus contactos usan el servicio. Lo que me llama la atención aquí es que también se sube el nombre con el que guardaste a cada persona. Con eso el servidor puede armar la red de relaciones de cada usuario, incluida gente que nunca instaló toDus.
Copias de seguridad con candado de juguete
Cualquiera que tenga una copia de seguridad de toDus y sepa el nombre de usuario de su dueño puede leerla. La clase se llama AESUtils, aunque usa Blowfish:
// cu/todus/android/backup/AESUtils.java:31-37
public AESUtils(String KEY) {
this.KEY = KEY;
this.ALGORITHM = "Blowfish";
this.MODE = "Blowfish/CBC/PKCS5Padding";
this.IV = "abcdefgh"; // siempre el mismo
}La clave es el nombre de usuario seguido de un texto fijo:
// cu/todus/android/backup/Exporter.java:110 y :349
aESUtils = new AESUtils(str3 + "9ABCDEF");
// ManageBackupViewModel$export$1.java:67-70
String username = ((Owner) mainRepository.getOwner().invoke()).getUsername();
obj = exporter.export(username, this);Con una clave así, quien encuentre la copia en un teléfono, una memoria USB o una computadora la descifra sin esfuerzo.
Permisos: pide 45 y usa bastantes menos
Revisé uno por uno los 45 permisos del manifiesto para ver si el código de toDus los usa. Varios no aparecen en ninguna parte. Pedir permisos de más es mala práctica, pero no demuestra espionaje.
Con los SMS el código es claro. El receptor solo se registra en la pantalla de registro y descarta cualquier mensaje que no venga de "toDus":
// cu/todus/android/utils/SMSReceived$register$1.java:79-80
if (y.d(str, "toDus")) {
String extractCode = SMSReceived.this.extractCode(...);Lo que no encontré
Busqué una forma oculta de que el servidor le ordene al teléfono recoger datos, y no la hay. Revisé los cerca de 100 espacios de nombres XMPP que la app define, y todos corresponden a funciones visibles: grupos, canales, estados, bloqueos, contactos, llamadas, actualizaciones y "Personas cerca".
El push de Firebase solo muestra una notificación y no interpreta órdenes. La analítica de Firebase está apagada en el propio código y no hay ninguna llamada a logEvent. Hay un módulo llamado "sentry", pero es un registro de errores que la app le enseña al usuario como un chat llamado "Registro de Errores", con un aviso. Todo el tráfico va a dominios de toDus (*.todus.cu), de la tienda Apklis (apklis.cu) o de Google Firebase.
Una función se llama allowAllCertificated(), y al verla pensé que la app aceptaba cualquier certificado. Me equivoqué: lo que hace es comparar la conexión con dos certificados de todus.cu que vienen dentro de la app.
Lo que sí quedó como descuido es la configuración de red. Permite tráfico sin cifrar hacia nueve direcciones privadas 10.37.41.x y acepta para ellas certificados instalados por el usuario. Parecen servidores de una red interna, quizás de desarrollo, y esa configuración no debería estar en la versión pública.
Mi conclusión
Empecé preguntándome si toDus espía desde el teléfono o desde el servidor, y el código apunta al servidor. Para vigilar no hace falta un programa espía en el cliente, porque el operador del servicio ya recibe:
El código no me dice qué hace el operador con esos datos ni quién accede a ellos. La app está firmada por la UCI, una universidad del Estado cubano. Yo, desde ahora, doy por hecho que todo lo que se escribe en toDus lo puede leer quien controle im.todus.cu.