A Talk about Logic Vulnerabilities of Android Components — Android Security

TutorialBoy
33 min readAug 1, 2023

--

Foreword

As society pays more and more attention to security, various defensive programming or vulnerability mitigation measures are gradually added to the operating system, such as code signature, pointer signature, address randomization, isolated heap, etc. Many common memory corruption vulnerabilities are in Stable exploitation is often difficult under these mitigations. Therefore, attackers are gradually focusing more on logical loopholes. Logical loopholes usually have good stability and are not affected but at the same time, they are hidden deep and are difficult to find in a large number of business codes. Moreover, due to the variety of forms, it is not very versatile, and may not be a high-priority research direction from the perspective of the input-output ratio. Regardless, this is always an attack surface to watch. Therefore, this article introduces some common logic vulnerabilities targeting the Android platform.

The Major Components

Anyone who has been in contact with Android should have heard of the “major components”. The first thing to learn when developing an application is the life cycle of each component. The so-called four major components refer to Activity, Service, Broadcast Receiver, and Content Provider respectively. For the implementation details of these components, please refer to the official document: Application Fundamentals.

In security research, the four major components deserve our special attention, because they are an important bridge for the application to communicate with the outside world, and even within the application, these components are used to build a loosely coupled relationship with each other. For example, the application does not need to apply for camera permission, but the (system) camera application can open the camera and obtain the captured photos through mutual communication between components as if it was taking pictures by itself.

In the process of component interaction, the core data structure is Intent, which is the carrier of communication between most components.

Intent 101

According to the official statement, Intent is an “abstract description of an operation to be performed”, which can also be called “intent” in literal translation, such as wanting to turn on the camera to take pictures, to open a browser to visit a website, to open the settings interface,… can be described by Intent.

There are two main forms of Intent, namely explicit Intent, and implicit Intent; the difference between the two is that the former explicitly specifies a Component, while the latter does not specify a Component, but it will use enough information to help the system understand the intent, such as Action, Category, etc.

The main function of Intent is to start Activity, so we take this scenario as an example to analyze the specific implementation of Intent from the source code. The general code snippet for starting an Activity is as follows:

Intent intent = new Intent(context, SomeActivity.class);
startActivity(intent);

An explicit Intent is used here, but that’s not the point. It is generally called in an Activity, so the Activity.startActivitycode is called in frameworks/base/core/java/android/app/Activity.java, and there is no copying and pasting here. In short, the calling link is as follows:

  • Activity. startActivity()
  • Activity. startActivityForResult()
  • Instrumentation. execStartActivity()
  • ActivityTaskManager.getService().startActivity()
  • IActivityTaskManager. startActivity()

The last call is an interface, which is a very common pattern. The next step is to find its implementation. If there is no accident, this implementation should be in another process. In fact it is also system_serverin :

  • ActivityTaskManagerService. startActivity()
  • ActivityTaskManagerService.startActivityAsUser()
  • ActivityStarter. execute()

The last method prepares to start the Activity through the information passed in before, including caller, userId, flags, callingPackage and the most important intent information, as follows:

private int startActivityAsUser(...) {
// ...
return getActivityStartController()
.obtainStarter(
intent, "startActivityAsUser")
.setCaller(caller)
.setCallingPackage(callingPackage)
.setCallingFeatureId(callingFeatureId)
.setResolvedType(resolvedType)
.setResultTo(resultTo)
.setResultWho(resultWho)
.setRequestCode(requestCode)
.setStartFlags(startFlags)
.setProfilerInfo(profilerInfo)
.setActivityOptions(bOptions)
.setUserId(userId)
.execute();
}

The main logic of ActivityStarter.execute() is as follows:

int execute() {
// ...
if (mRequest.activityInfo == null) {
mRequest.resolveActivity(mSupervisor);
}
res = resolveToHeavyWeightSwitcherIfNeeded();
res = executeRequest(mRequest);
}

Among them, it resolveActivityis used to obtain the information of the Activity to be started. For example, in the case of implicit start, there may be multiple targets that meet the requirements, and a pop-up menu will ask the user which application to choose to open. executeRequestIn the middle, it mainly checks related permissions, and calls startActivityUncheckedto .

I have already introduced most of the processes in the Android12 application startup process analysis , and here more attention is paid to the role of Intent itself. From the above analysis, it can be seen as a message carrier in multi-process communication, and its source code definition can also be seen that Intent itself is a structure that can be serialized and passed between processes.

public class Intent implements Parcelable, Cloneable { ... }

IntentIt has many methods and attributes, which will not be expanded here for the time being, and will be analyzed later when specific vulnerabilities are introduced. The following article mainly starts with the four major components, and introduces some common vulnerability modes and design traps respectively.

Activity

Activity , also known as the active window, is a graphical interface that directly interacts with the user. One of the main development tasks of APP is to design each activity and plan the jumps and links between them. Usually an activity represents a full-screen active window, but it can also exist in other forms, such as floating windows, multi-windows, etc. As a UI window, it generally uses XML files for layout, and inherits the Activity class to implement its life cycle functions onCreateand onPauseother life cycle methods.

If the Activity defined by the developer wants to be Context.startActivitystarted by , it must be declared in the manifest file of the APP, namely AndroidManifest.xml. When the application is installed, it PackageManagerwill parse the relevant information in its manifest file and register it in the system, so that it can be searched resolvewhen .

In the adb shell, you can use am start-activityto open the specified Activity, and start it by specifying the Intent:

am start-activity [-D] [-N] [-W] [-P <FILE>] [--start-profiler <FILE>]
[--sampling INTERVAL] [--streaming] [-R COUNT] [-S]
[--track-allocation] [--user <USER_ID> | current] <INTENT>

