SkCodecFuzzer
Fuzzing harness for testing proprietary image codecs supported by Skia on Android
Install / Use
/learn @googleprojectzero/SkCodecFuzzerREADME
Android Skia Image Fuzzing Harness
SkCodecFuzzer is a small utility for testing the security and reliability of C/C++ image codecs supported by the Skia graphics library. In Android, these parsers are reachable through standard interfaces such as BitmapFactory and BitmapRegionDecoder and execute in the context of local apps (not a sandboxed media server), which exposes them to remote attacks via MMS, chat apps, emails etc. While the decoders available in Android by default (bmp, png, jpeg, gif, ...) are all open-source and already subject to extensive fuzzing, there may exist additional lesser-known, proprietary codecs added by device manufacturers. Such codecs aren't put under the same scrutiny due to their closed-source nature, and they may go unaudited, non-fuzzed or even completely unnoticed for many years. A notable example is the Qmage format (.qmg file extension), which was introduced in Skia on Samsung Android phones in late 2014, but was only recognized as an attack surface at the end of 2019. It has been used as the container for image resources in built-in Samsung APKs and themes in some (but not all) firmwares.
The loader in this repository was used by Google Project Zero to run Qmage fuzzing at scale in January 2020, resulting in the uncovering of 5218 unique crashes, including hundreds of memory corruption issues (buffer overflows, use-after-free's etc.). They were subsequently reported to Samsung on January 28 as issue #2002 in the PZ bug tracker, and fixed by the vendor in May 2020. For additional context and more information about .qmg files, we recommend to refer to that tracker entry as it aims to explain our effort in great detail. The purpose of this harness is to link to Android's precompiled ARM(64) Skia libraries (libhwui.so or libskia.so on older versions) and use its SkCodec class to load an input file, in the same way that BitmapFactory::doDecode decodes images on real Android devices. It can run on both physical phones with ARM CPUs and in an emulated qemu-aarch64 environment on any host CPU of choice, enabling effective parallelized fuzzing.
One of the Qmage vulnerabilities was used to demonstrate successful zero-click exploitation of a Samsung Galaxy Note 10+ phone running Android 10 via MMS: see video. The exploit source code is available for reference here.
Features
The primary functionality of the tool is to load an input image with Skia, print out some basic information (dimensions, bpp), and optionally save the raw RGBA pixels of the decoded image to an output file. However, the loader also offers some features designed specifically to aid in the fuzzing and vulnerability research process:
- The default libc allocator is switched to AFL's libdislocator. Libdislocator is a special simplified allocator which places each new allocation directly before the end of a memory page (similarly to PageHeap on Windows). This facilitates more precise detection of out-of-bounds memory accesses, and makes crash deduplication based on stack traces more reliable. It is worth noting the default allocator and libdislocator have semantic differences (memory consumption, heap usage limits, address alignment and allocation poisoning). While they don't matter for fuzzing itself, they may affect the reproducibility of crashes on Android devices. For more information on the subject, see section "3.3. Libdislocator vs libc malloc" in the original bug report.
- The program registers its own custom signal handler, which prints out a verbose AddressSanitizer-like report when a crash is encountered. The report includes the type of the exception, a symbolized call stack, disassembly of the relevant code and CPU register values.
- There is an option to log all heap operations (
malloc,realloc,free) to stderr, which may prove helpful in understanding the memory allocation patterns used by the codec, figuring out the internal heap state at the time of the crash, as well as determining which chunk is overread or overwritten by a specific sample.
Building
In order to build the harness on a x86-64 Linux host, you will need:
- Android NDK, needed for the cross-compiler. I used version r20b, but r21b is already available at the time of this writing.
- Skia source code, needed for the headers (the linking is done against
libhwui.so) - Libbacktrace source code, needed for the headers (the linking is done against
libbacktrace.so) - Capstone, to disassemble the crashing instructions
- The
aarch64-linux-gnu-g++compiler, to build Capstone for aarch64 - The complete
/system/lib64directory and the/system/bin/linker64file from the tested Android system
Let's put all of the dependencies into a common deps directory (e.g. /home/j00ru/SkCodecFuzzer/deps), and start with cross-compiling Capstone:
j00ru@j00ru:~/SkCodecFuzzer/deps/capstone-4.0.1$ CAPSTONE_BUILD_CORE_ONLY=yes ./make.sh cross-android64
CC utils.o
CC cs.o
CC SStream.o
...
CC arch/EVM/EVMModule.o
CC MCInst.o
GEN capstone.pc
LINK libcapstone.so.4
AR libcapstone.a
aarch64-linux-gnu-ar: creating ./libcapstone.a
j00ru@j00ru:~/SkCodecFuzzer/deps/capstone-4.0.1$
With this, we are ready to compile the harness. Let's update the five paths at the top of Makefile to point to the corresponding dependency paths, and run make:
j00ru@j00ru:~/SkCodecFuzzer/source$ make
/home/j00ru/SkCodecFuzzer/deps/ndk/android-ndk-r20b/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang++ -c -o loader.o loader.cc -D_LIBCPP_ABI_NAMESPACE=__1 -I/home/j00ru/SkCodecFuzzer/deps/skia/include/core -I/home/j00ru/SkCodecFuzzer/deps/skia/include/codec -I/home/j00ru/SkCodecFuzzer/deps/skia/include/config -I/home/j00ru/SkCodecFuzzer/deps/skia/include/config/android -I/home/j00ru/SkCodecFuzzer/deps/capstone-4.0.1/include -I/home/j00ru/SkCodecFuzzer/deps/libbacktrace/include
/home/j00ru/SkCodecFuzzer/deps/ndk/android-ndk-r20b/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang++ -c -o common.o common.cc -D_LIBCPP_ABI_NAMESPACE=__1 -I/home/j00ru/SkCodecFuzzer/deps/skia/include/core -I/home/j00ru/SkCodecFuzzer/deps/skia/include/codec -I/home/j00ru/SkCodecFuzzer/deps/skia/include/config -I/home/j00ru/SkCodecFuzzer/deps/skia/include/config/android -I/home/j00ru/SkCodecFuzzer/deps/capstone-4.0.1/include -I/home/j00ru/SkCodecFuzzer/deps/libbacktrace/include
/home/j00ru/SkCodecFuzzer/deps/ndk/android-ndk-r20b/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang++ -c -o tokenizer.o tokenizer.cc -D_LIBCPP_ABI_NAMESPACE=__1 -I/home/j00ru/SkCodecFuzzer/deps/skia/include/core -I/home/j00ru/SkCodecFuzzer/deps/skia/include/codec -I/home/j00ru/SkCodecFuzzer/deps/skia/include/config -I/home/j00ru/SkCodecFuzzer/deps/skia/include/config/android -I/home/j00ru/SkCodecFuzzer/deps/capstone-4.0.1/include -I/home/j00ru/SkCodecFuzzer/deps/libbacktrace/include
/home/j00ru/SkCodecFuzzer/deps/ndk/android-ndk-r20b/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang -c -o libdislocator.o third_party/libdislocator/libdislocator.so.c
/home/j00ru/SkCodecFuzzer/deps/ndk/android-ndk-r20b/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang++ -o loader loader.o common.o tokenizer.o libdislocator.o -L/home/j00ru/SkCodecFuzzer/deps/capstone-4.0.1 -lcapstone -L/home/j00ru/SkCodecFuzzer/deps/android/system/lib64 -lhwui -ldl -lbacktrace -landroidicu -Wl,-rpath -Wl,/home/j00ru/SkCodecFuzzer/deps/android/system/lib64 -Wl,--dynamic-linker=/home/j00ru/SkCodecFuzzer/deps/android/system/bin/linker64
j00ru@j00ru:~/SkCodecFuzzer/source$
We should now find the following loader file in the current directory:
j00ru@j00ru:~/SkCodecFuzzer/source$ file loader
loader: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /home/j00ru/SkCodecFuzzer/deps/android/system/bin/linker64, with debug_info, not stripped
j00ru@j00ru:~/SkCodecFuzzer/source$
To run it locally, make sure you have qemu-aarch64 installed, update the two paths at the top of the run.sh script accordingly, and start it:
j00ru@j00ru:~/SkCodecFuzzer/source$ ./run.sh
Error: missing required --input (-i) option
Usage: [LIBC_HOOKS_ENABLE=1] ./loader [OPTION]...
Required arguments:
-i, --input <image path> specify input file path for decoding
Optional arguments:
-o, --output <file path> save raw decoded RGBA image colors to specified output file
-l, --log_malloc log heap allocator activity to stderr (LIBC_HOOKS_ENABLE=1 needed)
-d, --default_malloc use the default system heap allocator
-h, --help display this help and exit
j00ru@j00ru:~/SkCodecFuzzer/source$
The above process works fine with libraries from Android up to version 9. While using files pulled from Android 10, you may encounter the following error:
==31162==Sanitizer CHECK failed: /usr/local/google/buildbot/src/android/llvm-toolchain/toolchain/compiler-rt/lib/sanitizer_common/sanitizer_posix.cc:371 ((internal_prctl(0x53564d41, 0, addr, size, (uptr)name) == 0)) != (0) (0, 0)
libc: Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 31162 (qemu-aarch64), pid 31162 (qemu-aarch64)
