漏洞简介
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的时候直接就是读取了计算出的大小
// 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重构时消失),也可以引入新的漏洞,可以说是一把双刃剑。