Posted by Mateusz Jurczyk, Project Zero




This post is the first of a multi-part series capturing my journey from discovering a vulnerable little-known Samsung image codec, to completing a remote zero-click MMS attack that worked on the latest Samsung flagship devices. New posts will be published as they are completed and will be linked here when complete.





Introduction



In January 2023, I reported a large volume of crashes in a custom Samsung codec called "Qmage", present in all Samsung phones since late 2014 (Android version 4.4.4+). This codec is written in C/C++ code, and is baked deeply into the Skia graphics library, which is in turn the underlying engine used for nearly all graphics operations in the Android OS. In other words, in addition to the well-known formats such as JPEG and PNG, modern Samsung phones also natively support a proprietary Qmage format, typically denoted by the .qmg file extension. It is automatically enabled for all apps which display images, making it a prime target for remote attacks, as sending pictures is the core functionality of some of the most popular mobile apps.




In May 2023, Samsung released patches addressing the crashes (including a number of buffer overflows and other memory corruption issues) for devices that are eligible for receiving security updates. The issues were collectively assigned CVE-2023-8899 and a Samsung-specific SVE-2023-16747. On the day of the security bulletin being released, I derestricted the relevant #2002 tracker entry, and open-sourced my fuzzing harness on GitHub (SkCodecFuzzer). I recommend reading the original report, as it includes a detailed explanation of the then-current state of the codec, my approach to fuzzing it, and the bugs found as a result. It may provide some valuable context to better understand this and other upcoming blog posts in the series, although I will do my best to make them self-contained and easy to understand on their own.




