preface

Recently, in the development of application A, we connected with an application B of our partner, who soon sent the interface document and agreed that we would obtain relevant data through the XXXContentProvider provided by application B. It all seems so normal and simple, but from the moment you start debugging, something weird happens. Why did a 90-year-old woman come back from the dead? Why do hundreds of sows scream in the middle of the night? Why are girls’ dormitories being robbed frequently? Supermarket instant noodles why miserably recruit poison hand? Behind all this, is the distortion of human nature, or the collapse of morality? At the end of the event, I found a big hole in the Android system! Didi ~ the old driver will take you to this unforgettable experience

A quick review of Android’s Permissions mechanism

When declaring a ContentProvider in androidmanifest.xml, we can specify the corresponding readPermission and writePermission. In this way, we can restrict third-party applications to declare the specified read and writePermission. To further access and improve security.

<provider
    android:name=".provider.XXXContentProvider"
    android:authorities="com.aaa.bbb.ccc.provider.authorities"
    android:readPermission="com.aaa.bbb.ccc.provider.permission.READ_PERM"
    android:writePermission="com.aaa.bbb.ccc.provider.permission.WRITE_PERM"
    android:exported="true"/>
Copy the code

But first, we need to define the permissions of the relevant application through , and you can define the access level of the permissions through Android :protectionLevel. The following are commonly used. For more parameters, see permission-Element on the official website.

  • Signature: Calling App must declare thispermissionThe App uses the same signature
  • System: can be accessed only by the system App
  • Normal: the default value. The system automatically authorizes the App during App installation and invocation
<permission
    android:name="com.aaa.bbb.ccc.provider.permission.READ_PERM"
    android:protectionLevel="normal" />
<permission
    android:name="com.aaa.bbb.ccc.provider.permission.WRITE_PERM"
    android:protectionLevel="normal" />
Copy the code

What the fuck? SecurityException?

In calling App, declare the permissions required by the call with < uses-Permission />, and then query the data with the getContentResolver().query() method. At this point, the program crashes, throwing a SecurityException. Didn’t I declare the permissions according to the interface documentation? Why would you report a security problem? I must have opened it the wrong way.

03-29 12:08:12. 839. 4255-4271 / com codezjx. The provider/DatabaseUtils E: Writing exception to parcel java.lang.SecurityException: Permission Denial: reading com.aaa.bbb.ccc.XXXProvider uri content://com.aaa.bbb.ccc.xxx/getxxx/ from pid=22529, uid=10054 requires null, or grantUriPermission() at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:539) at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:452)
        at android.content.ContentProvider$Transport.query(ContentProvider.java:205)
        at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:112)
        at android.os.Binder.execTransact(Binder.java:500)
Copy the code

The above Log is sent with the ContentProvider app. Binder is used to communicate with each other. If an exception occurs Server, will write the exception in the reply comes back to the Client in the parcel, and then the Client through the android. OS. Parcel. ReadException () read the exception of the Server, and then thrown out. Yes, it is

At this point, I began to doubt the accuracy of the interface documentation, so I immediately pulled up my JADX to decompile the target APK and checked the target’s Androidmanifest.xml file. The default authorities attribute of the ContentProvider is true. The exported attribute is true.

SecurityException appears again

At that time, I did not think about it carefully. In order to quickly adjust the data, we temporarily removed the permission. Oh, mom, I thought I’d be able to tune in. Unexpectedly, the weird thing happened again. When the program runs, the SecurityException reappears, just like the Log above. Don’t you have all the fucking permissions removed? Why report this anomaly?

java.lang.SecurityException: Permission Denial: reading com.aaa.bbb.ccc.XXXProvider uri content://com.aaa.bbb.ccc.xxx/getxxx/ from pid=22529, uid=10054 requires null, or grantUriPermission()

Analyzing the key Log above, I found the key word requires null. The ContentProvider requires requires to tell you what permission is missing. But why null here? Just thinking about it doesn’t feel right.

Read the Fucking Source Code

The partner told us that this problem had not occurred since we had been debugging on the 4.4 machine. Running on the 5.1 machine this time, I found it would crash. After a variety of attempts and debugging (omitted ten thousand words here), still failed to find the cause of the error, and even began to doubt life for a time. At this time, can only go to gnaw the source code, to see what can be found.

ContentProvider source is located in the frameworks/base/core/Java/android/content/ContentProvider. Java, can be directly without system source SDK source code file. Directly to see the location of the error in the Log enforceReadPermissionInner () method.

This is a short and understandable method that checks whether the caller has some permission before operations such as query(). If not, a SecurityException is thrown directly.