As the carrier of the user interface, Activity carries many tasks such as user input/processing, external data reception/display, etc., so it is a main attack surface of the application. Several common attack scenarios are introduced below.

The Life Cycle

The classic life cycle diagram of Activity is as follows:

Usually developers only need to implement the onCreatemethod , but for some complex business scenarios, it is necessary to correctly understand its life cycle. Take an application that the author encountered in the internal test as an example. Some sensitive operations were performed in a certain Activity, such as turning on the camera streaming, or turning on the recording, onDestroybut the streaming/recording was only turned off in . This will cause these operations to still run in the background when the APP enters the background, and the attacker can construct a task stack so that the victim still executes the background functions of the target application when facing the phishing interface of the malicious application, thus forming a special phishing scenario. The correct approach should be to close sensitive operations in the onPausedcallback .

In fact, the attacker can precisely control the triggering timing of the target Activity lifecycle callback function by continuously sending different Intents. If no attention is paid during development, the state machine of the application function will be abnormal or even a security problem.

Implicit Exported

As mentioned earlier, if the Activity defined by the developer wants to use startActivityto start, it must <activity>be declared in AndroidManifest.xml. An example of a declaration is as follows:

<activity xmlns:android="http://schemas.android.com/apk/res/android" android:theme="@android:01030055" android:name="com.evilpan.RouterActivity">
<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="demo" android:host="router"/>
</intent-filter>
</activity>

There are many properties supported in activity . One of the important attributes is android:exportedto indicate whether the current Activity can be started by other application components. This attribute has several characteristics.

  • The attribute can be defaulted, and the default value defaults to false.
  • If the Activity does not explicitly set this property and it is defined in the Activity <intent-filter>, then the default value is true.

That is to say, the developer may not explicitly specify the Activity export, but because it is specified intent-filter, it is actually exported, that is, the corresponding Activity can be invoked by other applications. This situation was very common in the early days. For example, APP designed a set of interfaces for changing passwords. You need to enter the old password first and then jump to the interface for entering the new password. If the latter is exported, the attacker can directly evoke the input of the new password. The password interface, thus bypassing the verification logic of the old password.

Google has been deeply aware of this problem, so it stipulates that after Android 12, if the application’s Activity contains intent-filter, it must be explicitly specified android:exportedas true or false, and the default is not allowed. In Android 12, an application that does not explicitly specify the exported attribute and has an intent-filter Activity will be directly rejected by the PackageManager during installation.

Fragment Injection

Activity, as the core component of the UI, also supports modular development, such as displaying several reusable sub-interfaces in the same interface. The Fragments component, or “fragment”, was born with this design idea . FragmentActivityOne or more fragments can be combined in an Activity to facilitate code reuse. The life cycle of a fragment is affected by the host Activity.

The Fragment Injection vulnerability first broke out in 2013. Here we only introduce its principle. The original articles and papers are attached at the end of this section. The core of the vulnerability is the PreferenceActivityclass . Developers can inherit it to implement convenient setting functions. The onCreate function of this class has the following functions:

protected void onCreate() {
// ...
String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
// ...
if (initialFragment != null) {
switchToHeader(initialFragment, initialArguments);
}
}
private void switchToHeaderInner(String fragmentName, Bundle args) {
getFragmentManager().popBackStack(BACK_STACK_PREFS,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
if (!isValidFragment(fragmentName)) {
throw new IllegalArgumentException("Invalid fragment for this activity: "
+ fragmentName);
}
Fragment f = Fragment.instantiate(this, fragmentName, args);
}

It can be seen that a string and a Bundle parameter are obtained from the Intent, and finally passed switchToHeaderInnerinto to instantiate the specific one Fragment. The instantiation process is as follows:

public static Fragment instantiate(Context context, String fname, Bundle args) {
// ...
Class clazz = sClassMap.get(fname);
if (clazz == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(fname);
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment)clazz.newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.mArguments = args;
}
return f;
}

A classic reflection call, instantiates the incoming string as a Java class and sets its parameters. What is this, this is deserialization! And the actual vulnerability comes from here. Since the incoming parameter is controllable by the attacker, the attacker can set it as an internal class, thus touching the functions that the developer did not expect. In the original report, the author used a Fragment that sets the PIN password in the Settings application as the target input. This is a private fragment, which leads to the function of modifying the PIN code without authorization. Many other user applications at that time also used PreferenceActivity, so the vulnerability had a wide impact, and the resulting exploits varied according to the functions of the application itself (that is, whether there are useful Gadgets).

Note that the above code is excerpted from the latest Android 13, where the switchToHeaderInnermethod has added the isValidFragmentjudgment, which is one of Android’s original fixes, that is, forcing subclasses of PreferenceActivity to implement this method, otherwise an exception will be thrown at runtime. But even so, there are still many developers who directly inherit and return truefor the .

Fragment Injection seems to be a problem of PreferenceActivity, but its core is still the imperfect verification of untrusted input. In the following examples, we will see similar vulnerability patterns many times.

Reference Article:

ClickJacking

Since Activity is the main carrier of UI, the interaction with users is also a key function. In traditional web security, there has been a method of clickjacking, which is to place the case where the target website wants the victim to click on a specified location (such as an iframe), and use related components in the host to cover and guide the target, so that the victim The author performed sensitive operations unknowingly, such as liking, coining, collecting, and resigning with one click.

Similar attack methods have also appeared in Android, such as covering the attacker’s custom TextView in front of the system’s sensitive pop-up window to guide the victim to confirm certain harmful operations. Of course, this requires the attacker’s application to have the floating window permission ( SYSTEM_ALERT_WINDOW). In the newer Android system, the application for this permission requires multiple confirmations from the user.

