by RSS Kai Lu  |  Apr 13, 2016  |  Filed in: Security Research

Google fixed a denial of service vulnerability in Minikin library (CVE-2016-2414) with the Android patches of this month. I reported this vulnerability to Google in early March, 2016 and Google confirmed it was a duplicated report of bug 26413177 which had been reported by another researcher in November, 2015.

In this blog, we will provide an in-depth analysis of this vulnerability. It exists because the Minikin library fails to parse .TTF font files correctly. As a result, it could allow a local attacker to temporarily block access to an affected Android device. The attacker could have an untrusted font file loaded, causing an overflow in the Minikin component, which would lead to a crash.

This vulnerability is rated as High severity by Google because the crash would lead to continuous Android reboots.

What Products Are Affected?

Android 5.0.2, 5.1.1, 6.0, 6.0.1

Proof of Concept

The following code snippet can be used to trigger this issue. The function setTypeface is used to load and set custom font in TextView from external.

public class MyActivity extends Activity
    {
        public void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            TextView textView2 = (TextView) findViewById(R.id.textView2);
           Typeface typeface2 = Typeface.createFromFile(Environment.getExternalStorageDirectory() + "/fuzzed.ttf");
            textView2.setTypeface(typeface2);

        }
    }

Following is the crash log. 

--------- beginning of crash
04-04 16:44:43.230  5021  5021 F libc    : Fatal signal 11 (SIGSEGV), code 1, fault addr 0x36587 in tid 5021 (example.TTFFont)
04-04 16:44:43.248 32383 28153 I Icing   : Indexing done 215BF78BC46028A346E51FB3E9502079034D8D40
04-04 16:44:43.249 32383 28153 I Icing   : Indexing 4400EDF81586FA899DD2C6CB98D8E1B8279596F4 from com.google.android.gms
04-04 16:44:43.250 32383 28153 I Icing   : Indexing done 4400EDF81586FA899DD2C6CB98D8E1B8279596F4
04-04 16:44:43.332 10881 10881 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 
04-04 16:44:43.332 10881 10881 F DEBUG   : Build fingerprint: 'google/hammerhead/hammerhead:6.0.1/MMB29V/2554798:user/release-keys'
04-04 16:44:43.332 10881 10881 F DEBUG   : Revision: '0' 04-04 16:44:43.332 10881 10881 F DEBUG   : ABI: 'arm'
04-04 16:44:43.332 10881 10881 F DEBUG   : pid: 5021, tid: 5021, name: example.TTFFont  >>> com.example.TTFFont <<<
04-04 16:44:43.332 10881 10881 F DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x36587
04-04 16:44:43.344 10881 10881 F DEBUG   :     r0 0000d962  r1 0000d938  r2 0000001e  r3 00000006
04-04 16:44:43.344 10881 10881 F DEBUG   :     r4 acb4ef7c  r5 0000001e  r6 ffffffff  r7 00000043
04-04 16:44:43.344 10881 10881 F DEBUG   :     r8 ab173400  r9 ffffffff  sl 0000002b  fp 07fffffe
04-04 16:44:43.345 10881 10881 F DEBUG   :     ip 07fffffd  sp bef6ce20  lr 00001e0c  pc b5db01e0  cpsr 00030030
04-04 16:44:43.347 10881 10881 F DEBUG   : 
04-04 16:44:43.347 10881 10881 F DEBUG   : backtrace:
04-04 16:44:43.347 10881 10881 F DEBUG   :     #00 pc 0000b1e0  /system/lib/libminikin.so (android::SparseBitSet::initFromRanges(unsigned int const*, unsigned int)+311)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #01 pc 0000652b  /system/lib/libminikin.so (android::CmapCoverage::getCoverage(android::SparseBitSet&, unsigned char const*, unsigned int)+498)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #02 pc 00006ee1  /system/lib/libminikin.so (android::FontFamily::getCoverage()+116)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #03 pc 0000685b  /system/lib/libminikin.so (android::FontCollection::FontCollection(std::__1::vector<android::FontFamily*, std::__1::allocator<android::FontFamily*> > const&)+150)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #04 pc 0009784b  /system/lib/libandroid_runtime.so (android::TypefaceImpl_createFromFamilies(long long const*, unsigned int)+94)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #05 pc 0009752b  /system/lib/libandroid_runtime.so
04-04 16:44:43.347 10881 10881 F DEBUG   :     #06 pc 72e8db21  /data/dalvik-cache/arm/system@framework@boot.oat (offset 0x1ec9000)
04-04 16:44:43.649 10881 10881 F DEBUG   : 
04-04 16:44:43.649 10881 10881 F DEBUG   : Tombstone written to: /data/tombstones/tombstone_09
04-04 16:44:43.649 10881 10881 E DEBUG   : AM write failed: Broken pipe 

