360 core security technology blog

Posted by tzul at 2020-03-17

Sometimes, in order to realize some special requirements, such as interface skin changing and plug-in, we want to change the application running environment. For example, we want all classes (including custom application, which is called myapplication) to be loaded by a custom classloader when an application is running.

To achieve this requirement, you need to replace the default classloader of the API layer before myapplication is loaded, otherwise myapplication will be loaded by the default classloader. But this will lead to a paradox. Before myapplication is loaded, there is no application code to run, and it cannot be done by replacing classloader. The proxy / delegate application framework is used to solve such problems.

Introduction to proxy / delegate application

In the proxy / delegate application framework, there are two application objects, one is called proxyapplication and the other is delegateapplication

(1) Proxyapplication: the framework will provide a proxyapplication abstract base class. The user needs to inherit this class and overload its initproxyapplication() method, in which to change the surrounding, such as replacing classloader.

(2) Delegateapplication: that is to say, the application uses the original application, and all the applications obtained from getapplicationcontext() and other methods are delegateapplication. Note that delegateapplication is just a title, and no base class named delegateapplication exists.

With the proxy / delegate application framework, users can change the running environment of the whole application without making any changes to the original application class. All you need to do is add a new application class and modify androidmanifest.xml accordingly.

Old Android manifest.xml:

<application android:name=".MyApplication" android:icon="@drawable/icon" android:label="@string/app_name" >

Application class added:

public class MyProxyApplication extends ProxyApplication { @Override protected void initProxyApplication() { } } <application android:name=".MyProxyApplication" android:icon="@drawable/icon" android:label="@string/app_name" > <meta-data android:name="DELEGATE_APPLICATION_CLASS_NAME" android:value=".MyApplication" > </meta-data>

Myproxyapplication (proxyapplication) object is invisible to the application. The application seen by the application is myapplication (delegateapplication), which is the previous application object. It seems that nothing has changed for the application, but its running environment has changed, for example, all classes have been loaded by the new classloader. The whole implementation is non-invasive, and the existing code does not need to be modified. Only Android manifest.xml is slightly modified.

Let's start to explore how proxyapplication itself can be implemented. The core problem is two: one is when to call the initproxyapplication() method of the subclass to make the subclass change the surrounding; the other is how to load the delegateapplication and make the application think it is the real application. In addition, ContentProvider, one of the four major components of Android, will bring us a lot of troubles, which needs to be properly handled.

Proxyapplication implementation: timing

In theory, proxyapplication can replace (or hook, similar meaning) any accessible variable, including Java layer and native layer; in addition to classloader, it also has resources and binders. Many interesting functions can be realized by these means. How to replace classloader and resources is not discussed here. If you are interested, you can find a lot of relevant information on the Internet. This article focuses on the framework itself, replacing classloader as an example only.

The problem now is that the time to change the surrounding must be early enough, especially for classloader. Can I do it in application: oncreate()? We usually think that application is the first component of Android application to be loaded; however, when the application registers with ContentProvider, this is not correct. The ContentProvider: oncreate() call takes precedence over application: oncreate().

Fortunately, we have another method: attachbasecontext(). Several top-level components of Android (application, activity, service) are subclasses of contextwrapper. On the one hand, the contextwrapper inherits the context, on the other hand, it contains a context object (called mbase), which is implemented by forwarding the context to the mbase object for processing. This seemingly convoluted design is to delay init the context function in these top-level components. I won't discuss it here, just post some Android source code snippets for reference.

public class Application extends ContextWrapper { public application() { super(null); } } public class ContextWrapper extends Context { Context mBase; public ContextWrapper(Context base) { mBase = base; } protected void attachBaseContext(Context base) { if (mBase != null) { throw new IllegalStateException("Base context already set"); } mBase = base; } @Override public AssetManager getAssets() { return mBase.getAssets(); } @Override public Resources getResources() { return mBase.getResources(); } }

The way contextwrapper completes the delay init semantics is attachbasecontext(). It can be said that the application object is "disabled" when the construction is just completed, and the method accessing all contexts will throw NullPointerException. Its functionality is complete only after attachbasecontext() is executed.

In ContentProvider: oncreate(), we know that application: oncreate() is not running, but we can use getcontext(). Getapplicationcontext() function to get application object and access its context method. Obviously, the API designer of Android cannot allow the application obtained at this time to be "disabled". The conclusion is that application: attachbasecontext() must occur before ContentProvider: oncreate(), otherwise the API will appear bug; no matter how the Android system version changes, this cannot be changed.

Therefore, the initialization order of application and ContentProvider is as follows: application: attachbasecontext() is executed first, then ContentProvider: oncreate(), then application: oncreate(). Our solution is simple:

public abstract class ProxyApplication extends Application { protected abstract void initProxyApplication(); @Override protected void attachBaseContext (Context context) { super.attachBaseContext(context); initProxyApplication(); } }

Proxyapplication implementation: load delegateapplication

When initproxyapplication() of the subclass returns, proxyapplication will load delegateapplication to complete its historical mission. This part is completed in oncreate(). It's basically manual work, but there are also some points that need to be paid attention to. The following steps are briefly described.

(1) Get the class name of delegateapplication

That is to say, get the metadata value of delete? Application from Android manifest.xml. If it does not exist, use as the default. This step is relatively simple.

String className = ""; String key = "DELEGATE_APPLICATION_CLASS_NAME"; ApplicationInfo appInfo = getPackageManager().getApplicationInfo( super.getPackageName(), PackageManager.GET_META_DATA); Bundle bundle = appInfo.metaData; if (bundle != null && bundle.containsKey(key)) { className = bundle.getString(key); if (className.startsWith(".")) className = super.getPackageName() + className; }

(2) Load delegateapplication and generate objects

Which classloader should I pay attention to here? The answer is to use getclassloader() (that is, context: getclassloader()) instead of getclass(). Getclassloader(). The difference between the two should be carefully figured out.

Class delegateClass = Class.forName(className, true, getClassLoader()); Application delegate = (Application) delegateClass.newInstance();

(3) Replace all application references of API layer

That is, replace all saved proxyapplication objects in API layer with newly generated delegateapplication objects. With the basecontext of proxyapplication as the starting point, all positions can be found and replaced one by one with reflection. Note that the last mcallapplications are lists, and you need to replace their internal contents.

baseContext.mOuterContext baseContext.mPackageInfo.mApplication baseContext.mPackageInfo.mActivityThread.mInitialApplication baseContext.mPackageInfo.mActivityThread.mAllApplications

(4) Set basecontext and call oncreate

Give control to delegateapplication. Of course, the latter will think that it is a "genuine" application, and other subsequent components will think the same. This is exactly what we want.

Method attach = Application.class.getDeclaredMethod("attach", Context.class); attach.setAccessible(true); attach.invoke(delegate, base); delegate.onCreate();

Deal with ContentProvider again

As mentioned earlier, Android's top-level components, application, activity, and service, are contextwrapper, and there is no ContentProvider in this list. The ContentProvider is not a contextwrapper, or even a context. Instead, there is an mcontext variable inside. Get the context through the getcontext() function.

So, which context does ContentProvider: getcontext() get? Experiments show that the context obtained by ContentProvider: getcontext() is application; to be exact, in the proxy / delegate application framework, it is proxyapplication. This is not consistent with the semantics of the framework. So, do we need to replace it with delegateapplication just like other proxyapplication references? This is feasible: traverse the ContentProvider list of the API layer and replace the mcontext in each ContentProvider with delegateapplication.

But this processing method will further increase the dependence on the Android API layer source code. Is it necessary? After all, in the Android API document, there is no stipulation that the content provider: getcontext() must return application; if you want to get application, the correct way is getcontext(). Getapplicationcontext(). So why does getcontext () directly return the application object? We can find the answer from the source code:

if (context.getPackageName().equals(ai.packageName)) { c = context; } else if (mInitialApplication != null && mInitialApplication.getPackageName().equals(ai.packageName)) { c = mInitialApplication; } else { try { c = context.createPackageContext(ai.packageName, Context.CONTEXT_INCLUDE_CODE); } catch (PackageManager.NameNotFoundException e) { } }

It is easy to see that because the getpackagename() function of the proxyapplication object is the same as the package name corresponding to the ContentProvider, the proxyapplication object will be reused as the context instead of creating a new packagecontext. So the solution is simple:

@Override public String getPackageName() { return ""; }

Since proxapplication is not the final application, there will be no side effects.

Precautions for use

Do not keep a reference to the proxyapplication subclass object or do anything in any system callback, including oncreate. Oncreate() is used by the base class to load the delegateapplication, and no other callbacks will be received.

After the execution of proxyapplication: oncreate(), all thread stacks and Java objects in the virtual machine will no longer have references to proxyapplication objects. The proxyapplication object will be recycled at the next GC run time, which means that the replacement from proxyapplication to delegateapplication is thorough. Naturally, proxyapplication will not receive any other callbacks. Delegateapplication will receive all callbacks normally.

In addition, in the proxyapplication subclass, if you need to get the package name of the current APK, you need to use getbasecontext(). Getpackagename(), instead of simply calling getpackagename(). The reason is explained in "deal with ContentProvider again" above.