In the past two years, some clickjacking vulnerabilities have also appeared in AOSP, including but not limited to:

  • CVE-2020–0306: Bluetooth discovery request confirmation box override
  • CVE-2020–0394: Bluetooth pairing dialog override
  • CVE-2020–0015: Certificate installation dialog override
  • CVE-2021–0314: Uninstall confirmation dialog override
  • CVE-2021–0487: Calendar debug dialog override

For system applications, the way to defend against clickjacking is generally SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWSto.

For ordinary applications, it is impossible to apply for the HIDE_NON_SYSTEM_OVERLAY_WINDOWS permission. There are generally two defensive measures. One is to prohibit input events after the form is overwritten by filterTouchesWhenObscuredsetting true; the other is to overload the View.onFilterTouchEventForSecurity method and detect Coverage by other apps. In Android 12, the system has enabled the filterTouchesWhenObscured attribute by default, which is also a classic implementation of security by default.

Another vulnerability similar to clickjacking is called StrandHogg. For details, please refer to the original article below. The key point is to use the Activity’s allowTaskReparentingand taskAffinityattributes to disguise its task stack as the target application, so that when the target application is opened, due to the last-in-first-out feature of TaskStack, the user will see the attacker’s application, resulting in application phishing Scenes.

Later, the same security team proposed the StrandHogg 2.0 version, which mainly uses the ActivityStarterthe AUTOMERGE feature. Assuming there are two applications A and B, start activities (B1, A2, B2)after, the task stack will be merged from (A1, B1) and (A2, B2) to (A1, B1, A2, B2), that is, in the same task stack Activities of other applications are covered, resulting in phishing scenarios. However, this vulnerability is relatively specialized, so Google has already fixed it a long time ago. For details, please read the following reference articles:

Intent Redirection

Intent Redirection, as the name implies, forwards the untrusted input passed in by the user, which is similar to the SSRF vulnerability on the server side. An example of a typical vulnerability is as follows:

protected void onCreate (Bundle savedInstanceState) {
Intent target = (Intent) getIntent().getParcelableExtra("target");
startActivity(target);
}

ParcelableDirectly Intent converts the target passed in by the user into an object, and startActivitycall this object as a parameter of. As far as this example is concerned, the possible harm is that the attacker can use arbitrary structured Intent data to start any application in the target APP, even if it is a private application that has not been exported. However, the application whose target is not exported may further parse the parameters in the Intent provided by the attacker to cause further damage, such as executing arbitrary Javascript code in the built-in Webview, or downloading and saving files, etc.

Intent Redirection can be used for other interfaces besides possibly starting private Activity components, including:

Note: Each method may have several derivative methods, such as startActivityForResult

The first three may be easier to understand, namely start the interface, start the service, and send the broadcast. The last setResultmay be ignored during troubleshooting. This is mainly used to return additional data to the caller of the current Activity, mainly used startActivityForResultin scenarios, which may also pollute the user’s untrusted data to the caller.

From a defensive point of view, it is recommended not to directly send the incoming Intent as a parameter to the above four interfaces. If you must do this, you need to perform sufficient filtering and security verification in advance, such as:

  • android:exportedSet the component itself to false, but this only prevents the data sent by the user actively, and cannot intercept the data setResultreturned by.
  • Make sure that the obtained is Intent from the trusted application, such as calling in the component context getCallingActivity().getPackageName().equals(“trust. app”), but pay attention to malicious applications that can getCallingActivityreturn null.
  • Make sure that the to-be-forwarded Intent does not have any harmful behavior, such as the component does not point to its non-exported components, does not contain FLAG_GRANT_READ_URI_PERMISSIONetc. (see ContentProvider vulnerability below for details).

But it turns out that even Google itself may not be able to ensure perfect verification. The high-risk vulnerability recently submitted by Wuheng Lab CVE-2022–20223is a typical example:

private void assertSafeToStartCustomActivity(Intent intent) {
// Activity can be started if it belongs to the same app
if (intent.getPackage() != null && intent.getPackage().equals(packageName)) {
return;
}
// Activity can be started if intent resolves to multiple activities
List<ResolveInfo> resolveInfos = AppRestrictionsFragment.this.mPackageManager
.queryIntentActivities(intent, 0 /* no flags */);
if (resolveInfos.size() != 1) {
return;
}
// Prevent potential privilege escalation
ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
if (!packageName.equals(activityInfo.packageName)) {
throw new SecurityException("Application " + packageName
+ " is not allowed to start activity " + intent);
}
}

Among them, is used ActivityInfo.packageNameto judge whether the package name of the startup target is consistent with the package name of the current caller, but in fact, the explicit Intent is to specify the startup target through componentName, which has a higher priority than Intent.packageNameand the latter can be forged, which causes the check Bypass. There is another vulnerability in the above few lines of code. If you are interested, you can refer to the reference link below.

So, when you come across a potential Intent redirection issue, take a little more time to scrutinize it, and you might be able to find an exploitable scenario.

Service

Service has two main functions, one is to provide a background long-running environment for APP, and the other is to provide its services to the outside world. Similar to the definition of Activity, Service must be declared in the manifest before it can be used. Note that the code in the Service also runs on the main thread like the Activity, and is in the process of the application by default.

According to the distinction between the two main functions of the Service, there are two corresponding forms to start the Service:

  • Context.startService(): Start the background service and let the system schedule it.
  • Context.bindService(): Let the (external) application bind the service and use the interface it provides, which can be understood as the RPC server.

The life cycle diagram of the two ways to start the service is as follows:

The blue part is called on the client side. After the system receives the request, it will start the corresponding service. If the corresponding process is not started, it will also notify zygote to start it. Regardless of the method used to create the service, the system calls the onCreateand onDestroymethods for it. The overall process is similar to the startup process of Activity, so I won’t go into details here.