/ * * {@hide} * /
protected void enforceReadPermissionInner(Uri uri, IBinder callerToken)
        throws SecurityException {
    final Context context = getContext();
    final int pid = Binder.getCallingPid();
    final int uid = Binder.getCallingUid();
    String missingPerm = null;

    if (UserHandle.isSameApp(uid, mMyUid)) {
        return;
    }

    if (mExported && checkUser(pid, uid, context)) {
        final String componentPerm = getReadPermission();
        if(componentPerm ! =null) {
            if (context.checkPermission(componentPerm, pid, uid, callerToken)
                    == PERMISSION_GRANTED) {
                return;
            } else{ missingPerm = componentPerm; }}// track if unprotected read is allowed; any denied
        // <path-permission> below removes this ability
        boolean allowDefaultRead = (componentPerm == null);

        final PathPermission[] pps = getPathPermissions();
        if(pps ! =null) {
            final String path = uri.getPath();
            for (PathPermission pp : pps) {
                final String pathPerm = pp.getReadPermission();
                if(pathPerm ! =null && pp.match(path)) {
                    if (context.checkPermission(pathPerm, pid, uid, callerToken)
                            == PERMISSION_GRANTED) {
                        return;
                    } else {
                        // any denied <path-permission> means we lose
                        // default <provider> access.
                        allowDefaultRead = false; missingPerm = pathPerm; }}}}// if we passed <path-permission> checks above, and no default
        // <provider> permission, then allow access.
        if (allowDefaultRead) return;
    }

    // last chance, check against any uri grants
    final int callingUserId = UserHandle.getUserId(uid);
    finalUri userUri = (mSingleUser && ! UserHandle.isSameUser(mMyUid, uid)) ? maybeAddUserId(uri, callingUserId) : uri;if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION,
            callerToken) == PERMISSION_GRANTED) {
        return;
    }

    final String failReason = mExported
            ? " requires " + missingPerm + ", or grantUriPermission()"
            : " requires the provider be exported, or grantUriPermission()";
    throw new SecurityException("Permission Denial: reading "
            + ContentProvider.this.getClass().getName() + " uri " + uri + " from pid=" + pid
            + ", uid=" + uid + failReason);
}
Copy the code

Requires null because missingPerm is not assigned. If the following chunk of code is not executed, then missingPerm will not be assigned.

if (mExported && checkUser(pid, uid, context)) {
    ......
}
Copy the code

MExported is guaranteed to be true, so the checkUser() method returns false. As mentioned earlier, this SecurityException will not appear on Android4.4. Why? ContentProvider (Android5.0+, Android5.0+)

Take a look at the checkUser() method, which, by all indications, returns false, causing missingPerm not to be assigned and ultimately throwing a SecurityException.

boolean checkUser(int pid, int uid, Context context) {
    return UserHandle.getUserId(uid) == context.getUserId()
            || mSingleUser
            || context.checkPermission(INTERACT_ACROSS_USERS, pid, uid)
            == PERMISSION_GRANTED;
}
Copy the code

By reflection and other means, we can verify the values of each Boolean condition in the checkUser() method one by one:

  • (UserHandle.getUserId(uid) == context.getUserId()) -> false
  • mSingleUser -> false
  • (context.checkPermission(INTERACT_ACROSS_USERS, pid, uid) == PERMISSION_GRANTED) -> false

Userhandle.getuserid (uid) == context.getUserId() will return true under normal circumstances, and the userId returned is always 0 (because the test machine has only one user).

Indications are that context.getUserId() does not return 0 in the problem application provided by the partner. Driven by strong curiosity, I pulled up JADX to decomcompile the target APK again, searched the getUserId() method globally, and found that there is a similar method. In BaseApplication, there is such a getUserId() method, which is used to return the ID of the registered user.

In ContentProvider, mContext is the Application Context instance, that is, the getUserId() method is inadvertently overwritten. Therefore, the easiest way to resolve this SecurityException is simply to change the name of the getUserId() method in BaseApplication. At this point, the whole trample pit experience finally came to an end.

conclusion

This time, I discovered a hidden problem in Android. In a custom Application, if you declare the method public int getUserId() and return something other than the userId of the current user, your ContentProvider will be invalid on Android5.0+ machines. Don’t believe it? Try it for yourself

/ * *@hide* /
@Override
public int getUserId(a) {
    return mBase.getUserId();
}
Copy the code

Because this is an @hide method, this overwriting is usually unconscious and the IDE will not tell you that you overwrote the method in your Application. If you’re lucky enough to have an Android SDK Jar with Hidden-API, the IDE will give you a hint, but few people import Hidden-API except for system application development

Missing `@Override` annotation on `getUserId()` more…

SecurityException () : SecurityException () : SecurityException () : SecurityException () : SecurityException () : SecurityException () : SecurityException () : SecurityException () : SecurityException () : SecurityException (

Click here to read the original: codezjx. Making. IO / 2018/03/30 /…