漏洞简介
- 漏洞Patch:https://android.googlesource.com/platform/frameworks/base/+/18b5537c74e29f3420882c37f81e95bebdb54029%5E%21/#F0
- 这个漏洞位于
frameworks/base/libs/hwui/jni/Bitmap.cpp
,算是Android基础的功能,,不过在介绍漏洞之前需要先补一些前置知识。
Android Bitmap 背景知识
- Bitmap就是”位图“的概念,是图片在内存中的表示形式,这种格式并不是JPEG、PNG这类编码格式,这种格式通过直接描述每个像素的信息来表示图片,同时也是一种为未压缩的格式。可以将Bitmap转换为各种编码格式,有的格式是有损转换而有的格式是无损的。
- 在Android系统中,如果两个进程要分享一张图片,首选的是采用通过Binder IPC传递Bitmap的方法。当然也可以将图片以JPEG等格式保存到磁盘上,再通过IPC将文件名称发送给对方,不过直接使用内存来传递Bitmap明显比保存文件这种需要磁盘I/O的方法更具有效率。同时,在Android系统中负责屏幕缓冲区管理的surfaceflinger,在和App通信时也不可能使用保存图片的方式,所以必须采用将bitmap通过Binder IPC传递的方法。
- 在Android 4.0之前,Android的设计是非常简单的,只能通过Binder IPC直接传递Bitmap,也就是说只能将Bitmap写入Parcel缓冲区,然后再通过内核转发给其他进程,由于每个进程的Binder缓冲区存在大小的限制(一般只有几MB大小),所以早于Android 4.0的系统中,传递大于Binder缓冲区大小的Bitmap会出现Out Of Memory错误而导致无法传递。
- 为了解决Binder IPC传递Bitmap的效率问题,Android在4.0版本引入了通过ashmem的方式传递Bitmap的方法,也就是说利用匿名共享内存,直接把Bitmap写入这块共享内存中,然后再将fd发送给其他进程,这样一来就无需通过内核转发这块内存,也不会受Binder缓冲区大小的限制了。当然Android只会在Bitmap大于一定大小(IN_PLACE_BLOB_LIMIT)的时候才使用ashmem传递,如果是小Bitmap的话依旧使用Binder缓冲区直接进行传输,这种方式称为Blob传输。
漏洞分析
- 漏洞位于在Bitmap_createFromParcel中,是一个典型的从Parcel对象创建一个Bitmap的函数
static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { #ifdef __ANDROID__ // Layoutlib does not support parcel if (parcel == NULL) { jniThrowNullPointerException(env, "parcel cannot be null"); return NULL; } ScopedParcel p(env, parcel); //... const int32_t width = p.readInt32(); // [1] const int32_t height = p.readInt32(); const int32_t rowBytes = p.readInt32(); const int32_t density = p.readInt32(); //... auto imageInfo = SkImageInfo::Make(width, height, colorType, alphaType, colorSpace); size_t allocationSize = 0; //... if (!Bitmap::computeAllocationSize(rowBytes, height, &allocationSize)) { // [2] jniThrowExceptionFmt(env, RuntimeException, "Received bad bitmap size: width=%d, height=%d, rowBytes=%d", width, height, rowBytes); return NULL; } sk_sp<Bitmap> nativeBitmap; binder_status_t error = readBlob( // [3] p.get(), // In place callback [&](std::unique_ptr<int8_t[]> buffer, int32_t size) { nativeBitmap = Bitmap::allocateHeapBitmap(allocationSize, imageInfo, rowBytes); // [4] if (nativeBitmap) { memcpy(nativeBitmap->pixels(), buffer.get(), size); // [5] } }, // Ashmem callback [&](android::base::unique_fd fd, int32_t size) { //... }); //... }
- 在[1]处,从Parcel读取了width,height,rowBytes,density等数值,这些数值是发送者可控的
- 在[2]处会计算分配空间的大小,实际上就是rowBytes * height,这个函数里面做了完善的整数溢出检查,不存在问题。
- 计算好了allocationSize之后会在[3]处调用readBlob去读取Parcel中的blob数据,并且返回读取到的缓冲区指针buffer和读取的大小size。接下来在[4]处allocateHeapBitmap会根据allocationSize来分配存储Bitmap的堆空间,然后在[5]处将buffer中的内容全数拷贝到nativeBitmap→pixels(),也就是这块堆空间中。
- 这里面需要解释一下这个回调函数中size大小的来源:可以看到,回调函数中的size实际上来源于data结构体中的size成员,而这个data.size则来源于AParcel_readByteArray回调中的length,查看AParcel_readByteArray函数的实现可以知道,length实际上是总size除以单元大小得到的,这里面的单元是int8,也就是1个字节,所以length就相当于总长度。这里面AParcel_readByteArray并不会关心Bitmap那边分配的allocationSize的大小,而是完全按照通用Parcel读取的逻辑去实现的。
- 所以这里面存在的漏洞是,由于allocationSize是根据用户的输入计算出的,同时我们还可以控制blob的数据大小,如果我们指定一个较小的rowBytes和height,也就是计算出一个较小的allocationSize,但是放入很大的blob数据,使得AParcel_readByteArray识别出来的size大于allocationSize的话,则会造成堆缓冲区溢出,这里面看起来溢出的长度(size – allocationSize)和写入的内容(blob在allocationSize位置之后的内容)都是我们可控的。
- 不过我们不能放入超过Binder缓冲区大小的数据,否则会出现Parcel传输错误。
漏洞补丁
binder_status_t error = readBlob(
p.get(),
// In place callback
[&](std::unique_ptr<int8_t[]> buffer, int32_t size) {
if (allocationSize > size) {
android_errorWriteLog(0x534e4554, "213169612");
return STATUS_BAD_VALUE;
}
nativeBitmap = Bitmap::allocateHeapBitmap(allocationSize, imageInfo, rowBytes);
if (nativeBitmap) {
memcpy(nativeBitmap->pixels(), buffer.get(), allocationSize);
return STATUS_OK;
}
return STATUS_NO_MEMORY;
},
// Ashmem callback
[&](android::base::unique_fd fd, int32_t size) {
//...
});
- 可以看到,虽然还是根据allocationSize来分配空间,但是这次不会将buffer的内容全数拷贝,而是只会拷贝allocationSize的大小,和rowBytes和height的信息相匹配的。同时还判断了allocationSize不能过大,不能超过实际blob数据的大小。
影响范围
-
这个漏洞仅影响Android 12,我们看一下Android 11中相同地方的代码:
const int width = p->readInt32(); const int height = p->readInt32(); const int rowBytes = p->readInt32(); const int density = p->readInt32(); if (kN32_SkColorType != colorType && kRGBA_F16_SkColorType != colorType && kRGB_565_SkColorType != colorType && kARGB_4444_SkColorType != colorType && kAlpha_8_SkColorType != colorType) { SkDebugf("Bitmap_createFromParcel unknown colortype: %d\n", colorType); return NULL; } std::unique_ptr<SkBitmap> bitmap(new SkBitmap); if (!bitmap->setInfo(SkImageInfo::Make(width, height, colorType, alphaType, colorSpace), rowBytes)) { return NULL; } // Read the bitmap blob. size_t size = bitmap->computeByteSize(); android::Parcel::ReadableBlob blob; android::status_t status = p->readBlob(size, &blob); if (status) { doThrowRE(env, "Could not read bitmap blob."); return NULL; }
- 这里也是类似的,读取四个int值,然后调用computeByteSize去计算大小,只不过这里的参数提前通过setInfo传进去了,然后他这里面readBlob的时候直接就是读取了计算出的大小
```cpp
// Copy the pixels into a new buffer.
nativeBitmap = Bitmap::allocateHeapBitmap(bitmap.get());
if (!nativeBitmap) {
blob.release();
doThrowRE(env, "Could not allocate java pixel ref.");
return NULL;
}
memcpy(bitmap->getPixels(), blob.data(), size);
// Release the blob handle.
blob.release();
- 在下面allocateHeapBitmap分配Bitmap空间的时候也是使用的Bitmap中现成的参数,和上面的一致,最后将blob的数据拷贝到Bitmap缓冲区中,也是使用size作为大小,这里面size最大也不会超过Binder缓冲区的大小,Parcel的实现中有比较完善的检查,所以不存在漏洞。
总结
- Bitmap是Android系统的核心类型,在Android 12中这里面还可以出现一个漏洞是比较难以置信的。这个漏洞看起来是Google在代码重构的时候引入的,所以说有时候代码重构能消除漏洞(比如CVE-2020-0096在Android 10重构时消失),也可以引入新的漏洞,可以说是一把双刃剑。