The start-activity command to facilitate starting the service:

am start-service [--user <USER_ID> | current] <INTENT>

Let’s introduce some vulnerabilities related to the Service component.

The Life Cycle

The life cycle of Service startup was introduced earlier, which is generally similar to the Activity process, but there are a few differences that need to be noted:

  • Unlike the Activity life cycle callback method, it is not necessary to call the superclass implementation of the Service callback method, such as onCreate, onDestory, etc.
  • ServiceThe direct subclass of the class runs in the main thread and generally needs to be executed in a new thread when processing multiple blocked requests at the same time.
  • IntentServiceIt is a subclass of Service, designed to run in a Worker thread, and can process multiple blocked Intent requests serially; API-30 will be marked as an obsolete interface after API-30, and it is recommended to use WorkManager or JobIntentService to implement.
  • The client uses stopSelfor stop service to stop the binding service, but the server does not have a corresponding onStopcallback, and only receives it before it is destroyed onDestory.
  • The foreground service must provide a notification for the status bar, letting the user realize that the service is running.

For bound services, the Android system will automatically destroy the service based on the bound client reference count, but if the service implements an onStartCommand()callback , the service must be explicitly stopped because the system will treat it as a started state. In addition, if the service allows the client to bind again, it needs to implement the onUnbind method and return true, so that the client will receive the same IBinder when it binds next time, as shown in the example diagram below:

The declaration cycle of a service is more complicated than that of an Activity because it involves the binding relationship between processes, so it is more likely to write unrobust or even problematic code without understanding it.

Implicit Export

Like Activity, Service also needs to be declared with service in the manifest, and also has android:exportedattributes. Even the definition of the default value of this attribute is the same, that is, the default is false, but when the intent filter is included, the default is true. Similarly, in Android 12 and later, it is also mandatory to explicitly specify the export properties of the service.

Service Hijacking

Unlike Activity, Android does not recommend using implicit Intents to start services. Because the service runs in the background, there is no visible graphical interface, so the user cannot see which service was started by the implicit Intent, and the sender does not know who the Intent will be received.

Service hijacking is a typical vulnerability. An attacker can declare the same intent-filter as the target for his Service and set a higher priority, so that the Intent that should have been sent to the target service can be intercepted. If it contains sensitive information Otherwise, data leakage will occur.

In, the harm bindServiceof this situation is even more serious, the attacker can pretend to be the target IPC service to return wrong or even harmful data. Therefore, starting from Android 5.0 (API-21), using an implicit Intent to call bindService will directly throw an exception.

If the target application to be audited is provided in the Service intent-filter, it is necessary to focus on troubleshooting.e

AIDL

The binding service can be used as an IPC server. If the server returns an instance of the AIDL interface when binding, it means that the client can call any method of the interface. A practical example is Tiktok, IndependentProcessDownloadService which returns an instance of the above AIDL interface in onBind: DownloadService

if (this.downloadServiceHandler != null) {
return this.downloadServiceHandler.onBind(intent);
}

And there is a tryDownloadmethod that can specify the url and file path to download and save the file locally. Although the attacker does not have an AIDL file, he can still construct a legal request through reflection to call. The key code in the PoC is as follows:

