Internet Security Systems - AlertCon(TM)

iPhone Jailbreak Type2 Charstring Vulnerability from August

Posted by Takehiro Takahashi
on September 15, 2010 at 6:11 PM EDT.

Since comex (et al.) released his iPhone Jailbreak (Figure 1) work last month, we have been getting inquiries from concerned customers if the same vulnerability could be used by malicious users to launch attacks against unpatched or Jailbroken (and not patched) iPhones. The bad news is that the answer is yes. The good news is that we are working on it, and we are providing coverage soon. Since the analysis work involved in the pdf vulnerability was non-trivial, I figured it would be interesting to share what I've learned with the world. So read on for the detail! If you have any thoughts/comments/feedback, contact me at takahashi@us.ibm.com.



Figure 1. JailbreakMe



Background

On Aug 1st 2010, a developer Comex released a jailbreak software which would jailbreak any iPhones, iPod touches and iPads running the latest iOS3.1/2 and iOS4. His work first exploits a vulnerability in libCGFreetype.A.dylib, a module responsible for processing various fonts such as TrueType, Type1/2 font, OpenType, etc. The jailbreak payload then gains the system privilege by exploiting the IOSurface library. Since iOS's web browser MobileSafari uses libCGFreetype.A.dylib to render embedded fonts, the actual jailbreaking process was as simple as visiting 'www.jailbreakme.com' and sliding the 'Slide to jailbreak' button. I found the following article by Mr. Ching-Lan Huang helpful to learn the post-FreeType stackoverflow exploit process (http://digdog.tumblr.com/post/894317027/jailbreak-with-pdf-flatedecode-filter).