After reporting the bugs, I spent the next few months trying to build a zero-click MMS exploit for one of the flagship phones: Samsung Galaxy Note 10+ running Android 10. For reference, similar attacks against chat apps were shown to be possible on iPhones via iMessage by Samuel Groß and Natalie Silvanovich of Google Project Zero in 2023 (see demo video, and blog posts #1, #2, #3). On the other hand, to my knowledge, no exploitation attempts of this kind have been publicly documented against Android since the Stagefright vulnerabilities disclosed in 2015. To me, this seemed like a great opportunity to deep dive into the state of the exploit mitigations on Android today, and see how they fared against a relatively powerful bug – a heap-based buffer overflow with controlled buffer size, data length and the data itself. In the end, I managed to develop an exploit which remotely bypassed ASLR and obtained a reverse shell on a victim's phone (with the privileges of the SMS/MMS app) with no user interaction required, in around 100 minutes on average. The official recording of an attack demonstration is available here, and below I am presenting a director's cut of the same video, with a soundtrack added for your viewing pleasure. :)









By publishing this and further blog posts in the Qmage series, I am hoping to shed more light on how I found the codec, what I learned about it during reconnaissance and preparation for fuzzing, and finally how I managed to circumvent various Android mitigations and obstacles along the way to write a reliable MMS exploit. Please join me on this ride!


How it started



As is often the case in vulnerability research (in my experience), there was a bit of luck involved in the finding of this attack surface. In late 2023, Project Zero had a hackathon as part of a team bonding activity, with focus on Samsung phones. We chose Samsung phones as they were the most popular mobile devices in Europe in Q2 2023, when we were planning our event. Other results of that event can also be found in the PZ bug tracker – they were centered mostly around the security of Samsung kernel-mode components, and fixed in February 2023. During the hackathon, we used Samsung Galaxy A50 running Android 9 as our test devices, but the rest of this post is written based on the analysis of a newer Note 10+ and Android 10, which were the latest Samsung flagship devices at the time of this research.




When I was looking for potential bug hunting targets (naturally written in native languages, so that memory corruption issues would apply), my first thought was to look for inspiration in the bug tracker and existing reports from the past. There, the following issues reported by Natalie in 2015 immediately caught my eye:


Samsung Galaxy S6 image processing bugs in Project Zero bug tracker





Memory safety issues in image handling, how splendid! Please note the "Q" in the "libQjpeg" library name in some of the titles – without knowing it, this was my first encounter with the third-party Quramsoft software vendor. I dug into the bug descriptions, but I couldn't find any of the mentioned libSecMMCodec.so, libQjpeg.so or libfacerecognition.so modules on the test device. However, when I searched for some function names extracted from these reports, such as QURAMWINK_DecodeJPEG, I found them in three other files under /system/lib64:





  • libimagecodec.quram.so



  • libatomcore.quram.so



  • libagifencoder.quram.so






If you're wondering how many more libraries with "quram" in their names there are in the directory, there are three more on the Note 10+:





  • libSEF.quram.so



  • libsecjpegquram.so



  • libatomjpeg.quram.so






In general, the various Quram libraries used in a specific Samsung Android build are listed in /system/etc/public.libraries-quram.txt. I think it is worth highlighting that Quramsoft has a portfolio of software solutions related to audio, video, images and animations, both for encoding and decoding. Throughout Android's existence (and even before it), Samsung has worked closely with the third-party vendor and included a number of their libraries in their custom builds of the OS, mostly to support and advance built-in apps such as Camera or Gallery. Over the years, these libraries have been evolving, some of them were renamed and removed, while others were refactored and merged, up to the point where it is objectively hard for a member of the public to keep track which Samsung models have what subset of the libraries installed. However, many of them are still present and are used on the latest phones. I hope this helps clear up any confusion as to why I am referencing Quram libraries outside the context of just the Qmage codec.




Back to the story – after a brief analysis, I narrowed my interest down to the libimagecodec.quram.so library, which was the largest, most imported one, and seemed to implement support for a variety of image formats. I could easily trigger it through Gallery, but I still struggled to reach it through media scanning, something that Natalie used as the attack vector in many of her bugs. I began to investigate how the media scanner worked, starting with platform/frameworks/base/media/java/android/media/MediaScanner.java in AOSP, and specifically the scanSingleFiledoScanFileprocessImageFile path. Here, we can see that all it really boils down to is the usage of the standard BitmapFactory interface:







private boolean processImageFile(String path) {


    try {


        mBitmapOptions.outWidth = 0;


        mBitmapOptions.outHeight = 0;


        BitmapFactory.decodeFile(path, mBitmapOptions);


        mWidth = mBitmapOptions.outWidth;


        mHeight = mBitmapOptions.outHeight;


        return mWidth > 0 && mHeight > 0;


    } catch (Throwable th) {


        // ignore;


    }


    return false;


}






I later verified that very similar code was used by the MediaScanner service on my test Samsung device; the only difference being extra references to an com.samsung.android.media.SemExtendedFormat interface and a related libSEF.quram.so library, which appeared irrelevant to my goal of triggering Quram's custom JPEG decoder.


Discovering Skia and Qmage



Not being very familiar with Android and its graphics subsystem at the time, I wanted to dig even deeper into the stack of abstraction and see the actual image decoding code. To achieve that, I followed the execution path of a few more nested methods, first in Java and then crossing into C++ land: BitmapFactory.decodeFiledecodeStreamdecodeStreamInternalnativeDecodeStreamdoDecode. In here, we can finally see the actual logic of decoding an image from a byte stream, first by creating a Skia SkCodec object:







    // Create the codec.


    NinePatchPeeker peeker;


    std::unique_ptr<SkAndroidCodec> codec;


    {


        SkCodec::Result result;


        std::unique_ptr<SkCodec> c = SkCodec::MakeFromStream(std::move(stream), &result, &peeker);




        [...]




        codec = SkAndroidCodec::MakeFromCodec(std::move(c));


        if (!codec) {


            return nullObjectReturn("SkAndroidCodec::MakeFromCodec returned null");


        }






… and then calling the getAndroidPixels method on it:







    SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodingBitmap.getPixels(),


            decodingBitmap.rowBytes(), &codecOptions);






So, in order to learn what image formats are supported by the interface, we have to look into SkCodec::MakeFromStream. The upstream version of the method can be found on GitHub; there, we can see that depending on macros defined during compilation, the following types of images can be loaded (based mostly on the gDecoderProcs table):





  • png



  • jpeg



  • webp



  • gif



  • ico



  • bmp



  • wbmp



  • heif



  • raw (dng)






This is already a sizable list of formats. We can compare the open-source implementation with the compiled one found on Samsung phones in /system/lib64/libhwui.so, which is where the Skia code lives on Android now (on older systems, it was located in /system/lib64/libskia.so). When I originally opened the SkCodec::MakeFromStream method in IDA Pro, I saw an unrolled loop iterating over the standard Skia codecs, but also a few extra file signature checks, namely:







if ( png_sig_cmp(header, 0LL, length) && (!length || *(_DWORD *)header != 'OIP\x89') )


{


  if ( header[0] == 'Q' && header[1] == 'G' )


  {


    if ( QuramQmageDecVersionCheck(header) )


    {


      // ...


    }


    __android_log_print(6LL, "Qmage", "%s : stream is not a Qmage file\n", "IsQmg");


  }


  if ( header[0] == 'Q' && header[1] == 'M' )


  {


    if ( QuramQmageDecVersionCheck_Rev8253_140615(header) )


    {


      // ...


    }


    __android_log_print(6LL, "Qmage", "%s : stream_Rev8253_140615 is not a Qmage file\n", "IsQM");


  }


  else if ( *(_DWORD *)header == 0x5CA1AB13 )


  {


    // ...


  }






There were four additional signatures being checked for:





  1. \x89PIO, an alternative to the standard \x89PNG file magic.



  2. QG, a type of a Qmage file, as indicated by the log string.



  3. QM, another type of a Qmage file.



  4. \x13\xAB\xA1\x5C, magic bytes which represent an Adaptive Scalable Texture Compression (ASTC) image.






After brief analysis, I concluded that PIO and ASTC were not particularly interesting from a security research perspective, and I turned my eyes to Qmage. It looked like the obvious choice, considering that libhwui.so had hundreds of functions containing "quram", "qmage", or other related strings, these routines performed low-level file format parsing, and many of them were extremely long. The codec seemed so complex and so deeply integrated in Android that it got me really intrigued. An additional factor in all this was that I had never heard about it before, and even using Google search wasn't of much help either. In offensive security, this is usually a very strong indicator of an attractive research target, so I had no choice – I had to put my detective cap on and investigate further.


Learning more about the codec



At this stage, I had many questions roaming in my head, and hardly any answers:





  • What is this codec? How does it work?



  • What is the history behind it? How long has it been shipping? Is it present in all Samsung phones?



  • What is its intended use, and where to find samples to start playing with?



  • What is the security posture of the code?






Finding the answers took me many weeks, but for the sake of brevity, I will present an accelerated account of the events, which skips over some periods of confusion and attempts to make sense of the partial information available at the time. :)


Format versioning



So far, we know that there are two possible magic values for Qmage files: QM and QG. If we look deeper into QuramQmageDecVersionCheckQmageDecCommon_VersionCheck, which is the second part of the header check, we will see the following logic (in C-like pseudocode):







int QmageDecCommon_VersionCheck(unsigned __int8 *data) {




  if ((data[0] | (data[1] << 8)) != 'GQ') {


    debug_QmageDecError = -5;


    return 0;


  }




  if (data[2] > 2 || (data[2] == 2 && data[3] != 0)) {


    debug_QmageDecError = -5;


    return 0;


  }




  return 1;


}






The function verifies the QG signature again, and then treats the next two bytes as the version identifier. If we assume that data[2] and data[3] are major and minor version numbers respectively, then according to the code above, versions 2.0 are supported. In fact, this is a really permissive way of implementing the check, because it allows through a number of versions that don't really exist. At the time of this writing, I already know that there are three actual valid versions of the QG format:





  • QG 1.0



  • QG 1.1



  • QG 2.0






Other combinations of major/minor versions (such as 1.231) are either ignored by the codec, or resolve to one of the three above.




To learn more about the versioning of QM images, we can similarly follow the QuramQmageDecVersionCheck_Rev8253_140615 QmageDecCommon_VersionCheck_Rev8253_140615 functions in our disassembler, which will lead us to the following logic:







int QmageDecCommon_VersionCheck_Rev8253_140615(unsigned __int8 *data) {




  if (*(unsigned short *)&data[0] == 'MI') {


    if ( data[7] - 90 < 4 )


      return 1;


  }


  else if (*(unsigned short *)&data[0] == 'TI' ) {


    if ((data[5] & 0x7F) == 21)


      return 1;


  }


  else if (*(unsigned int *)&data[0] == 'GEFI') {


    if ((data[11] & 0x7F) == 21)


      return 1;


  }


  else if (*(unsigned short *)&data[0] == 'WQ') {


    if (data[2] > 0xC)


      return 1;


  }


  else if (*(unsigned int *)&data[0] == '`RFP') {


    if (*(unsigned int*)&data[4] == 0)


      return 1;


  }


  else if (*(unsigned short *)&data[0] == 'MQ' && data[2] == 1) {


    return 1;


  }




  debug_QmageDecError = -5;


  return 0;


}






This is definitely more code than expected. We are primarily interested in the last if statement, where we can see that a 0x01 byte is expected to follow the QM magic. Again assuming that this is the version number, we can note down that a version 1 of the QM format is supported by the modern Samsung build of Skia. However, there are also a number of other signatures being checked for: IM, IT, IFEG, QW and PFR. I don't know exactly what formats they represent, and since the above routine can only be reached through a QM header detected in SkCodec::MakeFromStream, the signatures don't really seem to be intentional there. More likely, they are leftover artifacts manifesting file formats parsed by Quramsoft code elsewhere, or got deprecated and aren't in active use anymore at all. We might see these constants again in the future so it's worth keeping them in mind.




In summary, there are four distinct versions of Qmage supported by Skia, in chronological order: QMv1, QG1.0, QG1.1, QG2.0. This is especially self-evident when looking at the list of debug symbols found in libhwui.so. For each symbol that has existed in the QMv1 codec all the way to QG2.0, there are now four copies of the given variable/function/etc., for example:


Example list of Qmage functions with four copies each



The names without any suffixes represent parts of the code for the latest format (QG 2.0). For each earlier format, there seems to be a fork of the code with all functions, structures, static objects etc. renamed to include a revision number and a second numeric part which looks like a date. If my understanding is correct, that would mean that the cut-off dates for the QMv1, QG1.0 and QG1.1 versions of the format were around June 2014, Oct 2014 and Feb 2015 – a relatively short period of time. The QG2.0 iteration was first seen in January 2023 in Android 10, but being the most recent, it lacks the convenient _RevXXXX_YYMMDD suffix to tell us exactly which revision number it is.




However, there are also other bits and pieces of information regarding the versioning of the codec itself to be found in the precompiled Skia binaries. For example, there is an empty function called QmageDecCommon_QmageVersion_1_11_00 in the current build of libhwui.so. Furthermore, there is also an unused QuramQmageGetDecoderVersion function in the library, which prints out some other kind of four-part version number, and the exact build date and time, for example:







void QuramQmageGetDecoderVersion() {


  __android_log_print(ANDROID_LOG_INFO, "QG", "Quram Qmage Decoder Info\n");


  __android_log_print(ANDROID_LOG_INFO, "QG", "Version\t : %d.%d.%d.%d\n", 2, 0, 4, 21541);


  __android_log_print(ANDROID_LOG_INFO, "QG", "Build Date\t : %s %s\n", "Mar 17 2023", "19:18:14");


}






If the version number is represented by four X.Y.Z.R integers, then the X.Y pair denotes the highest version of the QG format that the codec supports (in this case, QG2.0), and R is the revision number of the code. By studying the countless builds of libskia.so and libhwui.so found in archival Samsung firmwares released over the years, one could create a very accurate record of all the different Qmage compilations ever shipped with Samsung devices. My limited analysis has resulted in the following, undoubtedly incomplete table:
























Build Date


Build Time


X


Y


Z


R


Jun 15 2014


QMv1 codec


8253


Nov 14 2014


20:12:03


1


0


0


10484


Nov 17 2014


16:25:19


1


0


0


10484


Jun 12 2015


19:23:03


1


1


1


14470


Jul 3 2015


20:40:49


1


1


1


14470


Jul 6 2015


11:52:27


1


1


1


14470


Sep 2 2015


16:27:42


1


1


1


14470


Nov 27 2015


20:39:10


1


1


1


14470


Dec 21 2015


18:29:31


1


1


2


14470


May 27 2016


15:57:00


1


1


4


21541


Sep 8 2016


17:42:29


1


1


4


21541


Nov 8 2017


20:52:16


1


1


4


21541


Jan 11 2023


20:31:15


1


1


4


21541


Sep 26 2023


17:06:36


2


0


4


21541


Feb 28 2023


09:26:32


2


0


4


21541


Mar 17 2023


19:18:14


2


0


4


21541


May 28 2023


08:53:20


2


0


4


21541






Based on the above information, we can confirm some of our existing presumptions, and draw new conclusions:





  • The codec's appearance in Skia goes back to around mid-2014.



  • It saw the most activity in development between 2014 and 2016, followed by a few years of relative inactivity, to come back again with a new version 2.0 of the format, first compiled for production use in September 2023.



  • The Z component of the version (4) and the revision number (21541) haven't changed since 2016, limiting our insight into the volume of recent changes in the code base.






For those like me who are interested in small interesting pieces of metadata, there are even more artifacts to be found in the library. For example, there are three copies of the QuramQmageGetDecoderVersion function in libhwui.so on Samsung Android 10 (for each version of the QG format), and there are both 32-bit and 64-bit builds of the shared object in the system, so for one compilation of the Qmage codecs, we get six different timestamps taken during the process. On the example of the build from 26 September 2023:













Version


Bitness


Build timestamp


2.0


32-bit


17:05:08


1.1


32-bit


17:06:16


1.0


32-bit


17:06:17


2.0


64-bit


17:06:36


1.1


64-bit


17:07:45


1.0


64-bit


17:07:46






I don't think there is any information that can be derived from it with full certainty, but I still find it fascinating enough to include here. At the very least, the 2m36s gap between the first and last timestamp gives us a clue as to the extent of complexity of the codec.


Codec size, basic control flow and compression types



When we open up libhwui.so in IDA Pro and start inspecting the Qmage code in compiled form, the above build times may start to make sense. In the few builds of the library that I've tested, the Qmage-related code is placed in one continuous binary blob, which makes it easy to measure its size. For example, in the 26-Sep-2023 build (2.0.4.21541), the first function in the Qmage-related chunk is QmageDecoderLicenseCheck, and the last one is SetResidualCoeffs_C. The overall code region between them is almost 908 kB in size (!), or around 15% of the overall executable segment in the shared object. In large part, this is due to the QG2.0 codec added in Android 10, which introduces more code duplication (new forks of most Qmage-related functions) and imports a whole new copy of libwebp. But even on Android 9 and the 8-Nov-2017 build, the codec is ~425 kB long.




Below is a list of the 20 longest functions found in libhwui.so. Notice how 18/20 of them are related to Qmage:


Top 20 longest functions in libhwui.so



Let's now move onto the control flow and entry points of the codec. When a Qmage image is loaded through an Android interface such as BitmapFactory, execution ends up in the doDecode function, which then calls SkCodec::MakeFromStream, as discussed above. Then, if the first few bytes match the "QG" signature, execution reaches SkQmgCodec::MakeFromStream and further nested functions for header parsing:




SkQmgCodec::MakeFromStream


└── ParseHeader


    └── QuramQmageDecParseHeader


        ├── QmageDecCommon_ParseHeader


        │   └── QmageDecCommon_QGetDecoderInfo


        └── QmageDecCommon_MakeColorTableExtendIndex




The flow is very similar for the older QMv1 files. This basic parsing is sufficient to extract essential information about the bitmap such as its dimensions, so if the inJustDecodeBounds flag is set in BitmapFactory.Options, the processing of the file ends here. However, even though the header parsing logic is short and simple compared to the full bitmap decoding, I still managed to find memory corruption issues there related to building the color table in memory. So, even processes that only query the bounds of untrusted images, such as the MediaScanner service, were prone to attacks via Qmage. But let's not get ahead of ourselves. If the full bitmap data is requested by the caller (e.g. for an app to display it), execution proceeds to SkQmgCodec::onGetPixels and deeper down:




SkQmgCodec::onGetPixels


└── QuramQmageDecodeFrame


    └── Qmage_WDecodeFrame_Low


        └── _QM_WCodec_decode


            ├── PVcodecDecoderIndex


            ├── PVcodecDecoderGrayScale


            └── PVcodecDecoder




Up until this point, the consecutive functions are mostly simple wrappers over the next nested routines, without much data processing logic involved. This changes with the PVcodecDecoder[...] family, which finally choose the relevant low-level codec and call one of the corresponding long, complex functions which do the heavy lifting, such as PVcodecDecoder_1channel_32bits_NEW or QuramQumageDecoder32bit24bit. The subset of available compression types varies between different versions of Qmage; I have performed a cursory analysis and documented them in the table below. Some of them implement well-known concepts such as Run-Length Encoding (RLE) or zlib inflation, while others (the most complex ones) seem to execute custom, proprietary decompression algorithms.



















QMv1


QG1.0


QG1.1


QG2.0


PVcodecDecoder_1channel_16bits_NEW










PVcodecDecoder_1channel_32bits_NEW










PVcodecDecoder_GrayScale_16bits_NEW










PVcodecDecoder_zip










QmageDiffZipDecode










PVcodecDecoder_24bits_NEW










PVcodecDecoder_32bits_NEW










QuramQumageDecoder32bit24bit










QmageRunLengthDecodeCheckBuffer










QuramQumageDecoder8bit










QuramQmageGrayIndexRleDecode














It's worth noting that even as some of these compression types are no longer found in the most recent version of the codec, they are still present and reachable through the older versions supported on Samsung devices. A minor exception is the QMv1 format, which sometimes fails to load in Skia in certain contexts, probably due to its obsolescence and lack of proper testing on modern devices.




It's also interesting that at this level of abstraction, the QG2.0 format doesn't introduce any new codecs of its own. That doesn't mean that it's only a minor revision compared to QG1.1 – on the contrary, it does bring in a vast amount of new functions, just at a different level of the call hierarchy:




[...]




├── QuramQumageDecoder32bit24bit


│   ├── DecodePrediction2dZip


│   ├── DecodePrediction2dZip_1L


│   └── QmageDecodeStreamGet_GMH




├── QmageDiffZipDecode


│   └── QmageSubUnCompress


│       └── qme_uncompress


│           └── qme_inflateInit


│           └── qme_inflate


│           └── qme_inflateEnd




└── PVcodecDecoder_zip


    ├── Qmage7zUnCompress


    ├── QmageBinUnCompress


    └── Vp8XxD_QMG


        ├── VP8LNew_QMG


        ├── WebPResetDecParams_QMG


        ├── VP8InitIoInternal_QMG


        ├── VP8LDecodeHeader_QMG


        └── XxDecodeImageData__QMG




Instead of adding new compression types, the QG2.0 format seems to build on and improve the existing ones. It also imports the zlib 1.2.8 library, with each of its functions prepended with the qme_ prefix, and a whole second copy of libwebp (one is already used by Skia), with all of its symbols appended with a _QMG suffix. One could presume that the addition of libwebp indicates some kind of webp-in-qmage "inception" feature, but based on the fact that a great majority of the library is never referenced, it's more likely that Qmage simply borrows a few functions from libwebp, but just happens to link in the whole library. It was one of many unorthodox development practices that had been apparent in the codec so far.




I hope this sheds some light on the structure of the code and the versioning of .qmg files. With some basic knowledge of the inner workings of the format, we can now look for some actual examples of such images to play and experiment with.


Finding input samples



Based on the available information, we can presume that the Qmage format was not introduced in Skia for user-generated content, but rather for static resources in Samsung-manufactured APKs (built-in apps and themes). This is where we can look for an initial set of test cases for fuzzing and manual experimentation. However, please note that throughout the years, APK resources in Samsung firmwares have shipped in a variety of formats and file extensions:





  • Qmage (.qmg, .qio)



  • ASTC (.astc, .atc)



  • PNG (.png, .pio)



  • BMP (.bmp)



  • JPEG (.jpeg)



  • SVG (.svg)



  • webp (.webp)



  • SPR (.spr)



  • Binary XML (.xml)






It is not clear to me how the device model, Android version, year of release, country code or other characteristics of a specific Samsung firmware are factored in in the determination of which format to use for a given resource in a given app. Sometimes all bitmaps were encoded in a single format (e.g. PNG, Qmage, ASTC), and in other cases up to six different formats were used in the scope of one APK. This is still largely a mystery to me. However, I can say for sure that I have had the most luck finding .qmg files in the firmware of Android 4.4.4, 5.0.1, 5.1.1 and 6.0.1 for Samsung Galaxy Note 3, 4 and 5 released in 2014-2016. That said, please note that this is just an example and a variety of other firmwares also include Qmage samples.




As mentioned in the bug tracker entry, I have been able to identify legitimate QMv1, QG1.0 and QG1.1 samples during my research. While the codec for QG2.0 is present in Skia on Android 10, I haven't encountered any genuine bitmaps encoded in this new format thus far, despite spending a little bit of time looking for them. Consequently, in order to achieve a satisfying degree of code coverage of the QG2.0 codec, I had to synthesize such files through the fuzzing of existing QG1.x test cases that I had at my disposal. I'll go into this in more detail in the next blog post.




To obtain some actual files to look at, let's examine the firmware of Samsung Galaxy Note 4, Android 6.0.1, for Switzerland, built on 3 May 2016 (fun fact: about half of Project Zero is based in Switzerland). Once we have access to the system partition, we can dig into the default apps stored in /system/app and /system/priv-app. My go-to app to look for Qmage samples is /system/priv-app/SecSettings/SecSettings.apk. An APK is essentially a ZIP archive, so we can extract it, open it in our favorite file manager and browse to the res/ subdirectory. In there, we'll see:


Structure of the "res" APK directory



We are interested in the drawable subdirectories, for example drawable-xxxhdpi-v4:


Bitmap resources found in the "drawable-xxxhdpi-v4" subdirectory



There they are, actual embedded Qmage files! They are noticeably mixed with some .pio (png) images, as well as a few other formats (webp, xml, jpeg, spr) not shown in the screenshot. Let's take the accessibility_light_easy_off.qmg file out for testing. In a hex editor, we can see that it's in fact a QG1.1 file (see first four bytes):


Hex dump of an example Qmage file header



The basic header of Qmage files is 12 bytes long and has the following structure:










Magic


Version


Flags


Qual.


Width


Height


Extra


'Q'


'G'


0x01


0x01


0x01


0x28


0x5B


0x58


0x01


0x58


0x01


0x00


Interpreted as:


QG


1.1.1


0x28


91


344


344


0






So in this example, we are dealing with a lossy (quality 91/100) bitmap with 344x344 dimensions. Let's try to get it loaded on a real device to see if it's displayed correctly. To achieve that, it is important to give the file a standard image extension such as .png or .jpg, since .qmg files are not recognized as images by default. Once we change the extension and copy the file to a Samsung phone, we can view it in Gallery or any other app which displays bitmaps:


Qmage file displayed in the Gallery app



It works! What happens if we send the Qmage image via MMS?


Qmage file sent over MMS and displayed by the Messages app



Success again. This confirms that Qmage files are indeed seamlessly supported on Samsung devices the same way other standard formats are, which makes them an equally important attack surface. We can now fire up a debugger, attach it, for example, to the Gallery process (com.sec.android.gallery3d), set a breakpoint on one of the codec entry points and follow the execution flow to better understand how it works. We can also start manually flipping bits to see how resistant the codec is against corrupted input, or collect the samples in preparation for an automated fuzzing session. With a number of valid Qmage-encoded files to play with, our testing capabilities are suddenly greatly extended. Before we get to that, though, let's see if we can find any more debug symbols or other publicly available metadata, which might prove useful in future bug root cause analysis or exploit development.


Finding traces of open-source code



In the early stages of exploring old or obscure technologies, I like to refer to GitHub as a source of information. It can help identify open-source code related to the subject in question, otherwise not indexed by web search engines and hard to find by the usual means. This worked well here as well – when I typed "qmage" into the search box, I got a number of interesting hits. Perhaps the most helpful one revealed that Samsung used to open-source its custom Skia modifications, including a wrapper class for Qmage. To access it through the official channels, you can go to https://opensource.samsung.com/, navigate to RELEASE CENTER → Mobile, and look for packages with names containing "_LL_", and with over 1.0 GB in size (for example GT-I9505_EUR_LL_Opensource.zip). The two letters most likely signify the Android version, i.e. LL for Android Lollipop (version 5.x).


Code snippet of the SkQmageImageDecoder::onDecode method



The relevant file in the bundle is Platform.tar.gz/vendor/samsung/packages/apps/SBrowser/


src/platform/kk/external/skia/src/images/SkImageDecoder_libqmage.cpp. I suspect that the "kk" subdirectory name relates to Android KitKat (version 4.4.4), whose release time frame coincides with the period when Qmage in Skia was first spotted in Samsung firmware (around June 2014). The code itself doesn't contain too much detail about the internals of the codec, as it delegates the actual parsing work to the QuramQmageDecParseHeader and QuramQmageDecodeFrame functions mentioned before. On the other hand, it gave me some psychological comfort and motivation to look for further clues and information – perhaps I wouldn't have to be limited to just ARM assembly and reverse-engineered structure layouts with made up field names, after all.


More Qmage versions, parsers, and symbols – QMG boot animations



I will try to keep the section as brief as possible, even though it could easily fill a whole blog post on its own. The history of the Qmage format in Samsung devices is in fact much longer than the time span between 2014 and 2023 – before being incorporated in Skia, it had been used as the container format for boot and shutdown animations since early versions of Samsung Android, and theme resources in the pre-Android era. If you browse some mobile phone-related forums such as XDA Developers or oldph.one, you will find a number of references to "Qmage" and "qmg" in those contexts. You can also check for yourself, by looking for .qmg files on the file system of a modern Samsung phone. On my Note 10+, I can find the following three:





  • /system/media/bootsamsungloop.qmg



  • /system/media/bootsamsung.qmg



  • /system/media/shutdown.qmg






Since these files represent animations and not static bitmaps, they are encoded differently than the Qmage samples we have seen so far. Let's have a look at their headers:







d2s:/ $ for file in /system/media/*.qmg; do xxd -g 1 -l 16 $file; done


00000000: 51 4d 0f 00 80 00 a0 05 e0 0b 00 20 7b 50 00 00  QM......... {P..


00000000: 51 4d 0f 00 80 00 a0 05 e0 0b 00 20 7b 50 00 00  QM......... {P..


00000000: 51 4d 0f 00 80 00 a0 05 e0 0b 00 20 83 50 00 00  QM......... .P..


d2s:/ $






They are stored in the old, familiar "QM" format, but with the version byte set to 0x0F instead of 0x01. Based on my research, I have concluded that QM animations have been assigned versions starting with 0x0B (11) up to 0x0F (15), which is currently the most recent one. The exact logic behind this versioning system is Tulisan saya; one unconfirmed hypothesis is that QM animation versions are expressed as X+10 where X is the corresponding static format version. Importantly, the animated images don't seem to be compatible with the static ones, so they cannot be easily used as input test cases for Skia.




The animations are displayed by the /system/bin/bootanimation system executable, which in turn uses a dedicated libQmageDecoder.so library (currently at around ~600 kB) for parsing the files. On a high level, the libQmageDecoder interfaces are similar to Skia's, but the inner workings start to differ deeper down the call stack. A general overview of the header parsing control flow is shown below:




QmageDecParseHeader


└── QmageDecCommon_ParseHeader


    ├── QmageDecCommon_QmageAudioVersionCheck


    ├── QmageDecCommon_QGetDecoderInfo


    ├── QmageDecCommon_VGetDecoderInfo


    └── QmageDecCommon_WGetDecoderInfo




Within these functions, the following file signatures are being checked for: AUQM, NQ, QM, PFR, IM, IFEG, IT, QW. We have already seen most of them in Skia, but the first two on the list are completely new. This goes to show the depth of Quramsoft's portfolio in terms of the variety of invented file formats.




Furthermore, here is a simplified outline of the frame decoding process and the involved functions in the current libQmageDecoder.so on Android 10:




QmageDecodeAniFrame


├── checkDecodeQueue


│   └── QphotoThreadManager::checkDecodeQueue


│       └── QphotoThreadPool::run


│           └── QmageJob::run


│               └── Qmage_WDecodeAniFrameThreadJob_Low


│                   └── Qmage_WDecodeFrame_Low


│                       └── _QM_WCodec_decode


│                           └── PVcodecDecoderIndex


│                           └── PVcodecDecoderGrayScale


│                           └── PVcodecDecoder


└── Qmage_VDecodeAniFrame_Low


    ├── Qmage_VDecodeFrame_Low


    │   └── _QM_DecodeOneFrame_A9LL_TINY


    │   └── _QM_DecodeOneFrame_A9LL


    │   └── _QM_DecodeOneFrame_A9LL_alpha


    ├── _QM_DecodeOneFrame_A9LL_ani_LineSkip


    ├── _QM_DecodeOneFrame_A9LL_ani


    └── _QM_DecodeOneFrame_A9LL_ani_alpha




There are a number of previously Tulisan saya routines here under the Qmage_VDecode path, but there is also a very familiar subtree starting at Qmage_WDecodeFrame_Low, which we've already seen in Skia. But why is it even important, considering that boot animations are not really an attack surface? That's because the libQmageDecoder.so module in Samsung phones shipped with debug symbols for a long time – starting in mid-2010 with Android 2.1 (Eclair), up to various Samsung Android 4.3 firmwares published in 2013. During that time frame, the ELF file included not just function names, but also source file names and line numbers, full structure layouts, enum names, local variable names etc. It is a goldmine of useful information about the evolution of the codec during these years, and includes many details of the inner workings that still apply to this day.




For example, if we take libQmageDecoder.so from a Galaxy S Duos (Android 4.0.4, Jan 2013 build), we can use readelf and objdump to determine that:





  • The library compilation directory was /home2/cheus/Froyo/Froyo22_Qmage



  • It consisted of the following source files:




    • external/Qmage/QmageDecoderLIB/src/QmageDecCommon.c, 1547 lines of code



    • external/Qmage/QmageDecoderLIB/src/QmageDecoder.c, 219 LOC



    • external/Qmage/QmageDecoderLIB/src/Qmage_FDecoder_Low.c, 74 LOC



    • external/Qmage/QmageDecoderLIB/src/Qmage_VDecoder_Low.c, 3954 LOC



    • external/Qmage/QmageDecoderLIB/src/Qmage_WDecoder_Low.c, 5458 LOC



    • external/Qmage/QmageInterface/QmageInterface.c, 113 LOC




  • and the following headers:




    • external/Qmage/QmageDecoderLIB/src/QmageDecType.h



    • external/Qmage/QmageDecoderLIB/src/QmageDecCommon.h







We get access to some very helpful structures:







(gdb) ptype Qmage_DecderLowInfo


type = struct {


    QM_BOOL Ver200_SPEED;


    QM_BOOL IS_ANIMATION;


    QM_BOOL UseExtraException;


    QM_BOOL tiny;


    QM_BOOL IsDyanmicTable;


    QM_BOOL IsOpaque;


    QM_BOOL NearLossless;


    QMINT32 SIZE_SHIFT;


    QMINT32 ANI_RANGE;


    QMINT32 qp;


    QMINT32 mode_bit;


    QMINT32 header_len;


    QM_BOOL NotComp;


    QM_BOOL NotAlphaComp;


    QMINT32 alpha_decode_flag;


    QMINT32 depth;


    QMINT32 alpha_depth;


    Qmage_VDecoderVMODE_T mode;


    Qmage_V_DecoderVersion vversion;


    Qmage_F_DecoderVersion fversion;


    Qmage_DecoderVersion qversion;


    QmageDecodeCodecType rgb_encoder_mode;


    QmageDecodeCodecType alpha_encoder_mode;


    QmageRawImageType out_type;


}


(gdb)






and equally interesting enums:







(gdb) ptype QmageDecodeCodecType


type = enum {QMAGE_DEC_V16_SHORT_INDEX, QMAGE_DEC_W2_PASS, QMAGE_DEC_V16_BYTE_INDEX, QMAGE_DEC_W1_PASS, QMAGE_DEC_FCODEC, QMAGE_DEC_W1_PASS_FROM_W_ADAPTIVE, QMAGE_DEC_V24_SHORT_INDEX, QMAGE_DEC_W2_PASS_ONLY, QMAGE_DEC_PV, QMAGE_DEC_SLV, QMAGE_DEC_QV}


(gdb)






not to mention some very clean decompiler output:


Hex-Rays decompilation output



It was only after finding this extended debug metadata that I started to understand how parts of the codec actually worked. I would highly recommend referring to these symbols if you are planning to perform any Qmage-related research; many of them may be even cleanly ported to a modern Skia disassembly database.




During a similar time frame around 2010, Samsung was also still producing non-Android mobile phones, which also have traces of Qmage in their underlying custom OS. Two examples of such devices are Samsung GT-B5722 (released in 2009) and Samsung GT-C5010 Squash (released in 2010):


Official Samsung product photos



Their firmware was again compiled with debug data built in, with many references to Qmage too. Let's take a look at the B5722 firmware – it contains a B5722_Master.x file which is a fairly regular ARM ELF executable, so we can load it in gdb-multiarch or IDA Pro and browse around or dump some types. As an example, we can find our favorite Qmage_WDecodeFrame_Low function, and explore its ascendants and descendants in the function control flow:




lkres_IFEGBodyDecode




├── QmageDecodeAniFrame


│   └── Qmage_VDecodeAniFrame_Low


│       ├── Qmage_VDecodeFrame_Low


│       │   ├── _QM_DecodeOneFrame_A9LL_TINY


│       │   ├── _QM_DecodeOneFrame_A9LL


│       │   └── _QM_DecodeOneFrame_A9LL_alpha


│       ├── _QM_DecodeOneFrame_A9LL_ani


│       └── _QM_DecodeOneFrame_A9LL_ani_alpha


├── QmageDecodeFrame


│   ├── Qmage_VDecodeFrame_Low


│   ├── Qmage_FDecodeFrame_Low


│   └── Qmage_WDecodeFrame_Low


│       └── _QM_WCodec_decode


│           ├── _QM1st_decode


│           └── _QM_WCodec_2nd_decode


└── IFEGDecodeFrame


    ├── IFEGDecodeFrame_DCT


    ├── IFEGDecodeFrame_Pad


    └── IFEGDecodeFrame_NoPad




In the above tree, we can recognize the "V" codec and the subtree responsible for processing animations, and the "W" codec handling static bitmaps, but there is also a whole new branch of code related to the decoding of IFEG. As you may remember from earlier in this post, this was one of the left-over magic values looked for by QmageDecCommon_VersionCheck_Rev8253_140615 in modern Skia – now we can see the format was actually used 10+ years ago. Additionally, all three of these code paths have a common ancestor in the lkres_IFEGBodyDecode function, which shows even more clearly that the IFEG and Qmage formats are closely related, with the former likely being some form of a predecessor of the latter. We can also verify that GT-B5722's embedded resources were encoded in both formats, by inspecting the B5722_Master.cfg file which enumerates the contents of the B5722_Master.tfs binary blob:







FILE_NAME : /a/images/13_pictbridge_pictbridge_top.ifg


FILE_SIZE : 6908


FILE_NAME : /a/images/13_pictbridge_pictbridge_top_hui.ifg


FILE_SIZE : 4766


FILE_NAME : /a/images/13_pictbridge_progress_bg.ifg


FILE_SIZE : 1304


FILE_NAME : /a/images/13_pictbridge_progress_bg_hui.ifg


FILE_SIZE : 594


FILE_NAME : /a/images/13_pictbridge_sending_ani01.ifg


[...]


FILE_NAME : /a/multimedia/imgapp/imgapp_default_image.qmg


FILE_SIZE : 39128


FILE_NAME : /a/multimedia/imgapp/imgapp_touch_toolbox_detail.qmg


FILE_SIZE : 800


FILE_NAME : /a/multimedia/imgapp/imgapp_touch_toolbox_detail_focus.qmg


FILE_SIZE : 1152


FILE_NAME : /a/multimedia/imgapp/imgapp_touch_toolbox_edit.qmg


FILE_SIZE : 1340


FILE_NAME : /a/multimedia/imgapp/imgapp_touch_toolbox_edit_focus.qmg






Based on this, we now know that Qmage files (in some shape) made their first appearance in Samsung devices around 2009-2010. But what about IFEG, and for how long has Samsung been shipping Quramsoft decoders? To answer that question, I delved into even older builds of the firmware and tried to bisect the debut of the custom codecs. The first ever phone that I have confirmed to use the IFEG format is Samsung SGH-D600, which launched in 2005. Almost all of its resources are encoded as .ifg files, and the software itself contains a number of relevant strings such as lk_ifegDecode, IFEGGetVersion, IFEGGetImageSize, IFEGDecodeFrame etc. This seems in line with Quramsoft's own declaration that their collaboration with Samsung started around June 2005. 




To make the story even more complex, the vendor was not even called "Quram" at the time; it was known under the name "I-master". Even though the switch to the current name took place in 2007, artifacts such as symbol names containing "Imaster" or "IM" could be found in Samsung's libraries for a few more years, e.g. Imaster_Malloc, ImasterVDecoder_ParseHeader, ImasterVDecErr_FAIL, IM_DecodeOneFrame_A9LL, etc. This could explain the meaning of the obsolete "IM" file signature mentioned earlier in this post, which I have seen used only once – as the container for boot animations on the very first Samsung phone running Android, Samsung GT-I7500 Galaxy released in 2009. What a ride! :)




To gather all this information in one place, I have compiled the following timeline showing the development of all Quramsoft image codecs I have observed shipping in Samsung devices over the years. While the presented data is based on my examination of dozens of firmwares, the Android ecosystem is a vastly complex one and involves thousands of software builds, so I make no promises as to the accuracy of the analysis below. If you spot any errors or inconsistencies that you can correct, please reach out and I will be happy to update this post.


Timeline of Quramsoft image codecs found on Samsung devices



As we can see, while Qmage has "only" been natively supported on Samsung Android since late 2014, the history of collaboration between Samsung and Quramsoft has lasted much longer. Reverse engineering these older binaries provided me with a lot of interesting insights, and pragmatically useful information such as the debug symbols from non-stripped executables. The extra context proved valuable later in the project and quite honestly, it also satisfied my curiosity, which is an important part. :)


Initial pokes at the codec



With all this historic background behind us, it's time to see how the codec can be broken today, or rather could be broken before the May 2023 update. Let's go back to the original accessibility_light_easy_off.qmg sample we extracted before. We can open it in a hex editor again, and to start with a simple test, consider which parts are most likely to cause problems when corrupted. For bitmaps, the obvious candidates are the dimensions, so I modified the width and height just slightly (344 → 345) and tried to open it in Gallery on a system with the February 2023 patch level:


Failed attempt to display a corrupted Qmage file in Gallery



It wouldn't load anymore. It's worth noting that the Qmage codec is packed with __android_log_print calls, so we can grep for them with logcat to get some more information on what happened:







d2s:/ $ logcat -v tag | egrep "QG|Qmage"


E/QG      : Qmage decoder error return value -298


E/Qmage   : Qmage QuramQmageDecodeFrame offset <= 0, offset: -298






These log messages tend to be helpful from time to time, so it's worth keeping them in mind. With such a small change in the file, nothing bad seems to have happened, beyond a rightful error being thrown by the codec. What if we increase the dimensions even more, let's say from 0x158 (344) each to 0x558 (1368) each?


Bit flips manually applied to the Qmage header



Let's try it:


Gallery app crash notification



The Gallery app instantly crashes. The full context can be extracted with logcat:







130|d2s:/ $ logcat -b crash -v raw


Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x700b9ffc90 in tid 12442 (thumbThread2), pid 12395 (droid.gallery3d)


*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***


Build fingerprint: 'samsung/d2sxx/d2s:10/QP1A.190711.020/N975FXXS2BTA7:user/release-keys'


Revision: '24'


ABI: 'arm64'


pid: 12395, tid: 12442, name: thumbThread2  >>> com.sec.android.gallery3d <<<


uid: 10125


signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x700b9ffc90


    x0  0000000000000000  x1  0000000000000558  x2  fffffffffffffaa8  x3  0000000000000011


    x4  000000700ba00700  x5  0000000000000003  x6  0000000000000000  x7  0000000000000016


    x8  0000000000000000  x9  00000000000003d2  x10 0000000000000004  x11 000000701bc85a16


    x12 0000000000000000  x13 000000701bec0c0f  x14 000000700fc4e00d  x15 000000000000000d


    x16 0000000000000018  x17 0000000000000000  x18 000000703a846000  x19 0000000000000305


    x20 000000700b9ffc90  x21 000000700fb32a80  x22 000000700fb8f200  x23 0000000000088926


    x24 000000005650868a  x25 000000701bec0c00  x26 00000000000003d2  x27 0000000000000007


    x28 0000000000000020  x29 000000703ac3e5f0


    sp  000000703ac3e390  lr  000000700ba00740  pc  0000007136b20d18




backtrace:


      #00 pc 00000000002bbd18  /system/lib64/libhwui.so (PVcodecDecoder_GrayScale_16bits_NEW+3636) (BuildId: fcab350692b134df9e8756643e9b06a0)


      #01 pc 000000000029cefc  /system/lib64/libhwui.so (__QM_WCodec_decode+948) (BuildId: fcab350692b134df9e8756643e9b06a0)


      #02 pc 000000000029c9b0  /system/lib64/libhwui.so (Qmage_WDecodeFrame_Low_Rev14474_20150224+320) (BuildId: fcab350692b134df9e8756643e9b06a0)


      #03 pc 000000000029ae78  /system/lib64/libhwui.so (QuramQmageDecodeFrame_Rev14474_20150224+164) (BuildId: fcab350692b134df9e8756643e9b06a0)


      #04 pc 00000000006e1eec  /system/lib64/libhwui.so (SkQmgCodec::onGetPixels(SkImageInfo const&, void*, unsigned long, SkCodec::Options const&, int*)+1100) (BuildId: fcab350692b134df9e8756643e9b06a0)


[...]






The fact that such a trivial change to the file header was sufficient to trigger a crash did not bode well for the security of the codec. In my experience, no decently tested image parser would crash on such an obvious inconsistency. Then I remembered that the Samsung Messages app also displayed Qmage files, so I sent the malformed image via MMS to see what would happen. To my disbelief, the exact same crash reproduced again, this time in the com.samsung.android.messaging process, and with no user interaction required:







Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x702ae96b10 in tid 15454 (pool-5-thread-1), pid 7904 (droid.messaging)


[...]


pid: 7904, tid: 15454, name: pool-5-thread-1  >>> com.samsung.android.messaging <<<






This result had to mean that the Messages app automatically decoded images found in incoming messages, even before the user manually opened them. It was a big moment in my research, one which opened the seemingly fragile attack surface to remote exploitation by only knowing the victim's phone number. At that point, I was convinced that the best course of action was to run a thorough coverage-guided fuzzing session of the codec, and report all identified crashes to the vendor. This became my focus for the next couple of weeks, until documenting my findings and filing Issue #2002 in the PZ bug tracker at the end of January 2023. The fuzzing effort will be the subject of the next blog post in the Qmage series. Stay tuned!


Conclusion



It is remarkable that such an attractive vulnerability research area managed to stay out of the public eye for so long. I expect that it was caused primarily by the closed-source nature of the code, and the fact that the implementation was buried so deep down in the image decoding stack, that it was just not expected to find custom OEM code of that extent there. I know I likely wouldn't have found it, if not for my lack of familiarity with Skia on Android, and the desire to learn where the execution of the BitmapFactory interface eventually ended up at (coupled with having a Samsung build of libhwui.so at hand). The fact that there are virtually no references or mentions of this technology online certainly didn't help.




In this write-up, I shared the results of the reconnaissance phase I went through shortly after discovering the codec. As is often the case in my line of work, this process involved spelunking in some pretty archaic areas of code for extended periods of time, to become somewhat of an expert in an obscure field that will never again prove useful outside of this project. :-) Still, I am hoping that it was an interesting read for those who, like me, enjoy some software archeology, and that it also makes a good reference guide to anyone who plans to continue working on Qmage security in the future.  




It is crucial that the security claims made by vendors are constantly challenged, and relevant attack surfaces are exposed and documented. When mistakes happen, it's at the expense of end user security and privacy and rarely the vendor themselves. This is why it's increasingly important for both vendors to follow best practices for security and software testing, and the vigilant security community to ensure that the same mistakes aren't made again. 

Post a Comment

Lebih baru Lebih lama