rivate ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName cName, IBinder service) {
processBinder(service);
}
public void onServiceDisconnected(ComponentName cName) { }
};
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent("com.ss.android.socialbase.downloader.remote");
intent.setClassName(
"com.zhiliaoapp.musically",
"com.ss.android.socialbase.downloader.downloader.IndependentProcessDownloadService");
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
}
private void processBinder(IBinder binder) {
ClassLoader cl = getForeignClassLoader(this, "com.zhiliaoapp.musically");
Object handler = cl.loadClass("com.ss.android.socialbase.downloader.downloader.i$a")
.getMethod("asInterface", IBinder.class)
.invoke(null, binder);
Object payload = getBinder(cl);
cl.loadClass("com.ss.android.socialbase.downloader.downloader.i")
.getMethod("tryDownload", cl.loadClass("com.ss.android.socialbase.downloader.model.a"))
.invoke(handler, payload);
}
private Object getBinder(ClassLoader cl) throws Throwable {
Class utilsClass = cl.loadClass("com.ss.android.socialbase.downloader.utils.g");
Class taskClass = cl.loadClass("com.ss.android.socialbase.downloader.model.DownloadTask");
return utilsClass.getDeclaredMethod("convertDownloadTaskToAidl", taskClass)
.invoke(null, getDownloadTask(taskClass, cl));

The key is to use Context.getForeignClassLoaderto get the ClassLoader of other applications.

Vulnerability details reference: vulnerabilities in the TikTok Android app

Intent Redirect

This is similar to the corresponding vulnerability in the Activity. When the client starts/binds the Service, it also specifies an implicit or explicit Intent. If the untrusted data is used by the server as a parameter to start other components, there will be May cause the same Intent redirection problem. Note that there are other data sources getIntent()besides , such as onHandleIntentthe parameters implemented in the service.

The “LaunchAnywhere” vulnerability that first proposed the harm of Intent redirection comes from the system service, and it is AccountManagerServicea. The normal execution flow of AccountManager is:

  • A common application (denoted as A) requests to add a certain type of account, calling AccountManager.addAccount.
  • AccountManager will look for the Authenticator class of the application (denoted as B) that provides the account.
  • AccountManager calls B’s Authenticator.addAccount method.
  • AccountManager invokes B’s account login interface (AccountManagerResponse.getParcelable) according to the Intent returned by B.

In step 4, the system thinks that the data returned by B points to B’s login interface, but B can make it point to other components, even system components, which creates an Intent redirection vulnerability. The source of Intent here is rather tortuous, but the essence is still controllable by the attacker.

For details about this vulnerability and the exploit process, please refer to: launchAnyWhere: Activity Component Permission Bypass Vulnerability Analysis (Google Bug 7699048 )

Receiver

Broadcast Receiver, receiver for short, is the broadcast receiver. The linkage between Activity and Service described above is one-to-one, and in many cases, we may want one-to-many or many-to-many communication solutions, and broadcasting assumes this function. For example, the Android system itself will send broadcast notifications to all interested applications when various events occur, such as turning on airplane mode, network status changes, low battery, and so on. This is a typical publish/subscribe design pattern, as is the carrier of broadcast data Intent.

Different from the previous Activity and Service, Receiver can be declared and registered in the manifest, which is called static registration; it can also be registered dynamically during the running of the application. But in any case, the defined broadcast receiver must inherit from BroadcastReceiver and implement its life cycle method onReceive(context, intent).

Note that the parent class of BroadcastReceiver is Object, unlike Activity and Service which are Context, so onReceive will also pass in an additional context object.

The command to send a broadcast in the shell is as follows:

am broadcast [--user <USER_ID> | all | current] <INTENT>

Here are some frequently asked questions in order.

Implicit Export

There is nothing special about using statically registered receivers, examples are as follows:

<receiver android:name=".MyBroadcastReceiver"  android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
</intent-filter>
</receiver>

There is also the same default export problem as before. I believe everyone is tired of seeing it, so I won’t be verbose anymore. Then look at the dynamic registration situation, such as:

BroadcastReceiver br = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(br, filter);

It may be easier to ignore the issue of export permissions with dynamic registration than with definitions in the manifest. The above code snippet dynamically registers a broadcast but does not explicitly declare the exported attribute, so it is exported by default. In fact, there seems to be no easy way to set up using registerReceiver exported=false, and Google’s official suggestion is LocalBroadcastManager.registerReceiverto , or specify the permission when registering.

For the case of specifying the permission, if it is custom permission, it needs to be declared in the application manifest, for example:

<permission android:name="com.evilpan.MY_PERMISSION"
android:protectionLevel="signature"/>
<uses-permission android:name="com.evilpan.MY_PERMISSION" />

signature Indicates permission that the system will only grant if the app requesting authorization is signed with the same certificate as the app declaring the permission. If the certificates match, permissions are automatically granted without notifying the user or asking for their explicit permission. See protectionLevel for details.

Finally, specify this permission during dynamic registration:

this.registerReceiver(br, filter, "com.evilpan.MY_PERMISSION", null);

Registering an export broadcast receiver without permission restrictions will result in receiving malicious data forged by the attacker. If the verification is not done properly during onReceive, there may be vulnerabilities such as unauthorized access or Intent redirection, causing further security hazards.

There are many security issues of this kind, and a typical example is the PpmtReceiver vulnerability used to break through the Samsung Galaxy S8 on Pwn2Own.

Information Leakage

The above is mainly to set permissions from the perspective of restricting broadcast senders, but in fact, this permission can also restrict broadcast receivers, but the additional designation is required when sending messages, for example, if you want only receivers with the above permissions to receive broadcast, the sending code is as follows:

Intent it = new Intent(this, ...);
it.putExtra("secret", "chicken2beautiful")
sendBroadcast(it, "com.evilpan.MY_PERMISSION");

If there is no second parameter, the default is that all recipients that meet the conditions can receive the broadcast information. At this time, if the sent Intent contains sensitive data, it may cause information leakage.

A practical case is CVE-2018–9581 The system carries sensitive data RSSI when broadcasting android.net.wifi.RSSI_CHANGED. This broadcast can be received by all applications, which indirectly leads to the leakage of physical location information. (funny?)

It can be seen that for Broadcast Receiver, the role of the permission tag is particularly obvious. For system broadcasts, for example BOOT_COMPLETED, usually only system apps have permission to send them. This is defined in the AndroidManifest.xml of the framework.

As for the custom broadcast of the application, the above-mentioned custom permissions are usually used, so a question naturally comes to mind, what happens if multiple applications define the same permission? This is a historical vulnerability. In the early days of Android, the strategy was to give priority to the first defined permission, but after Android 5, it has been clearly defined that two applications have different definitions of the same permission (unless they have the same signature), otherwise, Apps installed later will show INSTALL_FAILED_DUPLICATE_PERMISSIONan error warning. Interested archaeologists can refer to the following related articles:

Intent Redirection

Not much to say about the principle, just look at the case. The vulnerability lies in TikTok NotificationBroadcastReceiver, which defines the intent filter, which causes the component to be set as export by default, so it can receive the broadcast of external applications, and directly use the untrusted data in the broadcast to start the Activity, as follows:

Vulnerability details can refer to: Oversecured detects dangerous vulnerabilities in the TikTok Android app

ContentProvider

Content Provider, the content provider, is referred to as Provider. Android applications are usually implemented as an MVC structure (Model-View-Controller), and the Model part is the data source for its View, or graphical interface, to display. But sometimes an application will want to provide its data for other data to use, or obtain data from other applications.

To define a ContentProvider, you only need to inherit from the ContentProvider class and implement six methods: query, insert, update, delete, getTypeand onCreate. Among them, except onCreate is called by the system in the main thread, while other methods are actively called by the client program. A custom provider must be declared in the program list, which will be described in detail later.

It can be seen that the Provider mainly implements the database-like addition, deletion, modification, and query interface. From the perspective of the client, the query process is similar to querying the traditional database. For example, the following is the code snippet for querying the system SMS:

Cursor cursor = getContentResolver().query(
Telephony.Sms.Inbox.CONTENT_URI,
new String[] { Telephony.Sms.Inbox.BODY },
selectionClause,
selectionArgs,
Telephony.Sms.Inbox.DEFAULT_SORT_ORDER);
while (cursor.moveToNext()) {
Log.i(TAG, "msg: " + cursor.getString(0));
}

Among them ContentResolveris ContentInterfacethe subclass, which is the client remote interface of ContentProvider, which can implement its transparent remote proxy invocation. content_uriCan be regarded as the table name of the query, projection can be regarded as the column name, and the returned cursor is the iterator of the query result row.

Unlike the previous three components, the tool for accessing the provider component in the shell is content.

Let’s introduce the common problems in Provider.

Permissions

Given the provider as a data carrier, security access and permission control are naturally the top priority. For example, in the above code example, the interface for accessing SMS, if everyone can access it at will, will cause information leakage. As briefly mentioned earlier, the Provider defined in the application must be declared in its program manifest file, using the provider tag. Among them are our common exported attributes, indicating whether it can be accessed externally, and permission attributes indicating the permissions required for access. Of course, different permissions can be used for reading and writing, such as readPermission/ writePermissionattributes.

For example, the SMS database mentioned above is declared as follows:

<provider android:name="SmsProvider"
android:authorities="sms"
android:multiprocess="false"
android:exported="true"
android:singleUser="true"
android:readPermission="android.permission.READ_SMS" />

If other applications want to access, they need to declare the corresponding permissions in the manifest file.

<uses-permission android:name="android.permission.READ_SMS" />

This is all well understood, and other components have similar characteristics. In addition, Provider itself provides more fine-grained permission control, namely grantUriPermissions. This is a boolean indicating whether to temporarily grant clients access to this provider. The operation process of temporarily granting permissions is generally as follows:

  • The client sends an Intent to the application where the Provider is located, specifying the Content URI to be accessed, such as using startActivityForResultto send.
  • After the application receives the Intent, it judges whether it is authorized. If it is confirmed, it prepares an Intent and sets the flag of the flag to FLAG_GRANT_[READ|WRITE]_URL_PERMISSIONindicate that it is allowed to read/write the corresponding Content URI (it may not be consistent with the requested URI), and finally setResult(code, intent)returns.
  • The client’s onActivityResult receives the returned Intent and uses the URI to temporarily access the target Provider.

Take read as an example, Intent. flags if FLAG_GRANT_READ_URI_PERMISSION is included in the Intent, the recipient of the Intent (ie the client) will be granted temporary read permission for Intent. data part of the URI until the life cycle of the recipient ends. In addition, the Provider application can also actively call the Context.grantUriPermissionmethod to grant the corresponding permissions to the target application:

public abstract void grantUriPermission (String toPackage, 
Uri uri,
int modeFlags)
public abstract void revokeUriPermission (String toPackage,
Uri uri,
int modeFlags)

The grantUriPermissions attribute can perform read-write control on permissions at the URI granularity, but there is one point to note: the permissions temporarily granted through grantUriPermissions will ignore the restrictions imposed by the readPermission, writePermission, permission, and export attributes. In other words, even if exported=falsethe client does not apply for the corresponding one uses-permission, once the permission is granted, it can still access the corresponding Content Provider!

In addition, <provider>there is a sub-label grant-uri-permission. Even if grantUriPermissions is set to false, you can still access the URI subset defined under this label by temporarily obtaining permissions. This subset can use prefixes or wildcards to specify the authorized path of the URI scope.

Improper setting of Provider permissions may lead to application data being accessed by unexpected malicious programs, which may lead to information leakage, or cause the sandbox data to be overwritten and cause RCE. We will see many such cases later.

FileProvider

As mentioned earlier, a custom Provider needs to implement six methods, but Android has written corresponding subclasses for Providers in some common scenarios. Users can inherit these subclasses and implement a few subclass methods as needed. One of the common scenarios is to use ContentProvider to share application files. The system provides FileProviderto to facilitate application custom file sharing and access. However, if it is not used properly, arbitrary file reading and writing problems may occur.

FileProvider provides the function of using XML to specify file access control. Generally, Provider applications only need to inherit the FileProvider class:

public class MyFileProvider extends FileProvider {
public MyFileProvider() {
super(R.xml.file_paths)
}
}

file_pathsis user-defined XML, and can also be specified in meta-data :

<provider xmlns:android="http://schemas.android.com/apk/res/android" android:name="com.evilpan.MyFileProvider" android:exported="false" android:authorities="com.evilpan.fileprovider" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@7F15000E"/>
</provider>

resource point to res/xml/file_paths.xml. The file paths that can be accessed are defined in this file, and FileProvider will only generate Content URIs for files specified in advance. An example file path configuration is as follows:

<paths>
<root-path name="root" path=""/>
<files-path name="internal_files" path="."/>
<cache-path name="cache" path=""/>
<external-path name="external_files" path="images"/>
</paths>

pathsTags support multiple types of sub-tags, corresponding to sub-paths of different directories:

  • files-path: Context.getFilesDir()
  • cache-path: Context.getCacheDir()
  • external-path: Environment.getExternalStorageDirectory()
  • external-files-path: Context.getExternalFilesDir()
  • external-cache-path: Context.getExternalCacheDir()
  • external-media-path: Context.getExternalMediaDirs()[0]

More specifically root-path, it represents the root directory of the system/. The URI format generated by FileProvider is generally content://authority/{name}/{path}, for example, for the above Provider, it can be content://com.evilpan.fileprovider/root/proc/self/mapsused to access /proc/self/mapsfiles.

It can be seen that the FileProvider specification root path is a dangerous sign. Once the attacker obtains temporary permissions, he can read all the private data of the application.

For example, there has been such a real loophole in the history of TikTok:

<provider android:name="android.support.v4.content.FileProvider" android:exported="false" android:authorities="com.zhiliaoapp.musically.fileprovider" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/k86"/>
</provider>

It is used directly here FileProvider, without even needing inheritance. The content of the xml/k86.xml file is as follows:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:amazon="http://schemas.amazon.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<root-path name="name" path=""/>
<external-path name="share_path0" path="share/"/>
<external-path name="download_path2" path="Download/"/>
<cache-path name="gif" path="gif/"/>
...
</paths>

After obtaining temporary permission, the application can read and write arbitrary files.

The Hidden

In the ContentProvider class, in addition to the 6 methods that must be implemented, there are some other hidden methods, which are generally implemented by default or can be overridden by subclasses, such as

  • openFile
  • openFileHelper
  • call

These hidden methods may inadvertently cause security problems. This section will analyze the reasons through some cases.

OpenFile

If ContentProvider wants to implement the function of reading and writing shared files, it can also be implemented by openFileoverriding the method. The default implementation of this method will throw a FileNotFoundExceptionan exception.

Although the developer will not directly return the opened local file in implementation, selectively return some subdirectory files. However, if the code is not written rigorously, problems such as path traversal may occur. A classic vulnerability implementation is as follows:

@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
File file = new File(getContext().getFilesDir(), uri.getPath());
if(file.exists()){
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException(uri.getPath());
}

Another similar method of the same family openAssetFile, whose default implementation calls openFile:

public @Nullable AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
ParcelFileDescriptor fd = openFile(uri, mode);
return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
}