Analysis

The vulnerability exists in the Minikin library because it fails to parse .TTF font files correctly. It causes an out-of-bound write that can lead to Denial of Service.

Let’s look into the specially crafted .TTF font file first. The minimized version of the PoC has four differences at offsets 0x60, 0x58D, 0x5D6, 0xAD60 respectively. The comparison between the normal TTF file and the minimized PoC file is shown below.

Figure 1. The Normal TTF File vs The Minimized PoC File (at Offset 0x60)

Figure 2. The Normal TTF File vs The Minimized PoC File (at Offsets 0x58D and 0x5D6)

Figure 3. The Normal TTF File vs The Minimized PoC File (at Offset 0xAD60)

In Figures 1 and 3, above, we used the 010 Editor with TTFTemplate to parse .TTF font file.  The four bytes at offset 0x60 is the ‘checksum’ field in the ‘cmap’ table; the four bytes at offset 0xAD60 is the ‘checkSumAdjustment’ field in the ‘thead’ head structure. These two checksum fields are recalculated and fixed during fuzzing. They don’t trigger the vulnerability, so ignore them. The following is the parsing of the minimized PoC file usingTTFTemplate.

Figure 4.  The Parsing of The Minimized PoC File (at Offsets 0x58D and 0x5D6)

From Figure 4, above, we can see that these modified bytes at offsets 0x58D and 0x5D6 are located in the ‘cmap’ table. The specification regarding the ‘cmap’ table can be found at https://www.microsoft.com/typography/otspec/cmap.htm. The specification regarding ‘Format 4’ is shown below.

The following is the comparison between ‘startCount[]’ and ‘endCount[]’.

Figure 6. The Comparison Between ‘startCount[]’ and ‘endCount[]’

Normally, the value of each element in the array startCount[] and endCount[] should grow in turn.  Meanwhile, for a given index i, endCount[i] should be equal or greater than startCount[i]. But we modified startCount[30] to 0x1E78 in the minimized PoC file, which causes startCount[30] > endCount[30]. And we modified startCount[67] to 0xE0FF, which causes startCount[67] < startCount[66]. startCount[67] is the last element and it should be 0xFFFF according to the specification.

From stack back trace, we can see the SIGSEGV occurs in function android::SparseBitSet::initFromRanges and gets the code location of crash. Following is the function SparseBitSet::initFromRanges.

void SparseBitSet::initFromRanges(const uint32_t* ranges, size_t nRanges) {
   ...
   ...
   ...
        size_t index = ((currentPage - 1) << (kLogValuesPerPage - kLogBitsPerEl)) +
            ((start & kPageMask) >> kLogBitsPerEl);
        size_t nElements = (end - (start & ~kElMask) + kElMask) >> kLogBitsPerEl;
        if (nElements == 1) {
           mBitmaps[index] |= (kElAllOnes >> (start & kElMask)) &
                (kElAllOnes << ((-end) & kElMask));
        } else {
            mBitmaps[index] |= kElAllOnes >> (start & kElMask);
            for (size_t j = 1; j < nElements - 1; j++) {
                mBitmaps[index + j] = kElAllOnes;                      
 //crash here, it’s an out-of-bound write.
            }
           mBitmaps[index + nElements - 1] |= kElAllOnes << ((-end) & kElMask);    
       }
    ...
    ...
    ...
}

Next, let’s analyze it dynamically using IDA Pro. We set breakpoint at the entry of function SparseBitSet::initFromRanges.  Next, we run the debugger and see the following code.

Figure 7. Function SparseBitSet::initFromRanges

From above Figure 7, R1 is the 1st parameter (const uint32_t* ranges), R2 is the 2nd parameter (size_t nRanges) and be equal to 0x43. Following is the memory pointed by R1.

Figure 8. The Memory Layout of const uint32_t* ranges

We can see the four bytes |78 1E 00 00| at offset 0xAB1734F0 store the value of startCount[30] (|1E 78|) and the subsequent four bytes |0C 1E 00 00| store the value of endCount[30]+1 (|1E 0B|+1). Then we continue tracing when the program handles ranges[30] (|78 1E 00 00 0C 1E 00 00|).

Figure 9.    The Analysis of loc_B5DB0128(1)

From above Figure 9, we can see that when R2 is equal to 0x1E, the register R10 points to ranges[30] (0xAB1734F0). Let’s continue the analysis.

Figure 10. The Analysis of loc_B5DB0128(2)

Then jump to loc_B5DB0176.

