Google Android - IMemory Native Interface is Insecure for IPC Use

EDB-ID:

39686




Platform:

Android

Date:

2016-04-11


Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=706

Android: IMemory Native Interface is insecure for IPC use
Platform: Tested on Android 6.0.1 January patches
Class: Elevation of Privilege

Summary:
The IMemory interface in frameworks/native/libs/binder/IMemory.cpp, used primarily by the media services can be tricked to return arbitrary memory locations leading to information disclosure or memory corruption.

Description:
The IMemory interface allows the passing of shared memory across the Binder IPC channel on Android. The interface supports a single remote call, GET_MEMORY which requests a separate IMemoryHeap interface along with an offset value and size for the shared memory buffer. The IMemoryHeap interface in turn supports a HEAP_ID call which marshals across a FileDescriptor, size, flags and an offset. This is passed to mmap to map the shared memory into the current process.

The underlying vulnerability is the sizes in IMemory and IMemoryHeap are not checked relative to one another, and nor is the offset in IMemory checked against the size of IMemoryHeap. This allows a local process to craft fake IMemory and IMemoryHeap objects such that they lie about their values and either cause information disclosure or memory corruption.

To understand this let’s look at how the pointer to the shared buffer is extracted from IMemory::pointer:

void* IMemory::pointer() const {
    ssize_t offset;
    sp<IMemoryHeap> heap = getMemory(&offset);
    void* const base = heap!=0 ? heap->base() : MAP_FAILED;
    if (base == MAP_FAILED)
        return 0;
    return static_cast<char*>(base) + offset; <- No check on IMemoryHeap size
}

Maybe we check sizes in getMemory() ?

sp<IMemoryHeap> BpMemory::getMemory(ssize_t* offset, size_t* size) const
{
    if (mHeap == 0) {
        Parcel data, reply;
        data.writeInterfaceToken(IMemory::getInterfaceDescriptor());
        if (remote()->transact(GET_MEMORY, data, &reply) == NO_ERROR) {
            sp<IBinder> heap = reply.readStrongBinder();
            ssize_t o = reply.readInt32();
            size_t s = reply.readInt32(); <- No check.
            if (heap != 0) {
                mHeap = interface_cast<IMemoryHeap>(heap);
                if (mHeap != 0) {
                    mOffset = o;
                    mSize = s;
                }
            }
        }
    }
    if (offset) *offset = mOffset;
    if (size) *size = mSize;
    return mHeap;
}

Nope, as we can see, no check is made of IMemoryHeap’s size, so you could specify a mapped file smaller than offset and create a pointer out of bounds. Of course if IMemoryHeap is invalid then the mmap process will return MAP_FAILED which will end up as NULL after the call to pointer(). 

So how can this be abused? Any IPC service which calls pointer() can be tricked into accessing an arbitrary location, either a relative offset to the file mapped or NULL. For example look at ICrypto::onTransact with the DECRYPT operation. It checks that the offset is within the total size (this has been exploited before) with:

} else if (totalSize > sharedBuffer->size()) {
  result = -EINVAL;
} else if ((size_t)offset > sharedBuffer->size() - totalSize) {
  result = -EINVAL;

The size is the value returned through IMemory, and not the actual mapped size from IMemoryHeap so in this case offset can be arbitrary. With the right plugin (such as the clearkey plugin) we can get this to read arbitrary memory. Even more so as there’s no NULL checking in pointer() we can cause IMemoryHeap to fail which causes pointer() to return NULL. Setting size to 0xFFFFFFFF means we can read any memory location from 0 to 0xFFFFFFFF. 

This can be turned into an arbitrary write as long as you can pass an arbitrary IMemory to another service. For example the BnCameraRecordingProxy::onTransact in frameworks/av/camera/ICameraRecordingProxy.cpp does the following for onReleaseRecordingFrame


case RELEASE_RECORDING_FRAME: {
            ALOGV("RELEASE_RECORDING_FRAME");
            CHECK_INTERFACE(ICameraRecordingProxy, data, reply);
            sp<IMemory> mem = interface_cast<IMemory>(data.readStrongBinder());

            if (CameraUtils::isNativeHandleMetadata(mem)) {
                VideoNativeHandleMetadata *metadata =
                        (VideoNativeHandleMetadata*)(mem->pointer());
                metadata->pHandle = data.readNativeHandle();

                // releaseRecordingFrame will be responsble to close the native handle.
            }
            releaseRecordingFrame(mem);

            return NO_ERROR;
        } break;

As you can coerce the pointer value, as long as the first 4 bytes make the integer 3 the next 4 bytes will be overwritten by the native handle value which can be controlled. 

Proof of Concept:
I’ve provided a PoC which exploits the issue in ICrypto::decrypt. I will just SIG_SEGV on reading an arbitrary location (in this case 1GiB relative to the mapped memory). If it succeeds then that’s good as well as it shouldn't succeed. You should be able to create default Android Studio project and replace the MainActivity with the provided Java file. When run it should cause media server to crash.


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/39686.zip