Sometimes developers know that they need to defend against path traversal, but the posture of defense is wrong, and there is also the possibility of being bypassed, for example:

public ParcelFileDescriptor openFile(Uri uri, String mode) {
File f = new File(DIR, uri.getLastPathSegment());
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}

Here I want to use getLastPathSegmentto get only the file name of the last level, but it can be bypassed by URL encoded path, for example %2F..%2F..path%2Fto%2Fsecret. txt will return /../../path/to/secret.txt.

Yet another false defense is to use the UriMatcher. match method to lookup ../, which is also bypassed by URL encoding. The correct way to defend and filter is as follows:

public ParcelFileDescriptor openFile (Uri uri, String mode) throws FileNotFoundException {
File f = new File(DIR, uri.getLastPathSegment());
if (!f.getCanonicalPath().startsWith(DIR)) {
throw new IllegalArgumentException();
}
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}

See: Path Traversal Vulnerability

OpenFileHelper

There is also a little-known openFileHelpermethod Its default implementation is to use the _datacolumn data in the current Provider to open the file. The source code is as follows:

protected final @NonNull ParcelFileDescriptor openFileHelper(@NonNull Uri uri,
@NonNull String mode) throws FileNotFoundException {
Cursor c = query(uri, new String[]{"_data"}, null, null, null);
int count = (c != null) ? c.getCount() : 0;
if (count != 1) {
// If there is not exactly one result, throw an appropriate
// exception.
if (c != null) {
c.close();
}
if (count == 0) {
throw new FileNotFoundException("No entry for " + uri);
}
throw new FileNotFoundException("Multiple items at " + uri);
}
c.moveToFirst();
int i = c.getColumnIndex("_data");
String path = (i >= 0 ? c.getString(i) : null);
c.close();
if (path == null) {
throw new FileNotFoundException("Column _data not found.");
}
int modeBits = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(new File(path), modeBits);
}

