Mark Brand and Sergei Glazunov, Project Zero (Originally posted on Project Zero blog 2021-02-04)

The Basics

Disclosure or Patch Date: 2 November 2020

Product: Google Chrome for Android

Advisory: https://chromereleases.googleblog.com/2020/11/chrome-for-android-update.html

Affected Versions: 86.0.4240.114 and previous

First Patched Version: 86.0.4240.185

Issue/Bug Report:

Patch CL: https://chromium.googlesource.com/chromium/src.git/+/e598fc599bd920392256d05c61826466c73c8e89

Bug-Introducing CL: Unknown - at least from the migration of the use of SkColorType from SkBitmap::Config but may have existed prior to that.

Reporter(s): Maddie Stone, Mark Brand, and Sergei Glazunov of Google Project Zero

The Code

Proof-of-concept: https://bugs.chromium.org/p/project-zero/issues/detail?id=2112

Exploit sample: N/A

Did you have access to the exploit sample when doing the analysis? Yes

The Vulnerability

Bug class: Buffer overflow

Vulnerability details:

ConvertToJavaBitmap doesn't handle all Skia supported color formats, and when converting a Skia bitmap with an unsupported SkColorType, it falls back to a default format (32-bits-per-pixel, ARGB_8888).

https://source.chromium.org/chromium/chromium/src/+/master:ui/gfx/android/java_bitmap.cc;drc=8811640591d59f0b489a7703224530b03efe127b;l=44

static int SkColorTypeToBitmapFormat(SkColorType color_type) {
  switch (color_type) {
    case kAlpha_8_SkColorType:
      return BITMAP_FORMAT_ALPHA_8;
    case kARGB_4444_SkColorType:
      return BITMAP_FORMAT_ARGB_4444;
    case kN32_SkColorType:
      return BITMAP_FORMAT_ARGB_8888;
    case kRGB_565_SkColorType:
      return BITMAP_FORMAT_RGB_565;
    case kUnknown_SkColorType:
    default:
      NOTREACHED(); // NOTREACHED has no effect in release builds.
      return BITMAP_FORMAT_NO_CONFIG;
  }
}

https://source.chromium.org/chromium/chromium/src/+/master:ui/android/java/src/org/chromium/ui/gfx/BitmapHelper.java;drc=8811640591d59f0b489a7703224530b03efe127b;l=61

/**
 * Provides a matching Bitmap.Config for the enum config value passed.
 *
 * @param bitmapFormatValue The Bitmap Configuration enum value.
 * @return Matching Bitmap.Config  for the enum value passed.
 */
private static Bitmap.Config getBitmapConfigForFormat(int bitmapFormatValue) {
    switch (bitmapFormatValue) {
        case BitmapFormat.ALPHA_8:
            return Bitmap.Config.ALPHA_8;
         case BitmapFormat.ARGB_4444:
            return Bitmap.Config.ARGB_4444;
         case BitmapFormat.RGB_565:
            return Bitmap.Config.RGB_565;
         case BitmapFormat.ARGB_8888:
         default:
            return Bitmap.Config.ARGB_8888; // <-- fallback to 32-bpp
    }
}

Since Skia supports formats with more than 32-bits-per-pixel (eg. 64-bits-per-pixel, RGBA_F16), this can cause a mismatch between the size of the input and output bitmap buffers.

This allows an attacker to supply a malicious bitmap that will cause CreateJavaBitmap to create an output JavaBitmap with a smaller backing store than the input SkBitmap. This leads to a heap buffer overflow when copying the raw pixel data into the destination bitmap.

The vulnerability can be fixed by ensuring that all of the input SkBitmap's parameters are supported before performing the conversion, and adding an additional size check to make sure that the destination bitmap buffer is large enough prior to doing the memcpy.

https://source.chromium.org/chromium/chromium/src/+/master:ui/gfx/android/java_bitmap.cc;drc=8811640591d59f0b489a7703224530b03efe127b;l=73

ScopedJavaLocalRef<jobject> ConvertToJavaBitmap(const SkBitmap* skbitmap,
                                                OomBehavior reaction) {
  DCHECK(skbitmap);
  DCHECK(!skbitmap->isNull());
  SkColorType color_type = skbitmap->colorType();
  DCHECK((color_type == kRGB_565_SkColorType) ||
         (color_type == kN32_SkColorType));
  ScopedJavaLocalRef<jobject> jbitmap = CreateJavaBitmap(
      skbitmap->width(), skbitmap->height(), color_type, reaction);
  if (!jbitmap) {
    DCHECK_EQ(OomBehavior::kReturnNullOnOom, reaction);
    return jbitmap;
  }
  JavaBitmap dst_lock(jbitmap);
  void* src_pixels = skbitmap->getPixels();
  void* dst_pixels = dst_lock.pixels();
  memcpy(dst_pixels, src_pixels, skbitmap->computeByteSize()); // <-- we use skbitmap size here, which may be larger than allocated size.

  return jbitmap;
}

Patch analysis: N/A

Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):

Most likely through source-code auditing. This issue (at least in the context that it was exploited) would have required an IPC fuzzer that was sufficiently aware to produce valid serialized SkBitmap objects, and that would be more work than simply auditing all of the relevant code.

(Historical/present/future) context of bug:

The Exploit

(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)

Exploit strategy (or strategies): Still under analysis

Exploit flow: Still under analysis

Known cases of the same exploit flow: Still under analysis

Part of an exploit chain? This vulnerability was chained as the sandbox escape on Android with CVE-2020-15999. It was delivered to Android devices running Chrome 81+, while a Chrome 1-day was delivered to Android devices running Chrome <= 80.

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why): Manual source-code auditing of similar image format conversion paths, especially where incoming image data comes from an untrusted source (IPC).

Found variants:

Structural improvements

This specific vulnerability would not have been exploitable if it wasn't for the surprising (lack of) behaviour of Chrome's NOTREACHED macro in release builds; but this would not address the general class of issue.

0-day detection methods

Other References