Figure 11. The Analysis of loc_B5DB0176

Next, enter the loop starting at loc_B5DB01D2. The register R12 is the condition of the loop and has too large a value. [R4+8] stores the variable ‘mBitmaps’ and R9 points to the variable ‘mBitmaps’ (from source code). It is shown below.

\

Figure 12. The Analysis of loc_B5DB01D2(1)

Then at address B5DB01E0 it executes a STR instruction and writes 0xFFFFFFFF to the memory [R9+R0<<2] in each loop, as shown below.

Figure 13. The Analysis of loc_B5DB01D2(2)

Next when we run the debugger (press F9 key), a popup dialog shows below.

A segmentation violation occurs at address 0xB5DB01E0. Let’s check the register and memory info.

Figure 14.  The Register And Memory Info When Segmentation Violation Occurs

The memory pointed to by R4 (0xACB4F0FC) was written with 0xFFFFFFFF in the loop loc_B5DB01D2. And [R4+8] was also overwritten with 0xFFFFFFFF, so R9 became 0xFFFFFFFF. R9+R0<<2 is an invalid memory address, so it causes a segmentation violation when executing STR instruction.

The following is the analysis of its source code. 

void SparseBitSet::initFromRanges(const uint32_t* ranges, size_t nRanges) {
   if (nRanges == 0) {
        mMaxVal = 0;
        mIndices.reset();
        mBitmaps.reset();
        return;
    }
    mMaxVal = ranges[nRanges * 2 - 1];
    size_t indexSize = (mMaxVal + kPageMask) >> kLogValuesPerPage;
    mIndices.reset(new uint32_t[indexSize]);
    uint32_t nPages = calcNumPages(ranges, nRanges);
    mBitmaps.reset(new element[nPages << (kLogValuesPerPage - kLogBitsPerEl)]);
    memset(mBitmaps.get(), 0, nPages << (kLogValuesPerPage - 3));
    mZeroPageIndex = noZeroPage;
    uint32_t nonzeroPageEnd = 0;
    uint32_t currentPage = 0;
    for (size_t i = 0; i < nRanges; i++) {      //when the variable i is eqaul to 0x1E, the program handles the modified data in 'startCount' array in PoC file.
        uint32_t start = ranges[i * 2];         //the variable start is eqaul to 0x1E78
        uint32_t end = ranges[i * 2 + 1];       //the variable end is eqaul to 0x1E0C
        uint32_t startPage = start >> kLogValuesPerPage;
        uint32_t endPage = (end - 1) >> kLogValuesPerPage;
       if (startPage >= nonzeroPageEnd) {
            if (startPage > nonzeroPageEnd) {
               if (mZeroPageIndex == noZeroPage) {
                    mZeroPageIndex = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl);
                }
                for (uint32_t j = nonzeroPageEnd; j < startPage; j++) {
                    mIndices[j] = mZeroPageIndex;
                }
            }
            mIndices[startPage] = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl);
        }
 
        size_t index = ((currentPage - 1) << (kLogValuesPerPage - kLogBitsPerEl)) +
            ((start & kPageMask) >> kLogBitsPerEl);                          // the variable index is equal to 0x2B from dynamic debugging.
        size_t nElements = (end - (start & ~kElMask) + kElMask) >> kLogBitsPerEl;  // nElements = (0x1E0C - (0x1E78 & ~0x1F) + 0x1F) >> 5 = 0x07FFFFFE, because start is greater than end, it causes a overflow when doing subtraction.
        if (nElements == 1) {
           mBitmaps[index] |= (kElAllOnes >> (start & kElMask)) &
                (kElAllOnes << ((-end) & kElMask));
        } else {
            mBitmaps[index] |= kElAllOnes >> (start & kElMask);
            for (size_t j = 1; j < nElements - 1; j++) {        // the loop condition is j < 0x07FFFFFD, it's a too large value.
                mBitmaps[index + j] = kElAllOnes;               //crash here, it's out-of-bound write.
            }
           mBitmaps[index + nElements - 1] |= kElAllOnes << ((-end) & kElMask);
       }
        for (size_t j = startPage + 1; j < endPage + 1; j++) {
            mIndices[j] = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl);
        }
        nonzeroPageEnd = endPage + 1;
    }
}

 

Conclusion

In short, this vulnerability exists because Minikin fails to check invalid ranges in the ‘cmap’ table. A corrupted or malicious font file may contain a negative range size, which in turn could lead to memory corruption. An attacker could load a corrupted font file and cause an overflow in the Minikin component. The crash would lead to continuous Android reboots.

 

by RSS Kai Lu  |  Apr 13, 2016  |  Filed in: Security Research

comments powered by Disqus