The main function of this method is to facilitate subclasses to quickly implement the openFilemethod, and it is usually not directly overridden in subclasses. However, due to the feature of opening files based on _datacolumns, attackers may insert malicious data and indirectly achieve arbitrary file reading and writing.

A classic case is a Samsung phone SemClipboardProviderthat does not verify user data when plugged in:

public Uri insert(Uri uri, ContentValues values) {
long row = this.database.insert(TABLE_NAME, "", values);
if (row > 0) {
Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row);
getContext().getContentResolver().notifyChange(newUri, null);
return newUri;
}
throw new SQLException("Fail to add a new record into " + uri);
}

The Provider is in the system_serverprocess and has extremely high operating authority. By exploiting this vulnerability, an attacker can read and write arbitrary files at the system level. The PoC is as follows:

ContentValues vals = new ContentValues();
vals.put("_data", "/data/system/users/0/newFile.bin");
URI semclipboard_uri = URI.parse("content://com.sec.android.semclipboardprovider")
ContentResolver resolver = getContentResolver();
URI newFile_uri = resolver.insert(semclipboard_uri, vals);
return resolver.openFileDescriptor(newFile_uri, "w").getFd();

This vulnerability has been used in wild attacks together with other vulnerabilities and was captured by the Google TAG team. For the analysis of this Fullchain, please refer to Project Zero’s recent article: A Very Powerful Clipboard: Analysis of a Samsung in-the-wild Exploit chain

Call

The call method used to call the method defined by the server, and its function signature is as follows:

public Bundle call (String authority, 
String method,
String arg,
Bundle extras)
public Bundle call (String method,
String arg,
Bundle extras)

The default implementation is an empty function that returns directly null. Developers can override this function to implement some dynamic methods, and the return value will also be passed back to the caller.

It looks similar to a regular RPC call, but there is a small trap here, which is also specially marked in the developer documentation: the Android system does not check the permissions of the call function because the system does not know whether the data is read or written in the call, so it is impossible to judge based on the permission constraints defined in the Manifest. Therefore, developers are required to perform permission verification on the logic in the call.

If the developer implements this method but does not perform verification or the verification is insufficient, unauthorized calls may occur. One case is CVE-2021–23243that in the implementation of HostContentProviderBasethe, DexClassLoader is used to directly load the incoming dex file, which directly causes the attacker’s code to run in a privileged process, and all Providers that inherit this base class will be affected ().