Also, Comex has released the source code for his jailbreak work at github (http://github.com/comex/star).

The purpose of this post is to provide details on the FreeType vulnerability, and explain how Comex's payload achieved the code execution for his jailbreak work.

Detail

The vulnerability is in _cff_decoder_parse_charstrings of libCGFreeType.A.dylib. Compact Font Format is a type of font formats used to represent a font set. Type2 charstrings are a sequence of encoded glyph procedures which compose each font, and are always used in a Compact Font Format or OpenType font file. _cff_decoder_parse_charstrings is responsible for parsing charstrings inside a Compact Font Format file. A specification for Compact Font Format and Type2 charstrings can be found in the following URLs. (Type2 Charstrings, Compact Font Format)

The Type2 charstring format supports a fixed size stack and various glyph operators. Each font is created by executing a sequence of glyph operations on this virtual stack. The vulnerability in _cff_decoder_parse_charstrings allowed a specially crafted Type charstring to overflow this virtual stack, and corrupt memory.

libCGFreeType::LoadGlyph first creates an object used for charstring operations including the virtual stack. It then calls _cff_get_glyph_data to retrieve an input charstring. Finally, LoadGlyph calls _cff_decoder_parse_charstrings with 3 parameters: (1) the charstring operation object, (2) the input charstring, and (3) length of the input charstring.



Figure 2. cff_decoder_parse_charstrings


_cff_decoder_parse_charstrings (Figure 2) is basically a huge loop with 3 major jump tables. Each iteration of the loop consumes an input charstring operation, and this input is forwarded to the jump tables for specific glyph operations. There are several operations that copy the input charstring data to the virtual stack. For example, '0xFF' is an operation which copies the next 4bytes from the input string to the virtual stack. At the end of each loop iteration, _cff_decoder_parse_charstrings tries to check whether the input charstring has been completely consumed, the virtual stack space has been exhausted, or the virtual stack has been underflown.  Unfortunately, a glyph operation '0x0C17' did not check the amount of space left in the virtual stack before incrementing the stack pointer, allowing a specially crafted charstring to overflow the stack.



Figure 3. Font payload


Now, let's take a look at the actual charstring payload crafted by Comex (Figure 3). The Type2 charstring starts at offset 0x71. The first operator is '0xFF' which consumes a DWORD "0x34596931" from the input string, and stores the result in the virtual stack. The next operation is '0xFF', and so on. Here's a summary of operators used in the sample.
  • 0xFF
    • stores the parameter in the virtual stack, increment the stack pointer, and the input charstring pointer
    • *virtual_sp = 32bit value from the input charstring
    • input_charstring += 5
    • virtual_sp += 4
  • 0x0C17
    • increment the stack pointer
    • *(virtual_sp) = some value
    • virtual_sp += 4
  • 0x0C04
    • decrement the stack pointer
    • writes 0 or 0x10000 to *(virtual_sp - 8)
    • *(virtual_sp - 8) = (*(virtual_sp - 8) == 0) && (*(virtual_sp - 4) == 0) ? 0 : 0x10000
    • virtual_sp -= 4
  • 0x0C1D
    • copies a 32bit value from the virtual stack to where the stack pointer points to
    • the stack pointer remains the same
    • offset = ((*virtual_sp >> 0x10) > 0) ? ((~(num_available_args - 2)<<2) : -4
    • *virtual_sp = *(virtual_sp + offset)
  • 0x0C12
    • decrements the stack pointer
    • virtual_sp -= 4
There are 4 sections in this payload:
  1. a series of 0xFF operations used to copy user data to the virtual stack space
  2. a series of (0x0C17, 0x0C17, 0x0C04, 0x0C1D) operations used to increment the stack pointer value
  3. a series of (0x0C17, 0x0C1D) operations used to increment the stack pointer value
  4. a series of (0x0C1D, 0x0C12) operations used to copy the user data to overwrite the return value of LoadGlyph call's stackframe

The section 1 uses 45 0xFF operations to copy data to the virtual stack. The section 2 and 3 are used to increment the stack pointer and align the user data at certain offsets within the virtual stack which has been overflown. Finally the section 4 is responsible for copying the aligned user data from the virtual stack to LoadGlyph's stackframe including its return address. When all these operations are completed successfully, _cff_decoder_parse_charstrings returns gracefully. The execution flow moves to the rest of jailbreak payload once LoadGlyph returns.

Apple addressed this vulnerability by checking virtual stack's current size in the 0x0C17 operation. I've looked around to overflow/underflow the virtual stack, but it's looking pretty solid to me so far (Darn it!).

I'm attaching my pseudo code for _cff_decoder_parse_charstrings at the end of this blog post for those who are interested.

Conclusion

It's obvious that attackers can exploit this vulnerability using custom payloads to attack many unpatched iPhone users. IBM X-Force recommends that you always keep your mobile phone's software up-to-date. In the meantime, we're working on getting a signature out to protect our mobile customers. Clearly, providing a useful protection means we must keep track of the size of Type2 Charstring virtual stack, and a simple regex would not cut it in this case.

Appendix

uchar cff_arg_cnts[] = {
0x00, 0xC2, 0xC1, 0xC1, 0x20, 0x20, 0x20, 0x20,\
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0D, 0x07,\
0x09, 0x0B, 0x80, 0x82, 0x82, 0x82, 0x82, 0x80,\
0x80, 0x00, 0x01, 0x02, 0x02, 0x02, 0x01, 0x00,\
0x02, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x01,\
0x02, 0x01, 0x04, 0x03, 0x02, 0x02, 0x01, 0x02,\
0x04, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,\
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // 64entries



int _cff_get_glyph_data(char **pInput_char_string, int *pInput_char_string_len)
{
    /*
     This function retrieves a data entry from the CharStrings INDEX object.
    It also increments a pointer so the subsequent _cff_get_glyph_data would
    retrieve the next entry
    */   
}


int _cff_decoder_parse_charstrings(char *input_char_string, int input_char_string_len)
{

char *curr_input_ptr = input_char_string;
char *input_end_ptr = input_char_string + input_char_string_len;
char *arg_stack_base = pseudo 32bit address
char *arg_stack_ptr = arg_stack_base;

while(1)
{
    uchar tbl1_idx = *curr_input_ptr;
    curr_input_ptr++;
    uint tmp1, shift_amt;
    uchar tbl2_idx;

    // jmp table 1
    if (tbl1_idx == 0x1C)
    {
        if (curr_input_ptr + 1 > input_end_ptr)
            return 3;
        tmp1 = ntohs(*((ushort *) curr_input_ptr));
        curr_input_ptr += 2;
        shift_amt = 0x10;
    }
    else if (tbl1_idx >=0x1F and tbl1_idx <= 0xF6)
    {
        tmp1 = tbl1_idx - 0x8B;
        shift_amt = 0x10;
    }
    else if (tbl1_idx > 0xF6 and tbl1_idx <= 0xFA)
    {
        if (curr_input_ptr > input_end_ptr)
            return 3;
        tmp1 = (tbl1_idx << 8) + (byte) *curr_input_ptr - 0xF694;
        shift_amt = 0x10;
        curr_input_ptr++;       
    }
    else if (tbl1_idx >= 0xFB and tbl1_idx < 0xFF)
    {
        if (curr_input_ptr > input_end_ptr)
            return 3;
        tmp1 = (tbl1_idx - 0xFB) << 8;
        tmp1 = tmp1 - (byte) *curr_input_ptr - 0x6C;
        curr_input_ptr++;
        shift_amt = 0x10;   
    }
    else if (tbl_idx == 0xFF)
    {
        if (curr_input_ptr + 3 > input_end_ptr)
            return 3;       
        uint tmp = ntohl(*((uint *) curr_input_ptr);
        curr_input_ptr += 4;
        uint tmp2 = 0x0;
    }
    else
    {
        //tbl_idx -> [0x00 - 0x1B, 0x1D - 0x1F]
        goto jmp_table1;
    }

    // checking the virtual stack size
    if ((int) (arg_stack_ptr - arg_stack_base) >= 0xBF)
        return 0x82;
   
    tmp1 = tmp1 << shift_amt;
    memcpy(arg_stack_ptr, &tmp1, 4);   
    arg_stack_ptr += 4;
   
    if (input_end_ptr > curr_input_ptr)
        continue;
   
    return 0;



jmp_table1:

    // 0x00 <= tbl1_idx <= 0x1F except for 0x1C
    switch(tbl1_idx)
    {
        case 0x01:
            tbl2_idx = 0x13;
            break;
        case 0x03:
            tbl2_idx = 0x14;
            break;
        case 0x04:
            tbl2_idx = 0x03;
            break;
        case 0x05:
            tbl2_idx = 0x04;
            break;
        case 0x06:
            tbl2_idx = 0x05;
            break;
        case 0x07:
            tbl2_idx = 0x06;
            break;
        case 0x08:
            tbl2_idx = 0x03;
            break;
        case 0x0a:
            tbl2_idx = 0x31;
            break;
        case 0x0b:
            tbl2_idx = 0x33;
            break;
        case 0x0c:
            if (curr_input_ptr > input_end_ptr)
                return 3;
            sub_tbl_idx = *curr_input_ptr;
            curr_input_ptr++;

            switch(sub_tbl_idx)
            {
                case 0x00:
                    tbl2_idx = 0x19;
                    break;
                case 0x03:
                    tbl2_idx = 0x2C;
                    break;
                case 0x04:
                    tbl2_idx = 0x2D;
                    break;
                case 0x05:
                    tbl2_idx = 0x2E;
                    break;
                case 0x08:
                    tbl2_idx = 0x2A;
                    break;
                case 0x09:
                    tbl2_idx = 0x1A;
                    break;
                case 0x0A:
                    tbl2_idx = 0x1B;
                    break;
                case 0x0B:
                    tbl2_idx = 0x1C;
                    break;
                case 0x0C:
                    tbl2_idx = 0x1D;
                    break;
                case 0x0D:
                    tbl2_idx = 0x2B;
                    break;
                case 0x0E:
                    tbl2_idx = 0x1E;
                    break;
                case 0x0F:
                    tbl2_idx = 0x2F;
                    break;
                case 0x12:
                    tbl2_idx = 0x23;
                    break;
                case 0x14:
                    tbl2_idx = 0x28;
                    break;
                case 0x15:
                    tbl2_idx = 0x29;
                    break;
                case 0x16:
                    tbl2_idx = 0x30;
                    break;
                case 0x17:
                    tbl2_idx = 0x1F;
                    break;
                case 0x18:
                    tbl2_idx = 0x20;
                    break;
                case 0x1A:
                    tbl2_idx = 0x21;
                    break;
                case 0x1B:
                    tbl2_idx = 0x27;
                    break;
                case 0x1C:
                    tbl2_idx = 0x24;
                    break;
                case 0x1D:
                    tbl2_idx = 0x25;
                    break;
                case 0x1E:
                    tbl2_idx = 0x26;
                    break;
                case 0x22:
                    tbl2_idx = 0x0F;
                    break;
                case 0x23:
                    tbl2_idx = 0x0E;
                    break;
                case 0x24:
                    tbl2_idx = 0x10;
                    break;
                case 0x25:
                    tbl2_idx = 0x11;
                    break;
                default:
                    return 3;
            }

        case 0x0e:
            tbl2_idx = 0x12;
            break;
        case 0x10:
            tbl2_idx = 0x22;
            break;
        case 0x12:
            tbl2_idx = 0x15;
            break;
        case 0x13:
            tbl2_idx = 0x17;
            break;
        case 0x14:
            tbl2_idx = 0x18;
            break;
        case 0x15:
            tbl2_idx = 0x01;
            break;
        case 0x16:
            tbl2_idx = 0x02;
            break;
        case 0x17:
            tbl2_idx = 0x16;
            break;
        case 0x18:
            tbl2_idx = 0x0A;
            break;
        case 0x19:
            tbl2_idx = 0x0B;
            break;
        case 0x1A:
            tbl2_idx = 0x0D;
            break;
        case 0x1B:
            tbl2_idx = 0x08;
            break;
        case 0x1D:
            tbl2_idx = 0x32;
            break;
        case 0x1E:
            tbl2_idx = 0x0C;
            break;
        case 0x1F:
            tbl2_idx = 0x09;
            break;
        default:
            return 3;       
    }

    int available_arg_cnt = ((int) (arg_stack_ptr - arg_stack_base)) >> 2; // # of available args on the stack
    uchar arg_cnt = cff_arg_cnts[tbl2_idx];
    unsigned char *arg_stack_op_base; // stores a stack address which is a base for this particular operation
    int available_arg_left_cnt;

    if (0x80 & arg_cnt)
    {
        // sorry this C-translation sucks
        uint tmp1, tmp2;
        int tmp_available_arg_cnt = available_arg_cnt;
        if (available_arg_cnt <= 0 || tbl2_idx > 0x18)
        {
            arg_stack_ptr = arg_stack_base;
            goto check;
        }

        tmp1 = 1 << tbl2_idx;
        if (tmp1 & 0x1F80002)
            tmp2 = 1 & available_arg_cnt;
        else if ((tmp1 & 0x40000) == 0)
        {
            if ((tmp1 & 0xFF) & 0x0C)
            {
                arg_stack_ptr = arg_stack_base;
                goto check;
            }
            tmp2 = 2 & available_arg_cnt;
        }
        else
        {
            if ((available_arg_cnt & 0x05 == 0) || (available_arg_cnt & 0x01 == 0))
            {
                tmp2 = 1;
            }
        }

        if (tmp2 == 0)
        {
            arg_stack_ptr = arg_stack_base;
            goto check;
        }

        arg_stack_ptr = arg_stack_base + 4;
        tmp_available_arg_cnt--;

check:
        if (tmp_available_arg_cnt <= 0)
            return 0x81;

    }
    else
    {
        arg_cnt &= 0x0F;
        if ((available_arg_cnt - arg_cnt) < 0)
            return 0x81;
    }
   
    arg_stack_op_base = arg_stack_ptr - (arg_cnt * 4);   
    available_arg_left_cnt = available_arg_cnt - arg_cnt;   


    // jmp table 2
    switch(tbl2_idx)
    {
        case 0x01: case 0x02: case 0x03:
            arg_stack_ptr = arg_stack_base;
            break;
        case 0x04:
            if (available_arg_left_cnt <= 1)
                return 0x81;

            if (arg_stack_ptr >= arg_stack_base)
            {
                uchar *p = arg_stack_base;
                while(1)
                {
                    p += 8;
                    if (p >= arg_stack_ptr)
                        break;
                }
            }
            arg_stack_ptr = arg_stack_base;
            break;

        case 0x05: case 0x06:
            arg_stack_ptr = arg_stack_base;
            break;

        case 0x07:
            arg_stack_ptr = arg_stack_base;
            break;

        case 0x08:
            arg_stack_ptr = arg_stack_base;
            break;

        case 0x09: case 0x0C:
            if (available_arg_left_cnt <= 3)
                return 0x81;

            if (((available_arg_left_cnt & 3) - 1) > 0)
                return 0x81;

            arg_stack_ptr = arg_stack_base;
            break;

        case 0x0a:
            if (available_arg_left_cnt <= 7)
                return 0x81;

            arg_stack_ptr = arg_stack_base;
            break;           

        case 0x0b:
            if (available_arg_left_cnt <= 7)
                return 0x81;

            if ((available_arg_left_cnt - 6) & 1)
                return 0x81;

            arg_stack_ptr = arg_stack_base;
            break;   

        case 0x0d:
            if (available_arg_left_cnt == odd_number)
                available_arg_left_cnt--;

            if ((available_arg_left_cnt & 0x000000FF) & 0x03)
                return 0x81;

            arg_stack_ptr = arg_stack_base;
            break;   

        case 0x0e: case 0x0f: case 0x10: case 0x11:
            arg_stack_ptr = arg_stack_base;
            break;   

        case 0x12:
            if (available_arg_left_cnt != 0x04)
                return 0;
           
            char *new_input_char_string;
            int new_input_char_string_len;

   
            if (_cff_get_glyph_data(&new_input_char_string, &new_input_char_string_len) == 0)
            {
                // this just means there is another CharString INDEX data entry in our context
                _cff_decoder_parse_charstrings(new_input_char_string, new_input_char_string_len);
            }
            return 0;


        case 0x13: case 0x14: case 0x15: case 0x16:

            arg_stack_ptr = arg_stack_base;
            break;   

        case 0x17: case 0x18:
            if (curr_input_ptr > input_end_ptr)
                return 3;
            arg_stack_ptr = arg_stack_base;
            break;   

        case 0x19: case 0x23:
            arg_stack_ptr = arg_stack_op_base;
            break;

        case 0x1a: case 0x1b: case 0x1C: case 0x1D: case 0x1E:
case 0x1F: case 0x20: case 0x21: case 0x25: case 0x27:
            arg_stack_op_base += 4;
            arg_stack_ptr = arg_stack_op_base;
            break;

        case 0x24:
            arg_stack_op_base += 8;
            arg_stack_ptr = arg_stack_op_base;
            break;
       
        case 0x26:
            uint offset = 0;
            int first_arg = *((int *) arg_stack_op_base) >> 0x10;
            offset = (first_arg > 0) ? first_arg * 4 : 4;

            if (arg_stack_base > (arg_stack_op_base - offset))
                return 0x81;

            arg_stack_ptr = arg_stack_op_base - offset;
            break;

        case 0x28:
            arg_stack_ptr = arg_stack_op_base;
            break;

        case 0x29: case 0x2C: case 0x2D: case 0x2F: case 0x30:
            arg_stack_op_base += 4;
            arg_stack_ptr = arg_stack_op_base;
            break;
       
        case 0x31: case 0x32: case 0x33:
            break;
        default:
            return 7;
    }

    if (input_end_ptr <= curr_input_ptr)
        return 0;


    // now, go back to the beginning of the loop
} // end of while loop
} // end of function

Comments or opinions expressed on this Weblog are the opinions of the authors alone. They are not necessarily reviewed in advance by anyone but the individual authors, and neither IBM Internet Security Systems nor any other party necessarily agrees with them. The views expressed by outside contributors and links to outside websites do not represent the views of IBM Internet Security Systems, its management or employees. All content on this Weblog has been made available on an “as-is” basis, and IBM Internet Security Systems shall not be liable for any direct or indirect damages arising out of use of this Weblog.