In addition, in some system providers, some remote object instances can be obtained through the call method. For example, in the article Special attack surface in Android (3) — hidden call function, the SliceProviderauthor KeyguardSliceProviderobtained the internal system application, and The PendingIntent object is further used to realize the function of forging any broadcast.

Other

In addition to the above-mentioned vulnerabilities directly related to the four major components, there are many vulnerabilities in the Android system that are not easy to classify. This section mainly selects a few of the most common vulnerabilities for a brief intro

PendingIntent

PendingIntent is a representation of Intent, not an Intent object itself, but a Parcelable object. After the object is passed to other applications, other applications can perform the operation specified by the pointed Intent as the sender. A PendingIntent is created using one of the following static methods:

  • getActivity(Context, int, Intent, int);
  • getActivities(Context, int, Intent[], int);
  • getBroadcast(Context, int, Intent, int);
  • getService(Context, int, Intent, int);

PendingIntent itself is just a reference to the raw data descriptor by the system, which can be roughly understood as Intent. Because of this, even after the application that created the PendingIntent is closed, other applications can still use the data. If the original app is later restarted and creates a PendingIntent with the same parameters, the PendingIntent returned will point to the same token as the one created earlier. Note that the filter equals method is used to judge whether the Intents are the same, which will judge whether the action, data, type, identity, class, and categories are the same. Note that extrait is not listed here, so Intents with only different extras will also be considered equal.

Since PendingIntent can represent the characteristics of other applications, it may be used for abuse in some scenarios. For example, if a developer creates a default PendingIntent like this and passes it to other applications:

pi = PendingIntent.getActivity(this, 0, new Intent(), 0);
bundle.putParcelable("pi", pi)
// send bundle

After receiving the PendingIntent, the malicious application can obtain the original intent and use Intent. fill in to fill the empty field. If the original Intent is the above empty Intent, the attacker can modify it to a specific Intent, thereby using the target’s identity to launch applications, including unexported private applications. A classic case is the early broadAnywhere vulnerability. The addAccount method in the Android Settings application creates a PendingIntent broadcast, but the content of the intent is empty, which leads to malicious applications that receive the intent to fill in the action of the broadcast, thereby achieving unauthorized transmission The system broadcasts to realize functions such as forging text messages and restoring factory settings.

To alleviate such problems, Android has made many restrictions on the rewriting of Intent. fill in, such as existing fields cannot be modified, component and selector fields cannot be modified (unless FILL_IN_COMPONENT/SELECTOR is additionally set), implicit Intent actions cannot be modified, etc.

However, some researchers have proposed a method for exploiting implicit Intent, that is, by modifying the flag to add FLAG_GRANT_WRITE_URI_PERMISSION and modifying the data URI to point to the victim’s private Provider, changing the package to the attacker; at the same time, the attacker declares the same in its Activity Intent filter, so that when the intent is forwarded, the attacker’s application will be launched, and at the same time, the access authority of the target private Provider will be obtained, to realize the theft or overwriting of private files. For details about the attack idea, you can read the following reference article.

Deep Link

In most operating systems, there is the concept of deep link, that is, to open a specific application through a custom schema. For example, by clicking https://evilpan.com/, you can evoke the default browser to open the target page, click will bring up the dial interface, click will evoke WeChat, and so on. Regardless of other systems, in Android, this is mainly achieved through implicit Intent.

If an application wants to register a similar custom protocol, it needs to declare it in the application manifest file:

<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="weixin" android:host="qr"/>
</intent-filter>

Because this type of implicit Intent can be triggered directly by clicking on a link, it is more popular with attackers. If the component that handles the corresponding Intent fails to filter the content passed in by the user, it is likely to cause a 1-click vulnerability.

Webview

In the Android system, Webview is mainly used for the application to display webpage content in its Activity and provides some additional interfaces for developers to implement custom control. Higher scalability means more possibilities for errors, especially now that Android client development is declining, and Java development is also developing in the direction of a “big front end”. Many logics that were originally implemented using native applications have been gradually transferred to web pages, such as h5, applets, etc. In this way, the attack surface of Webview has also expanded a lot.

Conventional Webview security issues are mainly related to some insecure configurations, such as overriding and onReceivedSslErrorignoring SSL errors, leading to man-in-the-middle attacks, and setAllowFileAccessFromFileURLsleakage of local private files. But now the vulnerabilities are more in JSBridge, which is a bridge between Java code and JavaScript code in web pages.

Due to the sandbox feature of the Webview or JS engine, the Javascript code in the webpage itself cannot perform many operations that can only be performed by native applications, such as sending broadcasts from Javascript, accessing application files, and so on. Due to the complexity of the business, many logics must be implemented in the Java layer or even the Native layer, so this requires the use of JSBridage. The traditional JSBridge is Webview.addJavascriptInterfaceimplemented, a simple example is as follows:

class JsObject {
@JavascriptInterface
public String toString() { return "injectedObject"; }
}
webview.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JsObject(), "injectedObject");
webView.loadData("", "text/html", null);
webView.loadUrl("javascript:alert(injectedObject.toString())");

Java layer returns data to Javascript by directly using loadUrl to execute JS code. Of course, in addition to registering Bridge in this way, there are many application-specific implementations, such as using to console. logtransmit data and use onConsoleMessagecallbacks in the Java layer to receive. But in any case, this leads to an increase in the attack surface. Large-scale applications even register hundreds of jsapis for web page calls.

From the perspective of historical vulnerabilities, the main cause of the Webview vulnerability is the jsapi domain name verification problem and the vulnerability of the Bridge code itself, which will not be expanded due to space reasons.

References

--

--

TutorialBoy
TutorialBoy

Written by TutorialBoy

Our mission is to get you into information security. We'll introduce you to penetration testing and Red Teaming. We cover network testing, Active Directory.